Skip to content

Use case: The ASHRAE 223 standard (soon to be open for public review) #343

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
steveraysteveray opened this issue Apr 1, 2025 · 13 comments
Labels
Inferencing For SHACL 1.2 Inferencing spec. UCR Use Cases and Requirements

Comments

@steveraysteveray
Copy link

The normative specification of ASHRAE standard 223 (under development) is a set of SHACL files including extensive use of both validation shapes and SHACL-AF rules, on the order of 41 sh:TripleRules and 23 sh:SPARQLRules. These can be browsed in a non-official way at https://open223.info/.

Rather than repeat the rationale and use of 223, please review the above link.

The main use of the inference rules is to flesh out a "minimal" model when possible, as well as to implement two OWL implications (symmetric relations and inverse relations). The standard avoids the use of OWL predicates altogether except for graph management (i.e. owl:imports and owl:Ontology declarations).

Overall, we found SHACL rules were up to the task of handling what we needed, although we have been at this for 5 years and I might have forgotten some things we tried and gave up on.

Conformance to the standard is largely handled by SHACL shapes, although satisfying all such shapes does not guarantee conformance.

@afs afs added Inferencing For SHACL 1.2 Inferencing spec. UCR Use Cases and Requirements labels Apr 2, 2025
@afs
Copy link
Contributor

afs commented Apr 2, 2025

@steveraysteveray thanks for the use case

  • RDFS inference of the data is assumed not to happen
  • There is use of negation in constraints
  • 15 sh:SPARQLRules and 32 TripleRules.

What are the assumptions on execution?

SHACL-AF has the text:

Note that this algorithm only covers a single "iteration" over all rules, without prescribing the behavior if the same rule needs to be applied multiple times after other rules have fired. The latter is left to future work.

Is it assumed that each rule is run once only? Or do some rules potentially depend on others running?

Does the output of running the rules get added back to the data graph? Storage elsewhere? Discarded after each run?

@steveraysteveray
Copy link
Author

We definitely iterate multiple times over the rules. Using TopBraidComposer, that is easily configured. For non-TopBraid users, in the GitLab build process we wrote a script that repeatedly invokes the SHACL inference API call distributed by TopQuadrant until no new triples are asserted. Typically on the order of 7 or so iterations are made. Following that, the resulting graphs are validated. The output is added back to the data graph on each iteration and is usually discarded after the validation, but that decision is left for the application developer. However, on the https://models.open223.info/intro.html site that contains example models, you will see links to the "original" and "compiled" versions of the models. The compiled version is the result of the inferences.

I agree with your three bullet points. Only SHACL inferencing is performed. Negation is indeed used, which is one reason for the SPARQL validation rules rather than native SHACL. We adopt a closed world assumption, and have written a SPARQLConstraint to enforce it (for entities within 223 or QUDT, which is used by 223).

One other point - While writing this issue, I noticed that the open223 website is a bit behind our GitLab repository. Did you count the rules from that site? If you would like access to our GitLab, it is available upon request to jjb5@cornell.edu. where you can view all the files. I'd be happy to answer any questions you might have about the organization of the repo.

@afs
Copy link
Contributor

afs commented Apr 2, 2025

One other point - While writing this issue, I noticed that the open223 website is a bit behind our GitLab repository. Did you count the rules from that site? If you would like access to our GitLab, it is available upon request to jjb5@cornell.edu. where you can view all the files.

I followed the given link to https://data.ashrae.org/BACnet/223p/223p.ttl

If you would like access to our GitLab

No thank you. There would be licensing, copyright and IP issues that I don't want to get involved with.

@steveraysteveray
Copy link
Author

OK. We will try to bring open223 up to the current state - not sure when.

Regarding access to the GitLab, we could add you without merge request capability - basically read only - if you like, but it's your call.

@steveraysteveray
Copy link
Author

@afs, it was just pointed out to me that the link you used is to the Advisory Public Review version of the model, which was frozen 10 months ago. The latest version is given one line lower on the page:

