Annotate a model

libCellML does not provide annotation functionality directly, but it can help with some of the things that you might need to know in order to implement your own. Each annotation is attached to a unique ID locator which can be retrieved and edited, or created automatically if you haven’t already set them yourself. Since CellML is a subset of XML, any item may have an ID attribute, including operations within the MathML blocks, CellML entities, and CellML non-entity items (connections, encapsulations etc). These ID attributes themselves must follow the specification rules: please see 1.2.5 XML ID attributes for details.

At present, items within MathML blocks which have id attributes are not retrieved or edited by the annotation class. Any editing or manipulation must be done external to libCellML.

Annotation tools for entity items

Entity item types are:

  • Model items;

  • Component items;

  • Variable items;

  • Units items;

  • Reset items; and

  • ImportSource items.

The id attribute for each of these entity types can be retrieved using the id() function for the object, or set through its setId() function.

// Set the ID attribute for a Model item.
model->setId("myModelId");

// Retrieve the ID attribute from a Model item.
std::string myModelIdString = model->id();

Annotation tools for other item types

Connections and variable equivalence

Items which specify the relationship between two entities (such as connections between components, and mappings between variables) are accessed and identified by the pair of entities. Consider the simple example shown below.

model:
  ├─ component: name = "c1", id = "c1id"
  │   └ variable: name = "v1", id = "v1id" <╴╴╴┐
  └─ component: name = "c2", id = "c2id"     equivalent variables
      └ variable: name = "v2", id = "v2id" <╴╴╴┘

Show CellML syntax

<model>
  <component name="c1" id="c1id">
    <variable name="v1" id="v1id" />
  </component>
  <component name="c2" id="c2id">
    <variable name="v2" id="v2id" />
  </component>

  <!-- The variable equivalence is stored separately to the variables themselves. -->
  <connection component1="c1" component2="c2" id="c1c2id" >
    <map_variables variable1="v1" variable2="v2" id="v1v2id" />
  </connection>
</model>
// Set the ID of the mapping between variables v1 and v2.
Variable::setEquivalentVariableId(v1, v2, "v1v2Id");

// Get the ID of the mapping between variables v1 and v2. Note that
// equivalences and connections go both ways, so the ID is the same
// whichever order the variables are specified.
std::string v1v2IdString = Variable::equivalentVariableId(v1, v2);

// Since a connection item between two components will only exist
// when there is a variable equivalence between at least one each
// of their children.  The connection is accessed through those
// child variable pairs, just as the equivalence mapping itself is.
Variable::setConnectionId(v1, v2, "c1c2id");

// Get the ID of the connection between the parent components of
// equivalent variable pair. Note that equivalences and connections
// go both ways, so the ID is the same whichever order the variables
// are specified.
std::string c1c2IdString = Variable::connectionId(v1, v2);

Encapsulation and component references

The model’s encapsulation and the component references which it contains may have ID attributes too. These are accessed through the encapsulationId() functions, as shown below.

model:
  └ encapsulation:
      └ component: grandparent
          └ component: parent
              └ component: child

Show CellML syntax

<model>
  <component name="grandparent" id="grandparentComponentId" />
  <component name="parent" id="parentComponentId" />
  <component name="child" id="childComponentId" />

  <!-- The ids of the components in the encapsulation structure are distinct
       from the ids on the components themselves. -->
  <encapsulation id="encapsId">
    <component_ref component="grandparent" id="grandparentEncapsId" >
      <component_ref component="parent" id="parentEncapsId" >
        <component_ref component="child" id="childEncapsId" >
      </component_ref>
    </component_ref>
  </encapsulation>
</model>

There is only one encapsulation in a model, and its id attribute is accessed using the very simple functions from the model instance. The position of each component within that encapsulation structure is referenced using the same functions, but on the component instance instead.

// Set the encapsulation ID.
model->setEncapsulationId("encapsId");
grandparentComponent->setEncapsulationId("grandparentEncapsId");

// Get the encapsulation ID.
std::string modelEncapsulationId = model->encapsulationId();
std::string grandparentEncapsulationId = grandparentComponent->encapsulationId();

Unit item children; reset_value, test_value item children

Some items are most readily accessed through their entity-type parents, these being:

  • unit items, a collection of which defines a Units item; and

  • test_value and reset_value children of Reset items.

