abm-covid-lux / abmlux

Agent-based epidemic modelling
Other
4 stars 0 forks source link

Stevew/scheduling #108

Closed StephenWattam closed 3 years ago

StephenWattam commented 3 years ago

All of the interventions are now configured with their own self-contained config block. This means I've had to add some keys to prevent them from reading keys out of other parts of the config, but it puts us in a much better/more modular situation. I've done the same thing to the disease model, so we can have another disease model if we wish.

Each intervention has two special keys: __type__, which specifies the class, and __schedule__, which specifies the, uh, schedule. This means we can for the first time have two interventions with the same class and config in the same sim, so we could have quarantine that sends people home and that which sends people outdoors at different times, or with different configs.

Schedules are collected together by abmlux.__init__.py and sent to the Scheduler. The Scheduler class is created by the simulator and pre-processes schedules to make its time callback as fast as possible. Basically, it takes config that looks like this:

"date": disable
"another date": enable
10000: disable

The key is a timestamp, which is either a human-readable absolute date (mapped onto the clock using the sim epoch) or a tick number. You can mix/match these on a whim. Or on the instructions of a stranger with a weapon who is forcing you to configure the simulation. Or as the result of a drug-fuelled simulate-a-thon. Whatever. The action has to be 'enable' or 'disable' right now.

The Scheduler class looks through all of these, sorts them, and makes a big array containing, for each tick, a list of actions to perform. It looks through the actions and finds the method to run, so this ends up being a list of n references, where n is the last event time (in the above example, 1000).

The Intervention class has been extended to have enable and disable methods. These can be overridden, but the default implementation merely updates an instance variable, self.enabled, which can be used to see if the class is enabled or not. In practice this is pretty neat and meant very little code in the interventions themselves --- most interventions will "cleanly shut themselves down" if you only disable part of their logic, so I have turned off only the incoming logic. When the incoming events peter out, they'll naturally stop operating in a graceful way.

One gotcha is that in the enable and disable methods it should be possible to subscribe/unsubscribe from the bus entirely. This is tempting for performance reasons, but the re-subscribing would lead to the order of events changing and everything would change behaviour. So for now, censor events by checking self.enabled rather than removing them. I may change this so there's a first-class mechanism for disabling events in the bus, but for now early exit of the callbacks is about as performant and quite explicit when viewing the code.

I expect there's some bugs here and there in the enable/disable logic, suggest having a play...

Enjoy.

github-actions[bot] commented 3 years ago

Two Thumbs Up GIF by UFC

Autogenerated by pr-status-giphy-action.