Docs/Timebase Components/Modeling Best Practices
AtlasConcept

Modeling Best Practices

A model that is built to last starts with a clear decision: reuse standard vocabulary before specialising it, and specialise before inventing.

Updated 9 Nov 2026Timebase Docs

Overview#

A model that is built to last starts with a clear decision: reuse standard vocabulary before specialising it, and specialise before inventing. This article covers how to make that decision repeatedly as you build your domain, how to shape your types to survive changes, and how to model measurements so they carry their time and quality with them.

The golden rule: model the thing, not its position. When you model a pump, you are not modelling "the pump in the north station". You are modelling Pump, then you create an instance of it that happens to live in the north station. That one discipline solves half the problems that undo domain models.

Three practical wins follow from building this way: your types are reusable across sites without renaming, your queries read what they appear to query, and your model shrinks because you are not duplicating types per location. This article teaches the decision tree that gets you there.


Start from standards: ISA-95 and ISA-88#

Atlas ships the two standards as a small set of libraries that build on one another. Before you invent any type, read what is already there.

The two standards and how they fit together#

ISA-95 (formally ANSI/ISA-95.00.02-2010 Part 2 and IEC 62264-2:2013) defines an equipment hierarchy by organizational and functional role: an Enterprise at the top, then Sites, then Areas, organizing down through the business structure. This is the spine of how a business organizes its operations.

ISA-88 (formally IEC 61512-1, the international equivalent) defines the physical structure of process equipment: ProcessCells, Units within them, EquipmentModules, then ControlModules. This is the vocabulary of process plants, batch facilities and their physical breakdown.

These two standards appear to conflict - they use different terms and answer different questions. Atlas ships them as libraries that build on one another, modelled on OPC 10030 (OPC Unified Architecture - Common Object Model: ISA-95), which harmonizes both specifications: isa95-equipment holds the ISA-95 equipment hierarchy, and isa88-physical adds the ISA-88 physical model on top of it, so a process cell is a kind of work center and an equipment module is a kind of equipment. Both models coexist. You do not choose one or the other; you use them together. isa95-material and isa95-personnel cover the other two ISA-95 object models, and isa88-procedural the ISA-88 procedural model, for when you need them.

The equipment hierarchy classes#

isa95-equipment declares one abstract root, Equipment, and the ISA-95 levels beneath it. Two of those levels, WorkCenter and WorkUnit, are abstract groupings; everything else is concrete. The classes inherit by kind, so a WorkCell is a WorkUnit and a WorkUnit is Equipment, and the hierarchy of "which is inside which" is expressed through containment relationships rather than through class inheritance, which is the standard's own expression of "model the thing, not its position".

Equipment (abstract)
  |- Enterprise
  |- Site
  |- Area
  |- WorkCenter (abstract)
  |    |- ProductionLine
  |    |- ProductionUnit
  |    |- StorageZone
  |    |- ProcessCell          isa88-physical
  |- WorkUnit (abstract)
  |    |- WorkCell
  |    |- Unit
  |    |- StorageUnit
  |- EquipmentModule           isa88-physical
  |- ControlModule             isa88-physical
Class Library Represents
Enterprise isa95-equipment The top-level organization
Site isa95-equipment A geographic or operational facility
Area isa95-equipment A functional subdivision within a site
WorkCenter isa95-equipment The abstract level the four kinds of work center derive from
ProductionLine isa95-equipment A discrete manufacturing line
ProductionUnit isa95-equipment A continuous production unit
StorageZone isa95-equipment A storage or inventory area
ProcessCell isa88-physical A batch process area (ISA-88)
WorkUnit isa95-equipment The abstract level the work units derive from
WorkCell isa95-equipment A cell within a production line
Unit isa95-equipment A processing unit within a process cell or production unit
StorageUnit isa95-equipment A specific storage location
EquipmentModule isa88-physical A functional grouping of control-related devices (ISA-88)
ControlModule isa88-physical A device-level module: a valve, a motor, a control loop (ISA-88)

