Nektar++
 All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Properties Friends Macros Pages
processors.py
Go to the documentation of this file.
1 """Copyright (c) 2005-2016, University of Oxford.
2 All rights reserved.
3 
4 University of Oxford means the Chancellor, Masters and Scholars of the
5 University of Oxford, having an administrative office at Wellington
6 Square, Oxford OX1 2JD, UK.
7 
8 This file is part of Chaste.
9 
10 Redistribution and use in source and binary forms, with or without
11 modification, are permitted provided that the following conditions are met:
12  * Redistributions of source code must retain the above copyright notice,
13  this list of conditions and the following disclaimer.
14  * Redistributions in binary form must reproduce the above copyright notice,
15  this list of conditions and the following disclaimer in the documentation
16  and/or other materials provided with the distribution.
17  * Neither the name of the University of Oxford nor the names of its
18  contributors may be used to endorse or promote products derived from this
19  software without specific prior written permission.
20 
21 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
22 AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
23 IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
24 ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
25 LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
26 CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
27 GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
28 HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
29 LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
30 OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
31 """
32 
33 """
34 This file contains various classes supporting modifications to CellML models.
35 """
36 
37 import pycml
38 from pycml import *
39 import validator
40 
41 
42 class ModelModificationError(ValueError):
43  """Error thrown if a model modification is invalid."""
44  pass
45 
46 
47 class ModelModifier(object):
48  """Base class supporting common model modification functionality.
49 
50  This class contains the logic to deal with adding/deleting variables, components, equations, etc.
51  and connecting things up. It also handles re-analysing the model when modifications have been
52  completed to ensure that PyCml's internal data structures are up-to-date.
53 
54  Instances should be created with the model to modify as a single parameter. Once all
55  modifications have been completed, the finalize method must be called to ensure later
56  processing of the model (e.g. code generation) will succeed.
57  """
58  def __init__(self, model):
59  """Constructor."""
60  self.model = model
61  self._units_converter = None
62  self.connections_made = set()
63 
64  def finalize(self, error_handler, pre_units_check_hook=None, check_units=True):
65  """Re-do the model validation steps needed for further processing of the model.
66 
67  Checks connections, etc. and builds up the dependency graph again, then performs
68  a topological sort.
69 
70  If any errors are found during re-validation, the error_handler will be called with the
71  list. Warnings are ignored.
72 
73  TODO: figure out how to determine how much of this is actually needed - InterfaceGenerator
74  can probably get away with less work.
75  """
76  self._clear_model_caches()
77  # We want to see any errors
78  logging_info = validator.CellMLValidator.setup_logging(show_errors=True, show_warnings=False)
79  # Re-run validation & analysis
80  self.model._check_variable_mappings()
81  if not self.model._cml_validation_errors:
82  assignment_exprs = self.model.search_for_assignments()
83  self.model._check_assigned_vars(assignment_exprs)
84  if not self.model._cml_validation_errors:
85  self.model._classify_variables(assignment_exprs)
86  self.model._order_variables(assignment_exprs)
87  if not self.model._cml_validation_errors and check_units:
88  if callable(pre_units_check_hook):
89  pre_units_check_hook()
90  self.model._check_connection_units(check_for_units_conversions=False)
91  self.model._check_dimensional_consistency(assignment_exprs,
92  xml_context=False,
93  warn_on_units_errors=self.model.get_option('warn_on_units_errors'),
94  check_for_units_conversions=False)
95  if self.model._cml_validation_errors:
96  error_handler(self.model._cml_validation_errors)
97  # Clear up logging
98  validator.CellMLValidator.cleanup_logging(logging_info)
99 
101  """
102  Clear cached links in the model, since we'll need to recompute many of them
103  once we've finished modifying it. Also clears dependency information.
104  """
105  for comp in getattr(self.model, u'component', []):
106  for math in getattr(comp, u'math', []):
107  math._unset_cached_links()
108  for var in self.model.get_all_variables():
109  var.clear_dependency_info()
110  assignment_exprs = self.model.search_for_assignments()
111  for expr in assignment_exprs:
112  expr.clear_dependency_info()
113 
114  def create_new_component(self, cname):
115  """Create a new component in the model, ensuring the name is unique.
116 
117  If a component with name cname already exists,
118  underscores will be added to the component name to make it unique.
119  """
120  while True:
121  try:
122  self.model.get_component_by_name(cname)
123  cname += u'_'
124  except KeyError:
125  # Component with this name doesn't exist
126  break
127  # Create the component
128  comp = cellml_component.create_new(self.model, cname)
129  self.model._add_component(comp)
130  return comp
131 
132  def connect_variables(self, source, target):
133  """Create a connection between the given source and target variables.
134 
135  The variables are both specified either by a pair (cname,vname), or as cellml_variable objects.
136  The source variable must exist within the model, whereas the target might not, in
137  which case it will be created.
138 
139  Note that in the case that both source and target exist, it might NOT be the case that
140  target obtains its value from source. They might already be connected, and source obtains
141  its value from target. Or they might both obtain their value from a common source.
142 
143  If the variable names are not identical, any variables created will have the same name as the
144  target, if possible. If there's an existing variable with that name, not connected to the
145  source, then underscores will be appended to the name to avoid conflicts. Note that we do
146  check for variables in intermediate components that have the same name as the source and are
147  connected to it, to avoid adding unnecessary variables.
148 
149  Returns the target variable object.
150  """
151  if isinstance(source, cellml_variable):
152  src_cname, src_vname = source.component.name, source.name
153  else:
154  src_cname, src_vname = source
155  if isinstance(target, cellml_variable):
156  target_cname, target_vname = target.component.name, target.name
157  else:
158  target_cname, target_vname = target
159  src_comp = self.model.get_component_by_name(src_cname)
160  target_comp = self.model.get_component_by_name(target_cname)
161  if src_comp == target_comp:
162  return target_comp.get_variable_by_name(target_vname)
163  # Determine encapsulation paths from target & source to the root
164  src_path = self._parent_path(src_comp)
165  target_path = self._parent_path(target_comp)
166  # At some point these will share a common path, even if it's just the root itself
167  meeting_index = self._find_common_tail(src_path, target_path)
168  # Construct path from source to target, leaving out the root (None)
169  path = src_path[:meeting_index]
170  if src_path[meeting_index]:
171  path.append(src_path[meeting_index])
172  path.extend(reversed(target_path[:meeting_index]))
173  # Traverse this path, adding connections at each step
174  next_src_var = src_comp.get_variable_by_name(src_vname)
175  for i, src_comp in enumerate(path[:-1]):
176  target_comp = path[i+1]
177  next_src_var = self._make_connection(next_src_var, target_comp, target_vname)
178  return next_src_var
179 
180  def _make_connection(self, src_var, target_comp, target_vname):
181  """Make a connection from a source variable to a given component and suggested local name.
182 
183  Note that in the case that both variables already exist and are connected, the existing
184  connection is allowed to flow in either direction.
185  """
186  src_comp = src_var.component
187  target_var = self._find_or_create_variable(target_comp.name, target_vname, src_var)
188  # Sanity check the target variable
189  if (target_var.get_type() == VarTypes.Mapped
190  and target_var.get_source_variable(recurse=True) is src_var.get_source_variable(recurse=True)):
191 # print "Connection exists between", src_var, "and target", target_var
192  return target_var
193  elif target_var.get_type() == VarTypes.Unknown:
194  # We've created this variable, so should be ok, but check for gotchas
195  assert not(hasattr(target_var, u'initial_value'))
196  if src_comp is target_comp.parent():
197  src_if = u'private'
198  target_if = u'public'
199  elif src_comp.parent() is target_comp:
200  src_if = u'public'
201  target_if = u'private'
202  else:
203  assert src_comp.parent() is target_comp.parent()
204  src_if = u'public'
205  target_if = u'public'
206  # One special case: if the src_var is actually obtained from a different
207  # component at this level or above, in which case we should use the real
208  # source, not that given.
209  if getattr(src_var, src_if + u'_interface', u'none') == u'in':
210  src_var = src_var.get_source_variable()
211  # Check and set the interface attributes
212 # print "Connecting source", src_var, src_if, getattr(src_var, src_if + u'_interface', u'none'),
213 # print "to", target_var, target_if, getattr(target_var, target_if + u'_interface', u'none')
214  assert getattr(src_var, src_if + u'_interface', u'none') != u'in'
215  assert getattr(target_var, target_if + u'_interface', u'none') != u'out'
216  src_var.xml_set_attribute((src_if + u'_interface', None), u'out')
217  target_var.xml_set_attribute((target_if + u'_interface', None), u'in')
218  # Create the connection element
219  self._create_connection_element(src_var, target_var)
220  self.connections_made.add(frozenset([src_var, target_var]))
221  # Ensure we handle a later connection attempt between these variables correctly
222  target_var._set_source_variable(src_var)
223  else:
224  # Naming conflict; try again with a different target name
225  return self._make_connection(src_var, target_comp, target_vname + u'_')
226  return target_var
227 
228  def _find_connection_element(self, var1, var2):
229  """Find any connection element containing a connection of the given variables.
230 
231  Returns a pair, the first element of which is either the element or None, and the
232  second of which is a boolean indicating whether the variables need to be swapped
233  in order to match the order of the components in the connection.
234  """
235  cn1, cn2 = var1.component.name, var2.component.name
236  cnames = set([cn1, cn2])
237  for conn in getattr(self.model, u'connection', []):
238  mc = conn.map_components
239  if set([mc.component_1, mc.component_2]) == cnames:
240  break
241  else:
242  conn = None
243  if conn:
244  swap = conn.map_components.component_1 == cn2
245  else:
246  swap = False
247  return conn, swap
248 
249  def _create_connection_element(self, var1, var2):
250  """Create a connection element connecting the given variables and add to the model.
251 
252  If there's already a connection element for the relevant pair of components,
253  we just add another map_variables element to that.
254  """
255  conn, swap = self._find_connection_element(var1, var2)
256  if conn:
257  if swap:
258  var1, var2 = var2, var1
259  else:
260  conn = var1.xml_create_element(u'connection', NSS[u'cml'])
261  mapc = var1.xml_create_element(u'map_components', NSS[u'cml'],
262  attributes={u'component_1': var1.component.name,
263  u'component_2': var2.component.name})
264  conn.xml_append(mapc)
265  self.model.xml_append(conn)
266  mapv = var1.xml_create_element(u'map_variables', NSS[u'cml'],
267  attributes={u'variable_1': var1.name,
268  u'variable_2': var2.name})
269  conn.xml_append(mapv)
270 
271  def remove_connection(self, var1, var2):
272  """Remove a connection between two variables.
273 
274  Removes the relevant map_variables element.
275  If this results in an empty connection element, removes that as well.
276  """
277  conn, swap = self._find_connection_element(var1, var2)
278  if not conn:
279  raise ModelModificationError("Cannot remove non-existent connection.")
280  if swap:
281  var1, var2 = var2, var1
282  # Find the relevant map_variables element
283  mapv = conn.xml_xpath(u'cml:map_variables[@variable_1="%s" and @variable_2="%s"]'
284  % (var1.name, var2.name))
285  if not mapv:
286  raise ModelModificationError("Cannot remove non-existent connection.")
287  conn.xml_remove_child(mapv[0])
288  if not hasattr(conn, u'map_variables'):
289  conn.xml_parent.xml_remove_child(conn)
290 
291  def remove_connections(self, var):
292  """Remove all connection elements for the given variable.
293 
294  Removes each relevant map_variables element.
295  If this results in an empty connection element, removes that as well.
296  """
297  cname, vname = var.component.name, var.name
298  for conn in list(getattr(self.model, u'connection', [])):
299  if cname == conn.map_components.component_1:
300  vid = u'variable_1'
301  elif cname == conn.map_components.component_2:
302  vid = u'variable_2'
303  else:
304  continue
305  for mapv in conn.map_variables:
306  if vname == getattr(mapv, vid, ''):
307  # Found a connection
308  conn.xml_remove_child(mapv)
309  if not hasattr(conn, u'map_variables'):
310  conn.xml_parent.xml_remove_child(conn)
311  # There can't be any more matching map_variables in this connection
312  break
313 
314  def _update_connections(self, oldVar, newVar):
315  """Change all variables connected to oldVar to be mapped to newVar instead."""
316  vars = [v for v in self.model.get_all_variables() if v.get_source_variable(True) is oldVar]
317  # Remove old connections, including interfaces and types so creating the new connection works
318  for v in vars:
319  self.remove_connections(v)
320  self.del_attr(v, u'public_interface')
321  self.del_attr(v, u'private_interface')
322  v.clear_dependency_info()
323  # Create new connections
324  for v in vars:
325  self.connect_variables(newVar, v)
326 
327  def _find_common_tail(self, l1, l2):
328  """Find the first element at which both lists are identical from then on."""
329  i = -1
330  try:
331  while l1[i] == l2[i]:
332  i -= 1
333  except IndexError:
334  # One list is the tail of the other
335  pass
336  # i now gives the last differing element
337  assert i < -1
338  return i+1
339 
340  def _parent_path(self, comp):
341  """Return a path of components from that given to the encapsulation root.
342 
343  The root is specified by None, since we're really dealing with a forest,
344  not a tree.
345  """
346  path = [comp]
347  while comp:
348  path.append(comp.parent())
349  comp = comp.parent()
350  return path
351 
352  def _process_operator(self, expr, operator, func, *args, **kwargs):
353  """Apply func to any application of the given operator within the given tree."""
354  for elt in self.model.xml_element_children(expr):
355  self._process_operator(elt, operator, func, *args, **kwargs)
356  if isinstance(expr, mathml_apply) and expr.operator().localName == operator:
357  func(expr, *args, **kwargs)
358 
359  def _find_or_create_variable(self, cname, vname, source):
360  """Find a given variable in the model, creating it if necessary.
361 
362  We look for a variable in the component named cname with the same name as the source.
363  If it doesn't exist, a variable named vname will be created in that component (unless
364  it already exists).
365  The variable will become a mapped variable with the given source.
366  Hence if it is created it will have the same units.
367  """
368  try:
369  var = self.model.get_variable_by_name(cname, source.name)
370  raise KeyError()
371  except KeyError:
372  # Have we created it already?
373  try:
374  var = self.model.get_variable_by_name(cname, vname)
375  except KeyError:
376  # Create it and add to model
377  units = source.component.get_units_by_name(source.units)
378  var = self.add_variable(cname, vname, units)
379  return var
380 
381  def add_variable(self, comp, vname, units, **kwargs):
382  """Add a new variable to the given component.
383 
384  Remaining arguments are as for cellml_variable.create_new.
385  Returns the new variable object.
386  """
387  if not isinstance(comp, cellml_component):
388  comp = self.model.get_component_by_name(comp)
389  units = self.add_units(units)
390  var = cellml_variable.create_new(comp, vname, units.name, **kwargs)
391  comp._add_variable(var)
392  return var
393 
394  def _get_units_object(self, units):
395  """Helper function to convert a units specification into a cellml_units object.
396 
397  The input can be a cellml_units object, in which case we just return it.
398  However, it can also be a serialised CellML units definition, in which case it
399  will be parsed to create the object.
400  """
401  if isinstance(units, cellml_units):
402  # We're done
403  pass
404  else:
405  units = amara_parse_cellml(unicode(units))
406  assert isinstance(units, cellml_units)
407  return units
408 
409  def add_units(self, units):
410  """Add a units definition to the model, if it doesn't already exist.
411 
412  If the definition isn't in the model, at whole-model level, it will be added. If the same
413  definition is already available, however, that definition should be used by preference.
414  Will return the actual units object to use.
415  """
416  units = self.model._get_units_obj(units)
417  try:
418  model_units = self.model.get_units_by_name(units.name)
419  except KeyError:
420  model_units = None
421  if model_units:
422  assert units.uniquify_tuple == model_units.uniquify_tuple
423  units = model_units
424  else:
425  units.name = self._uniquify_name(units.name, self.model.get_units_by_name)
426  self.model.add_units(units.name, units)
427  self.model.xml_append(units)
428  # Ensure referenced units exist
429  for unit in getattr(units, u'unit', []):
430  unit._set_units_element(self.add_units(unit.get_units_element()), override=True)
431  unit.units = unit.get_units_element().name
432  return units
433 
434  def add_expr_to_comp(self, comp, expr):
435  """Add an expression to the mathematics in the given component.
436 
437  comp may be a cellml_component instance or a component name.
438  """
439  if not isinstance(comp, cellml_component):
440  comp = self.model.get_component_by_name(comp)
441  if not hasattr(comp, u'math'):
442  # Create the math element
443  math = comp.xml_create_element(u'math', NSS[u'm'])
444  comp.xml_append(math)
445  # Append this expression
446  comp.math.xml_append(expr)
447 
448  def remove_expr(self, expr):
449  """Remove an expression (ODE or assignment) from its parent."""
450  assert isinstance(expr, mathml_apply)
451  if expr.xml_parent:
452  expr.xml_parent.safe_remove_child(expr)
453  expr.xml_parent = None # Not done by Amara...
454  return expr
455 
456  def remove_definition(self, var, keep_initial_value=False):
457  """Remove any existing definition (as an equation) of the given variable.
458 
459  If keep_initial_value is False, then also remove any initial_value attribute.
460 
461  If the variable is Mapped, throw a ModelModificationError.
462  """
463  if var.get_type() == VarTypes.Mapped:
464  raise ModelModificationError("Cannot remove the equation defining a mapped variable - remove the definition of its source instead")
465  if not keep_initial_value:
466  self.del_attr(var, u'initial_value')
467  # Note: if this is a variable added by a protocol, then it shouldn't have
468  # any dependencies set up yet, so this is a no-op.
469  for dep in var.get_all_expr_dependencies():
470  self.remove_expr(dep)
471  # We know don't know how it will be defined
472  var.clear_dependency_info()
473 
474  def del_attr(self, elt, localName, ns=None):
475  """Delete an XML attribute from an element, if it exists."""
476  for (pyname, (qname, ns_)) in elt.xml_attributes.items():
477  _, name = SplitQName(qname)
478  if ns_ == ns and name == localName:
479  delattr(elt, pyname)
480 
481  def _uniquify_var_name(self, varname, comp):
482  """Ensure varname is unique within the given component.
483 
484  Underscores will be appended to the name until it is unique. The unique name will be returned.
485  """
486  return self._uniquify_name(varname, comp.get_variable_by_name)
487 
488  def _uniquify_name(self, name, callable):
489  """Ensure the given name is unique within a particular context.
490 
491  The context is determined by the given function: it will be passed candidate names to test
492  for existence, and is expected to throw iff the name is not already used. Underscores will
493  be appended to the given name until callable throws, and the resulting unique name returned.
494  """
495  while True:
496  try:
497  callable(name)
498  name += u'_'
499  except:
500  break
501  return name
502 
503  def set_units_converter(self, converter):
504  """Set the object used to units-convert variable initial values."""
505  self._units_converter = converter
506 
508  """Get the units converter object, if any has been set."""
509  if not self._units_converter:
510  raise ModelModificationError("No units converter has been set.")
511  return self._units_converter
512 
513  def _convert_initial_value(self, var, units, do_conversion=True):
514  """Convert any initial value of the given variable into the given units.
515 
516  If there is no initial value, returns None.
517  If there is no units converter, leaves the initial_value unchanged.
518  """
519  if not hasattr(var, u'initial_value'):
520  return None
521  value = var.initial_value
522  if value and self._units_converter and do_conversion:
523  if not var.get_units().equals(units):
524  try:
525  value = self._units_converter.convert_constant(value, var.get_units(), units, var.component)
526  except EvaluationError, e:
527  raise ModelModificationError("Cannot units-convert initial value as requires run-time information:\n"
528  + str(e))
529  return unicode(value)
530 
531 
532 
534  """Class for generating an interface between a CellML model and external code.
535 
536  This contains functionality for users to describe the interface desired by the external code, i.e.
537  which variables are inputs and/or outputs, and expected units. It will then create a new component
538  within the CellML model containing these variables, and add units conversions where required. The
539  external code then only needs to interact with this new component.
540  """
541  def __init__(self, model, name='interface', units_converter=None):
542  super(InterfaceGenerator, self).__init__(model)
545  self.set_units_converter(units_converter)
546 
547  def add_input(self, var, units, annotate=True, convert_initial_value=True):
548  """Specify a variable as an input to the model.
549 
550  var should be a cellml_variable object already existing in the model.
551  units should be a suitable input to self._get_units_object.
552 
553  If adding both State and Free variables as inputs, make sure to add the Free variable first,
554  otherwise you will not be able to specify units for it.
555 
556  Set annotate to False if you do not wish a Constant variable to be annotated as a modifiable
557  parameter.
558 
559  If a units converter has been supplied, we will also try to units-convert initial values.
560  This may not be possible if special conversions are used, since they may involve variables
561  whose values are not known at this time. If this is the case, set convert_initial_value to
562  False to avoid applying the conversion. A proper solution requires CellML 1.1 features.
563 
564  The new variable added to the interface component is returned.
565  """
566  assert isinstance(var, cellml_variable)
567  units = self._get_units_object(units)
568  var = var.get_source_variable(recurse=True) # Ensure we work with source variables only
569  var_name = var.fullname(cellml=True)
570  # Check that the variable has a suitable type to be an input
571  t = var.get_type()
572  if t == VarTypes.Computed:
573  raise ModelModificationError("Cannot specify computed variable " + var.fullname() + " as an input")
574  elif t not in [VarTypes.Constant, VarTypes.Free, VarTypes.State]:
575  raise ModelModificationError("Variable " + var.fullname() + " has unexpected type " + str(t))
576  # Add a new variable with desired units to the interface component
577  comp = self.get_interface_component()
578  newvar = self.add_variable(comp, var_name, units, id=var.cmeta_id,
579  initial_value=self._convert_initial_value(var, units, convert_initial_value),
580  interfaces={u'public': u'out'})
581  newvar._set_type(t)
582  # Remove initial value and id from the original, if they exist
583  self.del_attr(var, u'initial_value')
584  self.del_attr(var, u'id', NSS['cmeta'])
585  # If the original variable was a state variable, split the defining equation
586  if t == VarTypes.State:
587  self._split_ode(newvar, var)
588  # Annotate the new variable as a parameter if the original was a constant
589  if t == VarTypes.Constant and annotate:
590  newvar.set_is_modifiable_parameter(True)
591 
592  self._update_connections(var, newvar)
593  return newvar
594 
595  def add_output(self, var, units, annotate=True):
596  """Specify a variable as an output of the model.
597 
598  var should be a cellml_variable object already existing in the model.
599  units should be a suitable input to self._get_units_object.
600  The new variable will take the cmeta:id of the original, and hence existing metadata
601  annotations will refer to the new variable.
602  If annotate is set to True, the new variable will also be annotated as a derived quantity.
603 
604  The new variable added to the interface component is returned.
605  """
606  assert isinstance(var, cellml_variable)
607  units = self._get_units_object(units)
608  var = var.get_source_variable(recurse=True)
609  var_name = var.fullname(cellml=True)
610  comp = self.get_interface_component()
611  newvar = self.add_variable(comp, var_name, units, id=var.cmeta_id)
612  self.del_attr(var, u'id', NSS['cmeta'])
613  self.connect_variables(var, newvar)
614  if annotate:
615  newvar.set_is_derived_quantity(True)
616  return newvar
617 
618  def add_output_function(self, resultName, operator, argVars, units):
619  """Add an output that's defined as a (MathML) function of existing model variables.
620 
621  The desired units are those of the function's result. The function arguments will be
622  imported with their units as given by the model, and the function calculated. This result
623  will then be units-converted if necessary.
624 
625  The new variable added to the interface component is returned.
626  """
627  # Add the result variable
628  comp = self.get_interface_component()
629  units = self._get_units_object(units)
630  result_var = self.add_variable(comp, resultName, units)
631  result_var.set_pe_keep(True)
632  # Map the argument variables
633  operands = []
634  for var in argVars:
635  operands.append(self.add_output(var, var.get_units(), annotate=False).name)
636  # Create the new function and assign it to result_var
637  expr = mathml_apply.create_new(self.model, operator, operands)
638  assign = mathml_apply.create_new(self.model, u'eq', [result_var.name, expr])
639  self.add_expr_to_comp(comp, assign)
640  return result_var
641 
642  def make_var_constant(self, var, value):
643  """Turn a variable into a constant."""
644  self.remove_definition(var)
645  var.clear_dependency_info()
646  var.initial_value = unicode(str(value))
647  var._set_type(VarTypes.Constant)
648 
649  def make_var_computed_constant(self, var, value):
650  """Turn a variable into a Computed variable with constant value definition."""
651  self.remove_definition(var)
652  var.clear_dependency_info()
653  defn = mathml_apply.create_new(self.model, u'eq',
654  [var.name, (unicode(str(value)), var.get_units().name)])
655  self.add_expr_to_comp(var.component, defn)
656  var._set_type(VarTypes.Computed)
657 
658  def finalize(self, *args, **kwargs):
659  """Override finalize to also set up standard interface elements not defined individually."""
662  super(InterfaceGenerator, self).finalize(*args, **kwargs)
663 
665  """Transform any equations with derivatives on the RHS to use the variable defining it instead.
666 
667  self._split_ode must have been used for all derivatives before calling this method. This means
668  that each ODE now has a variable to which the RHS is assigned. Rather than using the derivative
669  directly, which could break the dependency chain if units conversions are used for time, equations
670  should refer to this new variable instead.
671  """
672  for expr in self.model.search_for_assignments():
673  self._process_operator(list(expr.operands())[1], u'diff', self._transform_derivative_on_rhs)
674 
676  """Transform a derivative on the RHS of an equation to refer to the defining variable.
677 
678  Helper method used by self._transform_derivatives_on_rhs to do the actual transformation.
679  """
680  # Find the variable to use
681  dep_var = expr.diff.dependent_variable.get_source_variable(recurse=True)
682  indep_var = expr.diff.independent_variable.get_source_variable(recurse=True)
683  ode = dep_var.get_ode_dependency(indep_var)
684  rhs_var = ode.eq.rhs.variable.get_source_variable(recurse=True)
685  # Ensure there's something mapped to it in this component
686  rhs_var = self.connect_variables(rhs_var, (expr.component.name, rhs_var.name))
687  # Update this expression
688  parent = expr.xml_parent
689  parent.xml_insert_after(expr, mathml_ci.create_new(parent, rhs_var.name))
690  parent.safe_remove_child(expr)
691 
692  def _split_ode(self, newVar, oldVar):
693  """Split an ODE definition so the derivative goes into the interface component.
694 
695  The RHS stays where it is, and is assigned to a new variable, which is connected to the interface
696  component and assigned to the new derivative. newVar is the new state variable in the interface
697  component, and oldVar will soon be mapped to it by the caller.
698 
699  Any other equations in the model which use the derivative are transformed to use the new variable
700  instead.
701  """
702  # Get the free variable in the interface component
703  free_var = self.model.find_free_vars()[0]
704  if free_var.component is not newVar.component:
705  free_var = self.add_input(free_var, free_var.get_units())
706  # Add a new variable to assign the RHS to, with units of the original derivative
707  deriv_name = self._uniquify_var_name(u'd_%s_d_%s' % (oldVar.name, free_var.name), oldVar.component)
708  orig_ode = oldVar.get_all_expr_dependencies()[0]
709  orig_rhs_var = self.add_variable(oldVar.component, deriv_name, orig_ode.eq.lhs.get_units().extract())
710  # Add an output version of this in the interface, with desired units
711  desired_units = newVar.get_units().quotient(free_var.get_units())
712  mapped_rhs_var = self.add_output(orig_rhs_var, desired_units, annotate=False)
713  # Replace the original ODE with an assignment
714  orig_rhs = orig_ode.eq.rhs
715  orig_ode.safe_remove_child(orig_rhs)
716  self.remove_expr(orig_ode)
717  self.add_expr_to_comp(oldVar.component,
718  mathml_apply.create_new(self.model, u'eq',
719  [orig_rhs_var.name, orig_rhs]))
720  # Create a new ODE in the interface component
721  new_ode = mathml_diff.create_new(self.model, free_var.name, newVar.name, mapped_rhs_var.name)
722  self.add_expr_to_comp(newVar.component, new_ode)
723  new_ode.classify_variables(root=True, dependencies_only=True)
724 
726  """All the derivatives should be considered as model outputs, and state variables as model inputs.
727 
728  For any that haven't been done explicitly, this method will add the corresponding state variable
729  as an input, with its original units, which has the desired effect.
730  """
731  comp = self.get_interface_component()
732  for var in self.model.find_state_vars():
733  if var.component is not comp:
734  self.add_input(var, var.get_units())
735 
737  """Get the new component that will contain the interface.
738 
739  The name will be self._interface_component_name, unless a component with that name already exists,
740  in which case underscores will be added to the component name to make it unique.
741  """
742  if self._interface_component is None:
744  self.model.interface_component_name = unicode(self._interface_component_name)
745  assert not self._interface_component.ignore_component_name
746  return self._interface_component
747 
748 
750  """Top-level interface to the units conversion code in PyCml.
751  """
752  def __init__(self, model, warn_only=None, show_xml_context_only=False):
753  super(UnitsConverter, self).__init__(model)
754  if warn_only is None:
755  warn_only = model.get_option('warn_on_units_errors')
756  self.warn_only = warn_only
757  self.show_xml_context_only = show_xml_context_only
759  self._setup_logger()
760  self._converted_mappings = set()
761 
762  def __del__(self):
763  self._cleanup_logger()
764 
765  def _setup_logger(self):
766  logger = logging.getLogger('units-converter')
767  logger.setLevel(logging.WARNING)
768  formatter = logging.Formatter(fmt="%(name)s: %(message)s")
769  handler = logging.StreamHandler(sys.stderr)
770  handler.setFormatter(formatter)
771  logger.addHandler(handler)
772  self._log_handler = handler
773 
774  def _cleanup_logger(self):
775  """Flush logger & remove handler."""
776  logger = logging.getLogger('units-converter')
777  self._log_handler.flush()
778  logger.removeHandler(self._log_handler)
779 
780  def try_convert(self, func, *args, **kwargs):
781  """Call the given function, and log any units errors produced."""
782  try:
783  func(*args, **kwargs)
784  except UnitsError, e:
785  if self.show_xml_context_only:
786  e.show_xml_context_only()
787  if self.warn_only:
788  e.warn = True
789  e.level = logging.WARNING
790  logging.getLogger('units-converter').log(e.level, unicode(e).encode('UTF-8'))
791 
792  def _apply_special_conversion_for_nested_expr(self, expr, defn_units, desired_units):
793  """Apply a special conversion to the given (sub-)expression.
794 
795  This will get called by mathml_units_mixin._add_units_conversion if a special conversion is required by a nested sub-expression.
796  """
797  for from_units, to_units in self.special_conversions.iterkeys():
798  if (from_units.dimensionally_equivalent(defn_units)
799  and to_units.dimensionally_equivalent(desired_units)):
800  # We can apply this conversion
801  expr = self.special_conversions[(from_units, to_units)](expr)
802  DEBUG('units-converter', "Used nested special conversion from", repr(from_units), "to", repr(to_units))#, "giving", expr.xml())
803  break
804 # else:
805 # print "No on nested conv from", repr(from_units), "to", repr(to_units)
806  return expr
807 
808  def _check_special_conversion(self, expr):
809  """Check whether a special conversion applies to the given assignment.
810 
811  Special conversions allow us to do units conversion between dimensionally non-equivalent
812  quantities, by utilising biological knowledge. Available special conversions are added
813  using the add_special_conversion method.
814  """
815  lhs_units = expr.eq.lhs.get_units()
816  rhs_units = expr.eq.rhs.get_units()
817  if lhs_units.dimensionally_equivalent(rhs_units):
818  return
819  for from_units, to_units in self.special_conversions.iterkeys():
820  if (from_units.dimensionally_equivalent(rhs_units)
821  and to_units.dimensionally_equivalent(lhs_units)):
822  # We can apply this conversion
823  self.special_conversions[(from_units, to_units)](expr)
824  DEBUG('units-converter', "Used special conversion from", repr(from_units), "to", repr(to_units))#, "giving", expr.xml())
825  break
826 # else:
827 # print "No on conv from", repr(from_units), "to", repr(to_units)
828 
829  def add_special_conversion(self, from_units, to_units, converter):
830  """Add a new special conversion to the list available.
831 
832  Special conversions allow us to do units conversion between dimensionally non-equivalent
833  quantities, by utilising biological knowledge. The function "converter" will be called with
834  an assignment (top-level mathml_apply instance) that has RHS units equivalent to from_units,
835  and LHS units equivalent to to_units. It should alter the equation in-place (i.e. the
836  object passed to it must contain the final equation) to do an appropriate units conversion,
837  at least so that LHS and RHS dimensions match.
838  """
839  self.special_conversions[(from_units, to_units)] = converter
840 
841  def modify_rhs(self, expr, operator, var):
842  """Helper method of use to special units conversions.
843 
844  Will modify the given expr in-place, replacing the RHS by an application of the given operator.
845  The operands will be the existing RHS and a ci element referencing the supplied variable object.
846  Connections and variables will be added to ensure that the given variable is available in the
847  component in which expr appears.
848 
849  Returns expr, for ease of chaining expressions.
850  """
851  assert isinstance(var, cellml_variable)
852  # Ensure var is available in expr's component
853  local_var_name = var.name
854  source_comp = var.component
855  expr_comp = expr.component
856  if source_comp != expr_comp:
857  local_var = self.connect_variables(var, (expr_comp.name, var.fullname(cellml=True)))
858  local_var_name = local_var.name
859  # Change expr
860  rhs = expr.eq.rhs
861  expr.safe_remove_child(rhs)
862  new_rhs = mathml_apply.create_new(var.model, operator, [rhs, local_var_name])
863  expr.xml_append(new_rhs)
864  return expr
865 
866  def times_rhs_by(self, expr, var):
867  """Helper method of use to special units conversions.
868 
869  Will modify the given expr in-place, post-multiplying the RHS by a reference to the given variable object.
870  Connections and variables will be added to ensure that the given variable is available in the
871  component in which expr appears.
872 
873  Returns expr, for ease of chaining expressions.
874  """
875  return self.modify_rhs(expr, u'times', var)
876 
877  def divide_rhs_by(self, expr, var):
878  """Helper method of use to special units conversions.
879 
880  Will modify the given expr in-place, post-dividing the RHS by a reference to the given variable
881  object.
882  Connections and variables will be added to ensure that the given variable is available in the
883  component in which expr appears.
884 
885  Returns expr, for ease of chaining expressions.
886  """
887  return self.modify_rhs(expr, u'divide', var)
888 
889  def convert_assignments(self, exprs):
890  """Apply conversions to any assignments in the given iterable."""
891  boolean = self.model.get_units_by_name('cellml:boolean')
892  for expr in exprs:
893  if isinstance(expr, mathml_apply):
894 # print 'Converting? assignment', element_xpath(expr)
895  if self.special_conversions:
896  self.try_convert(self._check_special_conversion, expr)
897  self.try_convert(expr._set_in_units, boolean)
898 
899  def convert_constant(self, value, from_units, to_units, comp):
900  """Convert a constant value into desired units."""
901  from_units = self.add_units(from_units)
902  to_units = self.add_units(to_units)
903  expr = mathml_apply.create_new(self.model, u'eq', [(u'0', to_units.name),
904  (unicode(value), from_units.name)])
905  self.add_expr_to_comp(comp, expr)
906  # Nasty hack to make expr.is_top_level return True
907  expr._cml_assigns_to = expr.operands().next()
908  if self.special_conversions:
909  self.try_convert(self._check_special_conversion, expr)
910  self.try_convert(expr.eq.rhs._set_in_units, to_units)
911  self.remove_expr(expr)
912  return expr.eq.rhs.evaluate()
913 
914  def convert_mapping(self, mapping, comp1, comp2, var1, var2):
915  """Apply conversions to a mapping between two variables."""
916  model = self.model
917  # Check for being already converted
918  var_pair = frozenset([var1, var2])
919  if var_pair in self._converted_mappings:
920  DEBUG('units-converter', 'Skipping already converted mapping', var1, '<->', var2)
921  return
922  else:
923  self._converted_mappings.add(var_pair)
924  # Ensure mapping is var1 := var2; swap vars if needed
925  swapped = False
926  try:
927  if var2.get_source_variable() is var1:
928  swapped = True
929  var1, var2 = var2, var1
930  comp1, comp2 = comp2, comp1
931  except TypeError:
932  pass
933  # Get units
934  u1 = var1.get_units()
935  u2 = var2.get_units()
936  DEBUG('units-converter', "Converting mapping of", var1, ":=", var2,
937  "(units:", repr(u1), repr(u2), ")")
938  if not u1.equals(u2):
939  # We need a conversion
940  # Add a copy of var1 to comp1, with units as var2
941  if getattr(var1, u'public_interface', '') == u'in':
942  in_interface = u'public'
943  else:
944  in_interface = u'private'
945  var1_converter = self.add_variable(comp1, var1.name + u'_converter', u2, interfaces={in_interface: u'in'})
946  var1._cml_var_type = VarTypes.Computed
947  var1._cml_source_var = None
948  delattr(var1, in_interface + u'_interface')
949  var1_converter._set_source_variable(var2)
950  # Add assignment maths for var1 := var1_converter
951  app = mathml_apply.create_new(model, u'eq', [var1.name, var1_converter.name])
952  self.add_expr_to_comp(comp1, app)
953  var1._cml_depends_on = [app]
954  app._cml_assigns_to = var1
955  # Update mapping to var1_converter := var2
956  if swapped:
957  mapping.variable_2 = var1_converter.name
958  else:
959  mapping.variable_1 = var1_converter.name
960  # Fix usage counts - var1_converter is only used by app, and so var2 usage decreases
961  var1_converter._used()
962  for _ in xrange(var1.get_usage_count()):
963  var2._decrement_usage_count()
964  # Apply units conversion to the assignment
965  self.convert_assignments([app])
966  # Add the assignment into the sorted list
967  assignments = model.get_assignments()
968  idx = assignments.index(var1)
969  assignments[idx:idx+1] = [var1_converter, app]
970 
971  def convert_connections(self, connections):
972  """Add units conversions for all connections in the given set.
973 
974  :param connections: a set of variable pairs representing connections. For each pair of variables a units conversion
975  will be added if needed and not already performed.
976  """
977  model = self.model
978  for conn in getattr(model, u'connection', []):
979  comp1 = model.get_component_by_name(conn.map_components.component_1)
980  comp2 = model.get_component_by_name(conn.map_components.component_2)
981  for mapping in conn.map_variables:
982  var1 = model.get_variable_by_name(comp1.name, mapping.variable_1)
983  var2 = model.get_variable_by_name(comp2.name, mapping.variable_2)
984  if frozenset([var1, var2]) in connections:
985  self.convert_mapping(mapping, comp1, comp2, var1, var2)
986 
988  """Add all units conversions required by the given component.
989 
990  This allows us to only apply the conversions required by an interface component created
991  by an InterfaceGenerator.
992  """
993  model = self.model
994  if self.special_conversions:
995  self.model._cml_special_units_converter = self._apply_special_conversion_for_nested_expr
996  assignments = model.search_for_assignments(comp)
997  self.convert_assignments(assignments)
998  if self.special_conversions:
999  del self.model._cml_special_units_converter
1000  for conn in getattr(model, u'connection', []):
1001  cname1 = conn.map_components.component_1
1002  cname2 = conn.map_components.component_2
1003  if comp.name in [cname1, cname2]:
1004  comp1 = model.get_component_by_name(cname1)
1005  comp2 = model.get_component_by_name(cname2)
1006  for mapping in conn.map_variables:
1007  var1 = model.get_variable_by_name(cname1, mapping.variable_1)
1008  var2 = model.get_variable_by_name(cname2, mapping.variable_2)
1009  self.convert_mapping(mapping, comp1, comp2, var1, var2)
1010 
1012  """Add all units conversions required in the given model."""
1013  model = self.model
1014  # Mathematical expressions
1015  self.convert_assignments(model.get_assignments())
1016  # Connections
1017  for conn in getattr(model, u'connection', []):
1018  comp1 = model.get_component_by_name(conn.map_components.component_1)
1019  comp2 = model.get_component_by_name(conn.map_components.component_2)
1020  for mapping in conn.map_variables:
1021  var1 = model.get_variable_by_name(comp1.name, mapping.variable_1)
1022  var2 = model.get_variable_by_name(comp2.name, mapping.variable_2)
1023  self.convert_mapping(mapping, comp1, comp2, var1, var2)
1024  return
def amara_parse_cellml
Definition: pycml.py:191
#define encode(otri)