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
classification
enumerationrange
enumerationpartition
enumeration- Enumerations as dimensions
- Enumerations as types
- Enumerations and the
bounds_errors
option - Iterating enumerations
- Enumeration index validity and the
index_errors
option
This topic first describes the three kinds of enumerations in OpenM++:
-
classification
enumerations for categorical values, -
range
enumerations for sequential integer values like integer age or year, and -
partition
enumerations 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.
classification
enumeration - exampleclassification
enumeration - valueclassification
enumeration - assignmentclassification
enumeration - membersclassification
enumeration - 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
classification
type is one of the declared enumerators of theclassification
. - The default value is the first enumerator in the
classification
. - The enumerators of a
classification
can be used by name in model code (technically, the enumerators are a C++enum
). - Each enumerator has a corresponding integer value which is
0
for the first enumerator and increases by 1 for each subsequent enumerator. - Each enumerator of a
classification
has 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_enumerators
tooff
. 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
classification
type 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_errors
ison
.
[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.
range
enumeration - examplerange
enumeration - valuerange
enumeration - assignmentrange
enumeration - membersrange
enumeration - 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
range
type is an integer between the declared minimum and maximum values, inclusively. - Care should be taken to distinguish the value of a
range
, e.g.2027
from the corresponding 0-based index, e.g.2
. Member functions to convert between arange
value and arange
index are listed below. - A
range
can 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
range
type 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_errors
ison
.
[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.
partition
enumeration - examplepartition
enumeration - valuepartition
enumeration - assignmentpartition
enumeration - memberspartition
enumeration - 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
partition
type 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
0
for 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 stringinf
in the generated label by setting the optionascii_infinity
toon
. This can help deal with compatibility issues with external applications which are not compliant with the UTF-8 standard. - Each interval of a
partition
has 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
partition
type can be assigned the integer value of one of its intervals. - The integer interval of any real number
x
can be determined using thepartition
member 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_errors
ison
.
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 error
If 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 values
This 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.