"The most recent version of the 223P ontology is available here."

@afs
Copy link
Contributor

afs commented Apr 3, 2025

The two versions are quite different in size. The older one is larger - is that because it includes the rules output?

Typically on the order of 7 or so iterations are made.

Just to check - so some rules depend on other rules? How is the relationship between rules designed?

@steveraysteveray
Copy link
Author

The two versions are quite different in size. The older one is larger - is that because it includes the rules output?

No, a different reason entirely. The older one includes most of the QUDT ontology and vocabularies, which is much larger than the 223 standard. Since that time we agreed it does not belong as part of the standard - only by reference (import). Also, none of the rules output affect the standard - just data files that use the standard.

How is the relationship between rules designed?

Some of the rules fire as a result of triples appearing because of other rules, but there is no explicit use of sh:order or any other dependency mechanism. Each rule stands on its own. We just iterate until no new triples appear. One good example is illustrated by the following figure. Triples involving all the relations shown in the figure can be inferred from triples using the "base" relation cnx.

Image

@afs
Copy link
Contributor

afs commented Apr 9, 2025

For reference, here are the rules:

Extracted rules from https://open223.info/223p.ttl
CONSTRUCT 
  { 
    ?childCp s223:hasMedium ?medium .
  }
WHERE
  { ?this     s223:hasConnectionPoint  ?cp .
    ?childCp  s223:mapsTo           ?cp .
    ?cp       s223:connectsThrough  ?connection ;
              s223:hasMedium        ?medium
    FILTER NOT EXISTS { ?childCp  s223:hasMedium  ?something }
  }
----
CONSTRUCT 
  { 
    ?parentCp s223:hasMedium ?medium .
  }
WHERE
  { ?this  s223:hasConnectionPoint  ?cp .
    ?cp    s223:mapsTo           ?parentCp ;
           s223:connectsThrough  ?connection ;
           s223:hasMedium        ?medium
    FILTER NOT EXISTS { ?parentCp  s223:hasMedium  ?something }
  }
----
CONSTRUCT 
  { 
    ?this qudt:hasQuantityKind ?uniqueqk .
  }
WHERE
  { { SELECT  ?this (COUNT(DISTINCT ?qk) AS ?count)
      WHERE
        { FILTER NOT EXISTS { ?this  qudt:hasQuantityKind  ?something }
          ?this qudt:hasUnit/qudt:hasQuantityKind ?qk
        }
      GROUP BY ?this
    }
    FILTER ( ?count = 1 )
    ?this qudt:hasUnit/qudt:hasQuantityKind ?uniqueqk
  }
----
CONSTRUCT 
  { 
    ?this rdfs:label ?newLabel .
  }
WHERE
  { FILTER NOT EXISTS { ?this  rdfs:label  ?something }
    BIND(replace(str(?this), "^.*/([^/]*)$", "$1") AS ?localNameWithoutHash)
    BIND(replace(?localNameWithoutHash, "^.*#(.*)$", "$1") AS ?localName)
    BIND(replace(?localName, "-", " ", "i") AS ?newLabel)
  }
----
CONSTRUCT 
  { 
    ?this s223:hasObservationLocation ?something .
  }
WHERE
  { { SELECT  ?prop (COUNT(DISTINCT ?measurementLocation) AS ?count) ?this
      WHERE
        { FILTER NOT EXISTS { ?this  s223:hasObservationLocation  ?anything }
          ?this     s223:observes     ?prop .
          ?measurementLocation
                    s223:hasProperty  ?prop
        }
      GROUP BY ?prop ?this
    }
    FILTER ( ?count = 1 )
    ?something  s223:hasProperty  ?prop
      { ?something rdf:type/(rdfs:subClassOf)* s223:Connectable }
    UNION
      { ?something rdf:type/(rdfs:subClassOf)* s223:Connection }
    UNION
      { ?something rdf:type/(rdfs:subClassOf)* s223:ConnectionPoint }
  }