Unit children of Units items can be accessed either through the streamlined ID-only functions unitId(index) and setUnitId(index), or through the unitAttributes family of functions. Note that there are several overloads of the arguments for this function; please see the complete documentation on the Units functions API page.

// Create a Units item representing millimetre^3 per second.
auto mm3PerSecond = libcellml::Units::create("mm3PerSecond");

// Add the per second part with the ID "perSecondUnitId".
mm3PerSecond->addUnit("second", 0, -1.0, 1.0, "perSecondUnitId");

// Add the mm^3 part with with the ID "mmCubedUnitId".
mm3PerSecond->addUnit("metre", "milli", 3.0, 1.0, "mmCubedUnitId");

// Check that the ID has been assigned to the Unit children.
auto checkId1 = mm3PerSecond->unitId(0); // returns "perSecondUnitId".
auto checkId2 = mm3PerSecond->unitId(1); // returns "mmCubedUnitId".

// Change the ID of the second (ie: index = 1) child to be "millimetreCubedUnitId":
mm3PerSecond->setUnitId(1, "millimetreCubedUnitId");

// Retrieve the unit attributes for the first (index = 0) unit item, including the ID attribute:
std::string unitReference;
std::string unitPrefix;
double unitExponent;
double unitMultiplier;
std::string unitId;
mm3PerSecond->unitAttributes(0, unitReference, unitPrefix, unitExponent, unitMultiplier, unitId);

Annotator Type and returned types

enumeration value

Object type in the “any” container.

COMPONENT

Pointer to a Component with the given id.

COMPONENT_REF

Pointer to a Component, whose encapsulation component_ref has the given id.

CONNECTION

VariablePair containing pointers to two Variable items which span the connection.

ENCAPSULATION

Pointer to the Model item with the given id.

IMPORT

Pointer to an ImportSource item with the given id.

ISSUE

Pointer to an Issue item, created when something has gone wrong.

MAP_VARIABLES

VariablePair containing the two Variable items connected by a variable equivalence with the given id.

MODEL

Pointer to the Model item with the given id.

RESET

Pointer to the Reset item with the given id.

RESET_VALUE

Pointer to the parent Reset item containing a reset value with the given id.

TEST_VALUE

Pointer to the parent Reset item containing a test value with the given id.

UNIT

Unit pair, when the first attribute is a Units pointer to the parent of the unit with the given id, and the second attribute is the index within the Units item at which the unit can be found.

UNITS

Pointer to a Units item with the given id.

VARIABLE

Pointer to a Variable item with the given id.

Useful snippets for annotation

Automatically assign unique ids to items

Because the id attribute is simply a token, its contents (provided they follow the XML rules) could be anything. Ids are not intended to be interacted with by humans; they are there as a placeholder or location specifier so that other (more human-friendly) documentation can be added to the right objects. With this in mind, it’s possible to automate the creation of unique ids for a single item, a set of items of the same type (eg: all the Component items), or for every annotatable item in a CellML model.

// Create an Annotator instance.
auto annotator = libcellml::Annotator::create();

// ---------- OPTION 1: Automatic ids for everything -------------------------
//
// Submit the model to the Annotator, and instruct it to create and assign a
// set of unique ids to any annotatable item in the model.
// Note that this function replaces any previously stored model within the
// Annotator, and rebuilds the internal index with respect to this model.
// There is no need to either build the index first (as all the ids will
// be altered anyway) or afterwards (as it's built during the assignment
// process).
annotator->assignAllIds(model);

// ---------- OPTION 2: Automatic ids by type -----------------------
//
// First submit the model to the Annotator and build its index.
annotator->buildModelIndex(model);

// Next, specify the type of item to assign automatic ids to using the
// Type enumeration options.  This will assign every Variable in the model
// a unique id string.
annotator->assignIds(libcellml::Annotator::Type::VARIABLE);

// ---------- OPTION 3: Automatic id by item -------------------------
//
// Finally, you can submit single items for automatic id.
// There are two ways to do this as shown below.  Note that when you use the
// built-in Annotator functions to assign ids, there is no need to rebuild the
// annotator's index.
//
// The two options below are identical.
//
// EITHER Submit an AnyItem struct (a pair of Type enum, and the item) ...
AnyItem myComponentRef = std::make_pair(libcellml::Annotator::Type::COMPONENT_REF, model->component(0));
annotator->assignId(myComponentRef);

// ... OR submit both the Type enum and the item as separate arguments.
annotator->assignId(libcellml::Annotator::Type::COMPONENT_REF, model->component(0));

