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.

Related topics

Topic contents

Introduction and description

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).

[back to topic contents]

Experiment #1

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.

[back to topic contents]

Experiment #2

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.

[back to topic contents]

Remarks

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. 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.

[back to topic contents]

Model code

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;
        }
    }
}

[back to topic contents]

Model input

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,
    };
};

[back to topic contents]

⚠️ **GitHub.com Fallback** ⚠️