Containment is typed. equipmentContains is the root containment relationship, and each level narrows it to the pair it joins: enterpriseContains (Enterprise to Site), siteContains (Site to Area), areaContains (Area to WorkCenter), productionLineContains (ProductionLine to WorkCell), productionUnitContains (ProductionUnit to Unit) and storageZoneContains (StorageZone to StorageUnit), with processCellContains, workUnitContains, equipmentModuleContains and controlModuleContains added by isa88-physical. equipmentContainsTag hangs an atlas-common tag under any equipment. The levels branch into different manufacturing patterns:

  • Batch processes: ProcessCell contains Unit
  • Discrete manufacturing: ProductionLine contains WorkCell
  • Continuous production: ProductionUnit contains Unit
  • Storage: StorageZone contains StorageUnit

Use the levels that describe your plant and omit the rest. You are not required to use every level. control-modules builds on isa88-physical with the usual control-module kinds (AnalogInput, PIDController, Motor, Valve and others) and packml adds the PackML packaging machine as a work cell.

The role-based Equipment class#

One class deserves special attention: Equipment, the abstract root the whole hierarchy derives from, and the one that carries the Description every level shares. It is the ISA-95 concept that models the logical role a piece of equipment fills, independent of which physical asset currently fulfils it. A "Tank-101" is a role - a storage tank in the north area. That role can be assigned to different physical tanks over time without breaking the model.

Model the role, not the asset. When you create equipment instances, you are modelling the functional positions in the plant, not the serial numbers and part numbers of the physical machinery. Tank-101 is a role. When you replace the physical tank, Tank-101 stays the same. This discipline is what makes a model survive changes to the physical plant without rewriting the ontology.

How to build on the standard classes#

Do not use the standard classes directly as your only instance types. Create your own types and inherit from them. Example:

ProductionLine (from isa95-equipment)
  |- JuiceFillingLine
  |- PasteurizationLine

Create JuiceFillingLine as a new entity type with ProductionLine as the base type. Now you can attach properties specific to a filling line (pump speed, fill volume, line pressure) without changing the standard vocabulary. Your types are specialised and reusable, and the standard names keep their meaning for every other domain that imports the library.

Because the standard classes are shallow, abstract only at the grouping levels and concrete at the leaves, they form an open set that you can extend in any direction. A FillingLine may contain FillingStation, which may contain Pump. All of these inherit from the standard vocabulary and coexist naturally.


Reuse, specialise, invent: the decision tree#

Every type you create should pass this test in order, and you should stop at the first yes.

1. Does it already exist in an imported library?#

Check the Libraries page first. Open each library you have imported and read its types. atlas-common alone gives you Folder, the Tag entity types (AnalogTag, DiscreteTag, IntegerTag and StringTag), the TVQ data type, and the folderContains, feeds, relatesTo, connectsTo, controls and sameAs relationships. Many models need nothing beyond this.

Two libraries are always present:

  • atlas-core - the framework vocabulary (imported automatically, hidden from the interface).
  • atlas-common - measurements, tags, folders and four generic relationships.

Others, including isa95-equipment and isa88-physical with the ISA-95 and ISA-88 equipment hierarchies, are available during commissioning and are copied into your repository when you ask for them. Once copied, they are yours to edit.

If what you need exists in a library you have not imported yet, add that library to your domain on the Overview screen's Library imports section, then compile again. Do not re-define it.

2. Can you specialize a library type?#

Inheritance is powerful here. Rather than inventing PumpType, TurbineType and CompressorType as siblings, inherit all three from Equipment (which comes from isa95-equipment). Attach properties specific to each one - ImpellerDiameter to Pump, BladeCount to Turbine - and you have a model that reads like its domain without duplication.

Use this pattern: library type becomes the parent, your domain types are the children, and your domain's properties attach to the children. This keeps the standard vocabulary clean and makes your model portable - another site can import Equipment and specialize it differently, and both models coexist without conflict.

3. Do you need it in multiple domains?#

If the answer is yes - if a second site would want the type you are about to create - put it in a library and import that library into both domains. This is what libraries are for. A library you create becomes infrastructure that other domains build on. That makes it worth the ceremony of creation.

If the answer is no - the type is unique to this one domain - define it in the domain itself. Do not make a library that will never be imported again.

4. Now invent#

Only now do you create a type that does not exist elsewhere. It is a small proportion of what you will author.


Layer your libraries#

Libraries stack in layers, and the discipline matters for maintenance. Atlas allows you to create libraries with dependencies, and you should build a dependency pyramid that reflects who would want what.

