Driving instructions
Driving instructions model what the train has to do, and under what conditions. Driving instructions are generated using domain constraints such as:
- unsignaled line speed limits
- permanent signaled speed limits
- temporary speed limits
- dynamic signaling:
- block / moving block
- dynamically signaled speed restrictions
- neutral zones
- stops
- margins
There are two types of driving instructions:
- Abstract driving instructions model the high-level, rolling stock independent range of acceptable behavior: reach 30km/h at this location
- Concrete driving instructions model the specific range of acceptable behavior for a specific rolling stock, using limit curves: don’t go faster than this curve
flowchart TD Constraint[constraint] AbstractDrivingInstruction[abstract driving instruction] ConcreteDrivingInstruction[concrete driving instruction] RollingStockIntegrator[rolling stock integrator] Compiler([compiler]) Constraint -- generates one or more --> AbstractDrivingInstruction AbstractDrivingInstruction --> Compiler RollingStockIntegrator --> Compiler Compiler --> ConcreteDrivingInstruction
After reviewing the design document, the necessity to distinguish between abstract and concrete driving instructions was questioned.
Indeed, it isn’t clear whether the limit curves are used for the driving instructions interpretation algorithm. If it isn’t, the computation of limit curves could be moved inside the driver behavior module.
TODO: remove this message or fix the design document after implementation.
Interpreting driving instructions
During the simulation, driving instructions are partitioned into 4 sets:
PENDING
instructions may apply at some point in the futureRECEIVED
instructions aren’t enforced yet, but will be unless overriddenENFORCED
instructions influence train behaviorDISABLED
instructions don’t ever have to be considered anymore. There are multiple ways instructions can be disabled:SKIPPED
instructions were not receivedRETIRED
instructions expired by themselvesOVERRIDDEN
instructions were removed by another instruction
flowchart TD subgraph disabled skipped retired overridden end subgraph active received enforced end pending --> received pending --> skipped received --> enforced received --> overridden enforced --> retired enforced --> overridden
These sets evolve as follows:
- when an integration steps overlaps a
PENDING
instruction’s received condition, it isRECEIVED
and becomes a candidate to execution- existing instructions may be
OVERRIDDEN
due to anoverride_on_received
operation
- existing instructions may be
- if an instruction cannot ever be received at any future simulation state, it transitions to the
SKIPPED
state - when simulation state exceeds an instruction’s enforcement position, it becomes
ENFORCED
. Only enforced instructions influence train behavior.- existing instructions may be
OVERRIDDEN
due to anoverride_on_enforced
operation
- existing instructions may be
- when simulation state exceeds an instruction’s retirement position, it becomes
RETIRED
Overrides
When an instruction transitions to the RECEIVED
or ENFORCED
state, it can disable active instructions
which match some metadata predicate. There are two metadata attributes which can be relied on for overrides:
- the
kind
allows overriding previous instructions for a given domain, such as spacing or block signaled speed limits - the
rank
can be used as a “freshness” or “priority” field. If two instructions overriding each other are received (such as when a train sees two signals), the rank allows deciding which instruction should be prioritized.
This is required to implement a number of signaling features, as well as stops, where the stop instruction is overridden by the restart instruction.
Data model
struct ReceivedCond {
position_in: Option<PosRange>,
time_in: Option<TimeRange>,
}
struct InstructionMetadata {
// state transitions
received_when: ReceivedCond,
enforced_at: Position,
retired_at: Option<Position>,
// instruction metadata, used by override filters. if an instruction
// has no metadata nor retiring condition, it cannot be overridden.
kind: Option<InstructionKindId>, // could be SPACING, SPEED_LIMIT
rank: Option<usize>,
// when the instruction transitions to a given state,
// instructions matching any filter are overridden
override_on_received: Vec<OverrideFilter>,
override_on_enforced: Vec<OverrideFilter>,
}
enum AbstractInstruction {
NeutralZone,
SpeedTarget {
at: Position,
speed: Speed,
}
}
enum ConcreteInstruction {
NeutralZone,
SpeedTarget {
braking_curve: SpeedPosCurve,
},
}
struct OverrideFilter {
kind: InstructionKindId,
rank: Option<(RankRelation, usize)>,
}
enum RankRelation {
LT, LE, EQ, GE, GT
}
Design decisions
Lowering constraints to an intermediate representation
Early on, we started making lists of what domain constraints can have an impact on train behavior. Meanwhile, to simulate train behavior, we figured out that we need to know which constraints apply at any given time.
There’s a fundamental tension between these two design constraints, which can be resolved in one of two ways:
- either treat each type of constraint as its own thing during the simulation
- abstract away constraints into a common representation, and then simulate that
❌ Distinct constraint types
When we first started drafting architecture diagrams, the train simulation API directly took a bunch of constraint types as an input. It brought up a number of issues:
- the high diversity of constraint types makes it almost impossible to describe all interactions between all constraint types
- the domain of some of these interactions is very complex (block signaling)
- when simulating, it does not seem to matter why a constraint is there, only what to do about it
We couldn’t find clear benefits to dragging distinctions between constraint types deep into the implementation.
❌ Internal constraint types abstraction
We then realized that abstracting over constraint types during simulation had immense benefits:
- it allows expressing requirements on what constraints need to be enforceable
- it greatly simplifies the process of validating constraint semantics: instead of having to validate interactions between every possible type of constraints, we only have to validate that the semantics of each constraint type can be transferred to the abstract constraint type
We decided to explore the possibility of keeping constraint types distinct in the external API, but lowering these constraints into an intermediary representation internally. We found a number of downsides:
- the public simulation API would still bear the complexity of dealing with many constraint types
- there would be a need to incrementally generate internal abstracted constraints to support the incremental API
✅ External constraint types abstraction
We tried to improve over the previous proposal by moving the burden of converting many constraints into a common abstraction out of the simulation API.
Instead of having many constraint types as an input, the simulation API takes a collection of a single abstract constraint type. The task of converting domain constraints to abstract driving instructions is left to the API user.
We found that doing so:
- reduces the API surface of the train simulation module
- decouples behavior from constraint types: if a new constraint type needs to be added, the simulation API only needs expansion if the expected behavior expected for this constraint isn’t part of the API.
Interpreting driving instructions
As the train progresses through the simulation, it reacts according to driving instructions which depend on more than the bare train physics state (position, time, and speed):
- the behavior of a train on each block depends on the state of the last passed block signal
- if a train encounters a yellow light, then a red light, stops before the red light, and the red light turns green, the train may have to keep applying the driving instruction from the yellow signal until the green light is passed
Thus, given:
- set of all possible driving instructions (alongside applicability metadata)
- the result of previous integration steps (which may be extended to hold metadata)
There is a need to know what driving instructions are applicable to the current integration step.
Overrides are a way of modeling instructions which disable previous ones. Here are some examples:
- if a driver watches a signal change state, its new aspect’s instruction might take precedence over the previous one
- as block signaling slows a train down, new signals can override instructions from previous signals, as they encode information that is more up to date
We identified multiple filtering needs:
- overrides happen as a given kind of restriction is updated: SPACING instructions might override other SPACING instructions, but wish to leave other speed restrictions unaffected
- as multiple block signals can be visible at once, there’s a need to avoid overriding instructions of downstream signals with updates to upstream signals
We quickly settled on adding a kind field, but had a lengthy discussion over how to discriminate upstream and downstream signals. We explored the following options:
- ❌
adding
source
metadata, which was rejected as it does not address the issue of upstream / downstream - ❌ adding identifiers to instructions, and overriding specific instructions, which was rejected as it makes instruction generation and processing more complex
- ✅ adding some kind of priority / rank field, which was adopted