Hodgkin-Huxley 4: Generating code from a model¶
By the time you’ve worked through this tutorial you will be able to:
Use the Generator class to create C or Python code representing a CellML model.
This tutorial assumes you’re already comfortable with:
Parsing an existing CellML file into a model instance;
Using the diagnostic
Validatorclass to check for syntactic issues;Using the
Importerclass to resolve and flatten imports; andUsing the
Analyserclass to check for mathematical issues in the model.Writing to files.
Code (C++)
CMakeLists.txtThe CMake file for building this tutorial;generateMembraneModel.cppEither the skeleton code, or ..generateMembraneModel_completed.cppthe completed tutorial code; andutilities.cppandutilities.hHelper functions.
Code (Python)
generateMembraneModel.pyEither the skeleton code, or ..generateMembraneModel_completed.pythe completed tutorial code;utilities.pyHelper functions.
Resources
GateModel.cellmlthe generic gate model (from Tutorial 1);PotassiumChannelModel.cellmlthe potassium channel model (from Tutorial 2);SodiumChannelModel.cellmlthe sodium channel model (from Tutorial 3);LeakageModel.cellmlan import dependency representing current leakage;MembraneModelController.cellmlan import dependency for the membrane model controller; andMembraneModel.cellmlthe file to parse.
Contents
All of the ingredients have been assembled for us to parse a membrane model so that it can be turned into runnable code using the code generation functionality. We will parse the model, resolve its imports, flatten into an import-free model, validate, analyse, and generate. By this stage you should be familiar with most of these processes: we’ll go through the code generation in detail at the end.
Step 1: Parse the existing membrane model¶
Parse the model in the “MembraneModel.cellml” file provided and print its contents to the terminal.
Show C++ snippet
// 1.a
// Read a CellML file into a std::string.
std::ifstream inFile("MembraneModel.cellml");
std::stringstream inFileContents;
inFileContents << inFile.rdbuf();
// 1.b
// Create a Parser item.
auto parser = libcellml::Parser::create();
// 1.c
// Use the parser to deserialise the contents of the string you've read and return the model.
auto model = parser->parseModel(inFileContents.str());
// 1.d
// Print the parsed model to the terminal for viewing.
printModel(model, false);
Show Python snippet
# 1.a
# Read a CellML file into a std.string.
read_file = open('MembraneModel.cellml')
# 1.b
# Create a Parser item.
parser = Parser()
# 1.c
# Use the parser to deserialise the contents of the string you've read and return the model.
model = parser.parseModel(read_file.read())
# 1.d
# Print the parsed model to the terminal for viewing.
print_model(model, False)
MODEL: 'MembraneModel'
UNITS: 5 custom units
[0]: mV
[1]: ms
[2]: mS_per_cm2
[3]: microA_per_cm2
[4]: microF_per_cm2
COMPONENTS: 2 components
[0]: controller <--- imported from: 'controller' in 'Controller.cellml'
VARIABLES: 2 variables
[0]: i_stim
└──> membrane:i_stim [microA_per_cm2]
[1]: t
└──> membrane:t [ms]
[1]: membrane
VARIABLES: 3 variables
[0]: t [ms]
└──> membraneEquations:t [ms], controller:t
[1]: i_tot [microA_per_cm2]
└──> membraneEquations:i_tot [microA_per_cm2]
[2]: i_stim [microA_per_cm2]
└──> membraneEquations:i_stim [microA_per_cm2], controller:i_stim
COMPONENT membrane has 2 child components:
[0]: membraneEquations
VARIABLES: 8 variables
[0]: V [mV]
└──> membraneParameters:V [mV], sodiumChannel:V, potassiumChannel:V, leakage:V
[1]: t [ms]
└──> membrane:t [ms], sodiumChannel:t, potassiumChannel:t
[2]: i_K [microA_per_cm2]
└──> potassiumChannel:i_K
[3]: i_Na [microA_per_cm2]
└──> sodiumChannel:i_Na
[4]: i_L [microA_per_cm2]
└──> leakage:i_L
[5]: i_stim [microA_per_cm2]
└──> membrane:i_stim [microA_per_cm2]
[6]: i_tot [microA_per_cm2]
└──> membrane:i_tot [microA_per_cm2]
[7]: Cm [microF_per_cm2]
└──> membraneParameters:Cm [microF_per_cm2]
COMPONENT membraneEquations has 3 child components:
[0]: sodiumChannel <--- imported from: 'sodiumChannel' in 'SodiumChannelModel.cellml'
VARIABLES: 3 variables
[0]: t
└──> membraneEquations:t [ms]
[1]: i_Na
└──> membraneEquations:i_Na [microA_per_cm2]
[2]: V
└──> membraneEquations:V [mV]
[1]: potassiumChannel <--- imported from: 'potassiumChannel' in 'PotassiumChannelModel.cellml'
VARIABLES: 3 variables
[0]: t
└──> membraneEquations:t [ms]
[1]: i_K
└──> membraneEquations:i_K [microA_per_cm2]
[2]: V
└──> membraneEquations:V [mV]
[2]: leakage <--- imported from: 'leakage' in 'LeakageModel.cellml'
VARIABLES: 4 variables
[0]: i_L
└──> membraneEquations:i_L [microA_per_cm2]
[1]: g_L
[2]: E_L
[3]: V
└──> membraneEquations:V [mV]
[1]: membraneParameters
VARIABLES: 2 variables
[0]: Cm [microF_per_cm2], initial = 1
└──> membraneEquations:Cm [microF_per_cm2]
[1]: V [mV], initial = 1
└──> membraneEquations:V [mV]
Step 2: Resolve the imports and flatten¶
In all of the other tutorials we’ve used a flattened model only to analyse its mathematics. This time, we need to keep the flattened version and will use this as input to the code generator. Resolve the imports, and create a flattened version of the model. We do not expect any issues to be reported by the importer.
2.a Create an Importer instance and use it to resolve the imports in your model.
2.b Check that the importer has not raised any issues.
2.c Use the importer to create a flattened version of the model.
Show C++ snippet
// 2.a
// Create an Importer instance and use it to resolve the imports in your model.
auto importer = libcellml::Importer::create();
importer->resolveImports(model, "");
// 2.b
// Check that the importer has not raised any issues.
printIssues(importer);
// 2.c
// Use the importer to create a flattened version of the model.
auto flatModel = importer->flattenModel(model);
Show Python snippet
# 2.a
# Create an Importer instance and use it to resolve the imports in your model.
importer = Importer()
importer.resolveImports(model, '')
# 2.b
# Check that the importer has not raised any issues.
print_issues(importer)
# 2.c
# Use the importer to create a flattened version of the model.
flatModel = importer.flattenModel(model)
Step 3: Validate and analyse the flattened model¶
You know what to do … we do not expect any issues to be raised by either the validator or the analyser.
3.a Create a Validator instance, pass in the flattened model, and check that there are no issues raised.
3.b Create an Analyser instance, pass in the flattened model, and check that there are no issues raised.
Show C++ snippet
// 3.a
// Create a Validator instance, pass in the flattened model, and check that
// there are no issues raised.
auto validator = libcellml::Validator::create();
validator->validateModel(flatModel);
printIssues(validator);
// 3.b
// Create an Analyser instance,pass in the flattened model, and check that
// there are no issues raised.
auto analyser = libcellml::Analyser::create();
analyser->analyseModel(flatModel);
printIssues(analyser);
Show Python snippet
# 3.a
# Create a Validator instance, pass in the flattened model, and check that
# there are no issues raised.
validator = Validator()
validator.validateModel(flatModel)
print_issues(validator)
# 3.b
# Create an Analyser instance, pass in the flattened model, and check that
# there are no issues raised.
analyser = Analyser()
analyser.analyseModel(flatModel)
print_issues(analyser)
Step 4: Generate code and output¶
The Generator is a translator class that will change the CellML model and its MathML equations into a representation in another language.
This is done using a GeneratorProfile to specify a dictionary of mathematical operations.
Two profiles are already defined; for C++ and for Python.
4.a Create a Generator instance.
4.b Create a GeneratorProfile object, and use the constructor argument of the libcellml::GeneratorProfile::Profile enum for the language you want (C or PYTHON).
4.c Use the generator’s setProfile function to pass in the profile item you just created.
Show C++ snippet
// 4.a
// Create a Generator instance.
auto generator = libcellml::Generator::create();
// 4.b
// Create a GeneratorProfile object, and use the constructor argument of the
// libcellml::GeneratorProfile::Profile enum for the language you want (C or PYTHON).
auto profile = libcellml::GeneratorProfile::create(libcellml::GeneratorProfile::Profile::C);
// 4.c
// Use the generator's setProfile function to pass in the profile item you just created.
generator->setProfile(profile);
Show Python snippet
# 4.a
# Create a Generator instance.
generator = Generator()
# 4.b
# The generator uses a GeneratorProfile item to set up a translation between the
# model stored as CellML and the language of your choice (currently C or Python).
# Create a GeneratorProfile object, and use the constructor argument of the
# GeneratorProfile.Profile enum for the language you want (C or PYTHON).
profile = GeneratorProfile(GeneratorProfile.Profile.PYTHON)
# 4.c
# Use the generator's setProfile function to pass in the profile item you just created.
generator.setProfile(profile)
Instead of submitting a Model item (as we do for all other classes), the Generator class will work from something which has already been processed by the Analyser class: an AnalyserModel object.
4.d Retrieve the analysed model using the Analyser::model() function, and submit to the generator using the Generator::setModel(analysedModel) function.
4.e (C only) If you’re using the C profile then you have the option at this stage to specify the file name of the interface file you’ll create in the next step.
This means that the two files will be prepared to link to one another without manual editing later.
You can do this by specifying the header file name in the GeneratorProfile item using its setInterfaceFileNameString function.
This will need to be the same as the file which you write to in step 4.g below.
Show C++ snippet
// 4.d
// Retrieve the analysed model using the Analyser::model() function, and submit
// to the generator using the Generator::setModel(analysedModel) function.
generator->setModel(analyser->model());
// 4.e
// You can do this by specifying the header file name in the GeneratorProfile item
// using the setInterfaceFileNameString("yourHeaderFileNameHere.h") function.
// This will need to be the same as the file which you write to in step 4.g below.
profile->setInterfaceFileNameString("HodgkinHuxleyModel.h");
Show Python snippet
# 4.d
# Instead of submitting a Model item (as we do for all other classes),
# the Generator class will work from something which has already been processed
# by the Analyser class: an AnalyserModel object.
# Retrieve the analysed model using the Analyser.model() function, and submit
# to the generator using the Generator.setModel(analysedModel) function.
generator.setModel(analyser.model())
4.f Implementation code is the bulk of the model, and contains all the equations, variables, units etc.
This is needed for both of the available profiles, and would normally be stored in a *.cpp or *.py file.
Use the implementationCode function to return the implementation code as a string, and write it to a file with the appropriate extension.
4.g (C only) Interface code is the header needed by the C profile to define data types.
Use the interfaceCode function to return interface code as a string and write it to a *.h header file.
This needs to be the same filename as you specified in step 4.e above.
Show C++ snippet
// 4.f
// Use the Generator::implementationCode() function to return the implementation
// code as a string, and write it to a file with the appropriate extension.
std::ofstream outFile("HodgkinHuxleyModel.cpp");
outFile << generator->implementationCode();
outFile.close();
// 4.g
// (C only) Interface code is the header needed by the C profile to define data types.
// Use the Generator::interfaceCode() function to return interface code as a string
// and write it to a *.h header file. This needs to be the same filename as you
// specified in step 4.e above.
outFile.open("HodgkinHuxleyModel.h");
outFile << generator->interfaceCode();
outFile.close();
Show Python snippet
# 4.f
# Implementation code is the bulk of the model, and contains all the equations,
# variables, units etc. This is needed for both of the available profiles, and
# would normally be stored in a.cpp or.py file.
# Use the Generator.implementationCode() function to return the implementation
# code as a string, and write it to a file with the appropriate extension.
write_file = open('HodgkinHuxleyModel.py', 'w')
write_file.write(generator.implementationCode())
write_file.close()
# 4.g
# (C profile only) Interface code is the header needed by the C profile to define data types.
# Use the Generator.interfaceCode() function to return interface code as a string
# and write it to a.h header file. This needs to be the same filename as you
# specified in step 4.e above.
# write_file = open('HodgkinHuxleyModel.h', 'w')
# write_file.write(generator.interfaceCode())
# write_file.close()