Illustrative Model Align1 - openmpp/openmpp.github.io GitHub Wiki
Home > Model Development Topics > Illustrative Model Align1
Align1
is an experimental model which manipulates the event queue to align with external counts.
This topic describes the approach and implementation, and includes some experiments and notes.
- Introduction and description
- Experiment #1 Illustrative run
- Experiment #2 Computational cost
- Remarks Some notes on the approach and possible future steps
-
Align1
code Model code moduleAlignment.mpp
-
Align1
input Default parameter valuesAlignment.dat
The Align1
model is a proof of concept and testbed for dynamic alignment of time-based models using event queue look-ahead. Align1
steers itself to aggregate annual targets by reading and modifying the event queue dynamically at the beginning of each year.
Align1
is based on the NewTimeBased
model which is part of the OpenM++ distribution. It adds alignment apparatus in a new module Alignment.mpp
and associated parameters in Alignment.dat
but is otherwise unmodified. NewTimeBased
has a Ticker
entity with a timekeeping TickEvent
and Person
entities with Mortality
events. Align1
adds a new event AlignmentEvent
to Ticker
, which occurs at the same time as TickEvent
but at lower priority. The input parameter MortalityAlignmentTarget
contains target mortality counts by year. At the beginning of each year AlignmentEvent
reads the event queue and counts the number of deaths scheduled to occur during that year. If the count is higher than the target for the year, enough scheduled events are deferred to the subsequent year to hit the target. If the count is lower than the target, enough scheduled events are advanced from subsequent years to the current year to hit the target. The exact rules are mechanical and described in model code comments. This process is repeated at the beginning of each year.
The events which are deferred or advanced to hit alignment targets are those which are closest to the upper boundary of the current alignment year.
The Default run has 10,000 Persons and sets MortalityAlignmentTarget
to the mortality counts which would occur in the absence of alignment (using table MortalityCounts from a previous run with alignment off).
This experiment sets target mortality to 122 in each of the 20 years. 122 is the average annual mortality in the 20 years in the Default run with no alignment. The first column shows mortality counts in the Default run with no alignment. One observes a secular decrease (with some noise) because the population is progressively smaller so fewer die each year due to the lower base population (the morality hazard is constant). The target column, in contrast, sets the number of deaths to a fixed value in each year. Compared to the Default run, deaths need to be decreased in early years and increased in later years to hit the target.
Year | Mortality (Default) | Target | Mortality (Aligned) | Events Deferred | Events Advanced |
---|---|---|---|---|---|
0 | 141 | 122 | 122 | 19 | 0 |
1 | 144 | 122 | 122 | 41 | 0 |
2 | 114 | 122 | 122 | 33 | 0 |
3 | 146 | 122 | 122 | 57 | 0 |
4 | 142 | 122 | 122 | 77 | 0 |
5 | 126 | 122 | 122 | 81 | 0 |
6 | 129 | 122 | 122 | 88 | 0 |
7 | 139 | 122 | 122 | 105 | 0 |
8 | 111 | 122 | 122 | 94 | 0 |
9 | 118 | 122 | 122 | 90 | 0 |
10 | 112 | 122 | 122 | 80 | 0 |
11 | 118 | 122 | 122 | 76 | 0 |
12 | 102 | 122 | 122 | 56 | 0 |
13 | 110 | 122 | 122 | 44 | 0 |
14 | 104 | 122 | 122 | 26 | 0 |
15 | 116 | 122 | 122 | 20 | 0 |
16 | 114 | 122 | 122 | 12 | 0 |
17 | 124 | 122 | 122 | 14 | 0 |
18 | 112 | 122 | 122 | 4 | 0 |
19 | 112 | 122 | 122 | 0 | 6 |
The algorithm attained the annual targets by deferring mortality events in each of the first 19 years and advancing 6 mortality events for the final year. Mortality events were advanced only in the final year of the run because for all other years sufficient deaths had been deferred in previous years. The annual targets are hit exactly because event times are not recalculated during the year-in-progress in this model.
This second experiment explored computational cost and scaling behaviour, using a run with 10 million Person
entities.
With this population size, there were over 100,000 mortality events per year.
Three runs were done.
Run | Description | Time |
---|---|---|
1 | No alignment | 1m25s |
2 | Alignment with targets=actual | 1m27s |
3 | Alignment with targets=+/- 5% actual | 1m26s |
Run 1 had alignment disabled. Run 2 had alignment enabled, but the targets were the same as the results without alignment. So, no adjustment of the event queue was done, but the event queue was probed each year. Run 3 had random targets within +/- 5% of the original mortality results. So alignment was doing some work with the event queue to hit the targets in run 3.
The table shows that the run times were indistinguishable. For this model, anyway, the incremental cost of alignment was barely detectable.
- A natural way for continuous time models to align is to tinker with the timing of events which “would have occurred anyway”. This helps preserve aspects of model logic, since prohibited events remain prohibited under alignment.
- An event which was deferred or advanced by alignment will still have its event time recomputed if entity attributes change (perhaps to +inf if the entity is no longer eligible for the event). This maintains aspects of the internal causative logic of the model, even under alignment.
- The ‘event censoring’ optimization should perhaps not be used in models using this alignment technique, since that might deplete the pool of future events which can be advanced into an alignment interval. For example, in experiment 1 above, the 6 events which were advanced to year 19 to hit the target had original times beyond the end of the run (which ended at time=20). They would have been right censored hence never placed in the queue.
- In the current version of
Align1
, event times do not change during the simulation within an alignment window (year). It might be interesting to add a birthday event to the model and have a mortality schedule which varies by single year of age, to make tests more realistic. - The algorithm could be adapted to split an alignment interval (year) into sub-intervals (e.g. 10 equal intervals in each year), with recalculation of progress to the target for the current year. That would allow the algorithm to adjust for interacting events during the simulation of the current year. That would require counting events as they occur during the alignment interval, which was not needed in this version of
Align1
. - As the number of alignment targets increases, and if targets are classified by entity characteristics (e.g. age group), the code volume for alignment could become massive and error-prone (even though mechanical). That makes it a tempting candidate for new supporting functionality to automate some aspects.
Below is the model source code for the module Alignment.mpp
:
//LABEL (Alignment, EN) Alignment using event queue
#include "omc/optional_IDE_helper.h" // help an IDE editor recognize model symbols
#if 0 // Hide non-C++ syntactic island from IDE
parameters {
bool EnableAlignment;
int MortalityAlignmentTarget[REPORT_TIME];
};
actor Ticker
{
//EN Time of next Alignment event
TIME next_alignment;
//EN Mortality events deferred by alignment (cumulative)
int mortality_deferred;
//EN Mortality events advanced by alignment (cumulative)
int mortality_advanced;
//EN Deficit in advanced mortality events because queue was exhausted (cumulative)
int mortality_deficit;
// AlignmentEvent should be lower priority than any other event
// so that it executes after other tied events which might influence
// events in the current alignment interval.
event timeAlignmentEvent, AlignmentEvent, 1; //EN Alignment event
};
table Person MortalityCounts
{
report_time
* {
entrances(alive, false) //EN Mortality events
}
};
table Ticker AlignmentReport
{
report_time
* {
mortality_deferred, //EN Mortality events deferred
mortality_advanced, //EN Mortality events advanced
mortality_deficit //EN Mortality target deficit
}
};
#endif // Hide non-C++ syntactic island from IDE
TIME Ticker::timeAlignmentEvent()
{
// is synchronous with TickEvent, but lower priority
return EnableAlignment ? next_alignment : time_infinite;
}
void Ticker::AlignmentEvent(void)
{
// get event_id of MortalityEvent
int event_id_mortality = omr::event_name_to_id("MortalityEvent");
assert(event_id_mortality != -1); // MortalityEvent not found
// width of the target window
TIME alignment_window_width = 1.0;
// The upper bound of the target window, NB is just beyond the current alignment window
TIME alignment_window_upper_bound = time + alignment_window_width;
int alignment_window_target_count = MortalityAlignmentTarget[report_time];
//
// walk the event queue, from present to future
//
// add events to defer_list or advance_list
// as needed to hit the target count in the alignment time window
//
auto& event_queue = *BaseEvent::event_queue; // alias for the model event queue
std::forward_list<BaseEvent*> defer_list; // list of events to defer
std::forward_list<BaseEvent*> advance_list; // list of events to advance
int unadjusted_count = 0; // count of scheduled events in the alignment time window before alignment
int deferred_events = 0; // count of scheduled events deferred to the future beyond the alignment time window
int advanced_events = 0; // count of scheduled events advanced from the future to within the alignment time window
for (auto evt : event_queue) {
int id = evt->get_event_id();
if (id != event_id_mortality) {
// not a mortality event, skip
continue;
}
double evt_time = evt->event_time;
if (evt_time < alignment_window_upper_bound) {
// we are inside the target alignment time window
// update the count of currently scheduled events within the window
++unadjusted_count;
if (unadjusted_count > alignment_window_target_count) {
// there is an excess of scheduled events within the alignment time window
// so add this event to the defer list
defer_list.push_front(evt);
++deferred_events;
}
}
else {
// we are beyond the alignment time window
if (advanced_events + unadjusted_count >= alignment_window_target_count) {
// no need to find more events to advance, have found what's needed
// so stop queue walk
break;
}
// there is a deficit of events within the alignment time window
// so add this event to the advance list
advance_list.push_front(evt);
++advanced_events;
}
}
if (alignment_window_target_count > unadjusted_count && unadjusted_count - alignment_window_target_count != advanced_events) {
// there were insufficient events in the queue beyond the alignment window to meet the target
mortality_deficit += alignment_window_target_count - unadjusted_count - advanced_events; // for AlignmentReport
}
if (deferred_events > 0) {
mortality_deferred += deferred_events; // for AlignmentReport
assert(advanced_events == 0);
for (auto evt : defer_list) {
// defer this event
// remove it from the event queue
event_queue.erase(evt);
// postpone the event time by one alignment interval
evt->event_time += alignment_window_width;
// re-insert it to the event queue
event_queue.insert(evt);
}
}
else if (advanced_events > 0) {
mortality_advanced += advanced_events; // for AlignmentReport
// advance this event
for (auto evt : advance_list) {
// advance this event
// remove it from the event queue
event_queue.erase(evt);
// advance the event time by an integral number of alignment intervals
// until it falls within the alignment window
TIME new_time = evt->event_time;
while (new_time >= alignment_window_upper_bound) {
new_time -= alignment_window_width;
}
evt->event_time = new_time;
// re-insert it to the event queue
event_queue.insert(evt);
}
}
else {
// nothing to do
}
{
// schedule next alignment
Time t = next_alignment + alignment_window_width;
if (t >= SimulationEnd) {
next_alignment = time_infinite;
}
else {
next_alignment = t;
}
}
}
Below is the Default model input parmeters in Alignment.dat
:
parameters {
bool EnableAlignment = true;
int MortalityAlignmentTarget[REPORT_TIME] = {
141, //141,
144, //144,
114, //114,
146, //146.
142, //142,
126, //126,
129,
139,
111,
118,
112,
118,
102,
110,
104,
116,
114,
124,
112,
112,
};
};