def test_versions(self): # Tests writing different CellML versions # CellML 1.0 units = { myokit.parse_unit('pF'): 'picofarad', } w = cellml.CellMLExpressionWriter('1.0') w.set_unit_function(lambda x: units[x]) xml = w.ex(myokit.Number(1, myokit.units.pF)) self.assertIn(cellml.NS_CELLML_1_0, xml) # CellML 1.1 w = cellml.CellMLExpressionWriter('1.1') w.set_unit_function(lambda x: units[x]) xml = w.ex(myokit.Number(1, myokit.units.pF)) self.assertIn(cellml.NS_CELLML_1_1, xml) # CellML 1.2 self.assertRaisesRegex(ValueError, 'Unknown CellML version', cellml.CellMLExpressionWriter, '1.2') # CellML 2.0 w = cellml.CellMLExpressionWriter('2.0') w.set_unit_function(lambda x: units[x]) xml = w.ex(myokit.Number(1, myokit.units.pF)) self.assertIn(cellml.NS_CELLML_2_0, xml)
def setUpClass(cls): # CellML requires unit mapping units = { myokit.parse_unit('pF'): 'picofarad', } cls.w = cellml.CellMLExpressionWriter() cls.w.set_unit_function(lambda x: units[x]) model = myokit.Model() component = model.add_component('c') cls.avar = component.add_variable('a') # Requires valid model with unames set cls.avar.set_rhs(0) cls.avar.set_binding('time') model.validate() # MathML opening and closing tags cls._math = re.compile(r'^<math [^>]+>(.*)</math>$', re.S)
def model( self, path, model, protocol=None, add_hardcoded_pacing=True, pretty_xml=True): """ Writes a CellML model to the given filename. Arguments: ``path`` The path/filename to write the generated code too. ``model`` The model to export ``protocol`` This argument will be ignored: protocols are not supported by CellML. ``add_hardcoded_pacing`` Set this to ``True`` to add a hardcoded pacing signal to the model file. This requires the model to have a variable bound to `pace`. ``pretty_xml`` Set this to ``True`` to write the output in formatted "pretty" xml. Notes about CellML export: * CellML expects a unit for every number present in the model. Since Myokit allows but does not enforce this, the resulting CellML file may only validate with unit checking disabled. * Files downloaded from the CellML repository typically have a pacing stimulus embedded in them, while Myokit views models and pacing protocols as separate things. To generate a model file with a simple embbeded protocol, add the optional argument ``add_hardcoded_pacing=True``. """ path = os.path.abspath(os.path.expanduser(path)) import myokit.formats.cellml as cellml # Clear log self.logger().clear() self.logger().clear_warnings() # Replace the pacing variable with a hardcoded stimulus protocol if add_hardcoded_pacing: # Check for pacing variable if model.binding('pace') is None: self.logger().warn( 'No variable bound to "pace", unable to add hardcoded' ' stimulus protocol.') else: # Clone model before making changes model = model.clone() # Get pacing variable pace = model.binding('pace') # Set basic properties for pace pace.set_unit(myokit.units.dimensionless) pace.set_rhs(0) pace.set_binding(None) pace.set_label(None) # Should already be true... # Get time variable of cloned model time = model.time() # Get time unit time_unit = time.unit(mode=myokit.UNIT_STRICT) # Get correction factor if using anything other than # milliseconds (hardcoded below) try: time_factor = myokit.Unit.conversion_factor( 'ms', time_unit) except myokit.IncompatibleUnitError: time_factor = 1 # Create new component for the pacing variables component = 'stimulus' if model.has_component(component): root = component number = 1 while model.has_component(component): number += 1 component = root + '_' + str(number) component = model.add_component(component) # Move pace. This will be ok any references: since pace was # bound it cannot be a nested variable. # While moving, update its name to avoid conflicts with the # hardcoded names. pace.parent().move_variable(pace, component, new_name='pace') # Add variables defining pacing protocol period = component.add_variable('period') period.set_unit(time_unit) period.set_rhs(str(1000 * time_factor) + ' ' + str(time_unit)) offset = component.add_variable('offset') offset.set_unit(time_unit) offset.set_rhs(str(100 * time_factor) + ' ' + str(time_unit)) duration = component.add_variable('duration') duration.set_unit(time_unit) duration.set_rhs(str(2 * time_factor) + ' ' + str(time_unit)) # Add corrected time variable ctime = component.add_variable('ctime') ctime.set_unit(time_unit) ctime.set_rhs( time.qname() + ' - floor(' + time.qname() + ' / period) * period') # Remove any child variables pace might have before changing # its RHS (which needs to refer to them). pace_kids = list(pace.variables()) for kid in pace_kids: pace.remove_variable(kid, recursive=True) # Set new RHS for pace pace.set_rhs( 'if(ctime >= offset and ctime < offset + duration, 1, 0)') # Validate model model.validate() # Get time variable time = model.time() # Create model xml element emodel = et.Element('model') emodel.attrib['xmlns'] = 'http://www.cellml.org/cellml/1.0#' emodel.attrib['xmlns:cellml'] = 'http://www.cellml.org/cellml/1.0#' emodel.attrib['name'] = 'generated_model' if 'name' in model.meta: dtag = et.SubElement(emodel, 'documentation') dtag.attrib['xmlns'] = 'http://cellml.org/tmp-documentation' atag = et.SubElement(dtag, 'article') ttag = et.SubElement(atag, 'title') ttag.text = model.meta['name'] # Add custom units, create unit map exp_si = [si_units[x] for x in myokit.Unit.list_exponents()] unit_map = {} # Add si units later def add_unit(unit): """ Checks if the given unit needs to be added to the list of custom units and adds it if necessary. """ # Check if already defined if unit is None or unit in unit_map or unit in si_units: return # Create unit name name = self.custom_unit_name(unit) # Create unit tag utag = et.SubElement(emodel, 'units') utag.attrib['name'] = name # Add part for each of the 7 SI units m = unit.multiplier() for k, e in enumerate(unit.exponents()): if e != 0: tag = et.SubElement(utag, 'unit') tag.attrib['units'] = exp_si[k] tag.attrib['exponent'] = str(e) if m != 1: tag.attrib['multiplier'] = str(m) m = 1 # Or... if the unit doesn't contain any of those seven, it must be # a dimensionless unit with a multiplier. These occur in CellML # definitions when unit mismatches are "resolved" by adding # conversion factors as units. This has no impact on the actual # equations... if m != 1: tag = et.SubElement(utag, 'unit') tag.attrib['units'] = si_units[myokit.units.dimensionless] tag.attrib['exponent'] = str(1) tag.attrib['multiplier'] = str(m) # m = 1 # Add the new unit to the list unit_map[unit] = name # Add variable and expression units for var in model.variables(deep=True): add_unit(var.unit()) for e in var.rhs().walk(myokit.Number): add_unit(e.unit()) # Add si units to unit map for unit, name in si_units.items(): unit_map[unit] = name # Add components #TODO: Order components # Components can correspond to Myokit components or variables with # children! ecomps = {} # Components/Variables: elements (tags) cnames = {} # Components/Variables: names (strings) unames = set() # Unique name check def uname(name): # Create a unique component name i = 1 r = name + '_' while name in unames: i += 1 name = r + str(i) return name def export_nested_var(parent_tag, parent_name, var): # Create unique component name cname = uname(parent_name + '_' + var.uname()) cnames[var] = cname unames.add(cname) # Create element ecomp = et.SubElement(emodel, 'component') ecomp.attrib['name'] = cname ecomps[var] = ecomp # Check for nested variables with children for kid in var.variables(): if kid.has_variables(): export_nested_var(ecomp, cname, kid) for comp in model.components(): # Create unique name cname = uname(comp.name()) cnames[comp] = cname unames.add(cname) # Create element ecomp = et.SubElement(emodel, 'component') ecomp.attrib['name'] = cname ecomps[comp] = ecomp # Check for variables with children for var in comp.variables(): if var.has_variables(): export_nested_var(ecomp, cname, var) # Add variables evars = {} for parent, eparent in ecomps.items(): for var in parent.variables(): evar = et.SubElement(eparent, 'variable') evars[var] = evar evar.attrib['name'] = var.uname() # Add units unit = var.unit() unit = unit_map[unit] if unit else 'dimensionless' evar.attrib['units'] = unit # Add initial value init = None if var.is_literal(): init = var.rhs().eval() elif var.is_state(): init = var.state_value() if init is not None: evar.attrib['initial_value'] = myokit.strfloat(init) # Add variable interfaces, connections deps = model.map_shallow_dependencies( omit_states=False, omit_constants=False) for var, evar in evars.items(): # Scan all variables, iterate over the vars they depend on par = var.parent() lhs = var.lhs() dps = set(deps[lhs]) if var.is_state(): # States also depend on the time variable dps.add(time.lhs()) for dls in dps: dep = dls.var() dpa = dep.parent() # Parent mismatch: requires connection if par != dpa: # Check if variable tag is present epar = ecomps[par] tag = epar.find('variable[@name="' + dep.uname() + '"]') if tag is None: # Create variable tag tag = et.SubElement(epar, 'variable') tag.attrib['name'] = dep.uname() # Add unit unit = dep.unit() unit = unit_map[unit] if unit else 'dimensionless' tag.attrib['units'] = unit # Set interfaces tag.attrib['public_interface'] = 'in' edpa = ecomps[dpa] tag = edpa.find( 'variable[@name="' + dep.uname() + '"]') tag.attrib['public_interface'] = 'out' # Add connection for this variable comp1 = cnames[par] comp2 = cnames[dpa] vname = dep.uname() # Sort components in connection alphabetically to # ensure uniqueness if comp2 < comp1: comp1, comp2 = comp2, comp1 # Find or create connection ctag = None for con in emodel.findall('connection'): ctag = con.find( 'map_components[@component_1="' + comp1 + '"][@component_2="' + comp2 + '"]') if ctag is not None: break if ctag is None: con = et.SubElement(emodel, 'connection') ctag = et.SubElement(con, 'map_components') ctag.attrib['component_1'] = comp1 ctag.attrib['component_2'] = comp2 vtag = con.find( 'map_variables[@variable_1="' + vname + '"][variable_2="' + vname + '"]') if vtag is None: vtag = et.SubElement(con, 'map_variables') vtag.attrib['variable_1'] = vname vtag.attrib['variable_2'] = vname # Create CellMLWriter writer = cellml.CellMLExpressionWriter(units=unit_map) writer.set_element_tree_class(et) writer.set_time_variable(time) # Add equations def add_child_equations(parent): # Add the equations to a cellml component try: ecomp = ecomps[parent] except KeyError: return maths = et.SubElement(ecomp, 'math') maths.attrib['xmlns'] = 'http://www.w3.org/1998/Math/MathML' for var in parent.variables(): if var.is_literal(): continue writer.eq(var.eq(), maths) add_child_equations(var) for comp in model.components(): add_child_equations(comp) # Write xml to file doc = et.ElementTree(emodel) doc.write(path, encoding='utf-8', method='xml') if pretty_xml: # Create pretty XML import xml.dom.minidom as m xml = m.parse(path) with open(path, 'wb') as f: f.write(xml.toprettyxml(encoding='utf-8')) # Log any generated warnings self.logger().log_warnings()