Enumerations - openmpp/openmpp.github.io GitHub Wiki
Home > Model Development Topics > Enumerations
An enumeration is a fixed, finite sequence of sequential integer values.
An enumeration can be a dimension of a parameter, a dimension of a table, the type of an attribute, or the type of a local variable.
OpenM++ supports three kinds of enumerations:
classification,
range, and
partition.
- Introduction and outline
classificationenumerationrangeenumerationpartitionenumeration- Enumerations as dimensions
- Enumerations as types
- Enumerations and the
bounds_errorsoption - Iterating enumerations
- Enumeration index validity and the
index_errorsoption
This topic first describes the three kinds of enumerations in OpenM++:
-
classificationenumerations for categorical values, -
rangeenumerations for sequential integer values like integer age or year, and -
partitionenumerations for continuous values split into a set of intervals.
This is followed by a pair of subtopics on how enumerations are used:
- as dimensions, and
- as types.
Because an enumeration consists of a fixed set of specific values, the question arises on how to handle out-of-bounds values.
A subtopic is devoted on how to easily and safely iterate enumerations in model code.
Finally, an option is described which identifies invalid enumeration indices of multi-dimensional parameters or entity attribute arrays in model code at runtime.
A classification enumeration consists of an ordered set of enumerators, whose names are globally unique.
This subtopic contains the following sections.
classificationenumeration - exampleclassificationenumeration - valueclassificationenumeration - assignmentclassificationenumeration - membersclassificationenumeration - macros
A classification is declared in a syntactic island in model code.
Here's an example from the RiskPaths model:
classification UNION_STATE //EN Union status
{
US_NEVER_IN_UNION, //EN Never in union
US_FIRST_UNION_PERIOD1, //EN First union < 3 years
US_FIRST_UNION_PERIOD2, //EN First Union > 3 years
US_AFTER_FIRST_UNION, //EN After first union
US_SECOND_UNION, //EN Second union
US_AFTER_SECOND_UNION //EN After second union
};[back to classification enumeration]
[back to topic contents]
- The value of a variable with a
classificationtype is one of the declared enumerators of theclassification. - The default value is the first enumerator in the
classification. - The enumerators of a
classificationcan be used by name in model code (technically, the enumerators are a C++enum). - Each enumerator has a corresponding integer value which is
0for the first enumerator and increases by 1 for each subsequent enumerator. - Each enumerator of a
classificationhas an external name which is used in outputs. If heuristic names are activated for a model they can be selectively disabled for classification enumerators by setting the optionenable_heuristic_names_for_enumeratorstooff. That makes the external name of all enumerators be the enumerator given in the declaration of theclassification, e.g.US_SECOND_UNION, rather than a heuristic name based on the label of the enumerator in the default language of the model. The external name of an enumerator can also be given explicitly in model source code.
[back to classification enumeration]
[back to topic contents]
- A variable with a
classificationtype can be assigned a value using one of its enumerators. - The value can also be assigned using an integer, but this makes model code less clear.
- The value can be modified using the C++ operators
++and--. - An exception can be thrown on assignment or modification if
bounds_errorsison.
[back to classification enumeration]
[back to topic contents]
A classification enumeration has the following static members which can be used in C++ model code.
These static members cannot be used in syntactic islands at this time because the OpenM++ compiler does not currently support the C++ :: scope operator.
The Example and Result columns use the example above.
| Member | Example | Result | Description |
|---|---|---|---|
min |
UNION_STATE::min |
0 |
Minimum integer value of classification (always 0) |
max |
UNION_STATE::max |
5 |
Maximum integer value of classification |
size |
UNION_STATE::size |
6 |
Number of enumerators in classification |
indices() |
UNION_STATE::indices() |
0..5 |
Ascending iterator of all valid indices, see Iterating enumerations |
[back to classification enumeration]
[back to topic contents]
The following Modgen-style C++ macros are supported for a classification enumeration.
Generally, it is preferable to avoid the use of C++ macros when possible by using the data or function members in the table above.
A notable benefit is an improved ability of your IDE to understand model code and improve your coding experience, e.g. display values when hovering over model code.
| Modgen-style macro | member equivalent |
|---|---|
MIN(UNION_STATE) |
UNION_STATE::min |
MAX(UNION_STATE) |
UNION_STATE::max |
SIZE(UNION_STATE) |
UNION_STATE::size |
[back to classification enumeration]
[back to topic contents]
A range enumeration consists of an ordered set of all integers between a minimum and maximum value.
Negative values are allowed.
This subtopic contains the following sections.
rangeenumeration - examplerangeenumeration - valuerangeenumeration - assignmentrangeenumeration - membersrangeenumeration - macros
A range is declared in a syntactic island in model code.
Here's an example:
range YEAR //EN Year
{ 2025, 2034 };[back to range enumeration]
[back to topic contents]
- The value of a variable with a
rangetype is an integer between the declared minimum and maximum values, inclusively. - Care should be taken to distinguish the value of a
range, e.g.2027from the corresponding 0-based index, e.g.2. Member functions to convert between arangevalue and arangeindex are listed below. - A
rangecan potentially have a large number of possible values. An entity attribute of enumeration type with many possible values might not be eligible as microdata. For more, see Microdata Output > Attributes with many enumerators.
[back to range enumeration]
[back to topic contents]
- A variable with a
rangetype can be assigned an integer value. - The value can be modified using any of the C++ integer-modifying operators such as
++,--,+=, etc. - An exception can be thrown on assignment or modification if
bounds_errorsison.
[back to range enumeration]
[back to topic contents]
A range enumeration has the following static members which can be used in C++ model code.
These static members cannot be used in syntactic islands at this time because the OpenM++ compiler does not currently support the C++ :: scope operator.
The Example and Result columns use the example above.
| Member | Example | Result | Description |
|---|---|---|---|
min |
YEAR::min |
2025 |
Minimum integer value of range |
max |
YEAR::max |
2034 |
Maximum integer value of range |
size |
YEAR::size |
10 |
Number of distinct values in range |
indices() |
YEAR::indices() |
0..9 |
Ascending iterator of all indices, see Iterating enumerations |
values() |
YEAR::values() |
2025..2034 |
Ascending iterator of all values, see Iterating enumerations |
to_index(x) |
YEAR::to_index(2027) |
2 |
Convert the value x to the corresponding index. No exception is thrown. Result is clamped to valid indices. |
to_value(i) |
YEAR::to_value(3) |
2028 |
Convert the index i to the corresponding value. No exception is thrown. Result is NOT clamped. |
within(x) |
YEAR::within(2024) |
false |
Determine if the value x is within the range. No exception is thrown. |
clamp(x) |
YEAR::clamp(2099) |
2034 |
Clamps the value x to the range limits. No exception is thrown. |
[back to range enumeration]
[back to topic contents]
The following Modgen-style C++ macros are supported for a range enumeration.
Generally, it is preferable to avoid the use of C++ macros when possible by using the data or function members in the table above.
A notable benefit is an improved ability of your IDE to understand model code and improve your coding experience, e.g. display values when hovering over model code.
| Modgen-style macro | member equivalent |
|---|---|
MIN(YEAR) |
YEAR::min |
MAX(YEAR) |
YEAR::max |
SIZE(YEAR) |
YEAR::size |
RANGE_POS(YEAR, x) |
YEAR::to_index(x) |
WITHIN(YEAR, x) |
YEAR::within(x) |
COERCE(YEAR, x) |
YEAR::clamp(x) |
[back to range enumeration]
[back to topic contents]
A partition enumeration represents a map from a continuous real number to a complete, non-overlapping, finite ordered set of intervals spanning all real numbers from -inf to +inf.
The lower limit of the first interval is -inf and the upper limit of the last interval is +inf.
The boundaries between the intervals in a partition are given by a set of fixed real cut-points.
The number of intervals is, necessarily, one more than the number of cut-points.
An interval includes its lower cut-point but does not include its upper cut-point, except for the final interval whose upper cut-point is +inf.
This subtopic contains the following sections.
partitionenumeration - examplepartitionenumeration - valuepartitionenumeration - assignmentpartitionenumeration - memberspartitionenumeration - macros
A partition is declared in a syntactic island in model code which lists its cut-points.
Here's an example from the RiskPaths model:
partition AGEINT_STATE //EN 2.5 year age intervals
{
15, 17.5, 20, 22.5, 25, 27.5, 30, 32.5, 35, 37.5, 40
};This partition is specified by 11 cut-points. It consists of 12 intervals numbered 0,1,...,11.
[back to partition enumeration]
[back to topic contents]
- A variable with a
partitiontype is an integer denoting one of the intervals in thepartition. - The default value is the first interval in the
partition, which is always0. - Each interval has a corresponding integer value which is
0for the first interval and increases by 1 for each subsequent interval. - Each interval has a corresponding generated label showing its upper and lower bounds, e.g.
[17.5,20). The labels for the first and last intervals use the UTF symbol for infinity, ∞. The UTF symbol for infinity is replaced with the stringinfin the generated label by setting the optionascii_infinitytoon. This can help deal with compatibility issues with external applications which are not compliant with the UTF-8 standard. - Each interval of a
partitionhas an external name used in outputs, which is identical to the generated label.
[back to partition enumeration]
[back to topic contents]
- A variable with a
partitiontype can be assigned the integer value of one of its intervals. - The integer interval of any real number
xcan be determined using thepartitionmember functionvalue_to_interval(x), e.g.AGEINT_STATE::value_to_interval(36.0). - The value can be modified using several of the C++ integer-modifying operators, specifically
++,--,+=, and-=. - An exception can be thrown on assignment or modification if
bounds_errorsison.
The following table lists derived attributes which use a partition in their declaration, denoted P.
| Derived attribute | Description |
|---|---|
split(x, P) |
The interval in P containing the current value of the attribute x
|
self_scheduling_split(age, P) |
The interval in P containing the current age. If the next event would cause age to cross the next cut-point of P, time is advanced by an internal event so that age hits the cut-point first. |
self_scheduling_split(time, P) |
Similar to above, but for time
|
self_scheduling_split(duration(...), P) |
Similar to above, but for duration() and its variants. |
self_scheduling_split(active_spell_duration(...), P)` |
Similar to above, but for active_spell_duration(...). |
[back to partition enumeration]
[back to topic contents]
A partition enumeration has the following static members which can be used in C++ model code.
These static members cannot be used in syntactic islands at this time because the OpenM++ compiler does not currently support the C++ :: scope operator.
The Example and Result columns use the example above.
| Member | Example | Result | Description |
|---|---|---|---|
min |
AGEINT_STATE::min |
0 |
The minimum integer value of the partition. Always 0. |
max |
AGEINT_STATE::max |
11 |
The maximum integer value of the partition. Always one less than the number of intervals. |
size |
AGEINT_STATE::size |
12 |
The number of intervals in the partition. |
indices() |
AGEINT_STATE::indices() |
0..11 |
Ascending iterator of all indices in the partition, see Iterating enumerations
|
lower_bounds() |
AGEINT_STATE::lower_bounds() |
-inf,15,...40 |
std::array of lower bounds of all intervals in the partition. The lower bound of the first interval is always -inf. |
upper_bounds() |
AGEINT_STATE::upper_bounds() |
15,17.5,...,+inf |
std::array of upper bounds of all intervals in the partition. The upper bound of the last interval is alway +inf. |
value_to_interval(x) |
AGEINT_STATE::value_to_interval(36) |
9 |
Returns the integer interval containing the real value x
|
[back to partition enumeration]
[back to topic contents]
The following Modgen-style C++ macros are supported for a partition enumeration.
Generally, it is preferable to avoid the use of C++ macros when possible by using the data or function members in the table above.
A notable benefit is an improved ability of your IDE to understand model code and improve your coding experience, e.g. display values when hovering over model code.
| Modgen-style macro | C++ equivalent |
|---|---|
MIN(AGEINT_STATE) |
AGEINT_STATE::min |
MAX(AGEINT_STATE) |
AGEINT_STATE::max |
SIZE(AGEINT_STATE) |
AGEINT_STATE::size |
POINTS(AGEINT_STATE) |
AGEINT_STATE::upper_bounds() |
SPLIT(x, AGEINT_STATE) |
AGEINT_STATE::value_to_interval(x) |
[back to partition enumeration]
[back to topic contents]
Each dimension of a multi-dimensional parameter,
derived table or entity array attribute is declared using an enumeration.
In RiskPaths, for example,
parameters
{
//EN Age baseline for first pregnancy
double AgeBaselinePreg1[AGEINT_STATE];
...
};declares the 1-dimensional array parameter AgeBaselinePreg1 using the enumeration AGEINT_STATE,
which is a partition whose declaration is
partition AGEINT_STATE //EN 2.5 year age intervals
{
15, 17.5, 20, 22.5, 25, 27.5, 30, 32.5, 35, 37.5, 40
};In C++ model code, an element of an array can be referenced using an index. For example, in the following code fragment
TIME Person::timeFirstPregEvent()
{
...
dHazard = AgeBaselinePreg1[age_status]
...AgeBaselinePreg1 is indexed by the attribute age_status to obtain an age-specific hazard.
It is important that index limits be respected when indexing into an array.
An enumeration can be used to declare an entity attribute, a parameter, or a C++ local variable.
Here's an example of a declaration of an entity attribute from OzProj:
entity Person {
//EN Region
REGION region;
};where REGION is the classification:
classification REGION //EN Region
{
//EN New South Wales
REG_NSW,
//EN Victoria
REG_VIC,
//EN Queensland
REG_QLD,
...
};The default initial value of a variable declared with an enumeration type is the lowest allowable value of the enumeration.
For a classification or partition, that's always 0.
For a range, that's the lower bound of the range.
Values can be assigned to variables declared with an enumeration type. A variable of type range or partition can be assigned an integer value. A variable of type classification can be assigned a valid enumerator of the classification. A variable of type classification can also be assigned an integer value.
A value can be cast to an enumeration type, e.g.
entity Person //EN Individual
{
//EN Current integer age
LIFE integer_age = LIFE(self_scheduling_int(age));
};A cast is sometimes needed when working with entity attributes such as self_scheduling_int(age) in the example above.
modgen-specific: Modgen does not support parameters or attributes of type partition.
A model's run-time behaviour when an out-of-bounds value is assigned to a variable declared as an enumeration is given by the bounds_errors option.
This option affects the behaviour of variables with an enumeration type throughout a model.
Neither setting of bounds_errors is necessarily better than the other.
This is your choice as model designer.
The default value of bounds_errors, somewhat arbitrarily, is on.
Even if on, because of its potentially broad effects on model code, it's probably a good idea to make the choice of bounds_errors explicit in model code by including it in an options statement, e.g. in ompp_options.ompp.
If bounds_errors is on,
an out-of-bounds assignment produces a run-time error which includes information designed to aid debugging.
In this view, assignment of an out-of-bounds value is considered to be a bug in model logic.
If bounds_errors is off,
an out-of-bounds assignment silently clamps assignments to allowed values.
In this view, clamping an out-of-bounds value to allowed values is considered to be a language feature.
Whether bounds_errors is on or off, a variable declared with an enumeration type can never have an out-of-bounds value.
An out-of-bounds assignment will either produce a run-time error or will be silently clamped to allowed values.
options bounds_errors = on; // out-of-bounds assignment to an enumeration is an errorIf bounds_errors is on (the default) and an out-of-bounds value is assigned, the model will exit with a run-time error describing the issue.
For example, the following code fragment inserted into a function in RiskPaths
LIFE my_life;
my_life = 120;where where LIFE is declared in model code as
range LIFE //EN Simulated age range
{
0,100
};declares a local C++ variable my_life to be of type LIFE,
then attempts to assign an out-of-bounds value to it.
If that code is executed in a model run the model will exit with a run-time error message like
Simulation error: attempt to assign 120 to range LIFE which has limits [0,100] when current time is -inf in simulation member 0 with combined seed 1
In Debug mode you can use your IDE (e.g. Visual Studio) to activate C++ exception trapping and re-run the model to stop model execution exactly where the out-of-bounds assignment occurred in model code.
Then, you can determine the exact model code location responsible for the issue, and the values of other variables which may have led to the out-of-bounds assignment.
To do that in Visual Studio from the menu interface, do Debug > Windows > Exception Settings, then check the box C++ Exceptions.
When the exception is hit, click in the Call Stack window to navigate to the model code which caused the exception.
With bounds_errors on, declaring an index variable using an enumeration type can help identify some kinds of c/c++ array indexing errors in model code.
[back to Enumerations and the bounds_errors option]
[back to topic contents]
Alternatively, you can deliberately turn off bounds checking by
options bounds_errors = off; // assignment to an enumeration is clamped to allowed valuesThis implicitly turns on silent clamping of any assignment to a variable declared with an enumeration, ensuring that bounds are respected, with no issued error.
In the above example, with bounds_errors off,
the value of my_life after the assignment will be 100.
This silent automatic clamping works for any C++ operator which can modify the value, for example:
LIFE my_life;
my_life = 100;
my_life++;With bounds_errors off, the following code
entity Person //EN Individual
{
//EN Current integer age
LIFE integer_age = LIFE(self_scheduling_int(age));
};works as intended to clamp the integer value of self_scheduling_int(age) to the range enumeration LIFE without producing a run-time error.
With bounds_errors off, declaring an index variable using an enumeration type should be used with caution,
because it might inadvertently cause multiple assignment or endless loops in faulty model code.
[back to Enumerations and the bounds_errors option]
[back to topic contents]
In OpenM++, a
classification,
range, or
partition
has a special member function indices(), which is designed to be used in a C++ range-based for statement.
Here’s an example, where RISK_FACTOR is a classification:
// Verify that SimulateRiskFactor is true if the risk factor is passed to Boadicea in RA_BoadiceaUseRiskFactor.
for (auto j : RISK_FACTOR::indices()) {
if (RA_BoadiceaUseRiskFactor[j] && !SimulateRiskFactor[j]) {
ModelExit("Risk factors passed to Boadicea in RA_BoadiceaUseRiskFactor must be simulated in SimulateRiskFactor");
break; // not reached
}
}Here’s another example, where ONCOGENESIS_AGE_GROUP is a partition, and ONCOGENESIS_LAG is a range:
for (auto j : ONCOGENESIS_AGE_GROUP::indices()) {
for (auto k : ONCOGENESIS_LAG::indices()) {
double dValue = OncogenesisLags[j][k];
SetTableValue("IM_OncogenesisLags.VALUE", dValue, j, k);
}
}A key advantage of using indices() and range-based for to iterate an enumeration is that the index never goes out of bounds.
An out-of-bounds index is one of the most common c / c++ bugs.
For ranges, OpenM++ also includes the member function values(), which iterates the values of the range rather than the indices. Here’s an example where YEAR is a range:
for (auto year : YEAR::values()) {
if (year == 1970) {
theLog->logMsg("begin peak disco decade");
}
}A possible OpenM++ enhancement might be a member function for ranges which iterates indices and values as a std::pair, allowing one to access either inside the same range-based for, something like:
for (auto pr : YEAR::indices_values()) {
auto j = pr.first();
auto year = pr.second();
…
}which could be simplified further to
for (auto pr : YEAR::indices_values()) {
auto [j, year] = pr;
…
}or perhaps even further to
for (const auto& [j, year] : YEAR::indices_values()) {
…
}The const keyword above prevents inadvertent modification of j or year in the body of the for loop.
Note that the above code fragment is standard C++.
This potential enhancement is not a language enhancement to OpenM++, and has nothing to do with the OpenM++ compiler (omc).
It would add optional functionality to range enumeration objects.
Technically and specifically, range enumeration objects are implemented in the Range template which is declared in the header file OM_ROOT/include/omc/Range.h,
a file which is included indirectly in the C++ step of building an OpenM++ model.
Note that this sub-section describes a possible OpenM++ enhancement which has not been implemented.
Index errors are a common source of errors in C++ programs. Such errors are typically not detected by the C++ compiler and often manifest only indirectly when the program is executed. They can be hard to detect and resolve, because they manifest outside of a program's logic, by reading or modifying unrelated locations in memory.
In model code, an index is invalid if it falls outside the lower or upper integer limits of an enumeration dimension.
Note that the C++ ranged-based for statement in conjunction with the indices() member function can be used
to avoid index errors when iterating an enumeration,
as described
above.
Here's an example of an invalid index in a deliberately broken version of RiskPaths.
Consider the parameter ProbMort
parameters
{
...
double ProbMort[LIFE]; //EN Death probabilities
};declared using the range LIFE:
range LIFE //EN Simulated age range
{
0,100
};and the identity attribute integer_age declared as
entity Person
{
//EN Current integer age
int integer_age = self_scheduling_int(age);
};The model code fragment
TIME Person::timeDeathEvent()
{
...
if (ProbMort[integer_age] >= 1)
...
}would have undefined effects if integer_age was greater than 100.
That's because the value of ProbMort[integer_age] could be anything for such an out-of-bounds index,
and that value would be unrelated to model logic.
For historical reasons and for generality,
the C/C++ language and run-time allow array indices to take on any integer value.
An out-of-bounds index error can cause a model to crash at run-time, produce spurious errors, or produce erroneous outputs.
In this contrived example the run with broken RiskPaths did not complete, and issued the following spurious error message:
Simulation error: error : Event time -inf is earlier than current time 101.0000000000000 in event DeathEvent in entity_id 20 in simulation member 0 with combined seed 656222176
The option
options index_errors = on;
instructs the OpenM++ compiler to modify (aka 'mark up') a model's C++ code by inserting additional C++ code to verify indices of parameters and entity array members.
Specifically, the additional C++ code verifies that indices of multi-dimensional parameters and indices of entity array members in model code respect the bounds of the enumerations used as dimensions in their declarations. If such an index is out of bounds, the run will immediately fail with a runtime error like the following (the original single line error message has been split into multiple lines here for clarity):
Simulation error:
invalid index 101
in 0-based dimension 0 of ProbMort
with bounds [0,100]
when current time is 101.0000000000000
in entity_id 20
in or after event om_ss_event
in simulation member 0
with combined seed 656222176
at module Mortality.mpp[62]
Note that the invalid index value is given at the beginning of the message and the exact module and line at the end.
The error message includes additional runtime contextual information which can be useful to understand or debug the issue.
Incidentally, the built-in event om_ss_event in the message above maintains the value of self_scheduling_int(age),
and the error occurred when the time function of DeathEvent was called by the simulation framework in response to the consequent change in the identity attribute integer_age.
The model code location Mortality.mpp[62] is the same line shown in the code fragment above.
If index_errors is on, the OpenM++ compiler will output additional log lines during model build like
1>Model code markup - start
1> Marking up src/om_developer.cpp
1> Pattern 1 of 178
1> Pattern 100 of 178
1> Pattern 178 of 178
1>Model code markup - finish
The current implementation of index_errors may cause model build to take considerably longer,
especially for large, complex models.
In tests of several large models in Release mode,
index_errors had little impact on model run time.
modgen-specific: Unlike Modgen, index_errors in OpenM++ works in both Debug and Release mode.