----
CONSTRUCT 
  { 
    ?this s223:connectedTo ?equipment .
  }
WHERE
  { ?this  s223:hasConnectionPoint  ?cp .
    ?cp    rdf:type              s223:OutletConnectionPoint .
    ?cp s223:connectsThrough/s223:connectsTo ?equipment
  }
----
CONSTRUCT 
  { 
    ?this s223:connectedFrom ?equipment .
  }
WHERE
  { ?this  s223:hasConnectionPoint  ?cp .
    ?cp    rdf:type              s223:InletConnectionPoint .
    ?cp s223:connectsThrough/s223:connectsFrom ?equipment
  }
----
CONSTRUCT 
  { 
    ?this s223:connected ?d2 .
  }
WHERE
  { ?this s223:connectedThrough/^s223:connectedThrough ?d2
    FILTER ( ?this != ?d2 )
    FILTER NOT EXISTS { ?this (s223:contains)* ?d2 }
    FILTER NOT EXISTS { ?d2 (s223:contains)* ?this }
  }
----
CONSTRUCT 
  { 
    ?this s223:connectsTo ?equipment .
  }
WHERE
  { ?this  s223:connectsAt       ?cp .
    ?cp    rdf:type              s223:InletConnectionPoint ;
           s223:isConnectionPointOf  ?equipment
  }
----
CONSTRUCT 
  { 
    ?this s223:connectsFrom ?equipment .
  }
WHERE
  { ?this  s223:connectsAt       ?cp .
    ?cp    rdf:type              s223:OutletConnectionPoint ;
           s223:isConnectionPointOf  ?equipment
  }
----
CONSTRUCT 
  { 
    ?o ?p ?this .
  }
WHERE
  { ?this  ?p        ?o .
    ?p     rdf:type  s223:SymmetricProperty
  }
----
CONSTRUCT 
  { 
    ?o ?invP ?this .
  }
WHERE
  { ?this  ?p              ?o .
    ?p     s223:inverseOf  ?invP
  }

==================

[ rdf:type      sh:TripleRule;
  rdfs:comment  "Infer the `connected` relation using `connectedTo`";
  sh:name       "InferredEquipmentToEquipmentPropertyfromconnectedTo";
  sh:object     [ sh:path  s223:connectedTo ];
  sh:predicate  s223:connected;
  sh:subject    sh:this
] .

[ rdf:type      sh:TripleRule;
  rdfs:comment  "Infer a `hasDomain` relation by checking any enclosed Zones to determine the domain.";
  sh:object     [ sh:path  ( s223:hasZone s223:hasDomain )
                ];
  sh:predicate  s223:hasDomain;
  sh:subject    sh:this
] .

[ rdf:type      sh:TripleRule;
  rdfs:comment  "Infer the `connectsAt` relation using `cnx`";
  sh:object     [ sh:path  s223:cnx ];
  sh:predicate  s223:connectsAt;
  sh:subject    sh:this
] .

[ rdf:type      sh:TripleRule;
  rdfs:comment  "Infer `cnx` relation using `connectsThrough`";
  sh:object     [ sh:path  [ sh:inversePath  s223:connectsThrough ]
                ];
  sh:predicate  s223:cnx;
  sh:subject    sh:this
] .

[ rdf:type      sh:TripleRule;
  rdfs:comment  "Infer the `connected` relation using `connectedFrom`";
  sh:name       "InferredEquipmentToEquipmentPropertyfromconnectedFrom";
  sh:object     [ sh:path  s223:connectedFrom ];
  sh:predicate  s223:connected;
  sh:subject    sh:this
] .

[ rdf:type      sh:TripleRule;
  rdfs:comment  "Infer the `hasElectricalPhase` value from any connected `Conductor`.";
  sh:object     [ sh:path  ( s223:connectsThrough s223:hasElectricalPhase )
                ];
  sh:predicate  s223:hasElectricalPhase;
  sh:subject    sh:this
] .

