Nektar++
processors.py
Go to the documentation of this file.
1"""Copyright (c) 2005-2016, University of Oxford.
2All rights reserved.
3
4University of Oxford means the Chancellor, Masters and Scholars of the
5University of Oxford, having an administrative office at Wellington
6Square, Oxford OX1 2JD, UK.
7
8This file is part of Chaste.
9
10Redistribution and use in source and binary forms, with or without
11modification, 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
21THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
22AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
23IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
24ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
25LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
26CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
27GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
28HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
29LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
30OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
31"""
32
33"""
34This file contains various classes supporting modifications to CellML models.
35"""
36
37import pycml
38from pycml import *
39import validator
40
41
42class ModelModificationError(ValueError):
43 """Error thrown if a model modification is invalid."""
44 pass
45
46
47class 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 """
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()
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
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
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:
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:
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 add_input(self, var, units, annotate=True, convert_initial_value=True)
Definition: processors.py:547
def __init__(self, model, name='interface', units_converter=None)
Definition: processors.py:541
def add_output_function(self, resultName, operator, argVars, units)
Definition: processors.py:618
def add_output(self, var, units, annotate=True)
Definition: processors.py:595
def finalize(self, *args, **kwargs)
Definition: processors.py:658
def make_var_computed_constant(self, var, value)
Definition: processors.py:649
def _split_ode(self, newVar, oldVar)
Definition: processors.py:692
def _uniquify_name(self, name, callable)
Definition: processors.py:488
def add_variable(self, comp, vname, units, **kwargs)
Definition: processors.py:381
def connect_variables(self, source, target)
Definition: processors.py:132
def _convert_initial_value(self, var, units, do_conversion=True)
Definition: processors.py:513
def remove_definition(self, var, keep_initial_value=False)
Definition: processors.py:456
def remove_connection(self, var1, var2)
Definition: processors.py:271
def _make_connection(self, src_var, target_comp, target_vname)
Definition: processors.py:180
def _find_or_create_variable(self, cname, vname, source)
Definition: processors.py:359
def set_units_converter(self, converter)
Definition: processors.py:503
def del_attr(self, elt, localName, ns=None)
Definition: processors.py:474
def _process_operator(self, expr, operator, func, *args, **kwargs)
Definition: processors.py:352
def _find_connection_element(self, var1, var2)
Definition: processors.py:228
def add_expr_to_comp(self, comp, expr)
Definition: processors.py:434
def finalize(self, error_handler, pre_units_check_hook=None, check_units=True)
Definition: processors.py:64
def _uniquify_var_name(self, varname, comp)
Definition: processors.py:481
def _create_connection_element(self, var1, var2)
Definition: processors.py:249
def _update_connections(self, oldVar, newVar)
Definition: processors.py:314
def _apply_special_conversion_for_nested_expr(self, expr, defn_units, desired_units)
Definition: processors.py:792
def convert_connections(self, connections)
Definition: processors.py:971
def convert_constant(self, value, from_units, to_units, comp)
Definition: processors.py:899
def modify_rhs(self, expr, operator, var)
Definition: processors.py:841
def try_convert(self, func, *args, **kwargs)
Definition: processors.py:780
def __init__(self, model, warn_only=None, show_xml_context_only=False)
Definition: processors.py:752
def add_special_conversion(self, from_units, to_units, converter)
Definition: processors.py:829
def convert_mapping(self, mapping, comp1, comp2, var1, var2)
Definition: processors.py:914
def get_units_by_name(self, uname)
Definition: pycml.py:2725
def amara_parse_cellml(source, uri=None, prefixes=None)
Definition: pycml.py:191
def quotient(self, other_units)
Definition: pycml.py:3154
def extract(self, check_equality=False)
Definition: pycml.py:2657
def equals(self, other)
Definition: pycml.py:2634
def DEBUG(facility, *args)
Definition: utilities.py:95
scalarT< T > log(scalarT< T > in)
Definition: scalar.hpp:303