The five-layer model#

  1. Meta (atlas-core) - imported everywhere, hidden, never edited by you.
  2. Vocabulary (atlas-common) - measurements, tags, generic relationships. Import this into most domains.
  3. Standards (isa95-equipment, isa88-physical, control-modules, packml) - the ISA-95 and ISA-88 equipment hierarchies and what builds on them. Import when you are modelling a plant against those standards.
  4. Company library - types reusable across your sites but not industry-wide. Example: HydrocarbonFeed, CatalyticReactor. Declare atlas-common and isa95-equipment as dependencies (they bring atlas-core automatically).
  5. Domain - types unique to one site or one plant. Example: LonestarJuiceFactory with local branches like FillingLine, PasteurizationLine. Declare your company library as a dependency.

How the dependency graph actually behaves#

Three mechanics decide every library design choice you will make. All three are enforced, not advisory.

Imports are transitive. When you declare a dependency, Atlas writes it into your library's @context, and at compile time the resolver walks the full transitive closure and topologically sorts it. A domain that imports only your company library also gets everything your company library depends on, without naming any of it. This is why importing isa88-physical brings isa95-equipment and atlas-common with it.

The graph must be a DAG. Circular dependencies are a hard failure, not a warning: Circular dependency detected involving: {file}. If two libraries each need something from the other, you cannot simply cross-reference them. You must factor the shared part out into a third library that both depend on. Plan for this before you split, because it is the one refactor that a split can force.

Deletion is blocked by dependents. Removing a library that anything else references returns 409 with the error key Library.HasDependents, and the message names the library and counts the referencing libraries and domains separately, so you can see what is holding it. The more finely you split, the more edges exist, and the more of this you manage.

How big should a library be?#

The useful test is not size. It is: what changes together, and what gets adopted together?

A library is the unit of both. Everything in it versions as one file, and a consumer takes all of it or none of it. So the right contents are the types that a second site would want as a set.

That gives two failure modes, and they are not symmetric:

  • Too coarse. One big library holding everything means every consumer inherits types it does not want, and any change to any part makes the whole thing look changed to every dependent. Deletion of anything inside it is effectively impossible once several domains import it.
  • Too fine. Twenty tiny libraries means twenty dependency edges to reason about, a real chance of wanting a cycle, and a 409 every time you try to tidy one up.

Because the closure is transitive and automatic, erring slightly coarse costs less than erring fine. A consumer inheriting a few unused types pays almost nothing; the extra types simply sit in scope unused. Untangling an over-split graph, by contrast, means editing several libraries in the right order without tripping the cycle check.

Practical starting point: one library per layer, not one per concept. A company library, and a site or line library only when a second site genuinely needs to diverge.

What is easiest to share between sites#

The thing you hand another site should be one import with a self-contained closure. Because dependencies resolve transitively, a single company library that declares its own dependencies is already the easiest possible unit to share: the receiving domain names one library and gets a working set.

What makes sharing hard is not size, it is leakage of the site-specific into the shared. Once your company library contains NorthPlantFillingLine, every other site inherits a type it can never use, and the library stops being shareable in practice even though it still imports cleanly. Keep the decision honest with the layer test: if a second site would not want it, it belongs in the domain, not the library.

Extensibility: what "extend, do not fork" means here#

The shallow design of the standard classes is what makes extension cheap. Because isa95-equipment inherits by kind and never by position, you can subclass any of its classes from your own library without disturbing the others, and add as many intermediate levels as your plant needs.

Two habits use this properly:

  • Subclass across the library boundary, do not copy. Declare isa95-equipment as a dependency and make your FillingLine a subclass of ProductionLine. A copied type looks identical and shares nothing: queries written against the standard class will not match your instances.
  • Specialise relationships rather than inventing parallel ones. enterpriseContains is a specialisation of isa95-equipment:equipmentContains that restricts its endpoints. Anything that specialises equipmentContains is still matched by a query for equipmentContains, so containment keeps working generically while your specialisation documents intent.

Functions: one library, or with their types?#

The answer follows from one rule.

A function's input and output types must resolve in scope (rule O-F2). So the library hosting a function must depend on every library that defines a type that function touches. A function library's dependency footprint is the union of its functions' parameter types.

