Skip to content

Commit 9b129ff

Browse files
Merge pull request #349 from NeuroML/feat/annotations-miriam
feat(annotations): extend to allow both miriam and biosimulations styles
2 parents c5b102b + c5d2a0c commit 9b129ff

File tree

2 files changed

+113
-113
lines changed

2 files changed

+113
-113
lines changed

pyneuroml/annotations.py

Lines changed: 89 additions & 112 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
logger.setLevel(logging.INFO)
1919

2020
try:
21-
from rdflib import BNode, Graph, Literal, Namespace, URIRef
21+
from rdflib import BNode, Graph, Literal, Namespace, URIRef, Bag
2222
from rdflib.namespace import DC, DCTERMS, FOAF, RDFS
2323
except ImportError:
2424
logger.warning("Please install optional dependencies to use annotation features:")
@@ -89,6 +89,7 @@ def create_annotation(
8989
subject,
9090
title=None,
9191
abstract=None,
92+
annotation_style: typing.Literal["miriam", "biosimulations"] = "biosimulations",
9293
description: typing.Optional[str] = None,
9394
keywords: typing.Optional[typing.List[str]] = None,
9495
thumbnails: typing.Optional[typing.List[str]] = None,
@@ -101,7 +102,7 @@ def create_annotation(
101102
has_property: typing.Optional[typing.Dict[str, str]] = None,
102103
is_property_of: typing.Optional[typing.Dict[str, str]] = None,
103104
sources: typing.Optional[typing.Dict[str, str]] = None,
104-
isinstance_of: typing.Optional[typing.Dict[str, str]] = None,
105+
is_instance_of: typing.Optional[typing.Dict[str, str]] = None,
105106
has_instance: typing.Optional[typing.Dict[str, str]] = None,
106107
predecessors: typing.Optional[typing.Dict[str, str]] = None,
107108
successors: typing.Optional[typing.Dict[str, str]] = None,
@@ -131,8 +132,10 @@ def create_annotation(
131132
132133
For information on the specifications, see:
133134
134-
- https://github.com/combine-org/combine-specifications/blob/main/specifications/qualifiers-1.1.md
135-
- https://docs.biosimulations.org/concepts/conventions/simulation-project-metadata/
135+
- COMBINE specifications: https://github.com/combine-org/combine-specifications/blob/main/specifications/qualifiers-1.1.md
136+
- Biosimulations guidelines: https://docs.biosimulations.org/concepts/conventions/simulation-project-metadata/
137+
- MIRIAM guidelines: https://drive.google.com/file/d/1JqjcH0T0UTWMuBj-scIMwsyt2z38A0vp/view
138+
136139
137140
Note that:
138141
@@ -157,6 +160,14 @@ def create_annotation(
157160
:type title: str
158161
:param abstract: an abstract
159162
:type abstract: str
163+
:param annotation_style: type of annotation: either "miriam" or
164+
"biosimulations" (default).
165+
166+
There's a difference in the annotation "style" suggested by MIRIAM and
167+
Biosimulations. MIRIAM suggests the use of RDF containers (bags)
168+
wherewas Biosimulations does not. This argument allows the user to
169+
select what style they want to use for the annotation.
170+
:type annotation_style: str
160171
:param description: a longer description
161172
:type description: str
162173
:param keywords: keywords
@@ -181,8 +192,8 @@ def create_annotation(
181192
:type is_property_of: dict(str, str)
182193
:param sources: links to sources (on GitHub and so on)
183194
:type sources: dict(str, str)
184-
:param isinstance_of: is an instance of
185-
:type isinstance_of: dict(str, str)
195+
:param is_instance_of: is an instance of
196+
:type is_instance_of: dict(str, str)
186197
:param has_instance: has instance of another entity
187198
:type has_instance: dict(str, str)
188199
:param predecessors: predecessors of this entity
@@ -253,146 +264,78 @@ def create_annotation(
253264
doc.add((subjectobj, COLLEX.thumbnail, URIRef(f"{fileprefix}/{t}")))
254265
if organisms:
255266
doc.bind("bqbiol:hasTaxon", BQBIOL + "/hasTaxon")
256-
for idf, label in organisms.items():
257-
hasTaxon = BNode()
258-
doc.add((subjectobj, BQBIOL.hasTaxon, hasTaxon))
259-
doc.add((hasTaxon, DC.identifier, URIRef(idf)))
260-
doc.add((hasTaxon, RDFS.label, Literal(label)))
267+
_add_element(doc, subjectobj, organisms, BQBIOL.hasTaxon, annotation_style)
261268
if encodes_other_biology:
262269
doc.bind("bqbiol:encodes", BQBIOL + "/encodes")
263-
for idf, label in encodes_other_biology.items():
264-
encodes = BNode()
265-
doc.add((subjectobj, BQBIOL.encodes, encodes))
266-
doc.add((encodes, DC.identifier, URIRef(idf)))
267-
doc.add((encodes, RDFS.label, Literal(label)))
270+
_add_element(
271+
doc, subjectobj, encodes_other_biology, BQBIOL.encodes, annotation_style
272+
)
268273
if has_version:
269274
doc.bind("bqbiol:hasVersion", BQBIOL + "/hasVersion")
270-
for idf, label in has_version.items():
271-
hasVersion = BNode()
272-
doc.add((subjectobj, BQBIOL.hasVersion, hasVersion))
273-
doc.add((hasVersion, DC.identifier, URIRef(idf)))
274-
doc.add((hasVersion, RDFS.label, Literal(label)))
275+
_add_element(doc, subjectobj, has_version, BQBIOL.hasVersion, annotation_style)
275276
if is_version_of:
276277
doc.bind("bqbiol:isVersionOf", BQBIOL + "/isVersionOf")
277-
for idf, label in is_version_of.items():
278-
isVersionOf = BNode()
279-
doc.add((subjectobj, BQBIOL.isVersionOf, isVersionOf))
280-
doc.add((isVersionOf, DC.identifier, URIRef(idf)))
281-
doc.add((isVersionOf, RDFS.label, Literal(label)))
278+
_add_element(
279+
doc, subjectobj, is_version_of, BQBIOL.isVersionOf, annotation_style
280+
)
282281
if has_part:
283282
doc.bind("bqbiol:hasPart", BQBIOL + "/hasPart")
284-
for idf, label in has_part.items():
285-
hasPart = BNode()
286-
doc.add((subjectobj, BQBIOL.hasPart, hasPart))
287-
doc.add((hasPart, DC.identifier, URIRef(idf)))
288-
doc.add((hasPart, RDFS.label, Literal(label)))
283+
_add_element(doc, subjectobj, has_part, BQBIOL.hasPart, annotation_style)
289284
if is_part_of:
290285
doc.bind("bqbiol:isPartOf", BQBIOL + "/isPartOf")
291-
for idf, label in is_part_of.items():
292-
isPartOf = BNode()
293-
doc.add((subjectobj, BQBIOL.isPartOf, isPartOf))
294-
doc.add((isPartOf, DC.identifier, URIRef(idf)))
295-
doc.add((isPartOf, RDFS.label, Literal(label)))
286+
_add_element(doc, subjectobj, is_part_of, BQBIOL.isPartOf, annotation_style)
296287
if has_property:
297288
doc.bind("bqbiol:hasProperty", BQBIOL + "/hasProperty")
298-
for idf, label in has_property.items():
299-
hasProperty = BNode()
300-
doc.add((subjectobj, BQBIOL.hasProperty, hasProperty))
301-
doc.add((hasProperty, DC.identifier, URIRef(idf)))
302-
doc.add((hasProperty, RDFS.label, Literal(label)))
289+
_add_element(
290+
doc, subjectobj, has_property, BQBIOL.hasProperty, annotation_style
291+
)
303292
if is_property_of:
304293
doc.bind("bqbiol:isPropertyOf", BQBIOL + "/isPropertyOf")
305-
for idf, label in is_property_of.items():
306-
isPropertyOf = BNode()
307-
doc.add((subjectobj, BQBIOL.isPropertyOf, isPropertyOf))
308-
doc.add((isPropertyOf, DC.identifier, URIRef(idf)))
309-
doc.add((isPropertyOf, RDFS.label, Literal(label)))
294+
_add_element(
295+
doc, subjectobj, is_property_of, BQBIOL.isPropertyOf, annotation_style
296+
)
310297
if sources:
311-
for idf, label in sources.items():
312-
s = BNode()
313-
doc.add((subjectobj, DC.source, s))
314-
doc.add((s, DC.identifier, URIRef(idf)))
315-
doc.add((s, RDFS.label, Literal(label)))
316-
if isinstance_of:
298+
_add_element(doc, subjectobj, sources, DC.source, annotation_style)
299+
if is_instance_of:
317300
doc.bind("bqmodel:isInstanceOf", BQMODEL + "/isInstanceOf")
318-
for idf, label in isinstance_of.items():
319-
isInstanceOf = BNode()
320-
doc.add((subjectobj, BQMODEL.isInstanceOf, isInstanceOf))
321-
doc.add((isInstanceOf, DC.identifier, URIRef(idf)))
322-
doc.add((isInstanceOf, RDFS.label, Literal(label)))
301+
_add_element(
302+
doc, subjectobj, is_instance_of, BQMODEL.isInstanceOf, annotation_style
303+
)
323304
if has_instance:
324305
doc.bind("bqmodel:hasInstance", BQMODEL + "/hasInstance")
325-
for idf, label in has_instance.items():
326-
hasInstance = BNode()
327-
doc.add((subjectobj, BQMODEL.hasInstance, hasInstance))
328-
doc.add((hasInstance, DC.identifier, URIRef(idf)))
329-
doc.add((hasInstance, RDFS.label, Literal(label)))
306+
_add_element(
307+
doc, subjectobj, has_instance, BQMODEL.hasInstance, annotation_style
308+
)
330309
if predecessors:
331310
doc.bind("bqmodel:isDerivedFrom", BQMODEL + "/isDerivedFrom")
332-
for idf, label in predecessors.items():
333-
isDerivedFrom = BNode()
334-
doc.add((subjectobj, BQMODEL.isDerivedFrom, isDerivedFrom))
335-
doc.add((isDerivedFrom, DC.identifier, URIRef(idf)))
336-
doc.add((isDerivedFrom, RDFS.label, Literal(label)))
311+
_add_element(
312+
doc, subjectobj, predecessors, BQMODEL.isDerivedFrom, annotation_style
313+
)
337314
if successors:
338315
doc.bind("scoro:successor", SCORO + "/successor")
339-
for idf, label in successors.items():
340-
succ = BNode()
341-
doc.add((subjectobj, SCORO.successor, succ))
342-
doc.add((succ, DC.identifier, URIRef(idf)))
343-
doc.add((succ, RDFS.label, Literal(label)))
316+
_add_element(doc, subjectobj, successors, SCORO.successor, annotation_style)
344317
if see_also:
345-
for idf, label in see_also.items():
346-
sa = BNode()
347-
doc.add((subjectobj, RDFS.seeAlso, sa))
348-
doc.add((sa, DC.identifier, URIRef(idf)))
349-
doc.add((sa, RDFS.label, Literal(label)))
318+
_add_element(doc, subjectobj, see_also, RDFS.seeAlso, annotation_style)
350319
if references:
351-
for idf, label in references.items():
352-
r = BNode()
353-
doc.add((subjectobj, DCTERMS.references, sa))
354-
doc.add((r, DC.identifier, URIRef(idf)))
355-
doc.add((r, RDFS.label, Literal(label)))
320+
_add_element(doc, subjectobj, references, DCTERMS.references, annotation_style)
356321
if other_ids:
357322
doc.bind("bqmodel:is", BQMODEL + "/is")
358-
for idf, label in other_ids.items():
359-
oi = BNode()
360-
doc.add((subjectobj, BQMODEL.IS, oi))
361-
doc.add((oi, DC.identifier, URIRef(idf)))
362-
doc.add((oi, RDFS.label, Literal(label)))
323+
_add_element(doc, subjectobj, other_ids, BQMODEL.IS, annotation_style)
363324
if citations:
364325
doc.bind("bqmodel:isDescribedBy", BQMODEL + "/isDescribedBy")
365-
for idf, label in citations.items():
366-
cit = BNode()
367-
doc.add((subjectobj, BQMODEL.isDescribedBy, cit))
368-
doc.add((cit, DC.identifier, URIRef(idf)))
369-
doc.add((cit, RDFS.label, Literal(label)))
326+
_add_element(
327+
doc, subjectobj, citations, BQMODEL.isDescribedBy, annotation_style
328+
)
370329
if authors:
371-
for idf, label in authors.items():
372-
ac = BNode()
373-
doc.add((subjectobj, DC.creator, ac))
374-
doc.add((ac, FOAF.name, Literal(idf)))
375-
doc.add((ac, FOAF.label, Literal(label)))
330+
_add_element(doc, subjectobj, authors, DC.creator, annotation_style)
376331
if contributors:
377-
for idf, label in contributors.items():
378-
ac = BNode()
379-
doc.add((subjectobj, DC.contributor, ac))
380-
doc.add((ac, FOAF.name, Literal(idf)))
381-
doc.add((ac, FOAF.label, Literal(label)))
332+
_add_element(doc, subjectobj, contributors, DC.contributor, annotation_style)
382333
if license:
383334
assert len(license.items()) == 1
384-
for idf, label in license.items():
385-
ac = BNode()
386-
doc.add((subjectobj, DCTERMS.license, ac))
387-
doc.add((ac, FOAF.name, Literal(idf)))
388-
doc.add((ac, FOAF.label, Literal(label)))
335+
_add_element(doc, subjectobj, license, DCTERMS.license, annotation_style)
389336
if funders:
390337
doc.bind("scoro:funder", SCORO + "/funder")
391-
for idf, label in funders.items():
392-
ac = BNode()
393-
doc.add((subjectobj, SCORO.funder, ac))
394-
doc.add((ac, FOAF.name, Literal(idf)))
395-
doc.add((ac, FOAF.label, Literal(label)))
338+
_add_element(doc, subjectobj, funders, SCORO.funder, annotation_style)
396339
if creation_date:
397340
ac = BNode()
398341
doc.add((subjectobj, DCTERMS.created, ac))
@@ -410,3 +353,37 @@ def create_annotation(
410353
print(annotation, file=f)
411354

412355
return annotation
356+
357+
358+
def _add_element(
359+
doc: Graph,
360+
subjectobj: typing.Union[URIRef, Literal],
361+
info: typing.Dict[str, str],
362+
node_type: URIRef,
363+
annotation_style: str,
364+
):
365+
"""Add an new element to the RDF annotation
366+
367+
:param doc: main rdf document object
368+
:type doc: RDF.Graph
369+
:param subjectobj: main object being referred to
370+
:type subjectobj: URIRef or Literal
371+
:param info: dictionary of entries and their labels
372+
:type info: dict
373+
:param node_type: node type
374+
:type node_type: URIRef
375+
:param annotation_style: type of annotation
376+
:type annotation_style: str
377+
"""
378+
for idf, label in info.items():
379+
top_node = BNode()
380+
doc.add((subjectobj, node_type, top_node))
381+
if annotation_style == "biosimulations":
382+
doc.add((top_node, DC.identifier, URIRef(idf)))
383+
doc.add((top_node, RDFS.label, Literal(label)))
384+
elif annotation_style == "miriam":
385+
Bag(doc, top_node, [URIRef(idf)])
386+
else:
387+
raise ValueError(
388+
"Annotation style must either be 'miriam' or 'biosimulations'"
389+
)

tests/test_annotations.py

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
Copyright 2024 NeuroML contributors
88
"""
99

10-
1110
import logging
1211

1312
from pyneuroml.annotations import create_annotation
@@ -27,6 +26,30 @@ def test_create_annotation(self):
2726
"model.nml",
2827
"A tests model",
2928
abstract="lol, something nice",
29+
annotation_style="miriam",
30+
keywords=["something", "and something"],
31+
thumbnails=["lol.png"],
32+
organisms={
33+
"http://identifiers.org/taxonomy/4896": "Schizosaccharomyces pombe"
34+
},
35+
encodes_other_biology={
36+
"http://identifiers.org/GO:0009653": "anatomical structure morphogenesis",
37+
"http://identifiers.org/kegg:ko04111": "Cell cycle - yeast",
38+
},
39+
sources={"https://github.com/lala": "GitHub"},
40+
predecessors={"http://omex-library.org/BioSim0001.omex/model.xml": "model"},
41+
creation_date="2024-04-18",
42+
modified_dates=["2024-04-18", "2024-04-19"],
43+
)
44+
self.assertIsNotNone(annotation)
45+
print(annotation)
46+
47+
# biosimulations
48+
annotation = create_annotation(
49+
"model.nml",
50+
"A tests model",
51+
abstract="lol, something nice",
52+
annotation_style="biosimulations",
3053
keywords=["something", "and something"],
3154
thumbnails=["lol.png"],
3255
organisms={

0 commit comments

Comments
 (0)