Remove ids from all items

In addition to creating id attributes, the Annotator can be used to clear them from all items in the model. There are two ways in which this can be done. First, a Model instance can be passed to the annotator for clearing. This process will replace any previously stored model with the new one, as well as clearing all ids from the given model. Alternatively, if a model is already stored in the annotator instance, the ids of that stored model can be cleared by calling the clearAllIds() function without arguments.

// Assuming we have two models, with references model1, and model2.

// Create an Annotator instance.
auto annotator = libcellml::Annotator::create();

// EITHER: Pass a new Model to the annotator so that its ids can be cleared.
annotator->clearAllIds(model1);  // This will clear ids in model1,
                                 // and associate model1 with the annotator.

// Note: There is no need to build the annotator's index beforehand as
// clearing the ids will also clear the index.  The model1 given will be
// stored as the current model within the annotator.

// OR: Clear all ids in a model which is was previously associated with the annotator.
annotator->buildModelIndex(model2);
annotator->clearAllIds();  // This will clear ids in model2, the model stored in the annotator.

Retrieve an item of known type by id

Where you know the type of item (eg: Component, Variable etc) before retrieving it, a collection of helper functions exist, as demonstrated below. Where the known type is an entity type, a pointer to the item is returned. Where the type is a non-entity type, pointers to significant related items are returned, as discussed earlier.

// Create an Annotator.
auto annotator = libcellml::Annotator::create();

// Build the annotator to work with the model.
annotator->buildModelIndex(model);

// Retrieve entity items of known type using their id attribute.
auto myComponent = annotator->component("myComponentId");
auto myVariable = annotator->variable("myVariableId");
auto myReset = annotator->reset("myResetId");
auto myUnits = annotator->units("myUnitsId");
auto myImportSource = annotator->importSource("myImportSourceId");
auto myModel = annotator->model("myModelId");

// Retrieve non-entity items of known type using their id attribute.
// NOTE that the type of object returned by retrieving a non-entity item
// is defined in the text above.

// Connections are returned as a VariablePair type, where the first and second
// items in the pair define Variables in the first and second components of the
// connection.  Note that as multiple variable pairs could exist between two
// given components, the return value for this function is not unique.
auto myConnection = annotator->connection("myConnectionId");

// Variable equivalences (from the map_variables) are returned as a VariablePair,
// where the first and second items in the pair define the Variables which are made
// equivalent by this mapping.
auto myMappedVariables = annotator->mapVariables("myMapVariablesId");

// Unit items are returned as a Unit pair, where the first item is a pointer to
// the parent Units item, and the second is the index at which the child unit item's
// attributes can be found.
auto myUnitItem = annotator->unit("myUnitId");
std::string myUnitsReference;
std::string myPrefix;
std::string myId;
double myExponent;
double myMultiplier;

// The first item in the pair is a pointer to the parent Units.
myUnitItem.first->unitAttributes(myUnitItem.second, // The second item in the pair is the index.
                                 myReference, myPrefix, myExponent,
                                 myMultiplier,myId);

// The location of a component in an encapsulation hierarchy is set by a component_ref
// block.  Retrieving a component_ref item by id will return a pointer to the
// Component item which is located at that position in the encapsulation.
auto myReferencedComponent = annotator->componentRef("myComponentReferenceId");

// The reset_value and test_value block children of a Reset item are returned as a
// pointer to their parent Reset item.
auto myTestValueParent = annotator->testValue("myTestValueId");
auto myResetValueParent = annotator->resetValue("myResetValueId");
// Their values can then be retrieved using the test_value() and
// reset_value() functions on that Reset item parent.
auto myTestValue = myTestValueParent->test_value();
auto myResetValue = myResetValueParent->reset_value();

// An encapsulation item is returned as a pointer to its parent model.
auto myEncapsulatedModel = annotator->encapsulation("myEncapsulationId");

Retrieve an item of unknown type by id

In situations where you have an id string, but don’t know the type of the object it identifies, the item can be retrieved using a general Annotator::item(itemId) function. The general item function will return an AnyItem pair. The first part of the pair is an enum of the type of the object. The second part is an std::any type object, which can be cast into the correct type. This is demonstrated below.

// Create an Annotator.
auto annotator = libcellml::Annotator::create();

// Build the annotator to work with the model.
annotator->buildModelIndex(model);

