Release notes for sbmlutils 0.12.0
We are pleased to release the next version of sbmlutils including the following changes. This release makes the fbc, distrib and comp packages round trip (#469), which completes the work 0.11.0 started for SBML core. sbml_to_model reads their content now, so a model read from a file keeps its flux bounds, its gene products, its uncertainties and its hierarchy when it is written again. That changes part of the python API, listed under breaking changes: code which uses one of these has to be changed. It also changes how some existing model definitions are written, listed under behaviour changes: the model definition stays as it is, but the SBML written from it differs.
Breaking changes
Uncertainty.uncertSpans, the attribute, no longer exists: an uncertainty holds its parameters and its spans in one ordered list,uncertParameters, which is what makes the order of the document expressible. TheuncertSpans=constructor argument is unchanged and its spans are the front of that list. Code which readuncertainty.uncertSpansreadsuncertainty.uncertParametersand filters it withisinstance(child, UncertSpan). There is no compatibility property: it could only return a filtered copy, and an append to that copy would be lost in silence, which is worse than anAttributeErrorwith this note behind itUncertParameterandUncertSpanno longer raise aValueErrorwhen neither a value nor a variable is given, and anUncertSpanno longer raises for a bound it does not state: such an element is logged as an error instead. distrib requires neither, a parameter may carry only amath, adefinitionURLor a nested list of its own, and the parser has to be able to hold everything libsbml readsModelDefinitionis aModeland takesModel's constructor, which is what lets it hold everything a model holds. There is no positional shim: a call with eight or more positional arguments binds differently, since the 8th, 9th and 10th used to beunits,compartmentsandspeciesand arepackages,creatorsandmodel_unitsnow. Pass them by keyword, which is the documented style.packages,model_definitionsandexternal_model_definitionsraise aValueErroron a model definition, at construction and when the model is written: all three belong to the document. AModelDefinitionis frozen like aModel, so the camelCase names the old class walked,md.externalModelDefinitions,md.modelDefinitionsandmd.replacedElements, raise anAttributeError; the third isreplaced_elements- a
ModelDefinitionis not the model of a document:Document(model=...)andcreate_model(model=...)raise aValueErrornaming it. A model definition is written through themodel_definitionsof aModel ModelDefinition.create_sbml(model)no longer exists. A model definition is aModeland inheritsModel.create_sbml(doc), which takes thelibsbml.SBMLDocumentthe definition is created on, not thelibsbml.Modelit used to be written in; a call which passes a model is refused with aValueErrorwhich says what the argument is, where it used to end in anAttributeErroraboutcreateModelDefinition. A model definition is written by putting it in themodel_definitionsof aModeland writing that model- keywords which were accepted and never had an effect are refused with a
TypeErrorwhich names them:replacedBy=onPriority,GeneProduct,Objective,FluxObjective,UserDefinedConstraint,UserDefinedConstraintComponent,Uncertainty,KeyValuePairandLocalParameter,uncertainties=andkeyValuePairs=onKeyValuePair, andkeyValuePairs=onDocument. libsbml attaches no comp plugin to the first eight of those elements, so no reader ever saw such a replacement, and no<comp:replacedBy>on a<localParameter>is valid; replace the element which contains it, i.e. the event, the model, the parameter or the kinetic law, which is written now, and put an uncertainty on the element a key-value pair belongs to Port.create_sbmlwrites nosboTermwhen the port states none andportTypeisNone.portTypedefaults toPortType.PORT, so aPort(...)without an sboTerm still getsSBO:0000599; onlyportType=None, which used to raise anUnboundLocalError, behaves differently- three fixtures are removed from the packaged resources:
resources/distrib/uncertainty_distribution.xml,uncertainty_uncertspan.xmlanduncertainty_uncertvalue.xml. They use a pre-release distrib syntax which libsbml drops silently, so each of them read as a document without a single uncertainty. Nothing in the package, the tests, the examples or the documentation referenced them
Behaviour changes
write_sbmlandcreate_modelcreate the parent directory of the file they are asked to write and raise anOSErrornaming the path when the write fails all the same. A write into a directory which does not exist used to write nothing;create_modelthen re-read the path, got an empty document, reportedvalid: TRUEand returned a result whosesbml_pathdoes not exist. Validation is untouched and still reports rather than blocks- an annotation resource whose collection the canonical form would lose is written exactly as it was read, and reported once per collection with the count and an example.
http://identifiers.org/sabiork/1406used to be written as the bare1406, which resolves to nothing and names no collection; that is a loss in released 0.11.0. A resource of a collection the identifiers.org registry knows is normalized as before fbc:strictsurvives a round trip.Modelhas astrictfield which is written and read; it was hardcoded tofalsewhen the document was created. A document which declares fbc version 1, which has no such attribute, is read without a strictness claim and written asfbc:strict="false": libsbml's own converter inventsfbc:strict="true"for such a document, under which 11 of the 12 fbc version 1 cases of the SBML test suite become invalid- the active objective is the one the document names: the parser reads
fbc:activeObjectiveand setsObjective.activeon each objective accordingly. The default ofObjective.activeis unchanged, so several authored objectives still leave the last one active - the charge of a species is written as the fbc version of the document spells it, an integer in fbc version 2 and a double in fbc version 3. A charge which is not a whole number is reported and left unset in an fbc version 2 document, which has no place for it; it used to be written as
0 - a
Compartment.spatialDimensionswhich is not a whole number round trips, e.g.2.7; it used to come back as0 - the package namespaces on
<sbml>are written in a fixed order, comp, distrib, fbc. They followed the iteration order of a set before, which depends on the hash seed of the process, so the same model definition could write two different files in two runs Uncertainty(formula=...)takes the distribution of the value from the parsed formula, i.e. from the function the formula calls at its top level, instead of searching the text of the formula for the name of a distribution. Against 0.11.0 this changes five kinds of input:rayleigh(0.5)is written with the rayleighdefinitionURLand its math, where the name was misspelled and the parameter came out empty; andnormalization * 2,gamma_rate + 1,5 * normal(0, 1)and a formula which does not parse at all get nodefinitionURLand no math and are reported, where each of them used to be written as the distribution whose name it happens to contain. Every other formula is unchanged,lognormal(0, 1)included- key value pairs and user defined constraints are written only into a document which declares fbc version 3. In an fbc version 2 document, or in one which declares no fbc, nothing is written and the loss is reported once per kind of content for the whole document, with how many pieces on how many elements, an example and the version to declare, the way an attribute the document has no place for is reported; such a document used to get an
<fbc:listOfKeyValuePairs>of empty<fbc:keyValuePair/>elements and an empty, invalid<fbc:userDefinedConstraint>, and a document which declares no fbc raised anAttributeErrorwhich took the whole file with it. A charge which fbc version 2 cannot express stays one error per species: it is a refused value, different on each of them - a model declares the packages its content needs, in the main model as it already did in a model definition: gene products, an objective, key value pairs or an uncertainty declare fbc or distrib. Such a model without
packages=[...]used to write nothing of it or fail with anAttributeError. A version which the caller states is not raised by the content - a
<comp:port>names an element bycomp:metaIdRefwhere the document has no id for it: an initial assignment, an assignment rule, a rate rule, an algebraic rule, an event assignment and a local parameter always, and a kinetic law, a trigger, a priority, a delay, a constraint and a key value pair below SBML Level 3 Version 2, which is whatcreate_modelwrites by default. libsbml resolves acomp:idRefwithModel.getElementBySId, which answers with none of them, so such a port never validated. The id of a port which references by metaid is derived from that metaid, so two local parameters of the same id in two kinetic laws no longer produce two ports of the same id; an element which has no metaid where the document needs one is reported and gets no port, and it no longer makes the model declare an empty comp namespace - every attribute libsbml refuses is reported: 60 libsbml setter calls of
factory.pyanswered with a failing status and said nothing, and their status is checked now, which is 94 checked calls against the 34 of 0.11.0. An attribute which the SBML level and version, or the version of a package, has no place for at all is a different case and is reported once per kind of element and attribute, with the count, an example and what to write to keep it, rather than once per element; the per-element detail is logged at debug - the core
idandnameof aReplacedElement, aReplacedByand a nestedsBaseRefare not written by libsbml into any document, measured at Level 3 Version 1 and at Version 2, so they are no longer set and a value given for either is reported once per document and per kind of element, without advice. The report used to say "Write SBML Level 3 Version 2 to keep it", which does not keep it, and at Level 3 Version 2 the loss was silent.sidis optional on all three classes now, in the same position, so a comp author no longer has to state an id which is thrown away;elementRefandsubmodelRefof a replacement keep their position and default to the empty string, so a replacement which states noelementRefis refused when it is written, with theValueErrorwhich names anelementRefthe model has no element for. Thecomp:idandcomp:nameof aPortand of aDeletionare unaffected Uncertainty.formulais a property. It is normalized into the uncert parameter of the typedistributionwhich the uncertainty is written from, on an assignment as well as in the constructor:u.formula = "uniform(0, 1)"afterUncertainty(formula="normal(0, 1)")used to be kept in the field, written nowhere, andnormalwas written instead, in silence. Setting it toNoneremoves the parameter the formula stands for and leaves every other child of the uncertainty alone- a model which states
strictdeclares fbc for it,TrueandFalsealike.Model(strict=True)without any other fbc content wrote no fbc namespace, nofbc:strictand no report at all; a model which states neither leavesstrictatNoneand declares nothing. AModelDefinitionis not counted, since libsbml cannot writefbc:stricton a<comp:modelDefinition> sbml_to_modelreports nothing for a document which libsbml reads without an error. A<distrib:uncertParameter type="mean"/>was logged as stating nothing about the value, and an uncert span with one bound as missing the other: both are hints about a model definition being written, and distrib requires neither, so the reader of a file was told to fix the file it had just read- an element whose fbc content needs a plugin the document does not have is refused with a
ValueErrorwhich names the element, the package and why the package is absent, the way a comp port already was: the flux bounds of a reaction, a gene product and an objective dereferencedNoneand ended inAttributeError: 'NoneType' object has no attribute 'setUpperFluxBound'atsbml_level=2, and aDeletionwhosesubmodelRefnames no submodel in one aboutcreateDeletion, which names the submodel now. The charge and the chemical formula of a species are still reported rather than raised, and no longer advisepackages = ['fbc'], which a model with a charge declares by itself UnitDefinition.get_uid_for_unitraises aValueErrornaming the value for a unit which is neither aUnitDefinitionnor the id of one. It used to surface as a SWIGTypeErrorfromsetUnits, which names neither the value nor the element it was set onmerge_modelscopies the unit definitions of the models it merges, like every other merged list. Changing a unit definition of the merged model used to change the model it was merged from- the authoring hints,
'name' should be setand'sboTerm' should be set, are held in aContextVar, so writing a parsed model in one thread no longer suppresses the hints of a model written in another - the
sboTermof a reactant, a product or a modifier takes either spelling,SBO:0000011andSBO_0000011, like every other element; libsbml refused the underscore form on a species reference while accepting it on a species - an objective which omits the required
fbc:typeis read asmaximizewith an error in the log, wheresbml_to_modelused to raise aValueErrorand read nothing at all - an external model definition without a
modelRefis written without that attribute, instead of passing the empty string to libsbml, which refused it - two packaged example files were regenerated, because neither could round trip as it was:
resources/examples/fbc/fbc_key_value_pair.xml, whose key value pairs were three empty elements, andresources/examples/fbc_user_defined_constraints.xml, which spelled the coefficients of its components as numbers, which is invalid fbc version 3 with 13 validation errors and which libsbml refuses to write
Features
- the fbc, distrib and comp packages round trip (#469):
SBML -> sbml_to_model -> create_model -> SBMLpreserves their content, wheresbml_to_modelused to read none of it and a round trip ofe_coli_corediscarded its 137 gene products, 69 gene product associations, 95 flux bounds, 72 charges, 72 chemical formulas, its objective andfbc:strict, and a round trip oficg_body.xmlits submodel, its 16 ports, its 6 replaced elements and its external model definition, see Reading and writing - fbc: flux bounds, objectives and flux objectives with the type, the coefficients and which objective is active, gene products with their label and associated species, the gene product association of a reaction as an infix string of gene product ids, the charge and the chemical formula of a species, user defined constraints with their components, and key value pairs on every element, see Flux balance constraints
- distrib: the uncertainties of every element with their uncert parameters and spans in the order of the document, each with its type, value, variable, bounds, unit,
definitionURL,mathand the nested parameters of an external distribution, and the distribution functions in a formula, see Distributions and uncertainties - comp: submodels with their time and extent conversion factors, ports, deletions, replaced elements and
replacedBywith the whole nestedsBaseRefchain below them, model definitions with everything inside them, core as well as fbc and distrib, and external model definitions, which are preserved as the reference they are and never resolved, see Model composition Model(strict=...)writesfbc:strict, which says that every reaction of the model has constant bounds with a value and every species reference is constantUncertParameterandUncertSpanareSbasesubclasses, so each carries its own id, name, metaid, sboTerm, notes and annotations, and both takedefinitionURL=,math=anduncertParameters=, the nested parameters of an external distribution.Uncertainty.uncertParameterstakes parameters and spans in one list and writes them in the order they are given in, which is what preserves the order of a parsed document;uncertSpans=is unchanged and its spans are written in front. The typesdistributionandexternalParameter, which an uncertainty refused before, are writtenSbaseRef.sBaseRefholds the nested<comp:sBaseRef>of a port, a replaced element, areplacedByor a deletion, which continues the reference into a submodel of the submodel it names, to any depth. The SBML test suite nests it three levels deepModelDefinitionis aModel, so a<comp:modelDefinition>holds everything a model holds, its unit definitions included, and the document declares the packages the content of a model definition needs- the key value pairs of a reactant, a product and a modifier are written;
EquationPart.keyValuePairswas declared and never used. libsbml 5.21.2 writes them for all three and reads them back only for a modifier, which is recorded for an upstream report - a
portis written for a kinetic law, a local parameter, an event and its trigger, priority, delay and assignments, a constraint, an uncertainty, a gene product, an objective, a flux objective, a user defined constraint and its components and a key value pair. Each of them accepted aport, dropped it, and made the document declare an empty comp namespace for it - a
KeyValuePairwrites its id, name, metaid, sboTerm, notes and annotations, not only its key, value and uri, and theuncertaintiesof a kinetic law and of a local parameter and thereplacedByof a kinetic law are written
Fixes
- the gene pre-check of a reaction stripped
and,AND,or,ORand the parentheses out of the association string with a chain ofstr.replace, so a gene product whose label contains those letters was mangled,ORF1becameF1, and aGeneProduct missing in modelwas logged for a gene the model declares. The association is tokenized now UserDefinedConstraintComponentdropped avariableTypegiven as the libsbml constantFBC_VARIABLE_TYPE_LINEAR, which is0and therefore falsy, and with it the default ofUserDefinedConstraint, which is that constant and is passed on to every component. The string"linear"was never affectedSubmodel(sid=...)without amodelRefis written with the reference unset and one error naming it, wheresetModelRef(None)raised aTypeErrorout of SWIGPort(portRef=...)is reported: comp does not allow a<comp:port>to reference another port and libsbml answered every such call with a failure, silently- a distribution named
rayleighin anUncertainty(formula=...)was spelledraleigh, so the uncert parameter was written without itsdefinitionURLand without its math comp.flatten_sbmlleaves the working directory where it found it when the flattening raises. It changes into the directory of the document so that libsbml resolves acomp:sourcerelative to it, and changed back only on the way out, so a caller whose document could not be flattened was left in another directory, with every relative path of its own pointing somewhere else
Documentation
- Reading and writing describes what the packages preserve, how the three verification layers measure it, on which corpus, and what the round trip does not keep
- Flux balance constraints, Distributions and uncertainties and Model composition describe what each package keeps and what it does not, and the new fields of this release
- Validation describes what writing a model reports, which no validation of the result can show
- Annotations describes which annotation resources are written as they were given, and why
Development
tests/structural.pycompares the fbc, distrib and comp content of two documents attribute by attribute and returns every difference. Simulation and validation are blind to all three packages, so this is the measure of the package round trip; its module docstring is the comparison policy and every package test is judged by it.scripts/package_report.pysweeps the corpus with it, one process per case, and prints what every construct preserves, asscripts/roundtrip_report.pydoes for simulationtests/test_package_semantics.pychecks the two packages which have a semantics an independent tool can judge: a comp model and its round trip are flattened and simulated with roadrunner and their trajectories compared, an fbc model and its round trip are read as constraint based models with cobrapy and their stoichiometry, bounds, objective, gene reaction rules and flux balance solution compared. Both comparisons are shown to see a damaged round trip- the tox environment
cobrainstalls the optionalcobraextra and runs the tests which need cobrapy, which would otherwise skip in every environment and verify nothing. It has a job of its own in the CI workflow, which informs and is not part of the required checks Model._keys, whichmerge_modelsreads to decide how a field is merged, is derived from the field annotations ofModelinstead of being a hand-maintained copy of them, with the two deliberate exceptions in an explicit override table- what follows
--in a tox run is passed on to pytest, sotox r -e py3.14 -- tests/test_factory.pyruns one module in the environment continuous integration uses; the path used to be dropped and the whole suite ran
Your sbmlutils team
