Import debugger¶
This example walks through the processes involved with debugging models using the Importer class in conjunction with the diagnostic classes, Validator and Analyser.
For more general debugging use cases, please see the Model debugger example instead.
Parse an existing CellML model¶
The first step is to parse and instantiate a model from a CellML file.
Here we use the Parser class to read the file, and deserialise the CellML into a Model object.
// Read the file contents into a string.
std::string inFileName = "resources/importExample1.cellml";
std::ifstream inFile(inFileName);
std::stringstream inFileContents;
inFileContents << inFile.rdbuf();
// Create a Parser and use it to deserialise the string into a model.
auto parser = libcellml::Parser::create();
auto originalModel = parser->parseModel(inFileContents.str());
printIssues(parser);
// end 1
std::cout << "--------------------------------------------------------" << std::endl;
std::cout << " STEP 2: Create an Importer instance " << std::endl;
std::cout << "--------------------------------------------------------" << std::endl;
# Parse an existing CellML model from a file.
read_file = open('resources/importExample1.cellml', 'r')
parser = Parser()
original_model = parser.parseModel(read_file.read())
Create an Importer and resolve the model’s imports¶
The Importer class is a utility which can be used to handle models which import components or units from other models.
Passing the model and a base directory location will enable the Importer to resolve any imports required by the model, and add those dependencies to the importer’s internal library.
Since the Importer class contains a logger, we can check that the process has completed properly by printing any issues inside the importer to the terminal.
// Create the importer instance.
auto importer = libcellml::Importer::create();
// Resolve the imports.
importer->resolveImports(originalModel, "resources/");
// Check for issues.
std::cout << std::endl;
std::cout << "The importer found " << importer->issueCount() << " issues." << std::endl;
for(size_t i = 0; i < importer->issueCount(); ++i) {
auto issue = importer->issue(i);
std::cout << issue->description() << std::endl;
}
std::cout << std::endl;
// Fix the circular reference issue by setting the URL for sideB to be importExample3.cellml
// instead of circularImport1.cellml. The model can be accessed from the importer's library -
// you don't have to parse it yourself.
auto modelToRepair = importer->library("resources/importExample2b.cellml");
// Clear the imports from the model to repair.
importer->clearImports(modelToRepair);
// Fix the URL.
modelToRepair->component("sideB")->importSource()->setUrl("importExample3.cellml");
// Recheck the importer. You will need to clear previous issues first.
importer->removeAllIssues();
importer->resolveImports(originalModel,"resources/");
// Check that the import circular dependence has been removed.
std::cout << std::endl;
std::cout << "The importer found " << importer->issueCount() << " issues." << std::endl;
for(size_t i = 0; i < importer->issueCount(); ++i) {
auto issue = importer->issue(i);
std::cout << issue->description() << std::endl;
}
std::cout << std::endl;
// end 2
std::cout << "--------------------------------------------------------" << std::endl;
std::cout << " STEP 3: Flatten the model and use diagnostic tools " << std::endl;
std::cout << "--------------------------------------------------------" << std::endl;
# Create an Importer to resolve the imports in the model.
importer = Importer()
# Resolve the imports.
importer.resolveImports(original_model, 'resources/')
# Check for issues.
print('The importer found {} issues.'.format(importer.issueCount()))
for i in range(0,importer.issueCount()):
print(importer.issue(i).description())
print()
The code above prints to the terminal:
The importer found 0 issues.
Validate and analyse the model¶
The analysis tools - the Validator and Analyser - will read only the highest level of the model they are given; they do not look into any of the imported items, so they can’t check them.
In order to retain the import structure but be able to use the diagnostic tools, we can create a flattened copy of the model for testing.
This can be used to identify mistakes in the original version of the model.
// The analysis tools - the Validator and Analyser - will read only the submitted
// model; they do not look into any of the imported items, so they can't check them.
// In order to retain the import structure but be able to use the diagnostic tools,
// we can either:
// - create a flattened copy of the model for testing, which can be used to
// identify mistakes in the unflattened model too; or
// - make use of the Importer's library to iterate through all of the model's
// imported dependencies, and check them individually.
// Create a Validator and Analyser and submit the original, unflattened model.
// We don't expect either of these to report any issues because all of the mistakes are
// in imported models which are not read by these classes.
auto validator = libcellml::Validator::create();
validator->validateModel(originalModel);
std::cout << "Investigating the original model:" << std::endl;
std::cout << " - the validator found " << validator->issueCount() << " issues." << std::endl;
for(size_t i = 0; i < validator->issueCount(); ++i) {
std::cout << " - " << validator->issue(i)->description() << std::endl;
}
auto analyser = libcellml::Analyser::create();
analyser->analyseModel(originalModel);
std::cout << " - the analyser found " << analyser->issueCount() << " issues." << std::endl;
for(size_t i = 0; i < analyser->issueCount(); ++i) {
std::cout << " - " << analyser->issue(i)->description() << std::endl;
}
// Create a flattened version to demonstrate the diagnostics.
auto flatModel = importer->flattenModel(originalModel);
// Repeat the validation and analysis above on the flattened model, noting that the
// flat model contains errors that were hidden in the original one.
validator->validateModel(flatModel);
std::cout << std::endl << "Investigating the flattened model:" << std::endl;
std::cout << "The validator found " << validator->issueCount() << " issues." << std::endl;
for(size_t i = 0; i < validator->issueCount(); ++i) {
std::cout << " - " << validator->issue(i)->description() << std::endl;
}
analyser->analyseModel(flatModel);
std::cout << "The analyser found "<< analyser->issueCount() << " issues." << std::endl;
for(size_t i = 0; i < analyser->issueCount(); ++i) {
std::cout << " - " << analyser->issue(i)->description() << std::endl;
}
std::cout << std::endl;
// end 3
std::cout << "--------------------------------------------------------" << std::endl;
std::cout << " STEP 4: Investigate individual imported models " << std::endl;
std::cout << "--------------------------------------------------------" << std::endl;
# The analysis tools - the Validator and Analyser - will read only the submitted
# model they do not look into any of the imported items, so they can't check them.
# In order to retain the import structure but be able to use the diagnostic tools,
# we can create a flattened copy of the model for testing. This can be used to
# identify mistakes in the unflattened model too.
# Create a Validator and Analyser and submit the original, unflattened model.
# We don't expect either of these to report any issues.
validator = Validator()
validator.validateModel(original_model)
print('Investigating the original model:')
print(' - the validator found {} issues'.format(validator.issueCount()))
for i in range(0, validator.issueCount()):
print(' - {}'.format(validator.issue(i).description()))
analyser = Analyser()
analyser.analyseModel(original_model)
print(' - the analyser found {} issues'.format(analyser.issueCount()))
for i in range(0, analyser.issueCount()):
print(' - {}'.format(analyser.issue(i).description()))
print()
# Create a flattened version for diagnostics.
flat_model = importer.flattenModel(original_model)
# Repeat the validation and analysis above on the flattened model.
validator.validateModel(flat_model)
print('Investigating the flattened model:')
print(' - the validator found {} issues'.format(validator.issueCount()))
for i in range(0, validator.issueCount()):
print(' - {}'.format(validator.issue(i).description()))
analyser.analyseModel(flat_model)
print(' - the analyser found {} issues'.format(analyser.issueCount()))
for i in range(0, analyser.issueCount()):
print(' - {}'.format(analyser.issue(i).description()))
print()
Investigating the original model:
The validator found 0 issues.
The analyser found 0 issues.
Investigating the flattened model:
The validator found 4 issues.
- CellML identifiers must contain one or more basic Latin alphabetic characters.
- Variable 'i_need_units' does not have a valid units attribute.
- CellML identifiers must contain one or more basic Latin alphabetic characters.
- Variable 'i_need_units' does not have a valid units attribute.
The analyser found 4 issues.
- CellML identifiers must contain one or more basic Latin alphabetic characters.
- Variable 'i_need_units' does not have a valid units attribute.
- CellML identifiers must contain one or more basic Latin alphabetic characters.
- Variable 'i_need_units' does not have a valid units attribute.
Validate the import dependencies¶
The issues reported by the Validator and Analyser items in the previous step have been hidden from the original model, as they exist somewhere in its hierarchy of imported items.
We can make use of the library of import dependencies which is stored in the Importer class to locate the specific models in which the issues occurred, and repair them.
// The Validator and Analyser classes process only the contents of concrete items (ie: not the contents of
// imported items) of a model.
// After successfully resolving a model's imports using an importer, the importer will store instances
// of all of the dependencies of the resolved model. These are accessible through the "library" function.
// We can ascertain that all of import dependencies meet the diagnostic checks of the Validator and the
// Analyser individually by iterating through the importer's library.
// Loop through the importer library and call the validator for each model.
for(size_t m = 0; m < importer->libraryCount(); ++m) {
// Retrieve the library model by index, m.
validator->validateModel(importer->library(m));
// Retrieve the key under which it's stored: this will be the URL at which the imported model was found.
std::cout << "The validator found "<<validator->issueCount() << " issues in " <<importer->key(m) << std::endl;
for(size_t i = 0; i < validator->issueCount(); ++i) {
std::cout << " - " << validator->issue(i)->description() << std::endl;
}
}
std::cout << std::endl;
// end 4
std::cout << "--------------------------------------------------------" << std::endl;
std::cout << " STEP 5: Fix the validation errors " << std::endl;
std::cout << "--------------------------------------------------------" << std::endl;
# The Validator and Analyser classes process only the contents of concrete items (ie: not the contents of
# imported items) of a model.
# After successfully resolving a model's imports using an importer, the importer will store instances
# of all of the dependencies of the resolved model. These are accessible through the "library" function.
# We can ascertain that all of import dependencies meet the diagnostic checks of the Validator and the
# Analyser individually by iterating through the importer's library.
# Loop through the importer library and call the validator for each model.
for m in range(0, importer.libraryCount()):
# Retrieve the library model by index, m.
validator.validateModel(importer.library(m))
# Retrieve the key under which it's stored: this will be the URL at which the imported model was found.
print("The validator found {} issues in {}.".format(validator.issueCount(),importer.key(m)))
for i in range(0, validator.issueCount()):
print(" - {}".format(validator.issue(i).description()))
print()
The code above will print the following to the terminal:
The validator found 0 issues in resources/importExample2a.cellml
The validator found 0 issues in resources/importExample2b.cellml
The validator found 2 issues in resources/importExample3.cellml
- CellML identifiers must contain one or more basic Latin alphabetic characters.
- Variable 'iNeedUnits' does not have a valid units attribute.
Fix the errors in the imported model¶
Now that we’ve found where the error is (a variable named “iNeedUnits” within a component which is imported from a file at URL “resources/importExample3.cellml”), it’s now time to fix it.
This could be done by parsing that file directly and amending it as needed, but since we already have a copy of this model inside the Importer class, it’s better to use that one.
When the importer resolves imports, any dependencies it needs are instantiated inside its library.
These models can be accessed within the library by their key (the URL from which they were read) or by their index.
Here we print the list of the importer’s library keys to demonstrate, and then retrieve the model containing the error so it can be fixed.
Note that altering a model instance returned from the Importer changes the model instance which is used to resolve the original model’s dependencies.
The Importer checks its own library first before opening and parsing external files, so once we’ve fixed the error in the library’s model, re-flattening the original model uses the fixed version too.
// Fix the validation errors in the imported files.
// According to the printout above, we need to add units to the "iNeedUnits"
// variable, to be found inside the "importExample3.cellml" file.
// To fix this, we need to fix the model inside the "importExample3.cellml" file.
// When the originalModel's imports were resolved, this model was added to the
// library in the Importer. We can retrieve the model from there for repair.
// Retrieve from the library by key. Note the inclusion of the directory used to
// resolve imports for the original model is included in the key string.
auto importExample3 = importer->library("resources/importExample3.cellml");
// Add units to the variable that needs them to fix the validation error.
importExample3->component("shared")->variable("i_need_units")->setUnits("dimensionless");
// Check that the issues have been fixed.
validator->validateModel(importExample3);
std::cout << "Investigating the repaired model: importExample3" << std::endl;
std::cout << "The validator found "<<validator->issueCount() << " issues." << std::endl;
for(size_t i = 0; i < validator->issueCount(); ++i) {
std::cout << " - " << validator->issue(i)->description() << std::endl;
}
std::cout << std::endl;
// end 5
std::cout << "--------------------------------------------------------" << std::endl;
std::cout << " STEP 6: Fix the analysis errors " << std::endl;
std::cout << "--------------------------------------------------------" << std::endl;
# Fix the validation errors in the imported files.
# According to the printout above, we need to add units to the "iNeedUnits"
# variable, to be found inside the "importExample3.cellml" file.
# To fix this, we need to fix the model inside the "importExample3.cellml" file.
# When the original_model's imports were resolved, this model was added to the
# library in the Importer. We can retrieve the model from there for repair.
# Retrieve from the library by key. Note the inclusion of the directory used to
# resolve imports for the original model is included in the key string.
imported_model1 = importer.library('resources/importExample3.cellml')
# Add units to the variable that needs them to fix the validation error.
imported_model1.component('importThisOneToo').variable('iNeedUnits').setUnits('dimensionless')
# Check that the issues have been fixed.
validator.validateModel(imported_model1)
print('\nInvestigating the repaired model:')
print('The validator found {} issues.'.format(validator.issueCount()))
for i in range(0, validator.issueCount()):
print(' - {}'.format(validator.issue(i).description()))
print()
Analyse the import dependencies¶
This step repeats the two previous steps but uses the Analyser class instead of the Validator class to report on issues.
We can then fix the issues found in the same way, by accessing the library’s version of the imported model and repairing the problem there.
// Repeat steps 4 and 5 using the Analyser instead of the Validator.
// Loop through the importer library and call the analyser for each model.
for(size_t m = 0; m < importer->libraryCount(); ++m) {
// Retrieve the library model by index, m.
analyser->analyseModel(importer->library(m));
// Retrieve the key under which it's stored: this will be the URL at which the imported model was found.
std::cout << "The analyser found " << analyser->issueCount() << " issues in " <<importer->key(m) << std::endl;
for(size_t i = 0; i < analyser->issueCount(); ++i) {
std::cout << " - " << analyser->issue(i)->description() << std::endl;
}
}
std::cout << std::endl;
// Fix the error by setting an initial value for the variable named 'some_other_variable'
// inside component 'shared' inside model imported from importExample3.cellml.
importExample3->component("shared")->variable("some_other_variable")->setInitialValue(3);
// Check that the issue has been fixed.
analyser->analyseModel(importExample3);
std::cout << std::endl << "Investigating the repaired model: importExample3" << std::endl;
std::cout << "The analyser found " << analyser->issueCount() << " issues." << std::endl;
for(size_t i = 0; i < analyser->issueCount(); ++i) {
std::cout << " - " << analyser->issue(i)->description() << std::endl;
}
std::cout << std::endl;
// Recreate the flattened model, and check it again. This will use the updated model
// in the importer library as its source.
flatModel = importer->flattenModel(originalModel);
validator->validateModel(flatModel);
std::cout << "Investigating the flattened model:" << std::endl;
std::cout << "The validator found "<<validator->issueCount() << " issues." << std::endl;
for(size_t i = 0; i < validator->issueCount(); ++i) {
std::cout << " - " << validator->issue(i)->description() << std::endl;
}
analyser->analyseModel(flatModel);
std::cout << "The analyser found "<<analyser->issueCount() << " issues." << std::endl;
for(size_t i = 0; i < analyser->issueCount(); ++i) {
std::cout << " - " << analyser->issue(i)->description() << std::endl;
}
std::cout << std::endl;
// end 6
std::cout << "--------------------------------------------------------" << std::endl;
std::cout << " STEP 7: Write the corrected models to files " << std::endl;
std::cout << "--------------------------------------------------------" << std::endl;
# Repeat steps 4 and 5 using the Analyser instead of the Validator.
# Loop through the importer library and call the analyser for each model.
for m in range(0, importer.libraryCount()):
# Retrieve the library model by index, m.
analyser.analyseModel(importer.library(m))
# Retrieve the key under which it's stored: this will be the URL at which the imported model was found.
print('The analyser found {} issues in {}.'.format(analyser.issueCount(),importer.key(m)))
for i in range(0, analyser.issueCount()):
print(' - {}'.format(analyser.issue(i).description()))
print()
# Fix the error by adding a MathML block to the component named "concreteComponent"
# in the "importExample2b.cellml" model.
imported_model2 = importer.library("resources/importExample2b.cellml")
mathml = \
'<math xmlns="http://www.w3.org/1998/Math/MathML" xmlns:cellml="http://www.cellml.org/cellml/2.0#">\n'\
' <apply>\n'\
' <eq/>\n'\
' <ci>iAmNotCalculated</ci>\n'\
' <cn cellml:units="dimensionless">3</cn>\n'\
' </apply>\n'\
'</math>'
imported_model2.component('concreteComponent').setMath(mathml)
# Check that the issue has been fixed.
analyser.analyseModel(imported_model2)
print('\nInvestigating the repaired model:')
print('The analyser found {} issues.'.format(analyser.issueCount()))
for i in range(0,analyser.issueCount()):
print(' - {}'.format(analyser.issue(i).description()))
print()
# Recreate the flattened model, and check it again. This will use the updated model
# in the importer library as its source.
flat_model = importer.flattenModel(original_model)
validator.validateModel(flat_model)
print('\nInvestigating the flattened model:')
print('The validator found {} issues.'.format(validator.issueCount()))
for i in range(0, validator.issueCount()):
print(' - '.format(validator.issue(i).description()))
analyser.analyseModel(flat_model)
print('The analyser found {} issues.'.format(analyser.issueCount()))
for i in range(0, analyser.issueCount()):
print(' - '.format(analyser.issue(i).description()))
Output the repaired models to CellML files¶
The final step in this process is to output the repaired models to CellML files. The ability to use imports to combine model items is useful, so we want to retain that in the fixed models. For this reason, we will write all of the files involved - whether altered or not - to a new directory. By doing this, we maintain the same relationship between the model files as was there in the beginning.
For this example you will need to have created a directory structure ahead of time since the functions used here do not create those directories.
You will need to create two nested directories: repaired/resources within your working directory before continuing.
// Print the collection of repaired import models to files.
// To avoid over-writing existing files, we'll write the corrected files to a separate
// directory called "repaired/". Note that the relationship between the files needs
// to be maintained, so even files that have not been changed need to be written
// to the new location.
// You can also use the utility function makeDirectory to create the "repaired" directory needed below.
// makeDirectory("repaired");
// Write the original model to a file.
auto printer = libcellml::Printer::create();
std::ofstream outFile("importExample1.cellml");
outFile << printer->printModel(originalModel);
outFile.close();
// Write the dependency models in the importer library to files. Note that the
// library still contains the (now unneeded) circular reference files. In order
// to iterate through only those models which are actually used in the repaired version
// you can use the importer->requirements(model) function.
for(auto &info : importer->requirements(originalModel)) {
std::cout << "Writing import dependency: "<< info.first <<std::endl;
auto outFileName = info.first;
outFile.open(outFileName);
outFile << printer->printModel(info.second);
outFile.close();
}
std::cout << "The corrected models have been written to the working directory." << std::endl;
# Print the collection of repaired import models to files.
# To avoid over-writing existing files, we'll write the corrected files to a separate
# directory called "repaired/". Note that the relationship between the files needs
# to be maintained, so even files that have not been changed need to be written
# to the new location.
# Write the original model to a file.
printer = Printer()
model_string = printer.printModel(original_model)
write_file = open("repaired/import_debugger/importExample1.cellml", "w")
write_file.write(model_string)
# Write the dependency models in the importer library to files.
for m in range(0, importer.libraryCount()):
write_file = open("repaired/"+importer.key(m), "w")
model_string = printer.printModel(importer.library(m))
write_file.write(model_string)
print('The corrected models has been written to the \'repaired/resources/\' directory')