[ rdf:type      sh:TripleRule;
  rdfs:comment  "If an instance of s223:HeatingCoil matches the constraints defined by g36:HotWaterCoil, it will be declared as an instance of that class.";
  sh:condition  g36:HotWaterCoil;
  sh:object     g36:HotWaterCoil;
  sh:predicate  rdf:type;
  sh:subject    sh:this
] .

[ rdf:type      sh:TripleRule;
  rdfs:comment  "Infer the `connectedThrough` relation using `hasConnectionPoint` and `connectsThrough`";
  sh:name       "InferredEquipmentToConnectionProperty";
  sh:object     [ sh:path  ( s223:hasConnectionPoint s223:connectsThrough )
                ];
  sh:predicate  s223:connectedThrough;
  sh:subject    sh:this
] .

[ rdf:type      sh:TripleRule;
  rdfs:comment  "Heating coils must always have the role `Role-Heating`";
  sh:object     s223:Role-Heating;
  sh:predicate  s223:hasRole;
  sh:subject    sh:this
] .

[ rdf:type      sh:TripleRule;
  rdfs:comment  "If an instance of s223:ElectricResistanceElement matches the constraints defined by g36:ElectricHeatingCoil, it will be declared as an instance of that class.";
  sh:condition  g36:ElectricHeatingCoil;
  sh:object     g36:ElectricHeatingCoil;
  sh:predicate  rdf:type;
  sh:subject    sh:this
] .

[ rdf:type      sh:TripleRule;
  rdfs:comment  "If an instance of s223:Fan matches the constraints defined by g36:Fan, it will be declared as an instance of that class.";
  sh:condition  g36:Fan;
  sh:object     g36:Fan;
  sh:predicate  rdf:type;
  sh:subject    sh:this
] .

[ rdf:type      sh:TripleRule;
  rdfs:comment  "Cooling coils must always have the role `Role-Cooling`";
  sh:object     s223:Role-Cooling;
  sh:predicate  s223:hasRole;
  sh:subject    sh:this
] .

[ rdf:type      sh:TripleRule;
  rdfs:comment  "If an instance of s223:Zone matches the constraints defined by g36:Zone, it will be declared as an instance of that class.";
  sh:condition  g36:Zone;
  sh:object     g36:Zone;
  sh:predicate  rdf:type;
  sh:subject    sh:this
] .

[ rdf:type      sh:TripleRule;
  rdfs:comment  "Infer the `cnx` relation using `isConnectionPointOf`.";
  sh:name       "InferredEquipmentToConnectionPointCnxPropertyFromInverse";
  sh:object     [ sh:path  [ sh:inversePath  s223:isConnectionPointOf ]
                ];
  sh:predicate  s223:cnx;
  sh:subject    sh:this
] .

[ rdf:type      sh:TripleRule;
  rdfs:comment  "Infer the `cnx` relationship using `hasConnectionPoint`.";
  sh:name       "InferredEquipmentToConnectionPointCnxProperty";
  sh:object     [ sh:path  s223:hasConnectionPoint ];
  sh:predicate  s223:cnx;
  sh:subject    sh:this
] .

[ rdf:type      sh:TripleRule;
  rdfs:comment  "Infer the `hasElectricalPhase` value from any connected `ConnectionPoint`s.";
  sh:object     [ sh:path  ( s223:cnx s223:hasElectricalPhase )
                ];
  sh:predicate  s223:hasElectricalPhase;
  sh:subject    sh:this
] .

[ rdf:type      sh:TripleRule;
  rdfs:comment  "If an instance of s223:Fan matches the constraints defined by g36:FanWithVFD, it will be declared as an instance of that class.";
  sh:condition  g36:FanWithVFD;
  sh:object     g36:FanWithVFD;
  sh:predicate  rdf:type;
  sh:subject    sh:this
] .

[ rdf:type      sh:TripleRule;
  rdfs:comment  "A Chilled Beam must always have the role `Role-Cooling`";
  sh:object     s223:Role-Cooling;
  sh:predicate  s223:hasRole;
  sh:subject    sh:this
] .

