Custom Blueprint¶
A blueprint is a model class paired with a data class. To build your own
digital-twin structure, you implement both, respecting the small interface
contracts that ReplayBG.twin() and ReplayBG.replay() rely on. The built-in
single-meal pair is the best minimal reference implementation —
read src/py_replay_bg/model/single_meal_t1d.py and
src/py_replay_bg/data/single_meal_t1d_data.py alongside this page.
The model class contract¶
ReplayBG duck-types the model: any object with the methods and attributes below
works. The built-in models are compiled with Numba's @jitclass via the project's
jitclass_ switch; if you do the same, every state array and scalar field must be
declared up front in a JITCLASS_SPEC with explicit Numba types.
from py_replay_bg.environment import jitclass_
JITCLASS_SPEC = [
("u2ss", float64),
("G", float64[:]),
# ... every scalar/array field with its Numba type ...
]
@jitclass_(JITCLASS_SPEC)
class MyT1DModel:
...
The jitclass_ switch
jitclass_ (from src/py_replay_bg/environment/__init__.py) applies real JIT compilation, or
falls back to a no-op identity decorator when DEBUG=True — flip that flag to
debug your model in plain Python. Parameters are always passed between Python
and the jitclass through Numba typed dicts (numba.typed.Dict), which you
build with to_typed_f64_dict.
Your model must implement these methods:
| Method | Role |
|---|---|
__init__(u2ss, theta0, x0, tsteps, [t_start,] theta_prev) |
Allocate state arrays and initialize. u2ss is the steady-state basal insulin; theta0/x0/theta_prev are typed dicts (empty ⇒ defaults / cold start). |
reset(theta0, g0=nan, u0=<empty array>) |
Set parameters from a typed dict and re-initialize all state. g0 (first observed glucose) seeds the initial glucose on a cold start only; u0 (the t=0 input row) seeds the buffer that delayed inputs read from. |
step(u, t) |
Advance the ODE by one integration step given the input row u at time index t. |
output(t) |
Return the observable model output at time t (the CGM-comparable signal). |
get_final_x0() |
Optional. Return a typed dict of end-of-segment state, to seed the next segment's x0. |
get_theta() |
Optional. Return a typed dict of the current parameters, to seed the next segment's theta_prev. |
get_final_x0() and get_theta() are only needed to chain segments
(multi-day intervals). The
multi-meal models' get_theta() also returns SI_last, used to rescale the next
segment's X0: the sensitivity active at the segment's last minute (the extended
model always reports SI_D).
How ReplayBG drives the model:
- Twinning. For every objective evaluation the twinner builds a typed dict of
the candidate values (integer priors rounded first) and calls
model.reset(theta_dict, g0, rbg_data.u[0]), withg0the first non-missing glucose value. It then reads each estimated parameter back withgetattr(model, name)to evaluate the prior, and simulatesoutput(0), thenstep(u[k], k)andoutput(k)fork = 1 … tsteps-1. So every key ofunknown_parameters_priormust be a model attribute with that exact name. - Replay.
replay()callsmodel.reset(model.theta0, g0, rbg_data.u[0])first (only if the model hasresetandtheta0), then runs the same loop, invoking the callbacks before eachstep. Store the constructor'stheta0asself.theta0;plot_twinningalso uses it when called withouttheta.
theta0 during twinning
The twinner's dict holds only the estimated parameters, and reset replaces
the parameters wholesale. With the built-in models, every parameter you do not
estimate therefore takes the default hard-coded in reset, not the value
passed as the constructor's theta0, while twinning.
The constructor signature of the built-in models is:
# single-meal
SingleMealT1DModel(u2ss, theta0=<empty typed dict>, x0=<empty typed dict>,
tsteps=1440, theta_prev=<empty typed dict>)
# multi-meal / extended add t_start (minute-of-day the segment starts)
MultiMealT1DModel(u2ss, theta0=..., x0=..., tsteps=1440, t_start=240, theta_prev=...)
Missing keys in theta0 fall back to physiological defaults set inside reset.
Missing keys in x0 fall back to the steady state, except G0/IG0: they fall
back to g0 (on a cold start, i.e. empty theta_prev), then to Gb. This is
what lets you estimate only a subset of parameters (the rest keep population
values).
The data class contract¶
The data class turns a raw DataFrame (one row every 5 minutes) into the
arrays the model and twinner consume. Its constructor signature matches the
built-ins:
MyT1DData(data: pd.DataFrame = None, data_to_input=None,
body_weight=100., environment: Environment = None)
It must expose (at least) these attributes:
| Attribute | Meaning |
|---|---|
u |
Input matrix, shape (tsteps, n_channels), in model units; u[k] is the row passed to step. |
data_to_input |
Mapping {channel_index: channel_name} describing u's columns. |
tsteps |
Number of integration steps (from the t column and the sampling time). |
yts |
Data sampling stride in integration steps (5, since data is 5-minutely). |
y |
Observed output (glucose) at data resolution; may contain NaN. |
y_idxs |
Indices of the non-missing observations (np.where(~isnan(y))); y[y_idxs[0]] is the g0 passed to reset. |
t_hour |
Hour of day at each integration step (length tsteps); read by the replay context. |
u2ss |
Steady-state basal insulin — mean(basal) * 1000 / body_weight. |
body_weight |
Patient body weight (used to normalize inputs; read by the built-in callbacks). |
The data_to_input order is the contract between the data class and the model:
the model's step(u, t) reads channels by that index. By convention the last two
channels are forcing_ip and forcing_ra (zero unless a caller overrides them
for a counterfactual forcing scenario). The built-in
callbacks address channels by name: CorrectionBolus
needs a bolus channel and HypoTreatment a meal_H channel.
Wiring it up¶
Once both classes exist, they are used exactly like a built-in blueprint:
rbg = ReplayBG()
rbg_data = MyT1DData(data=df, environment=rbg.environment)
model = MyT1DModel(u2ss=rbg_data.u2ss, tsteps=rbg_data.tsteps)
result = rbg.twin(rbg_data=rbg_data, model=model,
unknown_parameters_prior=unknown_parameters_prior)
The twinner is model-agnostic: it only calls reset, step and output, and
reads back the parameters named in your priors. As long as your two classes honor the contracts
above, everything downstream — twinning, replay, callbacks,
sensors, plotting and
analysis — works unchanged.
The output is model-agnostic too
Sensors and plotting only ever see model.output(t) and the channel names in
data_to_input. Nothing downstream assumes what your output means, so a
custom model with a differently-named output still plots and analyzes fine.