def _get_stimulus(model, skip_chaste_stimulus_conversion): """ Store the stimulus currents in the model""" stim_params_orig, stim_params, return_stim_eqs = set(), set(), set() if not skip_chaste_stimulus_conversion: try: # Get required stimulus parameters for tag, unit, required in STIM_PARAM_TAGS: param = model.get_variable_by_ontology_term((OXMETA, tag)) stim_params_orig.add(param) # originals ones stim_params.add( model.convert_variable( param, model.conversion_units.get_unit(unit), DataDirectionFlow.INPUT)) except KeyError: if required: # Optional params are allowed to be missing LOGGER.info('The model has no default stimulus params tagged.') return set(), [] except TypeError as e: if str( e ) == "unsupported operand type(s) for /: 'HeartConfig::Instance()->GetCapacitance' and 'NoneType'": e = CodegenError( "Membrane capacitance is required to be able to apply conversion to stimulus current!" ) raise (e) return_stim_eqs = get_equations_for( model, stim_params, filter_modifiable_parameters_lhs=False) return stim_params | stim_params_orig, return_stim_eqs
def load_model_with_conversions(model_file, use_modifiers=False, quiet=False, skip_singularity_fixes=False, skip_conversions=False): if quiet: LOGGER.setLevel(logging.ERROR) try: model = cellmlmanip.load_model(model_file) except Exception as e: raise CodegenError('Could not load cellml model: \n ' + str(e)) if not skip_singularity_fixes: V = model.get_variable_by_ontology_term((OXMETA, 'membrane_voltage')) tagged = set( model.get_variables_by_rdf((PYCMLMETA, 'modifiable-parameter'), 'yes', sort=False)) annotated = set( filter(lambda q: model.has_ontology_annotation(q, OXMETA), model.variables())) excluded = (tagged | annotated) - set( model.get_derived_quantities(sort=False)) model.remove_fixable_singularities(V, exclude=excluded) if not skip_conversions: add_conversions(model, use_modifiers=use_modifiers) return model
def _get_time_variable(model): """ Get the variable representing time (the free variable) and convert to milliseconds""" try: time_variable = model.get_free_variable() except ValueError: raise CodegenError('The model has no free variable (time)!') # Annotate as quantity and not modifiable parameter set_is_metadata(model, time_variable, 'derived-quantity') try: # If the variable is in units that can be converted to millisecond, perform conversion return model.convert_variable( model.get_free_variable(), model.conversion_units.get_unit('millisecond'), DataDirectionFlow.INPUT) except DimensionalityError: raise CodegenError( ("Incorrect definition of free variable (time): " "time needs to be dimensionally equivalent to second!"))
def _get_membrane_voltage_var(model, convert=True): """ Find the membrane_voltage variable""" try: # Get and convert V voltage = model.get_variable_by_ontology_term( (OXMETA, 'membrane_voltage')) if not convert: return voltage voltage = model.convert_variable( voltage, model.conversion_units.get_unit('millivolt'), DataDirectionFlow.INPUT) except KeyError: raise CodegenError('Voltage not tagged in the model!') except DimensionalityError: raise CodegenError( 'Incorrect definition of membrane_voltage variable: ' 'units of membrane_voltage need to be dimensionally equivalent to Volt' ) annotate_if_not_statevar( model, voltage) # If V is not state var annotate as appropriate. return voltage
def _get_extended_ionic_vars(model): """ Get the equations defining the ionic derivatives and all dependant equations""" # Update equations to include i_ionic_eq & defining eqs, set stimulus current to 0 extended_eqs = [ eq if eq.lhs != model.membrane_stimulus_current_orig else Eq( eq.lhs, 0.0) for eq in get_equations_for(model, model.ionic_vars) if eq.lhs != model.membrane_stimulus_current_converted ] if model.time_variable in model.find_variables_and_derivatives( extended_eqs): raise CodegenError( 'Ionic variables should not be a function of time. ' 'This is often caused by missing membrane_stimulus_current tag.') return extended_eqs
def get_variables_transitively(model, term): """Return a list of variables annotated (directly or otherwise) with the given ontology term. Direct annotations are those variables annotated with the term via the bqbiol:is or bqbiol:isVersionOf predicates. However we also look transitively through the 'oxmeta' ontology for terms belonging to the RDF class given by ``term``, i.e. are connected to it by a path of ``rdf:type`` predicates, and return variables annotated with those terms. For example, the oxmeta ontology has a term ``oxmeta:ExtracellularConcentration``, and triples: - ``oxmeta:extracellular_calcium_concentration rdf:type oxmeta:ExtracellularConcentration`` - ``oxmeta:extracellular_sodium_concentration rdf:type oxmeta:ExtracellularConcentration`` So if you have variables ``Ca_o`` annotated with ``oxmeta:extracellular_calcium_concentration`` and ``Na_o`` annotated with ``oxmeta:extracellular_sodium_concentration``, then calling ``get_variables_transitively(model, oxmeta:ExtracellularConcentration)`` would give you the list ``[Ca_o, Na_o]``. :param term: the ontology term to search for. Can be anything suitable as input to :meth:`create_rdf_node`, typically a :class:`rdflib.term.Node` or ``(ns_uri, local_name)`` pair. :return: a list of :class:`cellmlmanip.model.Variable` objects, sorted by order added to the model. """ assert isinstance(term, tuple), "Expecting term to be a namespace tuple" assert isinstance(model, Model), "Expecting model to be a cellmlmanip Model" global _ONTOLOGY if _ONTOLOGY is None: # Load oxmeta ontology _ONTOLOGY = rdflib.Graph() ttl_file = os.path.join(MODULE_DIR, 'ontologies', 'oxford-metadata.ttl') _ONTOLOGY.parse(ttl_file, format='turtle') term = create_rdf_node(term) cmeta_ids = set() for annotation in _ONTOLOGY.transitive_subjects(rdflib.RDF.type, term): cmeta_ids.update(model.rdf.subjects(PRED_IS, annotation)) cmeta_ids.update(model.rdf.subjects(PRED_IS_VERSION_OF, annotation)) variables = [] for cmeta_id in cmeta_ids: try: variables.append(model.get_variable_by_cmeta_id(cmeta_id)) except KeyError as e: assert 'cmeta id' in str(e) and str(cmeta_id).replace('#', '', 1) in str(e), str(e) raise CodegenError('Rdf subject %s does not refer to any existing variable in the model.' % cmeta_id) variables.sort(key=lambda sym: sym.order_added) return variables
def process_command_line(): # add options for command line interface parser = argparse.ArgumentParser( description='Chaste code generation for cellml.') # Options for added pycml backwards compatibility, these are now always on parser.add_argument('--Wu', '--warn-on-units-errors', action='store_true', help=argparse.SUPPRESS) parser.add_argument('-A', '--fully-automatic', action='store_true', help=argparse.SUPPRESS) parser.add_argument('--expose-annotated-variables', action='store_true', help=argparse.SUPPRESS) parser.add_argument( '--version', action='version', version='%(prog)s {version}'.format(version=cg.__version__)) parser.add_argument( 'cellml_file', metavar='cellml_file', help='The cellml file or URI to convert to chaste code') group = parser.add_argument_group( 'ModelTypes', 'The different types of solver approach for which code can be ' 'generated; if no model type is set, "normal" models are generated') for k in TRANSLATORS: group.add_argument('--' + k, help='Generate ' + k + ' model type' + TRANSLATORS[k][4], action='store_true') group = parser.add_argument_group( 'Transformations', 'These options control which transformations ' '(typically optimisations) are applied in the generated code') group.add_argument( '-j', '--use-analytic-jacobian', dest='use_analytic_jacobian', default=False, action='store_true', help='use a symbolic Jacobian calculated by SymPy ' '(--use-analytic-jacobian only works in combination with --cvode' ' and is ignored for other model types)') group = parser.add_argument_group('Generated code options') group.add_argument( '-o', dest='outfile', metavar='OUTFILE', default=None, help='write program code to OUTFILE ' '[default action is to use the input filename with a different extension] ' 'NOTE: expects provided OUTFILE to have an extension relevant to code being generated ' '(e.g. for CHASTE/C++ code: .cpp, .c, .hpp, or .h)') group.add_argument('--output-dir', action='store', help="directory to place output files in", default=None) group.add_argument( '--show-outputs', action='store_true', default=False, help= "don't actually generate code, just show what files would be generated, one per line" ) group.add_argument('-c', default=None, dest='cls_name', help='explicitly set the name of the generated class') group.add_argument( '-q', '--quiet', action='store_true', default=False, help="quiet operation, don't print informational messages to screen") group.add_argument( '--skip-singularity-fixes', action='store_true', default=False, help="skip singularity fixes in Goldman-Hodgkin-Katz (GHK) equations.") group = parser.add_argument_group( 'Chaste options', description='Options specific to Chaste code output') group.add_argument( '-y', '--dll', '--dynamically-loadable', dest='dynamically_loadable', action='store_true', default=False, help= 'add code to allow the model to be compiled to a shared library and dynamically loaded ' ) group.add_argument( '--opt', action='store_true', default=False, help="apply default optimisations to all generated code") group.add_argument( '-m', '--use-modifiers', dest='modifiers', action='store_true', default=False, help= 'add modifier functions for metadata-annotated variables (except time & stimulus) ' 'for use in sensitivity analysis. Only works with one of the following model types and is ' 'ignored for others: ' + str(TRANSLATORS_WITH_MODIFIERS)) lut_metavar = ("<metadata tag>", "min", "max", "step") group.add_argument( '--lookup-table', nargs=4, default=None, action='append', metavar=lut_metavar, help= 'Specify variable (using a metadata tag) and ranges for which to generate lookup tables ' '(optional). --lookup-table can be added multiple times to indicate multiple lookup tables' '. Please note: Can only be used in combination with --opt. If the arguments are omitted, ' 'following defaults will be used: %s.' % print_default_lookup_params()) group.add_argument( '--use-model-factory', action='store_true', default=False, help='Make use of ModelFactoy method to allow creating models by name. ' 'Requires ModelFactory.hpp/cpp found in the ApPredict project.') # process options args = parser.parse_args() if not os.path.isfile(args.cellml_file): raise CodegenError("Could not find cellml file %s " % args.cellml_file) if args.outfile is not None and args.output_dir is not None: raise CodegenError("-o and --output-dir cannot be used together!") if args.lookup_table and not args.opt: raise CodegenError( "Can only use lookup tables in combination with --opt") # make sure --lookup-table entries are 1 string and 3 floats if args.lookup_table is None: args.lookup_table = DEFAULT_LOOKUP_PARAMETERS else: for _, lut in enumerate(args.lookup_table): lut_params_mgs = \ 'Lookup tables are expecting the following %s values: %s' % (len(lut_metavar), " ".join(lut_metavar)) assert len(lut) == len(lut_metavar), lut_params_mgs try: # first argument is a string float(lut[0]) is_float = True except ValueError: is_float = False # We are expecting this to be a string if is_float: raise CodegenError(lut_params_mgs) for i in range(1, len(lut)): # next 3 arguments are floats try: lut[i] = float(lut[i]) except ValueError: raise CodegenError(lut_params_mgs) # if no model type is set assume normal args.normal = args.normal or not any([ getattr(args, model_type.replace('-', '_')) for model_type in TRANSLATORS ]) # create list of translators to apply translators = [] for model_type in TRANSLATORS: use_translator_class = getattr(args, model_type.replace('-', '_')) if use_translator_class: # if -o or dynamically_loadable is selected with opt, only convert opt model type if not args.opt or (not args.outfile and not args.dynamically_loadable): translators.append(TRANSLATORS[model_type]) if args.opt and model_type in TRANSLATORS_OPT: translators.append(TRANSLATORS_OPT[model_type]) # An outfile cannot be set with multiple translations if args.outfile and len(translators) > 1: raise CodegenError( "-o cannot be used when multiple model types have been selected!") # Dynamically loadable models can only be built one at a time if args.dynamically_loadable and len(translators) > 1: raise CodegenError( "Only one model type may be specified if creating a dynamic library!" ) if not args.show_outputs: # Load model once, not once per translator, but only if we're actually generating code model = load_model_with_conversions( args.cellml_file, use_modifiers=args.modifiers, quiet=args.quiet, skip_singularity_fixes=args.skip_singularity_fixes, skip_conversions=skip_conversion(args)) if len(translators) > 1 and skip_conversion(args): raise CodegenError(( '--rush-larsen-labview and --rush-larsen-c ' 'cannot be used in combination with other model convertion types')) for translator in translators: # Make sure modifiers are only passed to models which can generate them args.use_modifiers = args.modifiers and translator[3] translator_class = translator[0] outfile_path, model_name_from_file, outfile_base, ext = \ get_outfile_parts(args.outfile, args.output_dir, args.cellml_file, translator_class) ext = ext if ext else translator_class.DEFAULT_EXTENSIONS if args.cls_name is not None: args.class_name = args.cls_name else: args.class_name = ('Dynamic' if args.dynamically_loadable else 'Cell') + model_name_from_file +\ translator[1] args.cellml_base = outfile_base if not args.outfile: outfile_base += translator[2] # generate code Based on parameters a different class of translator may be used get_files = [] for ex in ext: if ex is not None: get_files.append(os.path.join(outfile_path, outfile_base + ex)) if args.show_outputs: for file in get_files: print(file) else: with translator_class(model, outfile_base, header_ext=ext[0], **vars(args)) as chaste_model: chaste_model.generate_chaste_code() for file, code in zip(get_files, chaste_model.generated_code): write_file(file, code)