[ rdf:type      sh:TripleRule;
  rdfs:comment  "Infer the `hasConnectionPoint` relation using `cnx`";
  sh:name       "InferredEquipmentToConnectionPointProperty";
  sh:object     [ sh:path  s223:cnx ];
  sh:predicate  s223:hasConnectionPoint;
  sh:subject    sh:this
] .

[ rdf:type      sh:TripleRule;
  rdfs:comment  "If an instance of s223:ZoneGroup matches the constraints defined by g36:ZoneGroup, it will be declared as an instance of that class.";
  sh:condition  g36:ZoneGroup;
  sh:object     g36:ZoneGroup;
  sh:predicate  rdf:type;
  sh:subject    sh:this
] .

[ rdf:type      sh:TripleRule;
  rdfs:comment  "Infer `cnx` relation using `connectsAt`";
  sh:object     [ sh:path  s223:connectsAt ];
  sh:predicate  s223:cnx;
  sh:subject    sh:this
] .

[ rdf:type      sh:TripleRule;
  rdfs:comment  "If an instance of s223:Damper matches the constraints defined by g36:Damper, it will be declared as an instance of that class.";
  sh:condition  g36:Damper , g36:DamperOrShape1;
  sh:object     g36:Damper;
  sh:predicate  rdf:type;
  sh:subject    sh:this
] .

[ rdf:type      sh:TripleRule;
  rdfs:comment  "Infer a `hasDomain` relation by checking any enclosing `Zone` to determine the domain.";
  sh:object     [ sh:path  ( [ sh:inversePath  s223:hasDomainSpace ]
                             s223:hasDomain
                           )
                ];
  sh:predicate  s223:hasDomain;
  sh:subject    sh:this
] .

[ rdf:type      sh:TripleRule;
  rdfs:comment  "Infer a `hasDomain` relation by checking any enclosing `ZoneGroup` to determine the domain.";
  sh:object     [ sh:path  ( [ sh:inversePath  s223:hasZone ]
                             s223:hasDomain
                           )
                ];
  sh:predicate  s223:hasDomain;
  sh:subject    sh:this
] .

[ rdf:type      sh:TripleRule;
  rdfs:comment  "If an instance of s223:Damper matches the constraints defined by g36:TwoPositionDamper, it will be declared as an instance of that class.";
  sh:condition  g36:TwoPositionDamper;
  sh:object     g36:TwoPositionDamper;
  sh:predicate  rdf:type;
  sh:subject    sh:this
] .

[ rdf:type      sh:TripleRule;
  rdfs:comment  "If an instance of s223:Valve matches the constraints defined by g36:HotWaterValve, it will be declared as an instance of that class.";
  sh:condition  g36:HotWaterValve , g36:HotWaterValveOrShape1;
  sh:object     g36:HotWaterValve;
  sh:predicate  rdf:type;
  sh:subject    sh:this
] .

[ rdf:type      sh:TripleRule;
  rdfs:comment  "Infer the `hasRole` s223:Role-HeatTransfer relation for every instance of the listed targetClass values.";
  sh:object     s223:Role-HeatTransfer;
  sh:predicate  s223:hasRole;
  sh:subject    sh:this
] .

[ rdf:type      sh:TripleRule;
  rdfs:comment  "Infer the `hasBoundaryConnectionPoint` relation using `hasOptionalConnectionPoint`.";
  sh:name       "InferredSystemToBoundaryConnectionPointFromOptional";
  sh:object     [ sh:path  s223:hasOptionalConnectionPoint ];
  sh:predicate  s223:hasBoundaryConnectionPoint;
  sh:subject    sh:this
] .

[ rdf:type      sh:TripleRule;
  rdfs:comment  "Infer a `hasDomain` relation by checking any enclosed DomainSpaces to determine the domain.";
  sh:object     [ sh:path  ( s223:hasDomainSpace s223:hasDomain )
                ];
  sh:predicate  s223:hasDomain;
  sh:subject    sh:this
] .

