Flattening and other packages
Once you have a hierarchical model, there are two basic tasks you may wish to perform:
- Follow the rules of the comp package to construct a complete object model in-memory.
- Flatten the model.
What is meant by the first is the creation of an object model where each object corresponds to a simulatable SBML element. In other words, to follow the rules in the ‘rule elements’ from the comp package: submodel creation, replacements, and deletions. One might have a model with one rule saying ‘Make a copy of this submodel’, and second saying ‘Make another copy of this submodel’. A third rule might say ‘delete this element from the second submodel’, and a fourth might say ‘use element ‘S’ from the main model instead of the ‘Y’ elements from the submodels’. When you follow those rules, you would take the original model and make an actual copy of all of the elements in it twice, remove the deleted element from the second copy, then remove both ‘Y’ elements and point anything that used to point to those Ys to the ‘S’. What you have at this point is not a valid XML document, but it may be simulatable or at least manipulable in some useful sense. It is the first step towards flattening the model, but may also be a valid end-state in and of itself.
If you do wish to flatten a model, there are two processes to follow subsequent to creating a complete object model: first, you must combine lists of elements from the different models and submodels so there is (for example) only one ListOfSpecies or ListOfCompartments in the model. Secondly, you must flatten the namespace of the model: ensure that every remaining SId, UnitSId, and ID (metaId) in the model is unique within its namespace, possibly changing them if not, and that the corresponding SIdRefs, UnitSIdRefs, and IDREFs point to the correct newly-named elements.
Now we come to a complication: what do we do about other packages? The end goal is relatively straightforward to visualize: we want to use the same hierarchical rules (submodel creation, replacement, and deletion) and have this allow us to combine models that have package information in them. For some packages that only define new attributes or subelements of existing core Model elements (FunctionDefintions, UnitDefinitions, Compartments, Species, Parameters, InitialAssignments, Rules, Constraints, Reactions, and Events) this is simple: whenever the Model element is manipulated, its children are manipulated in the same way. If the new child elements and attributes additionally contained no SIds, UnitSIds, IDs, SIDRefs, UnitSIDRefs, or IDREFs, it would be possible to completely transform a model with both of the above processes without knowing anything else about the package:
For the object model process:
- If a model element is copied, the new children/attributes are copied as well.
- If a model element is deleted, the new children/attributes are deleted as well.
For the flattening process:
- If a model element is copied into a new ListOf* object, the new children/attributes are copied as well.
The first complication happens when a package adds any sort of ID or ID-reference. Even if the only IDs provided are the ‘metaID’ attributes of SBase, there is no requirement that all new elements that a package adds inherit from SBase, so there is no way to know whether any ‘package:metaId’ attribute on these new elements are actually of type ID. Even if we were to make this assumption, it would be an even riskier assumption to claim that any ‘package:id’ attribute was of type SId: even in core, this assumption is violated twice, with UnitDefinitions, and LocalParameters. Even the comp package violates this assumption, giving an ‘id’ attribute to Port objects, but placing these names in a newly-defined ‘PortSId’ namespace. This makes it impossible to perform the following tasks devoid of knowledge of the package:
For the object model process:
- Finding newly-defined elements with SIds, UnitSIds, or IDs, and deleting them or replacing them when requested by a comp Deletion or ReplacedElement object.
- Taking newly-defined SIdRefs, UnitSIdRefs, and IDREFs, and pointing them to the new object in the parent model when the original object is replaced.
For the flattening process:
- Renaming newly-defined elements with SIds, UnitSIds, or IDs which need new, unique identifiers in the flattened model.
- Changing any SIdRefs, UnitSIdRefs, and IDREFs whose targets have had their corresponding IDs renamed.
It might theoretically be possible to perform some or even all of the above tasks correctly for some packages by making certain assumptions about the nature of the information in the package (for example: assuming that all new classes inherit from SBase; that all ‘id’ attributes are of type SId; and that all ‘idRef’ attributes are of type SIdRef). However, the possibility for error here is too great, I feel, to go down this route.
So, instead of attempting to perform these tasks in the absence of package information, we instead need to figure out a standard process than any SBML manipulation program could use, and a standard API for the libSBML package system in particular so that these tasks may be performed.
Element Discovery: A model should be queryable to find a child element with a particular SId, UnitSId, or ID. This should be an error-resistant process: if no elements or multiple elements exist with the same id, this should be obvious.
Proposed libSBML API (recursive):
vector<SBase*> SBase::getElementsWithSId(string sid); (would not return LocalParameters) vector<SBase*> SBase::getElementsWithUnitSId(string unitid); vector<SBase*> SBase::getElementsWithMetaId(string metaid);
Element Removal: Once found, there should be a standard method for removing that element from its parent.
Proposed libSBML API (recursive, in that if the element is not its own child, tells its children to remove the element):
int SBase::removeElement(SBase* removed);
Id Renaming: It should be possible, in a general way, to rename SIds, UnitSIds, and IDs.
Proposed libSBML API:
int SBase::setId(string sid); (already exists, but see below) int SBase::setUnitSId(string sid); (might not need this one?) int SBase::setMetaId(string sid); (already exists!)
SBase::setId already exists, but we need a convention that if a package defined a new class with a new namespace (like PortSId) ‘setId’ will not set it, even if the attribute in question is ‘id’.
Id Reference Renaming: It should be possible to give an element an original id and a new id, and have it change all of its *idRefs for itself and its children.
Proposed libSBML API (recursive: runs for itself and calls this on all children):
int SBase::sIdRenamed(string oldsid, string newsid); int SBase::unitSIdRenamed(string oldsid, string newsid); int SBase::metaIdRenamed(string oldsid, string newsid);
One note: it will not be necessary that all new package-defined elements be SBase-derived, as long as the parent of that object does the right thing with its children. As an example, the ‘annotation’ element is derived from SBase, but its children are in the RDF namespace, and thus not SBase objects. However, because the annotation element knows that the rdf:about attribute in one of its children is of type ID, if you call annot1.metaIdRenamed(“oldname”, “newname”), it can check all of the rdf:about attributes it knows about, and change any instances of “oldname” to “newname”.
- * * * * * * *
If the above routines are available for all SBase-derived classes, and all package extension classes, a single routine can be written that accomplishes all the id-related tasks for flattening. All new package elements that are children of what I called ‘core Model elements’ are completely accounted for by this scheme, as well. This leaves one remaining issue: how to flatten models with package elements that are direct children of the Model object. For the Groups package, this would be the new ListOfGroups class. The spatial package has its ListOfGeometries; the comp package itself has a ListOfPorts. These all follow the pattern established in core of the Model object containing several ListOf classes, each containing a particular type of object. For core objects, flattening means collecting all the elements of the same class, making sure their ids are unique, and putting them all into the appropriate ListOf object for their class. Presumably, the majority of all packages that follow the same pattern would want to follow a similar rule for flattening. Therefore, it would make the most sense if libSBML provided a function on the ListOf base class that performed this basic flattening scheme. If the package did indeed want that routine performed, it could simply call that routine. If changes were needed, the function could be overridden.
Proposed libSBML API:
int ListOf::mergeWith(ListOf newlist, string prefix); (‘prefix’ would be used to create unique Ids if needed.)
This leaves only singleton elements that are children of the Model object that are not members of the ListOf class (the ‘annotation’ child of Model, if present, would be an example. The only example I can think of, actually.) Here, the package itself would have to know how to properly merge the objects in question. A dummy function would be called on any model plugin objects, which could be overridden by the package in question to run any needed logic.
Proposed libSBML API:
int SBasePlugin::mergeWith(SBasePlugin* othermodel);
This would, by default, return an error code. If the SBasePlugin was created on a non-Model object, it would never be called. But a merge routine would be written on the Model object that would cycle through all of its plugins, calling this mergeWith routine: if any returned that error code, the flattening routine would fail (or at least warn that package information from package X was being lost). Also of note here is that this routine could (and would have to) take care of any new namespace issues: the spatial package defines a new ‘spid’ namespace, so it would need to put hooks in these functions to do proper renaming throughout the model.