Nektar++
 All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Properties Friends Macros Pages
cellml_metadata.py
Go to the documentation of this file.
1 
2 """Copyright (c) 2005-2016, University of Oxford.
3 All rights reserved.
4 
5 University of Oxford means the Chancellor, Masters and Scholars of the
6 University of Oxford, having an administrative office at Wellington
7 Square, Oxford OX1 2JD, UK.
8 
9 This file is part of Chaste.
10 
11 Redistribution and use in source and binary forms, with or without
12 modification, 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 
22 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
23 AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
24 IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
25 ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
26 LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
27 CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
28 GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
29 HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
30 LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
31 OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
32 """
33 
34 """
35 This module abstracts the interface to RDF metadata about CellML models.
36 
37 The RdfProcessor class below pretends to be the module itself, so all its properties
38 are available at module-level, and these should typically be called by users.
39 
40 It also provides sets METADATA_NAMES and STIMULUS_NAMES, which contain the local names
41 of terms in the ontology that can annotate variables, and the subset of those names
42 which define properties of the stimulus current (but not the current itself), respectively.
43 """
44 
45 import logging
46 import os
47 import sys
48 import types
49 from cStringIO import StringIO
50 
51 # We now only support rdflib for RDF processing
52 import rdflib
53 
54 
55 def __init__(module):
56  # Import pycml here, to avoid circular import surprises
57  import pycml
58  module.pycml = pycml
59 
60 
61 class 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
72  self._metadata_names = self._stimulus_names = None
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 
350 p = RdfProcessor(__name__)
351 
352 if __name__ == '__main__':
353  # Just load the ontology to trigger TTL -> RDF/XML conversion if needed
354  p._load_ontology()