Nektar++
cellml_metadata.py
Go to the documentation of this file.
2"""Copyright (c) 2005-2016, University of Oxford.
3All rights reserved.
4
5University of Oxford means the Chancellor, Masters and Scholars of the
6University of Oxford, having an administrative office at Wellington
7Square, Oxford OX1 2JD, UK.
8
9This file is part of Chaste.
10
11Redistribution and use in source and binary forms, with or without
12modification, are permitted provided that the following conditions are met:
13 * Redistributions of source code must retain the above copyright notice,
14 this list of conditions and the following disclaimer.
15 * Redistributions in binary form must reproduce the above copyright notice,
16 this list of conditions and the following disclaimer in the documentation
17 and/or other materials provided with the distribution.
18 * Neither the name of the University of Oxford nor the names of its
19 contributors may be used to endorse or promote products derived from this
20 software without specific prior written permission.
21
22THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
23AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
24IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
25ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
26LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
27CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
28GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
29HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
30LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
31OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
32"""
33
34"""
35This module abstracts the interface to RDF metadata about CellML models.
36
37The RdfProcessor class below pretends to be the module itself, so all its properties
38are available at module-level, and these should typically be called by users.
39
40It also provides sets METADATA_NAMES and STIMULUS_NAMES, which contain the local names
41of terms in the ontology that can annotate variables, and the subset of those names
42which define properties of the stimulus current (but not the current itself), respectively.
43"""
44
45import logging
46import os
47import sys
48import types
49from cStringIO import StringIO
50
51# We now only support rdflib for RDF processing
52import rdflib
53
54
55def __init__(module):
56 # Import pycml here, to avoid circular import surprises
57 import pycml
58 module.pycml = pycml
59
60
61class RdfProcessor(object):
62 """Implements CellML metadata functionality using the RDFLib library."""
63 def __init__(self, name):
64 """Create the wrapper."""
65 # Magic for pretending to be a module
66 self._module = sys.modules[name]
67 sys.modules[name] = self
68 self._initializing = True
69 # Map from cellml_model instances to RDF stores
70 self._models = {}
71 # Oxford metadata will be loaded lazily
73 # Cope with differences in API between library versions
74 rdflib_major_version = int(rdflib.__version__[0])
75 if rdflib_major_version >= 3:
76 self.Graph = rdflib.Graph
77 else:
78 self.Graph = rdflib.ConjunctiveGraph
79
80 def __getattribute__(self, name):
81 """Provide access to real module-level variables as though they're class properties."""
82 # call module.__init__ after import introspection is done
83 baseget = super(RdfProcessor, self).__getattribute__
84 module = baseget('_module')
85 if baseget('_initializing') and not name[:2] == '__' == name[-2:]:
86 setattr(self, '_initializing', False)
87 __init__(module)
88 try:
89 return baseget(name)
90 except AttributeError:
91 return getattr(module, name)
92
93 def _debug(*args):
94 pycml.DEBUG('cellml-metadata', *args)
95
96 def _load_ontology(self):
97 """Load the Oxford metadata ontology the first time it's needed."""
98 pycml_path = os.path.dirname(os.path.realpath(__file__))
99 oxmeta_ttl = os.path.join(pycml_path, 'oxford-metadata.ttl')
100 oxmeta_rdf = os.path.join(pycml_path, 'oxford-metadata.rdf')
101
102 g = self.Graph()
103 # We allow a difference in modification time of 10s, so we don't get confused when checking out!
104 if os.stat(oxmeta_ttl).st_mtime > os.stat(oxmeta_rdf).st_mtime + 10.0:
105 # Try to regenerate RDF/XML version of ontology
106 try:
107 g.parse(oxmeta_ttl, format='turtle')
108 except Exception, e:
109 print >> sys.stderr, 'Unable to convert metadata from Turtle format to RDF/XML.'
110 print >> sys.stderr, 'Probably you need to upgrade rdflib to version 4.\nDetails of error:'
111 raise
112 g.serialize(oxmeta_rdf, format='xml')
113 else:
114 # Just parse the RDF/XML version
115 g.parse(oxmeta_rdf, format='xml')
116
117 annotation_terms = list(g.subjects(rdflib.RDF.type, rdflib.URIRef(pycml.NSS['oxmeta']+u'Annotation')))
118 self._metadata_names = frozenset(map(lambda node: self.namespace_member(node, pycml.NSS['oxmeta']), annotation_terms))
119
120 # Parameters for the stimulus current
121 self._stimulus_names = frozenset(filter(lambda name: name.startswith('membrane_stimulus_current_'), self._metadata_names))
122
123 @property
124 def METADATA_NAMES(self):
125 """Fake a module-level constant as a property for lazy loading."""
126 if self._metadata_names is None:
127 self._load_ontology()
128 return self._metadata_names
129
130 @property
131 def STIMULUS_NAMES(self):
132 """Fake a module-level constant as a property for lazy loading."""
133 if self._stimulus_names is None:
134 self._load_ontology()
135 return self._stimulus_names
136
137 def _create_new_store(self, cellml_model):
138 """Create a new RDF store for the given CellML model.
139 The new store will be available as self._models[cellml_model].
140 """
141 self._models[cellml_model] = self.Graph()
142
143 def _add_rdf_element(self, cellml_model, rdf_text):
144 """Add statements to the model's graph from the given serialized RDF."""
145 g = self.Graph()
146 g.parse(StringIO(rdf_text))
147 rdf_model = self._models[cellml_model]
148 for stmt in g:
149 rdf_model.add(stmt)
150
151 def _serialize(self, cellml_model):
152 """Serialize the RDF model for this CellML model to XML."""
153 return self._models[cellml_model].serialize()
154
155 def get_rdf_from_model(self, cellml_model):
156 """Get the RDF graph of the given CellML model.
157
158 If this model is already in our map, return the existing RDF store.
159 Otherwise, extract metadata from all RDF elements in the cellml_model,
160 create a new RDF graph from these, and delete the original elements.
161 """
162 if not cellml_model in self._models:
163 rdf_blocks = cellml_model.xml_xpath(u'//rdf:RDF')
164 self._create_new_store(cellml_model)
165 for rdf_block in rdf_blocks:
166 rdf_text = rdf_block.xml()
167 self._add_rdf_element(cellml_model, rdf_text)
168 rdf_block.xml_parent.xml_remove_child(rdf_block)
169 return self._models[cellml_model]
170
171 def remove_model(self, cellml_model):
172 """The given model is being deleted / no longer needed."""
173 if cellml_model in self._models:
174 del self._models[cellml_model]
175 self._debug('Clearing RDF state for model', cellml_model.name)
176
177 def update_serialized_rdf(self, cellml_model):
178 """Ensure the RDF serialized into the given CellML model is up-to-date.
179
180 If we have done any metadata processing on the given model, will serialize
181 our RDF store into the rdf:RDF element child of the model.
182 """
183 if cellml_model in self._models:
184 # Paranoia: ensure it doesn't already contain serialized RDF
185 rdf_blocks = cellml_model.xml_xpath(u'//rdf:RDF')
186 if rdf_blocks:
187 pycml.LOG('cellml-metadata', logging.WARNING, 'Removing existing RDF in model.')
188 for rdf_block in rdf_blocks:
189 rdf_block.xml_parent.xml_remove_child(rdf_block)
190 # Serialize the RDF model into cellml_model.RDF
191 rdf_text = self._serialize(cellml_model)
192 rdf_doc = pycml.amara.parse(rdf_text)
193 cellml_model.xml_append(rdf_doc.RDF)
194 # Remove the RDF model
195 self.remove_model(cellml_model)
196
197 def create_rdf_node(self, node_content=None, fragment_id=None):
198 """Create an RDF node.
199
200 node_content, if given, must either be a tuple (qname, namespace_uri),
201 or a string, in which case it is interpreted as a literal RDF node.
202
203 Alternatively, fragment_id may be given to refer to a cmeta:id within the
204 current model.
205
206 If neither are given, a blank node is created.
207 """
208 if fragment_id:
209 node = rdflib.URIRef(str('#'+fragment_id))
210 elif node_content:
211 if type(node_content) == types.TupleType:
212 qname, nsuri = node_content
213 if nsuri[-1] not in ['#', '/']:
214 nsuri = nsuri + '#'
215 ns = rdflib.Namespace(nsuri)
216 prefix, local_name = pycml.SplitQName(qname)
217 node = ns[local_name]
218 elif type(node_content) in types.StringTypes:
219 node = rdflib.Literal(node_content)
220 else:
221 raise ValueError("Don't know how to make a node from " + str(node_content)
222 + " of type " + type(node_content))
223 else:
224 node = rdflib.BNode()
225 return node
226
227 def create_unique_id(self, cellml_model, base_id):
228 """Create a fragment identifier that hasn't already been used.
229
230 If base_id hasn't been used, it will be returned. Otherwise, underscores will
231 be added until a unique id is obtained.
232 """
233 while True:
234 node = self.create_rdf_node(fragment_id=base_id)
235 if not self.get_targets(cellml_model, node, None):
236 break
237 base_id += u'_'
238 return base_id
239
240 def add_statement(self, cellml_model, source, property, target):
241 """Add a statement to the model."""
242 self._debug("add_statement(", source, ",", property, ",", target, ")")
243 rdf_model = self.get_rdf_from_model(cellml_model)
244 rdf_model.add((source, property, target))
245
246 def replace_statement(self, cellml_model, source, property, target):
247 """Add a statement to the model, avoiding duplicates.
248
249 Any existing statements with the same source and property will first be removed.
250 """
251 self._debug("replace_statement(", source, ",", property, ",", target, ")")
252 rdf_model = self.get_rdf_from_model(cellml_model)
253 rdf_model.set((source, property, target))
254
255 def remove_statements(self, cellml_model, source, property, target):
256 """Remove all statements matching (source,property,target).
257
258 Any of these may be None to match anything.
259 """
260 self._debug("remove_statements(", source, ",", property, ",", target, ")")
261 rdf_model = self.get_rdf_from_model(cellml_model)
262 rdf_model.remove((source, property, target))
263
264 def get_target(self, cellml_model, source, property):
265 """Get the target of property from source.
266
267 Returns None if no such target exists. Throws if there is more than one match.
268
269 If the target is a literal node, returns its string value. Otherwise returns an RDF node.
270 """
271 rdf_model = self.get_rdf_from_model(cellml_model)
272 try:
273 target = rdf_model.value(subject=source, predicate=property, any=False)
274 except rdflib.exceptions.UniquenessError:
275 raise ValueError("Too many targets for source " + str(source) + " and property " + str(property))
276 if isinstance(target, rdflib.Literal):
277 target = str(target)
278 self._debug("get_target(", source, ",", property, ") -> ", "'" + str(target) + "'")
279 return target
280
281 def get_targets(self, cellml_model, source, property):
282 """Get a list of all targets of property from source.
283
284 If no such targets exist, returns an empty list.
285 If property is None, targets of any property will be returned.
286 Alternatively if source is None, targets of the given property from any source will be found.
287
288 For each target, if it is a literal node then its string value is given.
289 Otherwise the list will contain an RDF node.
290 """
291 rdf_model = self.get_rdf_from_model(cellml_model)
292 targets = list(rdf_model.objects(subject=source, predicate=property))
293 for i, target in enumerate(targets):
294 if isinstance(target, rdflib.Literal):
295 targets[i] = str(target)
296 return targets
297
298 def find_variables(self, cellml_model, property, value=None):
299 """Find variables in the cellml_model with the given property, and optionally value.
300
301 property (and value if given) should be a suitable input for create_rdf_node.
302
303 Will return a list of cellml_variable instances.
304 """
305 self._debug("find_variables(", property, ",", value, ")")
306 rdf_model = self.get_rdf_from_model(cellml_model)
307 property = self.create_rdf_node(property)
308 if value:
309 value = self.create_rdf_node(value)
310 vars = []
311 for result in rdf_model.subjects(property, value):
312 assert isinstance(result, rdflib.URIRef), "Non-resource annotated."
313 uri = str(result)
314 assert uri[0] == '#', "Annotation found on non-local URI"
315 var_id = uri[1:] # Strip '#'
316 var_objs = cellml_model.xml_xpath(u'*/cml:variable[@cmeta:id="%s"]' % var_id)
317 assert len(var_objs) == 1, "Didn't find a unique variable with ID " + var_id
318 vars.append(var_objs[0])
319 return vars
320
321 def get_all_rdf(self, cellml_model):
322 """Return an iterator over all RDF triples in the model."""
323 rdf_model = self.get_rdf_from_model(cellml_model)
324 for triple in rdf_model:
325 yield triple
326
327 def namespace_member(self, node, nsuri, not_uri_ok=False, wrong_ns_ok=False):
328 """Given a URI reference RDF node and namespace URI, return the local part.
329
330 Will raise an exception if node is not a URI reference unless not_uri_ok is True.
331 Will raise an exception if the node doesn't live in the given namespace, unless
332 wrong_ns_ok is True. In both cases, if the error is suppressed the empty string
333 will be returned instead.
334 """
335 local_part = ""
336 if not isinstance(node, rdflib.URIRef):
337 if not not_uri_ok:
338 raise ValueError("Cannot extract namespace member for a non-URI RDF node.")
339 if node.startswith(nsuri):
340 local_part = node[len(nsuri):]
341 elif not wrong_ns_ok:
342 raise ValueError("Node is not in correct namespace.")
343 self._debug("namespace_member(", node, ",", nsuri, ") = ", local_part)
344 return local_part
345
346####################################################################################
347# Instantiate a processor instance that pretends to be this module
348####################################################################################
349
350p = RdfProcessor(__name__)
351
352if __name__ == '__main__':
353 # Just load the ontology to trigger TTL -> RDF/XML conversion if needed
354 p._load_ontology()
def find_variables(self, cellml_model, property, value=None)
def remove_statements(self, cellml_model, source, property, target)
def _add_rdf_element(self, cellml_model, rdf_text)
def create_unique_id(self, cellml_model, base_id)
def get_targets(self, cellml_model, source, property)
def get_target(self, cellml_model, source, property)
def replace_statement(self, cellml_model, source, property, target)
def namespace_member(self, node, nsuri, not_uri_ok=False, wrong_ns_ok=False)
def add_statement(self, cellml_model, source, property, target)
def create_rdf_node(self, node_content=None, fragment_id=None)