chc.zones¶
Stability tier: experimental.
A city's zones over time: idle supply and open requests per zone, an incentive and a price per zone as levers, recruits drawn from neighbouring zones, and supply that answers an incentive late.
The marketplace plant the loop runs on. chc.marketplace's softmax equilibrium is not
control-affine, so it stays a validation target; this plant is control-affine by construction, so
identification and safety read the same object. Per zone i, K zones on a ring::
d(supply)/dt = gamma (sigma - supply) + (I - P)(recruit incentive + carry stock) - m - eta xi
d(queue)/dt = demand (1 + xi - elasticity price) - m - alpha queue
d(stock)/dt = incentive - theta stock
m is the zone's rate of trips (Matching) and xi its demand shock. The levers are
moves from the do-nothing point, both on [0, 1]: the incentive in units of the budget, the price
a rise over the list price as a share of it. An incentive recruits recruit drivers a period at
once and carry a period per unit of its stock, the incentive exposure decaying at theta, so
part of the response arrives late, 1/theta periods on average. A share spill of the recruits
come from the neighbouring zones, P[j, i] = spill / 2 for each neighbour j of i, so the
city gains 1 - spill of what the zone does. A full price rise turns away elasticity of the
zone's demand.
The do-nothing point is the parameterisation: supply and queue are each zone's idle supply
and open requests with no lever moved and no shock, and sigma and demand are derived so that
the point is the steady state::
sigma = supply + m0 / gamma, demand = m0 + alpha queue,
m0 = mu supply queue / (supply + queue)
HONEST SCOPE, and it bounds what a number read off this plant means:
- The logged operator chased the shock. Both levers rose with the zone's shock, which raises
demand and takes supply off the road. A fit that ignores it reads a price rise as raising demand
and an incentive as recruiting a fraction of what it does. The shock is logged (
shock_<i>), so it can be adjusted for, andZoneMarketSystem.graphderives that adjustment. Shocks are independent from period to period, so the stock's drift is not confounded; a persistent shock would confound it too. - The log's actions share its noise. Under the logged operator an action and the plant's noise
both move with the period's shock, and an off-policy evaluation that weights actions by
u | xalone assumes they do not.ZoneMarketSystem.linear_gaussianis the one-period law under a policy that does not see the shock. - A switchback on one zone reads the zone's recruits, not the city's: the zone gains
recruitand its neighbours losespill * recruitbetween them. - Under harmonic matching the drift is nonlinear, and a fit whose drift is a polynomial in the
state is a local surrogate of it, as in
chc.mmm: audit a plan on this plant, not on the fit's forecast. Under linear matching every closed form here is exact. thetais taken as known, aschc.mmmtakes its adstock rates:ZoneMarketSystem.stock_dynamicshands the stock rows to a fit asknown=.- Nothing here is anyone's data. The parameters are generic, chosen to put the confounding in the levers' effects, which is the mechanism under study.
Matching = Literal['harmonic', 'linear']
module-attribute
¶
How a zone's idle supply and open requests turn into trips:
"harmonic":mu supply queue / (supply + queue), never more than the smaller side;"linear": its tangent at the do-nothing point, which passes through zero because the harmonic law is homogeneous of degree one, so the two laws share the do-nothing steady state.
ZoneMarketPlant
¶
Bases: Module
The true continuous plant: x = [supply, queue, stock] and u = [incentive, price],
each block one entry per zone.
Control-affine with a constant channel: an incentive recruits (I - P) diag(recruit) at once
and a price rise turns away diag(demand * elasticity); everything else is drift. shock
is the demand shock each zone is sitting at, a field rather than an input so that the plant
stays a plain Dynamics.
trips(x)
¶
Each zone's rate of trips at state x.
ZoneMarketSystem
dataclass
¶
A synthetic city log, one row per day and period, whose operator chased each zone's shock.
The per-zone tuples set the number of zones and must agree in length. Zone 0 is the centre, with the most supply and demand and the strongest recruitment.
Raises:
| Type | Description |
|---|---|
ValueError
|
on per-zone tuples of different lengths, a do-nothing point or a rate that is
not positive, a negative channel, shock or noise, an |
state_columns
property
¶
The plant's state order: every zone's supply, then every queue, then every stock.
lever_columns
property
¶
The plant's action order: every zone's incentive, then every price.
do_nothing
property
¶
The steady state with no lever moved and no shock, in state_columns order.
incentive_channel
property
¶
Drivers a period an incentive held at one recruits once settled, per zone:
recruit + carry / theta. The channel zone_decision's b stands for.
plant(shock=None)
¶
The true vector field at a demand shock per zone, none by default.
sample(*, n_days=40, n_periods=48, dt=1.0, seed=0)
¶
Simulate the log period by period, each zone's levers moved with its own shock.
Every day starts at the do-nothing point, with supply and queue jittered by noise. A
row holds the state at the start of its period and the levers and shock over it. Integrated
with chc.integrate.rk4_step, which the planner rolls out with, so the log carries no
discretisation gap from the model class fitted to it.
graph()
¶
Each zone's shock moves its levers and both its states; an incentive moves its stock and the supply of every zone it recruits in or from, and so does the stock; a price moves its queue; supply moves its queue.
Supply and queue drain each other through the trips within a period, and only supply's edge is drawn: a cycle has no adjustment set, and the reverse edge opens no back-door path from a lever, whose every confounder is a zone's shock either way.
levers(*, incentive_cost=0.0, price_cost=0.0)
¶
Every lever on [0, 1], in lever_columns order.
stock_dynamics()
¶
The mechanical rows, exactly: the stock decays at theta and the incentive adds to it.
In state_columns and lever_columns order and zero on every other row;
handed to a fit as known=, it leaves only supply and the queue to learn.
zone_decision(*, target, state_weight, action_weight)
¶
The incentives to hold, as a one-shot ZoneDecision, prices at
the list price.
Its state is each zone's trips a period once the incentives have settled, its lever each
zone's incentive on [0, 1], its channel incentive_channel::
trips = m0 + diag(alpha mu_s / D)(I - P) diag(channel) incentive,
D = gamma mu_q + gamma alpha + mu_s alpha,
mu_s and mu_q the linear law's weights on supply and the queue (derived in
validation/zone_market.mac). Exact under linear matching; under harmonic matching, the
settled trips' first-order response at the do-nothing point, where the two laws touch.
linear_gaussian(dt=1.0)
¶
One period of sample's log as x' = a x + b u + offset + w, under a policy
that does not see the shock.
The Jacobians of one RK4 period at the do-nothing point, so exact under linear matching.
w carries the period's shock through the step and the supply and queue noise, with
covariance shock_sd^2 e e' plus noise^2 on those rows, e the step's response to
a shock. The logged operator did see the shock, so its actions and w are correlated.