// Retrieve an item of unknown type from the annotator.
auto anyItem = annotator->item("findThisId");

// Depending on the item's type (which is stored in the first part of the AnyItem
// pair), cast the second part to the appropriate pointer type. Note that this means
// first declaring a range of variables of different types to which the cast
// pointer can be assigned.

libcellml::ComponentPtr itemComponent;
libcellml::VariablePtr itemVariable;
libcellml::ResetPtr itemReset;
libcellml::UnitsPtr itemUnits;
libcellml::ImportSourcePtr itemImportSource;
libcellml::VariablePair itemVariablePair;
libcellml::Unit itemUnit;

switch (anyItem.first) {
    case libcellml::Annotator::Type::COMPONENT:
    case libcellml::Annotator::Type::COMPONENT_REF:
        itemComponent = std::any_cast<libcellml::ComponentPtr>(anyItem.second);
        break;
    case libcellml::Annotator::Type::CONNECTION:
    case libcellml::Annotator::Type::MAP_VARIABLES:
        itemVariablePair = std::any_cast<libcellml::VariablePair>(anyItem.second);
        break;
    case libcellml::Annotator::Type::IMPORT:
        itemImportItem = std::any_cast<libcellml::ImportSourcePtr>(anyItem.second);
        break;
    case libcellml::Annotator::Type::ENCAPSULATION:
    case libcellml::Annotator::Type::MODEL:
        itemModel = std::any_cast<libcellml::ModelPtr>(anyItem.second);
        break;
    case libcellml::Annotator::Type::RESET:
    case libcellml::Annotator::Type::RESET_VALUE:
    case libcellml::Annotator::Type::TEST_VALUE:
        itemReset = std::any_cast<libcellml::ResetPtr>(anyItem.second);
        break;
    case libcellml::Annotator::Type::UNIT:
        itemUnit = std::any_cast<libcellml::Unit>(anyItem.second);
        break;
    case libcellml::Annotator::Type::UNITS:
        itemUnits = std::any_cast<libcellml::UnitsPtr>(anyItem.second);
        break;
    case libcellml::Annotator::Type::VARIABLE:
        itemVariable = std::any_cast<libcellml::VariablePtr>(anyItem.second);
        break;
    }
}

// Note also that attempting to cast into the wrong type will trigger a
// "bad any cast" exception.
try {
    assert(itemVariable.first == Annotator::Type::VARIABLE);
    auto thisWillNotWork = std::any_cast<libcellml::UnitsPtr>(itemVariable.second);
}
catch(const std::bad_any_cast& e) {
    // Depending on your system, this will return a "bad any cast", "bad any_cast"
    // or "bad_any_cast" message.
    std::cout << e.what() << std::endl;
}

Locate items with duplicate ids

The Annotator functionality can be used to report id strings which have been duplicated within a model’s scope, as well as to return all items associated with a duplicated id string.

// Create an Annotator instance.
auto annotator = libcellml::Annotator::create();

// Create a model, with the id string "duplicateId" used on a component
// and a variable item, and an id string "anotherDuplicateId" on a units
// and encapsulation item.
auto model = libcellml::Model::create("myModelName");
auto component = libcellml::Component::create("myComponentName");
auto variable = libcellml::Variable::create("myVariableName");
auto units = libcellml::Units::create("myUnitsName");

model->addComponent(component);
model->addUnits(units);
component->addVariable(variable);

// Set the ids.
component->setId("duplicateId");
variable->setId("duplicateId");
units->setId("anotherDuplicateId");
model->setEncapsulationId("anotherDuplicateId");

// Pass the model to the annotator and build the index.
annotator->buildModelIndex(model);

// Retrieve a list of duplicated ids from the annotator.
auto duplicateIdList = annotator->duplicateIds();

// Now duplicateIdList is a vector of strings of the ids which are duplicated.
// In this example it will contain "duplicateId", and "anotherDuplicateId".

Retrieve items with duplicated ids

Items with a unique id can be retrieved using the item("uniqueId") function, items whose ids are not unique must be retrieved with the items("duplicatedId") function instead.

// Create an Annotator instance.
auto annotator = libcellml::Annotator::create();

// Pass the model to the annotator and build the index.
annotator->buildModelIndex(model);

auto duplicatedIdItems = annotator->items("duplicatedId");
// The duplicateIdItems is a vector of AnyItem items; pairs whose first
// attribute is an Annotator::Type enum, and the second is an std::any cast
// of the item.