[ rdf:type      sh:TripleRule;
  rdfs:comment  "If an instance of s223:CoolingCoil matches the constraints defined by g36:ChilledWaterCoil, it will be declared as an instance of that class.";
  sh:condition  g36:ChilledWaterCoil;
  sh:object     g36:ChilledWaterCoil;
  sh:predicate  rdf:type;
  sh:subject    sh:this
] .

[ rdf:type      sh:TripleRule;
  rdfs:comment  "If an instance of s223:Valve matches the constraints defined by g36:ChilledWaterValve, it will be declared as an instance of that class.";
  sh:condition  g36:ChilledWaterValve , g36:ChilledWaterValveOrShape1;
  sh:object     g36:ChilledWaterValve;
  sh:predicate  rdf:type;
  sh:subject    sh:this
] .

@VladimirAlexiev
Copy link
Contributor

VladimirAlexiev commented Apr 11, 2025

I posted some analysis that I did in Dec 2003: https://github.com/VladimirAlexiev/ashrae-rules, see README.html for easier reading.
It's perhaps on an older version of ASHRAE, and reflects my lack of understanding at the time (I hadn't seen the diagram above).
@steveraysteveray does it make any sense?

@afs: thanks for the export above!

There is use of negation in constraints

AFAIR, it is mostly for setting of defaults (eg see the first two rules about "medium")
from "parent" to "child", or vice versa.
I wondered:

  • what happens if two children have different "medium"?
    Then the parent will inherit one of the two at random
  • shouldn't all connected things have the same "medium"?
    Everybody knows: you shan't connect water to electricity ;-)

Steve, can you comment?


Andy, does it make sense for me to write an UCR ifMissing?

  • perl has operators $x //= $y (set x to y if x was unbound) and $x ||= $y (set x to y if x was false)
  • shell has something like $(foo:bar): use $foo but if missing then use bar
  • this ASHRAE inheriting of "medium" is very similar

@afs
Copy link
Contributor

afs commented Apr 12, 2025

@VladimirAlexiev I think the default value requirement is quite well-covered already.

It is too early to say whether there should be exactly that feature or whether it is done by a feature with wider applicability (c.f. coalesce).

Node expressions might benefit as convenience syntax based on sh:if.

@VladimirAlexiev
Copy link
Contributor

@afs A special construct ifMissing saves the need to specify the target (value to be examined/set) twice.
And may be easier to implement than a more general construct (SPARQL not exists, coalesce or sh:if).
But we can't increase the number of special constructs ad-nauseam...
Can we define some criteria or principles on when to define new constructs?

@steveraysteveray
Copy link
Author

@VladimirAlexiev, all connected things don't need to have exactly the same Medium, but they do need to be "compatible". For example, one vendor might have specified chilled water as the medium, and another as just water. That's OK. We check 7 different cases for compatibility between ConnectionPoints, Connections, etc. There are 7 validation cases because of mixtures (see https://docs.open223.info/explanation/medium_mixtures.html). Even these tests are a little too forgiving, but we felt it is better to allow an invalid case than it is to deny a valid case.

Regarding your parent-children question, I'm assuming you are talking about inferring the medium in a mapsTo situation between ConnectionPoints. First, a mapsTo can only involve two ConnectionPoints, so there is no "multiple children" situation. If there are multiple contained ConnectionPoints, they get combined inside the containing Equipment either via a Connection or a Junction, and only one ConnectionPoint mapsTo the containing ConnectionPoint. Incompatibility between the contained ConnectionPoints is handled via the above validation tests.

@VladimirAlexiev
Copy link
Contributor

Thanks for the explanation @steveraysteveray !

From examining the rules, the only negation that doesn't match ifMissing is in rule s223:connected that infers that relation but only if one entity is not contained in the other.

There are also several rules that check for uniqueness (count=1).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Inferencing For SHACL 1.2 Inferencing spec. UCR Use Cases and Requirements
Projects
None yet
Development

No branches or pull requests

3 participants