That single fact splits the decision cleanly:

  • Functions over primitives only (xsd:double, xsd:string, and the other XSD types) need no type dependencies at all. A unit conversion, a scaling factor, an arithmetic helper. These are genuinely well served by one shared function library, sitting low in the stack, that anything can import cheaply. This is the case where "one library to host them all" is the right answer.
  • Functions over compounds (anything taking or returning TVQ or your own data types) must have those types in scope, so they drag those libraries into their dependency closure. Put these with the types they operate on, not in a global bucket.

The reason the second case matters: a single function library that touches every compound in your estate must depend on every library defining them. Because the closure is transitive, every consumer of that function library then inherits all of it - a domain that wanted two conversion helpers takes your entire type estate with them. Keeping compound functions beside their types keeps each consumer's footprint proportionate to what it uses.

There is one argument in favour of concentrating functions, and it is worth knowing:

Shared parameter predicates must agree on type across everything in scope (rule O-F3). Reusing a predicate with a different type returns:

Input 'flow' reuses shared predicate 'urn:...:FlowRate' with type 'xsd:double',
but it is already declared with type 'atlas-common:TVQ'.

Keeping related functions together makes that consistency easy to see and maintain. Spreading them across libraries does not remove the constraint, because scope spans imports either way.

Two smaller function facts worth designing around: a function must have exactly one output and a non-empty body (O-F4), and zero-input functions are allowed, which makes a constant generator a legitimate way to publish a shared constant.

Build beside the system library, not inside it#

atlas-core is system-owned. Atlas compares it against the shipped version on every start and restores the shipped version if they differ, which keeps the framework vocabulary consistent across upgrades.

To add to it, create your own library that imports atlas-common and put your additions there. Your library is yours and is never rewritten. Libraries you copied in during commissioning, such as atlas-common and isa95-equipment, are yours in the same way: an edit to one survives, and a newer shipped version reaches you only when you import it again, so keeping your own additions in a library of your own is still the cleaner split.


Model the thing, not its position#

This is the highest-leverage decision you make about a type. The standard classes in isa95-equipment inherit by kind, never by position: a WorkCell is a WorkUnit, but Unit is not a child class of ProcessCell. The hierarchy - "which is inside which" - is expressed through containment relationships, which is the pattern to follow in your own model.

Type per location#

A first pass at a beverage factory often creates types like this:

PumpNorthStation
PumpSouthStation
TankFillingArea
TankPasteurizationArea

Each type is a near-duplicate that differs only in where it sits, so queries become location-specific and need rewriting when the plant is reorganised. A pump is a pump - its properties are speed, flow rate, power - wherever it sits. The location is a relationship, not an identity.

The Types screen with four location-named entity types, Pump North Station, Pump South Station, Tank Filling Area and Tank Pasteurization Area, sitting beside Filler under Work CellThe Types screen with four location-named entity types, Pump North Station, Pump South Station, Tank Filling Area and Tank Pasteurization Area, sitting beside Filler under Work Cell

The anti-pattern on the Types screen: four types that differ only in where the equipment sits.

Types describe kinds#

Create one Pump type and one Tank type. Define the properties they carry: speed, flow rate, motor temperature, etc. Then create instances - pump-north-01, pump-north-02, pump-south-01 - that are individual pumps.

The hierarchy is not in the type names; it is in the containment relationships and instance structure. A pump instance knows it lives in the north station because a relationship says (station-north)-[:productionLineContains]->(pump-north-01). Change the organization and you move the relationship, not the type. The query still works.

The Instances screen with ten Filler instances nested under Line 1 in the containment tree, Filler 1 selected, and the inspector reading Instance, FillerThe Instances screen with ten Filler instances nested under Line 1 in the containment tree, Filler 1 selected, and the inspector reading Instance, Filler

The pattern on the Instances screen: one Filler type, ten instances, and the place each one sits expressed by containment under Line 1.

This is exactly how the standard classes work. ProcessCell and Unit are not parent and child classes. They are both kinds of Equipment. An instance of ProcessCell contains instances of Unit through the processCellContains relationship. If you reorganize the plant, you rearrange containment edges, not class definitions.

Why this matters#

Three things improve:

  1. Reusability - a second site can reuse your Pump and Tank types without renaming them or creating site-specific variants. The types are infrastructure rather than site-specific.

  2. Correctness - a query written MATCH (n:Pump) WHERE n.FlowRate > 100 RETURN n behaves the same whether the pump sits north or south. You get what you write.

  3. Compactness - you have one Pump type, not six. Your ontology shrinks and stays maintainable as the plant grows.

