From 5646fd10291af1180e2cb54ac7610ab8b619a6fe Mon Sep 17 00:00:00 2001 From: cmungall Date: Mon, 3 Apr 2023 17:23:37 -0700 Subject: [PATCH] Adding test for tr function in templates --- linkml_owl/dumpers/owl_dumper.py | 13 ++- tests/inputs/owl_dumper_test.yaml | 68 ++++++++++++-- tests/output/chromo.schema.owl.ttl | 108 +++++++++++------------ tests/test_compliance/test_owl_dumper.py | 33 +++++-- 4 files changed, 147 insertions(+), 75 deletions(-) diff --git a/linkml_owl/dumpers/owl_dumper.py b/linkml_owl/dumpers/owl_dumper.py index 187c2e5..f650a22 100644 --- a/linkml_owl/dumpers/owl_dumper.py +++ b/linkml_owl/dumpers/owl_dumper.py @@ -33,7 +33,7 @@ SubObjectPropertyOf, TransitiveObjectProperty, SymmetricObjectProperty, AsymmetricObjectProperty, \ ReflexiveObjectProperty, IrreflexiveObjectProperty, Annotation, ObjectMinCardinality, ObjectHasValue, \ NamedIndividual, DataSomeValuesFrom, DataHasValue, DataAllValuesFrom, AnnotationProperty, DataProperty, Datatype, \ - DisjointClasses + DisjointClasses, DisjointUnion from linkml_runtime.dumpers.dumper_root import Dumper from linkml_runtime.utils.yamlutils import YAMLRoot @@ -194,7 +194,7 @@ def transform(self, element: YAMLRoot, schema: SchemaDefinition, is_element_an_o """ Recursively transform a LinkML element - Each field is introspected, and translated to an OWL axiom. + Each field is introspected, and translated to an OWL axiom or expression. The field value is recursively transformed :param element: @@ -461,6 +461,7 @@ def transform(self, element: YAMLRoot, schema: SchemaDefinition, is_element_an_o for op_key, operands in eai.operand_list_index.items(): _, interp, operator = op_key logging.debug(f'EntityAxiomIndex {subj}: {interp} => {operator} over {operands}') + # pre-process operands if len(operands) == 0: raise ValueError(f'Too few operands: {operands} for {operator} in {subj}') if len(operands) == 1: @@ -476,12 +477,15 @@ def transform(self, element: YAMLRoot, schema: SchemaDefinition, is_element_an_o expr = ObjectIntersectionOf(*operands) else: raise ValueError(f'Cannot handle operator: {operator}') + # interpret as axiom if interp == EquivalentClasses.__name__: axiom = EquivalentClasses(subj, expr) elif interp == SubClassOf.__name__: axiom = SubClassOf(subj, expr) elif interp == DisjointClasses.__name__: axiom = DisjointClasses(subj, expr) + elif interp == DisjointUnion.__name__: + axiom = DisjointUnion(operands[0], operands[1:]) else: raise ValueError(f'Not handled: {interp}') logging.debug(f'Adding axiom: {axiom}') @@ -698,7 +702,10 @@ def tr(e: YAMLRoot): def _tr(x): fw = FunctionalWriter() expr = self.transform(x, schema) - return expr.to_functional(fw) + if isinstance(expr, URIRef): + return str(expr) + else: + return expr.to_functional(fw) d["tr"] = _tr owl_str = jt.render(**d) axioms = self.parse_axioms_string(owl_str).ontology.axioms diff --git a/tests/inputs/owl_dumper_test.yaml b/tests/inputs/owl_dumper_test.yaml index 5068eb3..8e0e1e1 100644 --- a/tests/inputs/owl_dumper_test.yaml +++ b/tests/inputs/owl_dumper_test.yaml @@ -77,6 +77,12 @@ slots: range: NamedThing multivalued: true description: named class this is subclass of + subclass_of_anon: + slot_uri: rdfs:subclass_of + range: AnonPartOf + multivalued: true + inlined: true + inlined_as_list: true operands: range: NamedThing multivalued: true @@ -197,10 +203,10 @@ classes: is_a: NamedThing description: test metaclass illustrating classes with parents that are class expressions slots: - - subclass_of + - subclass_of_anon slot_usage: - subclass_of: - range: AnonPartOf + subclass_of_anon: + required: true annotations: owl: SubClassOf DirectEquivalent: @@ -246,7 +252,7 @@ classes: operands: required: true annotations: - owl: DisjointUnionOf + owl: DisjointUnion EquivIntersection: is_a: NamedThing description: test metaclass illustrating classes defined by a simple intersection of classes @@ -323,7 +329,7 @@ classes: annotations: owl: EquivalentClasses, IntersectionOf part_of: - required: true + required: false description: the part-of differentiae annotations: owl: EquivalentClasses, IntersectionOf, ObjectSomeValuesFrom @@ -458,20 +464,22 @@ classes: range: NamedThing multivalued: false annotations: - owl: SomeValuesFrom + owl: SomeValuesFrom, ObjectProperty count: range: integer minimum_value: 1 annotations: - owl: HasValue + owl: HasValue, DataProperty state: range: ActivationStateEnum annotations: - owl: SomeValuesFrom + owl: SomeValuesFrom, ObjectProperty CollectionOfPartsWithCounts: is_a: NamedThing - description: test metaclass that illustrates a complex nested multi-part structure, where a whole is defined by a collection of repeared parts in specified states + description: >- + test metaclass that illustrates a complex nested multi-part structure, + where a whole is defined by a collection of repeated parts in specified states. slots: - has_part slot_usage: @@ -494,6 +502,48 @@ classes: ) {% endfor %} + CollectionOfPartsWithCounts2: + is_a: NamedThing + description: >- + as CollectionOfPartsWithCounts but using tr function + slots: + - has_part + slot_usage: + has_part: + range: PartWithCounts2 + inlined: true + annotations: + owl.template: |- + {% for p in has_part %} + SubClassOf( {{id}} + ObjectSomeValuesFrom( BFO:0000051 + {{ tr(p) }} + + ) + ) + {% endfor %} + + PartWithCounts2: + is_a: Anonymous + description: test metaclass that generates anonymous classes used as part of a more complex class + attributes: + unit: + range: NamedThing + multivalued: false + annotations: + owl: ObjectSomeValuesFrom, ObjectProperty + count: + range: integer + minimum_value: 1 + annotations: + owl: DataHasValue, DataProperty + state: + range: ActivationStateEnum + annotations: + owl: ObjectSomeValuesFrom, ObjectProperty + annotations: + owl: IntersectionOf + GCIPropagationMetapattern: mixin: true description: mixin for patterns that require GCI inference diff --git a/tests/output/chromo.schema.owl.ttl b/tests/output/chromo.schema.owl.ttl index 1244e15..851b649 100644 --- a/tests/output/chromo.schema.owl.ttl +++ b/tests/output/chromo.schema.owl.ttl @@ -23,7 +23,7 @@ chromoschema:GenomeBuild, chromoschema:OrganismTaxon ; dcterms:license "https://creativecommons.org/publicdomain/zero/1.0/" ; - linkml:generation_date "2023-03-21T13:21:57" ; + linkml:generation_date "2023-04-03T17:22:51" ; linkml:metamodel_version "1.7.0" ; linkml:source_file "chromo.yaml" ; linkml:source_file_date "2022-01-12T21:11:50" ; @@ -44,14 +44,14 @@ chromoschema:ChromosomePartCollection a owl:Class, linkml:ClassDefinition ; rdfs:label "ChromosomePartCollection" ; rdfs:subClassOf [ a owl:Restriction ; - owl:allValuesFrom chromoschema:OrganismTaxon ; - owl:onProperty dcterms:hasPart ], - [ a owl:Restriction ; owl:allValuesFrom chromoschema:ChromosomePart ; owl:onProperty dcterms:hasPart ], [ a owl:Restriction ; owl:allValuesFrom chromoschema:Genome ; owl:onProperty dcterms:hasPart ], + [ a owl:Restriction ; + owl:allValuesFrom chromoschema:OrganismTaxon ; + owl:onProperty dcterms:hasPart ], [ a owl:Restriction ; owl:maxQualifiedCardinality 1 ; owl:onClass linkml:String ; @@ -206,24 +206,24 @@ chromoschema:Genome a owl:Class, linkml:ClassDefinition ; rdfs:label "Genome" ; rdfs:subClassOf [ a owl:Restriction ; + owl:allValuesFrom chromoschema:GenomeBuild ; + owl:onProperty chromoschema:previous_builds ], + [ a owl:Restriction ; + owl:onClass linkml:String ; + owl:onProperty chromoschema:id ; + owl:qualifiedCardinality 1 ], + [ a owl:Restriction ; owl:maxQualifiedCardinality 1 ; owl:onClass chromoschema:GenomeBuild ; owl:onProperty biolink:genome_build ], - [ a owl:Restriction ; - owl:maxQualifiedCardinality 1 ; - owl:onClass chromoschema:TaxonIdentifier ; - owl:onProperty RO:0002162 ], - [ a owl:Restriction ; - owl:allValuesFrom chromoschema:GenomeBuild ; - owl:onProperty chromoschema:previous_builds ], [ a owl:Restriction ; owl:onClass chromoschema:LabelType ; owl:onProperty rdfs:label ; owl:qualifiedCardinality 1 ], [ a owl:Restriction ; - owl:onClass linkml:String ; - owl:onProperty chromoschema:id ; - owl:qualifiedCardinality 1 ] ; + owl:maxQualifiedCardinality 1 ; + owl:onClass chromoschema:TaxonIdentifier ; + owl:onProperty RO:0002162 ] ; skos:definition """Represents a sequenced genome, one per species. Each genome can be associated with one or more builds""" . @@ -233,7 +233,7 @@ chromoschema:OrganismTaxon a owl:Class, rdfs:subClassOf [ a owl:Restriction ; owl:maxQualifiedCardinality 1 ; owl:onClass chromoschema:LabelType ; - owl:onProperty rdfs:label ], + owl:onProperty OIO:hasExactSynonym ], [ a owl:Restriction ; owl:onClass linkml:String ; owl:onProperty chromoschema:id ; @@ -241,7 +241,7 @@ chromoschema:OrganismTaxon a owl:Class, [ a owl:Restriction ; owl:maxQualifiedCardinality 1 ; owl:onClass chromoschema:LabelType ; - owl:onProperty OIO:hasExactSynonym ] ; + owl:onProperty rdfs:label ] ; skos:definition "Represents a species, e.g. Homo sapiens" ; skos:exactMatch biolink:OrganismTaxon . @@ -323,69 +323,69 @@ chromoschema:ChromosomePart a owl:Class, linkml:ClassDefinition ; rdfs:label "ChromosomePart" ; rdfs:subClassOf [ a owl:Restriction ; - owl:onClass linkml:String ; - owl:onProperty chromoschema:id ; - owl:qualifiedCardinality 1 ], + owl:maxQualifiedCardinality 1 ; + owl:onClass chromoschema:BandDescriptor ; + owl:onProperty chromoschema:band_descriptor ], [ a owl:Restriction ; owl:maxQualifiedCardinality 1 ; - owl:onClass chromoschema:LocationType ; + owl:onClass chromoschema:ChromosomePart ; owl:onProperty BFO:0000050 ], [ a owl:Restriction ; - owl:maxQualifiedCardinality 1 ; - owl:onClass chromoschema:LabelType ; - owl:onProperty rdfs:label ], + owl:allValuesFrom linkml:String ; + owl:onProperty OIO:hasExactSynonym ], [ a owl:Restriction ; owl:maxQualifiedCardinality 1 ; owl:onClass chromoschema:ChromosomeNameType ; owl:onProperty chromoschema:chromosome_name ], [ a owl:Restriction ; owl:maxQualifiedCardinality 1 ; - owl:onClass chromoschema:BandDescriptor ; - owl:onProperty chromoschema:band_descriptor ], - [ a owl:Restriction ; - owl:allValuesFrom linkml:Uriorcurie ; - owl:onProperty skos:exactMatch ], + owl:onClass chromoschema:TaxonIdentifier ; + owl:onProperty RO:0002162 ], [ a owl:Restriction ; owl:maxQualifiedCardinality 1 ; - owl:onClass linkml:Integer ; - owl:onProperty gff:start ], - [ a owl:Restriction ; - owl:allValuesFrom linkml:String ; - owl:onProperty OIO:hasExactSynonym ], + owl:onClass chromoschema:SexChromosomeType ; + owl:onProperty chromoschema:sex_chromosome_type ], [ a owl:Restriction ; owl:maxQualifiedCardinality 1 ; owl:onClass chromoschema:EntityType ; owl:onProperty rdf:type ], + [ a owl:Restriction ; + owl:maxQualifiedCardinality 1 ; + owl:onClass linkml:Integer ; + owl:onProperty gff:end ], + [ a owl:Restriction ; + owl:maxQualifiedCardinality 1 ; + owl:onClass chromoschema:LabelType ; + owl:onProperty rdfs:label ], [ a owl:Restriction ; owl:maxQualifiedCardinality 1 ; owl:onClass chromoschema:GenomeBuild ; owl:onProperty biolink:genome_build ], [ a owl:Restriction ; - owl:allValuesFrom linkml:String ; - owl:onProperty OIO:hasBroadSynonym ], + owl:allValuesFrom chromoschema:ChromosomePart ; + owl:onProperty BFO:0000051 ], [ a owl:Restriction ; owl:maxQualifiedCardinality 1 ; - owl:onClass chromoschema:ChromosomePart ; - owl:onProperty BFO:0000050 ], + owl:onClass chromoschema:AutosomeVsSexChromosome ; + owl:onProperty chromoschema:somal_type ], [ a owl:Restriction ; - owl:maxQualifiedCardinality 1 ; - owl:onClass chromoschema:SexChromosomeType ; - owl:onProperty chromoschema:sex_chromosome_type ], + owl:allValuesFrom linkml:String ; + owl:onProperty OIO:hasBroadSynonym ], [ a owl:Restriction ; owl:maxQualifiedCardinality 1 ; - owl:onClass chromoschema:TaxonIdentifier ; - owl:onProperty RO:0002162 ], - [ a owl:Restriction ; - owl:allValuesFrom chromoschema:ChromosomePart ; - owl:onProperty BFO:0000051 ], + owl:onClass chromoschema:LocationType ; + owl:onProperty BFO:0000050 ], [ a owl:Restriction ; owl:maxQualifiedCardinality 1 ; owl:onClass linkml:Integer ; - owl:onProperty gff:end ], + owl:onProperty gff:start ], [ a owl:Restriction ; - owl:maxQualifiedCardinality 1 ; - owl:onClass chromoschema:AutosomeVsSexChromosome ; - owl:onProperty chromoschema:somal_type ] ; + owl:onClass linkml:String ; + owl:onProperty chromoschema:id ; + owl:qualifiedCardinality 1 ], + [ a owl:Restriction ; + owl:allValuesFrom linkml:Uriorcurie ; + owl:onProperty skos:exactMatch ] ; skos:definition "A Chromosome or a part of a chromosome (includes whole chromosomes, arms, and bands)" ; skos:note """OWL Notes: when translated to OWL, instances of this class will be treated as OWL classes, with the superclass determined by the type field""", @@ -401,13 +401,13 @@ chromoschema:GenomeBuild a owl:Class, linkml:ClassDefinition ; rdfs:label "GenomeBuild" ; rdfs:subClassOf [ a owl:Restriction ; - owl:onClass linkml:String ; - owl:onProperty chromoschema:id ; - owl:qualifiedCardinality 1 ], - [ a owl:Restriction ; owl:maxQualifiedCardinality 1 ; owl:onClass chromoschema:LabelType ; - owl:onProperty rdfs:label ] ; + owl:onProperty rdfs:label ], + [ a owl:Restriction ; + owl:onClass linkml:String ; + owl:onProperty chromoschema:id ; + owl:qualifiedCardinality 1 ] ; skos:closeMatch edam:operation_0525 ; skos:definition "Represents a specific build of a sequenced genome" . diff --git a/tests/test_compliance/test_owl_dumper.py b/tests/test_compliance/test_owl_dumper.py index c76ea2d..b1429c4 100644 --- a/tests/test_compliance/test_owl_dumper.py +++ b/tests/test_compliance/test_owl_dumper.py @@ -20,7 +20,7 @@ from rdflib.namespace import Namespace, SKOS, DCTERMS from linkml_owl.dumpers.owl_dumper import OWLDumper from funowl import Axiom, AnnotationAssertion, Literal, SubClassOf, ObjectSomeValuesFrom, \ - ObjectAllValuesFrom, ObjectUnionOf, EquivalentClasses, ObjectIntersectionOf, Annotation, DataHasValue + ObjectAllValuesFrom, ObjectUnionOf, EquivalentClasses, ObjectIntersectionOf, Annotation, DataHasValue, DisjointUnion from linkml_owl.util.trim_yaml import trim_yaml from tests import INPUT_DIR, OUTPUT_DIR @@ -93,7 +93,7 @@ def test_owl_dumper(self): sv = SchemaView(SCHEMA_IN) schema = sv.schema py_str = PythonGenerator(SCHEMA_IN).serialize() - #print(py_str) + # print(py_str) py_mod = compile_python(py_str) md = "# linkml-owl Test Cases\n\n" @@ -172,9 +172,14 @@ def add_check(*args, **kwargs): [AnnotationAssertion(RDFS.label, X.a, Literal("foo")), SubClassOf(X.a, ObjectSomeValuesFrom(BFO['0000050'], X.b))], """Demonstrates a mix of slots, some annotation, some logical""") - #add_check("SubClassOf SomeValuesFrom, nested", - # [py_mod.ChildOfAnon('x:a', subclass_of=py_mod.AnonPartOf(part_of='x:b'))], + #anon_part_of = py_mod.AnonPartOf(part_of='x:b') + #o = py_mod.ChildOfAnon('x:a', subclass_of_anon=[anon_part_of]) + #add_check("SubClassOf SomeValuesFrom nested", + # [o], # [SubClassOf(X.a, ObjectSomeValuesFrom(BFO['0000050'], X.b))], + # "") + # #[SubClassOf(X.a, ObjectSomeValuesFrom(BFO['0000050'], X.b))], + # [], # "create a subClassOf-partOf-some using a nested structure") # NOTE: assumes order-preserving add_check("SubClassOf Union", @@ -271,6 +276,16 @@ def add_check(*args, **kwargs): [], """Demonstrates nesting """) + p1 = py_mod.PartWithCounts2(unit='x:p1', count=2, state='ACTIVATED') + p2 = py_mod.PartWithCounts2(unit='x:p2', count=3, state='ACTIVATED') + add_check("Parts collection with counts v2", + [py_mod.CollectionOfPartsWithCounts2('x:collection', + has_part=[p1, + p2, + ])], + [], + """Demonstrates tr function + """) add_check("Parts collection", [py_mod.CollectionOfParts('x:collection', has_part=['x:p1', 'x:p2'])], [], @@ -282,7 +297,7 @@ def add_check(*args, **kwargs): """Things that are defined exhaustively by an arbitrary list of parts """) for check in checks: - print(f'** CHECK: {check.title}') + #print(f'** CHECK: {check.title}') md += f'## {check.title}\n\n' md += f'\n__Description__: _{check.description}_\n\n' md += f'\n__Schema__:\n\n```yaml\n{check.schema_section}\n```\n\n' @@ -297,7 +312,7 @@ def add_check(*args, **kwargs): container = py_mod.Container(entities=check.records) dumper.object_index = ObjectIndex(container, schemaview=sv) dumper.autofill = True - print(f"RECORDS = {check.records}") + # print(f"RECORDS = {check.records}") doc = dumper.to_ontology_document(check.records, schema) md += '\n__Generated axioms__:\n\n' md += f'```\n{str(doc)}\n```\n\n' @@ -307,15 +322,15 @@ def add_check(*args, **kwargs): ontology_str_trimmed = str(doc).replace('\n', '') for axiom in check.axioms: - print(f'TESTING FOR: {axiom}') + # print(f'TESTING FOR: {axiom}') if not isinstance(axiom, str): if axiom not in doc.ontology.axioms: logging.error(f'COULD NOT FIND: {axiom}') for a in doc.ontology.axioms: logging.error(f' HAS: {a}') - assert axiom in doc.ontology.axioms + self.assertIn(axiom, doc.ontology.axioms) else: - print(f' LOOKING IN: {ontology_str_trimmed}') + # print(f' LOOKING IN: {ontology_str_trimmed}') assert axiom.replace(' ', '') in ontology_str_trimmed.replace(' ', '') with open(MD_OUT, 'w') as stream: stream.write(md)