Callbacks¶
PassengerSim includes a variety of optimized data collection processes that run automatically during a simulation, but these pre-selected data may not be sufficient for every analysis. To supplement this, users can choose to additionally collect any other data while running a simulation. This is done by writing a "callback" function. Such a function is invoked regularly while the simulation is running, and can inspect and store almost anything from the Simulation object.
import passengersim as pax
pax.versions()
passengersim 0.51.dev11+g921f2a8 passengersim.core 0.51.dev2+g02d3ce20
Here, we'll run a quick demo using the "3MKT" example model. We'll give AL1 the 'P' RM system to make it interesting.
cfg = pax.Config.from_yaml(pax.demo_network("3MKT"))
cfg.simulation_controls.num_samples = 100
cfg.simulation_controls.burn_samples = 50
cfg.simulation_controls.num_trials = 1
cfg.db = None
cfg.outputs.reports.clear()
cfg.carriers.AL1.rm_system = "P"
sim = pax.Simulation(cfg)
Types of Callback Functions¶
To collect data, we can write a function that will interrogate the simulation and grab whatever info we are looking for. There are three different points where we can attach data collection callback functions:
begin_sample
, which will trigger data collection at the beginning of each sample, after the RM systems for each carrier are initialized (e.g. with forecasts, etc) but before any customers can arrive.end_sample
, which will trigger data collection at the end of each sample, after customers have arrive and all bookings have be finalized.daily
, which will trigger data collection once per day during every sample, just after any DCP or daily RM system updates are run.
The first two callbacks (begin and end sample) are written as a function that accepts one argument
(the Simulation
object), and either returns nothing (to ignore that event)
or returns a dictionary of values to store, where the keys are all strings
naming what's being stored and the values can be whatever is of interest.
We can attach each callback to the Simulation by using a Python decorator.
Example Callback Functions¶
For example, here we create a callback to collect carrier revenue at the end of every sample. Note that we skip the burn period by returning nothing for those samples; this is not required by the callback algorithm but is good practice for analysis.
@sim.end_sample_callback
def collect_carrier_revenue(sim):
if sim.sim.sample < sim.sim.burn_samples:
return
return {c.name: c.revenue for c in sim.sim.carriers}
The daily callback operates similarly, except it accepts a second argument that gives the number of days prior to departure for this day. You don't need to use the second argument in the callback function, but you need to including in the function signature (and you can use it if desired, e.g. to collect data only at DCPs instead of every day). In the example here, we collect daily carrier revenue, but only every 7th sample, which is a good way to reduce the overhead from collecting detailed data.
@sim.daily_callback
def collect_carrier_revenue_detail(sim, days_prior):
if sim.sim.sample < sim.sim.burn_samples:
return
if sim.sim.sample % 7 == 0:
return {c.name: c.revenue for c in sim.sim.carriers}
Multiple callbacks of the same kind can be attached (i.e. there can be two end_sample callbacks). The only limitation is that the named values in the return values of each callback function must be unique, or else they will overwrite one another.
Once we have attached all desired callbacks, we can run the simulation as normal.
summary = sim.run()
Task Completed after 0.69 seconds
All the usual summary data remains available for review and analysis.
summary.fig_carrier_revenues()
Callback Data¶
In addition to the usual suspects, the summary object includes the collected callback data from our callback functions.
summary.callback_data
<passengersim.callbacks.CallbackData from end_sample, daily>
Because we connected a "daily" callback, the data we collected is available under the
callback_data.daily
accessor.
summary.callback_data.daily[:5]
[{'trial': 0, 'sample': 56, 'days_prior': 63, 'AL1': 0.0, 'AL2': 0.0}, {'trial': 0, 'sample': 56, 'days_prior': 62, 'AL1': 600.0, 'AL2': 2125.0}, {'trial': 0, 'sample': 56, 'days_prior': 61, 'AL1': 1350.0, 'AL2': 4225.0}, {'trial': 0, 'sample': 56, 'days_prior': 60, 'AL1': 2025.0, 'AL2': 6250.0}, {'trial': 0, 'sample': 56, 'days_prior': 59, 'AL1': 3125.0, 'AL2': 6825.0}]
As you might expect, the "begin_sample" or "end_sample"
callbacks are available under callback_data.begin_sample
or callback_data.end_sample
,
respectively.
summary.callback_data.end_sample[:5]
[{'trial': 0, 'sample': 50, 'AL1': 100475.0, 'AL2': 103700.0}, {'trial': 0, 'sample': 51, 'AL1': 101475.0, 'AL2': 97425.0}, {'trial': 0, 'sample': 52, 'AL1': 108575.0, 'AL2': 95000.0}, {'trial': 0, 'sample': 53, 'AL1': 104275.0, 'AL2': 98825.0}, {'trial': 0, 'sample': 54, 'AL1': 101300.0, 'AL2': 97000.0}]
The callback data can include pretty much anything, so it is stored in a
very flexible (but inefficient) format: a list of dict's. If the content
of the dicts is fairly simple (numbers, tuples, lists, or nexted dictionaries thereof),
it can be converted into a pandas DataFrame using the to_dataframe
method
on the callback_data
attribute. This may make subsequent analysis easier.
summary.callback_data.to_dataframe("daily")
trial | sample | days_prior | AL1 | AL2 | |
---|---|---|---|---|---|
0 | 0 | 56 | 63 | 0.0 | 0.0 |
1 | 0 | 56 | 62 | 600.0 | 2125.0 |
2 | 0 | 56 | 61 | 1350.0 | 4225.0 |
3 | 0 | 56 | 60 | 2025.0 | 6250.0 |
4 | 0 | 56 | 59 | 3125.0 | 6825.0 |
... | ... | ... | ... | ... | ... |
443 | 0 | 98 | 4 | 51800.0 | 61225.0 |
444 | 0 | 98 | 3 | 53800.0 | 63225.0 |
445 | 0 | 98 | 2 | 56225.0 | 66400.0 |
446 | 0 | 98 | 1 | 61025.0 | 70050.0 |
447 | 0 | 98 | 0 | 62450.0 | 73450.0 |
448 rows × 5 columns
Users are free to process this callback data now however they like, with typical Python tools: analyze, visualize, interpret, etc.
import altair as alt
alt.Chart(
summary.callback_data.to_dataframe("daily").eval("DIFF = AL1 - AL2")
).mark_line().encode(
x=alt.X("days_prior", scale=alt.Scale(reverse=True)),
y="DIFF",
color="sample:N",
)