The rule to follow: name the kind of thing, not its place in the hierarchy.


Relationships: reified, reusable, directional#

A relationship in Atlas is a first-class instance with its own archetype, which changes how you model associations that carry data.

Relationships carry properties#

Every relationship type you define becomes an instance archetype, just like an entity type. It can carry properties, and those properties read and write like any other property. So "a pump feeds a tank, and the feed carries a flow rate" is modelled on the relationship itself, with no intermediate entity type.

Define a feeds relationship type and attach a FlowRate property to it:

MATCH (p:Pump)-[r:feeds]->(t:Tank) RETURN r.FlowRate

The query is one hop, and the relationship carries the data.

Decide whether the relationship is reusable, then name it accordingly#

Ask one question: would a second pair of types want to use this relationship?

If yes, name the bare verb in camelCase: contains, feeds, drives, measures. A relationship called feeds works for pump-to-tank, compressor-to-receiver, and any other pair that makes sense. This is what the shipped libraries do, and it is why equipmentContains serves every containment from Enterprise down to ControlModule, with one narrowed specialisation per level beneath it.

If no, lead with the source, still camelCase: enterpriseContains, fillerHasFlowTotalizer. An endpoint-embedded name is the right choice for a specialisation that genuinely narrows its endpoints, and isa95-equipment uses exactly this form for enterpriseContains.

The mistake is only ever applying an endpoint-specific name to something meant to be general. Reach for the bare verb first, and specialise when you have a reason.

Whichever you pick, relationship types are camelCase and start lower case - see Naming Conventions for Atlas Models, which is canonical. Atlas suggests a name from your chosen endpoints with a leading capital, so lower-case the first letter before you save. The name is locked afterwards.

Directionality and inverse naming#

A relationship is directional. (pump)-[:feeds]->(tank) is not the same as (tank)-[:feeds]->(pump). When you define a relationship type, you declare a name for the forward direction and optionally an inverse name. Example:

  • Forward: contains - "a station contains a pump"
  • Inverse: containedIn - "a pump is contained in a station"

If the relationship reads naturally in both directions, you do not need an inverse. connectsTo is symmetric; sameAs is symmetric. Declare only a forward name.

Inheritance and endpoint constraints in relationships#

Relationship types can inherit from each other, just like entity types. Declare a general feeds and specialise it:

feeds
  |- feedsLiquid
  |- feedsSolid

A query for feeds matches feedsLiquid and feedsSolid edges automatically. Edge properties attached to the parent are inherited by the children.

You can also narrow the endpoint types when specializing. The library provides a worked example: enterpriseContains is a specialization of atlas-common:contains that restricts its source to Enterprise and its target to either Site or Area (the union allows skipping the Site level, as the standard permits). The base atlas-common:contains restricts both endpoints to atlas:NodeBase - in other words, anything - which is why it is the universal containment relationship. enterpriseContains narrows both ends.

Note: endpoint restrictions document what the model intends rather than constraining a query at run time. Where a query depends on the endpoint type, add a WHERE clause for it.

This is the cleanest way to model a family of related associations with tighter endpoint semantics than the parent relationship.


Measurements: always compound, always time-aware#

How you type a measurement decides whether its value binding reads what you expect, so this is worth getting right before you author many properties.

The TVQ compound and why it matters#

TVQ is a compound value shipped in atlas-common. It holds three fields:

  • Timestamp - when the measurement was taken
  • Value - the measured value (a number, string, or boolean)
  • Quality - the OPC UA quality code (0 is Bad, 64 is Uncertain, 192 is Good)

A historian or OPC UA server that sends a measurement always sends all three together. They are one atomic thing. When you model a property that carries a historian measurement, you must use a compound value type, not a scalar.

Bind measurements at the compound, not at a scalar#

A property bound to a historian source needs a compound value type - typically TVQ from atlas-common. A property with a scalar value type such as xsd:double reads the source's first column, which for a TVQ-shaped source is the Timestamp, so the property reads as a date rather than a measurement:

Property: Speed, range: xsd:double
Instance: Binding path: historian/Plant/131-PM-01.PV
Result: Speed reads a date rather than a measurement

Instead: declare a TVQ compound, bind the property to it, and query through the field:

