# Information Model Tutorial

The Ability Platform Information Model service is all about modeling various entities. It allows you to digitalize your physical (or completely abstract!) things in the cloud. This enables various use cases, such as the Digital Twin in the IoT world, or more general data storage in a graph-oriented way.

The service consists of several concepts that you have to understand in order to use it efficiently. This article is meant to take you through a process of how to design an Information Model based on a simple IoT scenario.

# Scenario

In this tutorial, we'll be working with the following system:

Factory Plan

As you can see in the picture, we're dealing with a factory setup consisting of various actors. First of all, let's define the entities that we'll need to model:

  • A factory - there are two factories, but there could be more of them.
  • A motor - motors might be in working condition, or they might be serviced due to being faulty. Our requirements specify that the cost generated by a motor is an important part.
  • A sensor - there are different kinds of sensors (temperature, vibration, bearing condition) that collect various measurements about the motor they are attached to.

Now, let's focus on the relations between the entities. Again, looking at the illustration, we can learn that:

  • There may be a few factories. Factories do not seem to have any relations with each other.
  • There may be multiple motors. A motor is always placed in a factory. A single factory may contain multiple motors.
  • A single motor may have various sensors attached (or none at all).

Relations in the system

We're dealing with a simple hierarchical structure, which can very well be represented as a graph. Such designs are perfect to be represented using the Ability Platform Information Model.

# Type Definitions

Just like we have first listed the entities that take part in the system presented on the illustration, we will now take these entities and turn them into type definitions. Type Definitions are the Ability Information Model way of representing blueprints of the things that we want to later instantiate. You can think of type definitions as an analogy to a "class" in an object-oriented programming language. The type definition will contain all the components that a given entity kind should always have. It's best to explain it using an example. Have a look at the following type definition. It is a type representing a factory in our design:

{
  "model": "abb.ability.device",
  "typeId": "training.factory",
  "version": "1.0.0",
  "properties": {
    "name": {
      "dataType": "string",
      "isMandatory": true
    },
    "company": {
      "dataType": "string",
      "value": "ABB"
    },
    "geograpicalLocation": {
      "latitude": {
        "dataType": "number"
      },
      "longitude": {
        "dataType": "number"
      }
    }
  }
}

Looking at the type above, we can point out a few things:

  • Type definitions are represented as JSONs - all of the structures participating in the Information Model design are represented in JSON format.
  • Type definitions are versioned - semantic versioning is used, which allows updating types in a manner that conveys the information if the given update was "major", "minor", or a "patch". In this way, the compatibility between types stays intact even if we update one of them in a way that could be a breaking change. The Information Model service will not allow such an update to go through if a major version of the types has not been incremented.
  • Type definitions have their own typeIds - each type definition that you create, needs to have a unique typeId.
  • Type definitions are based on an underlying model definition (the model field in the JSON above) - before you create a type definition, you need to have a model definition defined. Model definitions allow you to specify which components can be used by the type definitions built on top of them. For example, the abb.ability.device model definition, which is available by default on any Ability Platform instance, allows the following components to be used: properties, variables, methods, attributes, relatedModels, and references. For now, only "properties" are being used in our type definition. We won't be focusing on model definitions yet - you will understand much better what their purpose is as you keep reading.
  • Type definitions may have properties (and other components, as we just mentioned) - in this case there are four of them. Properties represent data that characterizes a given entity. Values of the properties may be updated, however, the Ability Inforamtion Model will not track the historical values of properties. Every property needs to have a dataType.
  • A property may be marked as mandatory via the isMandatory field. In such case, whenever we try to create an instance of the type, that property needs to be provided with a value. Otherwise, it will not be created. Alternatively, we could also provide a default value in a type definition (using a value field in the property's definition), which would automatically assign that value to all newly created instances (unless an instance overwrites it with its own value).
  • A property may have a default value via the value field.
  • Properties may be nested (e.g. geograpicalLocation.latitude and geograpicalLocation.longitude)

As you can see, even such a simple type already gave us a good overview of what type definitions have to offer. Some of the outlined facts will be expanded on as we progress through this tutorial.

Let's continue with another type definition:

{
  "model": "abb.ability.device",
  "typeId": "training.motor.device",
  "version": "1.0.0",
  "unique": ["serialNumber"],
  "properties": {
    "serialNumber": {
      "dataType": "string",
      "isMandatory": true
    }
  },
  "variables": {
    "rotorSpeed": {
      "dataType": "number",
      "unit": "rpm"
    }
  }
}

This time, we have created a type of motor. As you can see, there are a few new keywords that we did not see yet. Again, let's do a brief analysis of this type definition:

  • We're using the abb.ability.device model definition again for simplicity. Don't worry though, you will learn how to create your own very soon.
  • The typeId is training.motor.device and it will be a unique identifier for this new type definition.
  • We again start versioning from 1.0.0. As you progress through the tutorial, you will see how that changes.
  • Properties may be marked as unique. The array of unique properties of a given type is analogical to composite key concept in database systems. Whenever you instantiate a selected type definition, the provided values of the properties will be checked against the existing instances and their set of unique properties. If another object with the same values of the unique properties already exists, the creation will fail.
  • The serialNumber property was marked as mandatory, just like the name of a factory was.
  • Type definition may contain other components than properties. This time, we've included variables as an addition. Variables represent values that are timeseries kind of data. rotorSpeed is a perfect example of that. Our motor could (as an IoT appliance) report its current speed. This data could then be used for various graphs, aggregates, etc. Variables are defined very similarly as properties, "dataType" is always required.
  • Variables (and properties) may contain various attributes. In this case we've added a "unit" attribute to the rotorSpeed variable. The Ability Platform has three predefined attributes - "unit", "min", "max". You can also define your own if you need to (out of the scope of this tutorial).

The last entity kind that we did not yet cover is sensors. Let's have a look at how this one could look like:

{
  "model": "abb.ability.device",
  "typeId": "training.sensor",
  "version": "1.0.0"
}

When you saw this type definition you probably thought: "That's it?". Type definitions may be quite elaborate, but, on the other hand, they may also be pretty minimalistic. What you're looking at is the most minimal type definition that you are able to create. Every type definition has to include: a "model", a "typeId", and a "version". Since we have included these three, we're good to go. This type is not very useful, though. We will make it useful by using inheritance a little bit further into the tutorial. Let's summarize what we have achieved and learned so far:

  • We have defined three type definitions.
  • Type definitions may contain various components like properties, variables, and others.
  • Properties may be mandatory, and they may be unique.
  • Definitions of properties and variables have to contain the data type.
  • Properties and variables may be optionally characterized with various attributes.

For now, our type definitions are very simple and do not yet meet the requirements that were outlined with the system illustration. We're getting there, step by step. Next, let's have a look at how we can add cost-related information to our motors, which was one of the goals.

Since we want to track cost realated data for our motors, we need to extend our model. Let's assume that the cost information is being generated on some interval and we can treat is as a variable. In the end, every, say hour, a new data point would be sent with the cost that the motor generated during that period. It's a great simplification, but for our needs it's perfectly fine.

The easiest approach to the new variable would be to slightly update the type definition that we already have defined previously:

Ignore the type below

If you're creating types on your own as you follow the tutorial, ignore the type below. As you're going to see, we will use a different approach to add the cost information.

















 
 
 
 



{
  "model": "abb.ability.device",
  "typeId": "training.motor.device",
  "version": "1.0.1",
  "unique": ["serialNumber"],
  "properties": {
    "serialNumber": {
      "dataType": "string",
      "isMandatory": true
    }
  },
  "variables": {
    "rotorSpeed": {
      "dataType": "number",
      "unit": "rpm"
    },
    "cost": {
      "dataType": "number",
      "unit": "$"
    }
  }
}

This approach definitely would work, however, it's not necessarily the best one we can take. Cost information, in this case very simplified, does not really belong to the same domain as other physical data about the motor (currently we only track rotorSpeed, in reality we would probably have a much richer model). Putting all possible data in one "bucket" like this creates a bit of a mess, and makes the training.motor.device type overloaded and hard to reason about.

Let's ignore the type definition above. We'll attach cost information in a different way. A better approach will be to create a separate type definition for out motor, representing a separate aspect of this device - a cost aspect.

Before we add it though, we need to understand a bit more about the concept of model definitions. Until now, we've been using a single model definition called "abb.ability.device". It is a model that comes predefined with every instance of Ability Platform. Let's look at its JSON representation:

{
  "modelId": "abb.ability.device",
  "components": {
    "properties": {
      "hubName": {
        "dataType": "string"
      },
      "deviceId": {
        "dataType": "string"
      }
    },
    "variables": {},
    "methods": {},
    "alarms": {},
    "events": {},
    "attributes": {},
    "references": {}
  }
}

Just like we did before, let's analyze this model:

  • The name of this model definition is "abb.ability.device".
  • A model definition has is a list of the components it enables.
  • This model definition enables the following components: properties, variables, methods, alarms, events, attributes, and references.
  • Some of the components might be restricted in some ways. In this case, if a type definition is based on "abb.ability.device" and it contains properties such as "hubName" or "deviceId", they have to be defined as strings.

Object-Oriented Programming Analogy

Unfortunately, the "model definition" concept does not have a good analogy in object-oriented programming like type definition does. We could say that it is a bit similar to an interface/protocol, but that would be a bit of a stretch. Instead, let's put it in the following way. A class in an OOP language (like C#) can have properties, fields, methods, and events. Imagine that there is a way to limit these and say that a given class may only use properties and fields, or another class would only be able to use properties and events. If a feature like that was available in programming languages, that would be a much better analogy.

The "abb.ability.device" model was defined on the Platform primarily to be used with IoT devices. The assumption was that, in typical scenarios, IoT devices would benefit from being able to have some properties, variables, methods, and so on. A real IoT device would also have the "deviceId" and "hubName" properties defined as strings. In our case we don't really need those, and since they are not mandatory on the "abb.ability.device" model definition, we'll skip them.

Since we want to have multiple (two) type definitions for our motor, we need to have two separate model definitions. Every type definition representing a different aspect of an entity (like a motor) needs to be based on a different model definition. Let's define a new model definition:

{
  "modelId": "training.cost",
  "components": {
    "properties": {},
    "variables": {}
  }
}

The model definition above enables two component types: variables and properties. We have assumed that all the cost information about a motor may be stored in a variable called "cost". That's why the "variables" component has been enabled. The "properties" component has been added as well to attach the serial number information to the model, just like we did it in the "training.motor.device" type definition. This way both representations of the motor will contain the full set of unique information about it, and it will be easy to correlate a given digital twin with an actual device.

With the model definition defined, it's time to define a new type definition:

{
  "model": "training.cost",
  "typeId": "training.motor.cost",
  "version": "1.0.0",
  "unique": ["serialNumber"],
  "properties": {
    "serialNumber": {
      "dataType": "string",
      "isMandatory": true
    }
  },
  "variables": {
    "cost": {
      "dataType": "number",
      "unit": "$"
    }
  }
}

As you can see, the new type uses our new "training.cost" model definition, and contains a "serialNumber" property and a "cost" variable. This is sufficient for this aspect of our motor.

Additionally, let's also enhance our "abb.ability.device" based type definition with the relatedModels component:




 













 
 
 
 
 
 
 
 


{
  "model": "abb.ability.device",
  "typeId": "training.motor.device",
  "version": "1.0.1",
  "unique": ["serialNumber"],
  "properties": {
    "serialNumber": {
      "dataType": "string",
      "isMandatory": true
    }
  },
  "variables": {
    "rotorSpeed": {
      "dataType": "number",
      "unit": "rpm"
    }
  },
  "relatedModels": {
    "training.cost": {
      "type": "training.motor.cost@1",
      "uniqueMapping": {
        "serialNumber": "serialNumber"
      }
    }
  }
}

The role of "relatedModels" is to instruct the Information Model service to automatically created the models defined as related when the "main" model gets created. In our case, whenever we create an instance of "training.motor.device@1", an instance of "training.motor.cost@1" will be created as well, with the same objectId identifier.

relatedModels is just for automatic creation of models

"RelatedModels" component is not necessary. All it does is it automates the creation of models defined in the "relatedModels" section of the type definition. If we did not specify that, we'd have to manually create all the models that should belong to a given entity (and rememember to set the same objectIds on them).

Two things have changed in our "training.motor.device" type definition:

  • A version has been bumped up - any change in the type definition requires the version to be updated. In this case, when we added a new "relatedModels" component to the type, the "patch" part of the version string was enough to be updated.
  • The "relatedModel" component has been added. It contains a list of all the other aspects of the modeled entity, in this case a motor. You can see that we had to include the name of the model definition, a type definition, and optionally a list of properties that we want to map between the types.

TIP

You might think that we didn't really gain much with this new type, since the training.motor.device type definition is rather small and adding a single "cost" variable wouldn't hurt too much. However, remember that this is a simplified scenario. Real type for a physical device would probably be much richer to create a proper digital twin for it.

With the latest change in our model design, we achieved the following:

  • Our motor model has been split into two separate type definitions, each responsible for its own domain or aspect:
    • The "training.motor.device" type definition is responsible for storing measurements about the device's physical characteristics (we have just "rotorSpeed", but we could track many more data).
    • The "training.motor.cost" type definition is responsible for storing inforamtion about cost-related data for a motor
  • Anytime we create an instance of the "training.motor.device" type definition, all its related models will be instantiated automatically and they will all share the same objectId. Thanks to the defined mapping, the automatically created instance of "training.motor.cost" will have a proper "serialNumber" configured.

If the concept of mutliple models per entity is still a bit fuzzy to you, we could compare it to partial classes in the C# programming language. If your class is huge, but for some reason you want to keep it that way, you can split it into different files and use the partial keyword to mark the split classes as parts of one bigger class. The same way when you split your type definition into multiple models, you can treat them all as parts of the same entity.

Just to summarize this section of the tutorial, let's list the definitions that we both used and created so far:

Model definitions:

  • abb.ability.device - It came predefined in the Ability Platform instance, and we used it for a few of our type definitions.
  • training.cost - We defined it by ourselves in order to create a separate aspect of our motor.

Type Definitions:

  • training.factory@1.0.0 - This type represents a factory site.
  • training.motor.device@1.0.1 - This type represents the motor in an aspect of its physical measurements.
  • training.motor.cost@1.0.0 - This type represents the motor in an aspect of its cost.
  • training.sensor@1.0.0 - This type represents a sensor that may be attached to the motor. For now it's almost empty, but we'll get back to it.

# References

We're slowly getting closer to what was required in our modeling strategy. One important area that we have not covered yet is the relations between various entities. If you recall the illustration shown at the beginning of the tutorial, our models should reflect the hierarchical structure of different entities. The factories should contain motors, while the motors should have sensors attached to them. That's a great opportunity to look at references, another kind of component that we can use in our type definitions. Since the references are quite easy to use, let's just look at how we'd update the "training.factory" type to allow the motors to be contained within a factory:




 


















 
 
 
 
 
 
 
 
 
 
 
 


{
  "model": "abb.ability.device",
  "typeId": "training.factory",
  "version": "1.0.1",
  "properties": {
    "name": {
      "dataType": "string",
      "isMandatory": true
    },
    "company": {
      "dataType": "string",
      "value": "ABB"
    },
    "geograpicalLocation": {
      "latitude": {
        "dataType": "number"
      },
      "longitude": {
        "dataType": "number"
      }
    }
  },
  "references": {
    "workingMotors": {
      "isHierarchical": true,
      "isContainment": false,
      "to": [ { "type": "training.motor.device@1" } ]
    },
    "servicedMotors": {
      "isHierarchical": true,
      "isContainment": false,
      "to": [ { "type": "training.motor.device@1" } ]
    }
  }
}

As usually, let's analyze the updated type definition:

  • A version was bumped to "1.0.1". A patch part update is the minimum necessary change in version string when the "references" component is added.
  • A new component was added - "references". You can scroll up a bit and look at the "abb.ability.device" model definition that we're using here. That model allows "references" to be used in its type definitions.
  • We have defined two types of references that the factory might have:
    • "workingMotors" - it will be a list of motors that are in working order, actively participating in processes in the factory.
    • "servicedMotors" - it will be a list of motors that are being serviced due to some malfunctioning.
  • Both our references use the "isHierarchical": true configuration - it restricts the graph relationship to disallow loops. If our "training.motor.device" type had a possibility to refer to a factory (it doesn't, since we did not include the "references" component there) it would be impossible to refer from "Factory A" to "Motor B" to "Factory A" again. In our case, it's probably not needed, but we included it to be explicit
  • Both our references use the isContainment: false configuration - if the value was set to true instead, deleting a factory instance would automatically delete all motor instances that it referred to. Let's assume we don't want that, because if a factory got shut down, we'd want to move the motors to another factory.

Type definitions vs instances

Mind that up until now we have not created any actual references! We have just extended the type definition of "training.factory" with the possibility of attaching instances of "training.motor.device" to the instance of "training.factory". Just like classes in OOP define blueprints for actual objects/instances, the same goes with type definitions. We're now defining what capabilities future instances of our types will have. It will become clear when we start to create the instances a bit further in the tutorial.

Reference Attributes

Instead of specifying two separate references for the factory's type definition, we could also make use of reference attributes. In such case, we could have just one reference defined (like "motors") and then for any attached motor, we could assign a value for an attribute "isWorking" or something similar. There are a lot of ways to achieve the goal!

Let's now move on to the "training.motor.device" type definition. We need to enable instances of this type to be able to refer to sensors. Here's the updated version of it:




 





















 
 
 
 
 
 
 


{
  "model": "abb.ability.device",
  "typeId": "training.motor.device",
  "version": "1.0.2",
  "unique": ["serialNumber"],
  "properties": {
    "serialNumber": {
      "dataType": "string",
      "isMandatory": true
    }
  },
  "variables": {
    "rotorSpeed": {
      "dataType": "number",
      "unit": "rpm"
    }
  },
  "relatedModels": {
    "training.cost": {
      "type": "training.motor.cost@1",
      "uniqueMapping": {
        "serialNumber": "serialNumber"
      }
    }
  },
  "references": {
    "sensors": {
      "isHierarchical": true,
      "isContainment": true,
      "to": [ { "type": "training.sensor@1" } ]
    }
  }
}

Here's what we've changed:

  • The version is now "1.0.2", since we added a new component.
  • Instances of "training.motor.device" will be able to have references to the instances of "training.sensor".
  • The reference that we defined (called "sensors") is hierarchical ("isHierarchical": true) and has isContainment: true. In this case, we assume that any time a motor is decommissioned, its sensors are decomissioned as well.

That's all that we had to do to enable relations between our entities. Now, when we start instantiating our types, we will be able to tie them together logically to reflect the actual physical structure of our system. Doing that will enable us to query our data more effectively, and by that I mean using scenarios such as "give me a list of all the serviced motors of Factory A". You will see examples of this in the sections below.

Now, let's try to make our sensors more useful by enriching their modeling.

# Inheritance

Until now, we've made use of quite a few capabilities of the Ability Platform Information Model:

  • model definitions
  • type definitions and their components:
    • properties
    • variables
    • relatedModels
    • references

Now, let's add inheritance to that list. If you recall, our "training.sensor" type definition was quite poor. Here's what it looked like:

{
  "model": "abb.ability.device",
  "typeId": "training.sensor",
  "version": "1.0.0"
}

It's basically empty and that's fine. We will leave it as it is, and create new types for different kinds of sensors that we might use in our factory. According to the requirements outlined at the beginning of this article, there are three types of motor sensors that we will use:

  • temperature sensor
  • vibration sensor
  • bearing condition sensor

That basically means that we need three new type definitions. We can define them like so:

Vibration Sensor

{
  "model": "abb.ability.device",
  "typeId": "training.sensor.vibration",
  "version": "1.0.0",
  "baseTypes": ["training.sensor@1.0.0"],
  "variables": {
    "vibration": {
      "dataType": "number",
      "unit": "velocity rms"
    }
  }
}

Temperature Sensor

{
  "model": "abb.ability.device",
  "typeId": "training.sensor.temperature",
  "version": "1.0.0",
  "baseTypes": ["training.sensor@1.0.0"],
  "variables": {
    "skinTemperature": {
      "dataType": "number",
      "unit": "degrees"
    }
  }
}

Bearing Condition Sensor

{
  "model": "abb.ability.device",
  "typeId": "training.sensor.bearing",
  "version": "1.0.0",
  "baseTypes": ["training.sensor@1.0.0"],
  "variables": {
    "bearingCondition": {
      "dataType": "string",
      "enum": ["good", "medium", "bad"]
    }
  }
}

We added three more types, all of them are similar. Here are a bunch of facts about them:

  • All of them use the "abb.ability.device" model definition.
  • All of them are version "1.0.0" (we just started the modeling, we'd update them in the future).
  • All of them use inheritance via the baseTypes keyword.
  • All of them define a single variable that gives a purpose to each of these entities.
  • The "training.sensor.bearing" type has a variable called "bearingCondition". It has, well known to us already, the dataType keyword to specify that the values of this variable shoud be strings. Additionally, we've added an enum keyword. This means that the values of this variable will be limited to these three values. If any other value is be submitted by an actual instance, it would be dropped by the data processing pipeline.

Just like before, we can use the OOP analogy to understand the purpose of the baseTypes keyword. Specifying types in this array causes the affected type to inherit all of the characteristics of the base types. If our base type had a few properties, variables, or any other components, the derived type would treat them as its own as well. Let's look at the following C# example:

class Animal {
  public int LegsCount {get; set;} 
}

class Dog : Animal {
  public int LegsCount {get; set;} = 4;

  public void Bark() {
    Console.WriteLine("Woof")
  }
}

class Spider : Animal {
  public int LegsCount {get; set;} = 8;

  public void MakeWeb() {
    Console.WriteLine("Making web")
  }
}

We have a base class called Animal and two derived classes called Dog and Spider. Both Dog and Spider set the value for the LegsCount property appropriately. Additionally, they add some additional methods that are relevant for their species.

Pretty much the same functionality is offered by inheritance in the Ability Platform Information Model. You can learn all the details about it in the Inheritance article. For now though, let's go ahead with the tutorial.

You are probably wondering what the purpose of inheriting from an empty type definition is. It has nothing more than a "typeId", "model", and a "version". Our derived types do not really gain any components that were defined in the base types. They don't, however, in this case inheritance gives us something else - polymorphism. If you go back to the type definition of "training.motor.device" you will see that we have included there a reference called "sensors". This reference targets the "training.sensor" type definition, which is a base type for all our sensor types. References are covariant, which means that the type specified in the referene target may also be replaced by a derived type. Our motors may have a few sensors attached, and each of them might be of a different kind/type. The motor's type definition does not need to know about it, it treats all of them as the instances of "training.sensor". You could say that our "training.sensor" type definition is a litle bit like a marker interace in object-oriented programming. Its only purpose is to allow other entities to refer to all kinds of sensors by one type.

You will see how all that works in practice a bit later.

Our toolkit got a bit more complex. Let's again summarize where we are at now:

We still use just two model definitions:

  • "abb.ability.device"
  • "training.cost"

We have seven type definitions:

  • for the factory:
    • "training.factory@1.0.1"
  • for the motor:
    • "training.motor.device@1.0.2"
    • "training.motor.cost@1.0.0"
  • for the sensors:
    • "training.sensor@1.0.0"
    • "training.sensor.vibration@1.0.0"
    • "training.sensor.temperature@1.0.0"
    • "training.sensor.bearing@1.0.0"

The following relations (references) were defined:

  • "training.factory" may contain:
    • working motors
    • serviced motors
  • "training.motor.device" may contain:
    • sensors of different kinds

Before we wrap up this section, I'd like to point out a few more facts:

  • Our factory refers to motors. Note that the motors could also make use of inheritance. Realistically, it would probably make sense to model various kinds of motors with different type definitions. Otherwise, we'd end up with one huge type definition that would have to encompass all possible motor characteristics, no matter what type of motor we're dealing with. In such a case, the situation would be a bit similar as what we ended up with for the sensors - a covariant reference.
  • Our sensor modeling strategy assumes that one sensor device measures just one physical characteristic of a motor. In reality, motor sensors are much more capable (e.g. Smart Sensor) and allow you to measure a few characteristics. Maybe it would make sense to change our design a bit, and define a few base classes for different kinds of sensors. Then, we could create more complex sensors that combine multiple other sensors' characteristics by inheriting from these base classes. Ability Platform Information Model allows types to derive from multple other types! Note that we can't do that with our current design, because it is not allowed to inherit from multiple types that share the same base type somewhere in their inheritance chain (in our case, all our sensors share the "training.sensor" base).

# Instantiating

Now, that our types are ready, let's make use of them and try to create the actual objects.

Getting back to our initial design illustration, the system currently looks like this:

Entities in the system as a graph

  • there are two factories: Factory A and Factory B
  • Factory A has:
    • two motors that are in working condition. The Motor A uses the temperature sensor, while the Motor B uses both the temperature sensor and vibration sensor.
    • one motor that is being services. It has bearing sensor attached
  • Factory B has:
    • one motor that is in working condition. This motor does have all three kinds of sensors attached
    • two motors that are being serviced - both of them do not have any sensors attached.

We will have to create the following objects:

  • two instance of the "training.factory" type
  • six instances of the "training.motor.device" type (six additional instances of "training.motor.cost" will be automatically created for us as well)
  • seven instances of various sensor types

After creating all of these, we will have to connect them together via references.

Do It Yourself

You are free to create all of these instances on your own using either the API Playground or your own instance of the Platform if you have it.

# Objects

# Factories

First of all, let's create two factories. Here are the payloads that we'd send to the HTTP POST endpoint:

    What you see above are the JSON representations of the instances of our type definition. As you can see we created two instances based on one type definition (just like we could create two objects based on one class in OOP).

    After POSTing any of the objects, we get a reponse containing objectIds for each of them, like the example below:

    {
      "objectId": "ec9581d3-8ff6-4542-85b1-059db23242d5",
      "model": "abb.ability.device"
    }
    

    Every object instance in the Ability Platform Information Model service has a unique objectId, which acts as an identifier. We are going to need these objectIds to connect various objects together via references later on.

    # Motors

    Next, we'll create six motors:

      As you can see, even though the type definition for the motor was of considerable size (~30 lines of JSON), the actual objects are much smaller. That's because the objects only need to provide necessary data for the components that were defined in the type. In our case, the only thing that was missing was the value for the serialNumber. If you recall, this property was marked as mandatory and unique. If we tried to create a motor without a serialNumber, or two motors with the same serialNumber, both these operations would fail.

      As previously mentioned, right after we created the factories, objects are assigned with unique objectIds. In case of type definitions that include related models (liek the one for motors), the API response is a bit extended:

      {
        "objectId": "8960eb03-fc28-4f3a-bb97-272bf07b4114",
        "model": "abb.ability.device",
        "relatedModels": [
          {
            "objectId": "8960eb03-fc28-4f3a-bb97-272bf07b4114",
            "model": "training.cost"
          }
        ]
      }
      

      The returned message informs us that two objects have been created (one in the "abb.ability.device" domain and another in the "training.motor.cost" domain). Both of these objects use the same objectId! This is a bit in conflict to what I mentioned previously:

      Every object instance in the Ability Platform Information Model service has a unique objectId (...)

      However, let's not forget that in the case of related models we can treat the participating objects as a part of one bigger object. With such approach, the quote stays valid.

      # Sensors

      Let's go ahead and create all the sensors we need:

        As you can see, the payloads to create sensors were quite small. That's due to the fact that we did not include any properties in their type definitions. Just like before, the API will return individual objectIds for each sensor.

        At this point we should have 21 object models and 15 objectIds attached to them (let's not forget about 6 pairs of object models for the motors, with each pair having one objectId assigned).

        Objects without any connections betweeen them

        Like on the image above, you could imagine these objects just floating around without any connections. Let's now create references between them!

        ObjectIds are unique

        If you try to create these objects by yourself (for example using the API Playground), the objectId identifiers will be different than the ones you see on the image above. ObjectIds are unique!

        # References

        Objects may be freely connected together in a graph, as long as their type definitions allow to do so. Since we have designed our system with ability to create references between different entities, we can just use the appropriate APIs to do so.

        In this section, we're not going to write the appropriate API payloads for all of the individual references. Most of them will look basically the same. Instead, we'll be showing you a general form of these requests.

        # Factories and Motors

        The following is a template for the API call allowing us to attach a motor to the "workingMotors" set of a factory:

        URL:

        POST https://{Ability Platform URL}/objects/{factory's objectId}/models/abb.ability.device/references/out

        Payload:

        [
          {
            "name": "workingMotors",
            "to": {
              "objectId": "{motor's objectId}",
              "model": "abb.ability.device"
            }
          }
        ]
        

        The following is a template for the API call allowing us to attach a motor to the "servicedMotors" set of a factory:

        URL:

        POST https://{Ability Platform URL}/objects/{factory's objectId}/models/abb.ability.device/references/out

        Payload:

        [
          {
            "name": "servicedMotors",
            "to": {
              "objectId": "{motor's objectId}",
              "model": "abb.ability.device"
            }
          }
        ]
        

        The only thing that differs between these two calls is the name of the reference that we're attaching the motor two. Our factory may have any number of "workingMotors", and any number of "servicedMotors". You can also freely move motors between these categories (or even factories) by DELETing some reference and then POSTing another one.

        # Motors and Sensors

        The same as we did with the factories and motors, we can now attach sensors to the motors. Here's a template of what such an API call would look like:

        URL:

        POST https://{Ability Platform URL}/objects/{motor's objectId}/models/abb.ability.device/references/out

        Payload:

        [
          {
            "name": "sensors",
            "to": {
              "objectId": "{sensor's objectId}",
              "model": "abb.ability.device"
            }
          }
        ]
        

        Since there are seven sensors, you'd have to invoke seven HTTP API calls like the one above. Thanks to covariance, you are able to attach any sensor type to any motor.

        Let's look at a graphical representation of our current system:

        Graph of Information Modelobjects

        Looks like we got what we wanted! We have connected factories to motors, and motors to sensors, which should be a digital twin representation of a real factory.

        # Objects Extensibility

        Our types have been designed and used to create actual instances. Now, let's say that a situation has come up. It turns out that Factory B is placed in a location where air pollution is a serious issue and all industrial sites are required to report on their participation in generation of the pollution. Factory B is not too "green", and it is going to have to start measuring the amount of various chemical substances being released to the air. How does it affect our models? Ideally, we should probably think about creating a related model for our factory to gather the new required data there. However, there is another, much simpler solution - object extensions.

        Object extensions allow you to introduce a bit more "dynamic" approach to modeling. Until now, all of the components of our objects were statically defined in the type definitions, which is directly reflected in how our objects look like. In some cases you might want to break out of that and make some objects different than others. You could obviously do that by using a different type definition for that object. Or you could extent that individual object.

        First of all, not all type definitions allow their objects to be extended. None of our type definitions allows that. Here's how we need to modify the "training.factory" type definition to allow extensibility:




         
         
































        {
          "model": "abb.ability.device",
          "typeId": "training.factory",
          "version": "2.0.0",
          "isExtensible": true,
          "properties": {
            "name": {
              "dataType": "string",
              "isMandatory": true
            },
            "company": {
              "dataType": "string",
              "value": "ABB"
            },
            "geograpicalLocation": {
              "latitude": {
                "dataType": "number"
              },
              "longitude": {
                "dataType": "number"
              }
            }
          },
          "references": {
            "workingMotors": {
              "isHierarchical": true,
              "isContainment": false,
              "to": [ { "type": "training.motor.device@1" } ]
            },
            "servicedMotors": {
              "isHierarchical": true,
              "isContainment": false,
              "to": [ { "type": "training.motor.device@1" } ]
            }
          }
        }
        

        There are two changes that we've made to our factory type definition:

        • We added the "isExtensible": true - this is a bit that is required for the objects of a given type to be allowed to have extensions.
        • Version of the type was bumped from "1.0.1" to "2.0.0". Changing the value of the isExtensible field is considered as a breaking change (its value is false by default), so we had to increment the major part of the version string.

        The second point of the list above is quite important. Versioning of type definitions keeps the entities compatible with each other. Even though we have updated the type to version "2.0.0", the older versions are still available in the system. Our factory objects are actually still using version "1.0.1" of "training.factory".

        Major updates involve breaking changes

        Major updates require explicit request to be sent asking for the update of objects. Minor and Patch updates of type definitions automatically update the objects of that type.

        Before we extend the object, we need to update Factory B's object model to use the version "2.0.0" of its type definition. We can do that with the PUT endpoint of the API.

        URL: PUT https://{Ability Platform URL}/objects/{Factory B's objectId}/models/abb.ability.device

        Payload:




         
















        {
          "objectId": "{Factory B's objectId}",
          "model": "abb.ability.device",
          "type": "training.factory@2",
          "properties": {
            "name": {
              "value": "Factory B"
            },
            "geograpicalLocation": {
              "latitude": {
                "value": 36.846207404781445
              },
              "longitude": {
                "value": 10.271616833712198
              }
            }
          },
          "version": 1
        }
        

        Version field

        The payload included the version field. It refers to the last version of the Factory B's information model object, before the update. Including that field is necessary to disallow updates in case of race condition situations where one process would be updating an object while it has already been updated by another process.

        After sending the request above, Factory B's object is now an instance of "training.factory@2.0.0". The other factory, Factory A, is still using the older type definition - "training.factory@1.0.1".

        Finally, we can add the desired change to Factory B's information model object:

        URL: PUT https://{Ability Platform URL}/objects/{Factory B's objectId}/models/abb.ability.device/extension

        Payload:

        {
          "variables": {
            "releasedPollution": {
              "dataType": "number",
              "unit": "cubic meters"
            }
          }
        }
        

        With that modification, Factory B differs from Factory A by the additional variable called "releasedPollution". Factory A will be able to report on this new measurement.

        Other Solutions

        We used the object extension mostly to show you that such possiblity exists, but you should be aware that other solutions also exist.

        A separate type definition could be defined and added as a related model of the "training.factory". Another way would be to define a type definition called "training.airQuality.device" that would represent an appliance that makes the measurements. Object of this type could then be added to Factory B as a reference.

        The solution that you would choose depends on the shape of your information model and the required amount of changes that you would have to apply to get to the desired state. It's also worth to consider if the new requirement is only going to touch on a single object in your system or if it's possible for it to need to be extended to other objects as well in the future.

        At this point we consider the models to be ready and we can move on to the discussion of how to make a good use of them in terms of querying.

        # Retrieving the data

        Our factory is working, and our models have been put to a good use. We'll now look at how we can retrieve various information about out system.

        # Getting a specific object

        To get a specific object we can use the GET endpoint:

        GET https://{Ability Platform URL}/objects/{objectId}/models/{modelId}

        This endpoint requires us to provide the exact objectId and modelId (useful in the case of related models where multiple modelIds are used for a single objectId). When we use it, we will get a view on the selected object. Here's an example of one of the motors:

        {
          "model": "abb.ability.device",
          "type": "training.motor.device@1",
          "version": 1,
          "lastModified": "2021-11-24T08:19:29.13Z",
          "properties": {
            "serialNumber": {
              "value": "003"
            }
          },
          "objectId": "2e858e02-1157-4d2f-b0e3-545a3581d5d7",
          "tenantId": "f4831d89-9f60-471a-b063-5fbcda46584b"
        }
        

        The returned object contains:

        • a set of properties together with that latest value - in this case have just one property
        • the current version - every time we update an object (with a PATCH or a PUT]) the version number is incremented by one. Since we did not need to update the motors, the version is equal to 1.
        • the lastModified field informing about the time of last update of this object. In this case, it's the time of when the object was created.
        • the tenantId field informing about the tenant that this object belongs to (Ability Platform is multitenant)

        Variables data

        If you expected to find the timeseries data of your variables, these may be retrieved using a different endpoint.

        If you want to see your object in the "holistic" form, that is merged together with its type definition, use the following URL:

        GET https://{Ability Platform URL}/objects/{objectId}/models/{modelId}?isHolistic=true

        The Examplary response:

        {
          "model": "abb.ability.device",
          "unique": [
            "serialNumber"
          ],
          "properties": {
            "serialNumber": {
              "dataType": "string",
              "isMandatory": true,
              "value": "003"
            }
          },
          "variables": {
            "rotorSpeed": {
              "dataType": "number",
              "unit": "rpm"
            }
          },
          "relatedModels": {
            "training.cost": {
              "type": "training.motor.cost@1",
              "uniqueMapping": {
                "serialNumber": "serialNumber"
              }
            }
          },
          "references": {
            "sensors": {
              "isHierarchical": true,
              "isContainment": true,
              "to": [
                {
                  "type": "training.sensor@1"
                }
              ]
            }
          },
          "isDeleted": false,
          "type": "training.motor.device@1",
          "version": 1,
          "lastModified": "2021-11-24T08:19:29.13Z",
          "objectId": "2e858e02-1157-4d2f-b0e3-545a3581d5d7",
          "tenantId": "f4831d89-9f60-471a-b063-5fbcda46584b"
        }
        

        Holistic view and inheritance

        Holistic view is also useful when retrieving type definitions that make use of inheritance. In such case, by default (isHolistic=false) the returned type JSON will not include components that are inherited. If you set the flag to true (isHolistic=true) the returned JSON will contain all of the type's components merged with all of the base types (recursively).

        This way of querying is good if you already know the objectId of the object you're looking for. However, in cases where you do not know the identifier, this enpoint is useless.

        # Querying with DSL

        Domain Specific Language (DSL) is a format of filtering that allows you to retrieve the objects that you care about. In this section, we will present a few examples of filters that might be interesting in our factory scenario. Together with the DSL queries, you will see responses that you could get for the exemplary system that was outlined in this tutorial.

        The DSL query endpoint is described here, while all the possible DSL constructs are listed here.

        Example of payload to the DSL Query endpoint:

        {
          "query": "models('abb.ability.device')"
        }
        

        A query above will return all the objects defined in the "abb.ability.device" domain. That was just a quick warm up, let's now move on to the real examples.

        # Get all the motors

          This way you will get all the "abb.ability.device" representations of your motors. If you're interested in the "training.cost" perspective as well, you can modify your query like this:

            # Get motors from a specific factory

              Alternatively, if you do not know the objectId of the factory, you could query for its name:

                This query will return all the motors in a factory with a provided objectId.

                # Get motors being serviced from a specific factory

                  This query will limit the returned motors to only those that are being serviced. It uses the fact that serviced and working motors are separated into different references of a factory.

                  # Get all temperature sensors in a specified factory

                    In this DSL example we are traversing the graph from the "top" to the "bottom". Knowing that there is a motor between a sensor and a factory, we may construct a proper DSL traversal.

                    # Get a factory where a specified sensor is located

                      We could also modify this query in a way that it would return both the factory and the motor to which the sensor is attached:

                        In this DSL example we are traversing the graph from the "bottom" to the "top".

                        # Summary

                        We reached the end of the tutorial. We've designed a modeling strategy for a factory containing motors and various sensors. We hope that the article helped you to to gain a good understanding of the concepts established by the Ability Platform Information Model. During this journey we tried to show you as much as possible, but there's a lot of information that we had to skip to keep the tutorial "digestible". Feel free to look around to learn more details about the topics that interest you the most. In case of any issues with your own models don't hesitate to reach us at the ABB Ability Platform Community.

                        Author: Marcin Jahn
                        Last updated: 12/14/2021, 12:49:42 PM
                        Feedback