Property: Speed, range: TVQ, Value pinned to xsd:double
Instance: Binding path: historian/Plant/131-PM-01.PV
Query: RETURN n.Speed.Value

Import atlas-common into your domain. It gives you TVQ already defined. Declare a property with range TVQ, pin its Value field to xsd:double for a measurement (in the Property drawer's Pin fields table), attach it to your type, and bind it to the historian. Query the .Value field. An unpinned TVQ property is Defer, and every concrete type that uses it cannot compile until it is pinned (O-B23). Where to pin it is the next decision.

Pin on the property or on the type#

A Defer property can be pinned in two places, and the right one depends on whether the concept has the same shape everywhere.

The concept is... Pin it Example
One shape on every type that uses it On the property, in the Property drawer's Pin fields table Temperature is always a double, so pin Value to xsd:double once. Every type that attaches it is resolved
A different shape depending on the type On the type, on the drawer's Pins tab Running is a boolean on Pump and a double (a duty cycle) on Tank. One Running property, pinned on each type

The property pin is the default. It keeps the decision in one place, and a new type that attaches the property is resolved without anyone remembering to pin it. Reach for a type pin when you would otherwise declare two near-identical properties, RunningState and RunningLevel, only to give them different types, or when you use a library property whose pin you cannot change. A domain can pin a library property on its own type without editing the library.

Two habits keep type pins manageable:

  • Pin on the base type when the subtypes agree. A subtype can only pin a leaf its base types left open, so a pin on Equipment is final for every type beneath it. Pin on the base only when no subtype will need another type.
  • Leave the leaf open on an abstract type. An abstract type never compiles, so it does not report O-B23. Let each concrete subtype pin what it needs.

A property pin is final: a type cannot override a leaf the property already fixed. If one type needs a different type, remove the property pin and pin on the types instead.

Quality flows from connectors#

The Quality field is populated by seven connector plugins: Timebase Historian, OPC UA, i3X, Aveva Historian, Aveva PI System, CanaryLabs Historian and InfoHub. Connectors that do not supply quality - SQL dialects, HTTP - leave it empty, which reads as 0 (Bad).

Design your application to read quality alongside the value: 192 is Good, 64 is Uncertain and 0 is Bad. A measurement whose source is offline reports quality 0 while its value stays at the column default, so quality is the field to test.

The time axis makes time queries work#

When you define a compound type, you mark one field as the time axis. For TVQ, that is Timestamp. This marking enables:

  • Time-range queries: "give me all measurements between 2pm and 3pm".
  • Point normalization: aligning measurements from different sources onto one timeline.
  • Calculated range outputs: functions that compute a value over a time window.

If a compound has no time axis declared, it is treated as atomic and cannot participate in these operations. When you define a custom compound type, always mark the time field as the axis.


Design for the queries you will actually run#

A model that supports your queries is different from a model that looks neat. Think forward.

Case sensitivity and spelling discipline#

Property names are compared case-sensitively everywhere, including in Cypher, so r.flowrate does not match flowRate and the column comes back empty.

Pick one casing convention - the shipped libraries use PascalCase for properties - and apply it consistently. Type names in a query the same way you named them in the type definition, and test a query against a live instance as you build.

Variable-length containment traversal has a limit#

You can traverse a containment hierarchy to find all descendants. Put the depth in the pattern and the type in WHERE:

MATCH (site:Site)-[:contains*1..15]->(e)
WHERE e:Equipment
RETURN e

A bare asterisk walks 10 hops, and 50 is the maximum for any depth you write, which is why the depth is stated above.

Ten hops covers a plain ISA-95 chain, where Enterprise to ControlModule is six containment steps. Real models add intermediate levels - a skid between Unit and EquipmentModule, a panel between ControlModule and Instrument, a Folder or two for grouping - and those levels count.

State the bound in the query rather than leaving it to the default:

MATCH (site:Site)-[:contains*1..15]->(e:Equipment) RETURN e

Now the depth is visible in the query.

Type every node in a traversal#

A type written on any node of a pattern filters that node, so this query returns only Tanks that a Pump feeds:

MATCH (p:Pump)-[:feeds]->(t:Tank) RETURN t

A property waiting for its first sample reads null#

A property bound to a connector that has not yet sent a sample holds no value, so its Value, Timestamp and Quality each read null.

The whole property is null until a sample arrives, so WHERE n.Temperature IS NULL identifies these. Once any field holds a value the property is a compound; test a single field with WHERE n.Temperature.Value IS NULL.


Anti-patterns: what goes wrong and how to avoid it#

Pattern to avoid Why it is tempting What to do instead
Editing atlas-core or atlas-common in place The type you need is almost there, just missing one property. Create your own library with that library as a dependency, and add the property to yours. Your library is never rewritten; atlas-core is restored to the shipped version at startup, and an edited atlas-common is yours to maintain from then on.
Binding a scalar property to a historian source You only care about the value, not the timestamp or quality. Bind a TVQ compound instead. Query the .Value field. You get the time and quality automatically for any future need.
Creating a type per location (PumpNorth, PumpSouth) It simplifies instance creation; you have one instance per type. Create one Pump type. Create instances pump-north-01, pump-south-01. Nest them under location instances with containment relationships. Your types scale to any number of locations.
Attaching a property by creating a shared parent type Two unrelated types share one attribute; creating a parent to carry it is the quickest path. Declare the property once and attach it to both types. Property attachment is many-to-many; you do not need a parent to share an attribute.
Giving a general-purpose relationship an endpoint-specific name (pumpFeedsTank for anything that feeds) It reads like a sentence and is immediately clear. Name the bare verb: feeds. Reserve source-led names such as enterpriseContains for specialisations that genuinely restrict their endpoints.
Using IS NULL to find properties with no data yet Missing data reads as null elsewhere. Test the timestamp for a year-1 value, or attach a calculated property that checks quality.
Assuming instances exist because the instance map names them Instances are created when the domain compiles. Compile the domain and open Instances to confirm what was built.
Declaring a type without a base type when you need inheritance Every type can inherit; starting from scratch feels cleaner. Check the standards libraries first. Even if your type is a root, it probably inherits from something in isa95-equipment or atlas-common. Specializing standard types is the right move.

Modeling checklist#

Before you compile a domain, run through this list.

  • [ ] Have you imported the right libraries? If you are modelling a plant, atlas-common and isa95-equipment are the defaults. If you have site-specific types you are reusing, your company library.

  • [ ] Do any of your types duplicate a library type under a different name? Search the imported libraries before creating a new type. Especially check for Equipment, ProcessCell, Unit in the standards library.

  • [ ] Would a second site want everything in your shared library? If any type in it names one site, one line or one building, it belongs in the domain instead. That single leak is what stops a library being shareable in practice.

  • [ ] Does your library graph have a cycle waiting to happen? If two of your libraries each want a type from the other, factor the shared part into a third. A cycle is reported at compile time.

  • [ ] Do your functions sit where their types resolve? Primitive-only functions (xsd types) can live in one shared low-level library. Functions taking or returning a compound need those types in scope, so keep them with the types they operate on and each consumer takes only what it needs.

  • [ ] Do your measurements use compounds? Every property bound to a historian or connector should range at TVQ or another compound, not a scalar. Spot-check one binding to verify.

  • [ ] Is every open leaf pinned on every concrete type? An open one leaves the type unable to compile (O-B23). Pin it on the property when the shape is the same everywhere, or on the type (the Pins tab) when it depends on the type, to xsd:double, xsd:string or xsd:boolean.

  • [ ] Did you declare a time axis on your compounds? If you defined a custom compound (not using TVQ), is the time field marked as the time axis? TVQ ships with this already set.

  • [ ] Is each relationship named for its reach? A reusable one gets the bare verb (feeds, drives); one that restricts its endpoints leads with the source (enterpriseContains). Both start lower case.

  • [ ] Does any type name encode a location? Search for location names (North, South, Area A, Building 2) in your type names. If you find them, rename the types and move the locations into the containment hierarchy.

  • [ ] Have you tested at least one instance query against live data? Create one instance map, bind it to a connector, compile, and open Instances. Turn on Live and verify values are flowing. Do this early, not after you have authored 50 types.

  • [ ] Are there any types with no instances that you are not planning to inherit from? Dead types clutter the model. Delete them or create an instance map.

  • [ ] Did you check the Outstanding items before compiling? Warnings do not block the compile, and they often point at something you meant to do differently. Review them.


esc
↑↓ navigate · ↵ open · ⌘K anywhere