title | linkTitle | description | weight |
---|---|---|---|
Train simulation v3 |
Train simulation v3 |
Modeling and API design of train simulations |
45 |
{{% pageinfo color="warning" %}} This is a work in progress {{% /pageinfo %}}
After two years of implementing features into a very simple model, it appeared that fundamental changes are required to meet our needs. This document goes into:
- what are the system's current and projected requirements
- how train simulation is currently implemented
- the shortcomings of the current model
- the specification for the new design
- what was discussed during the design process, and the outcomes of individual discussions
- a discussion of the new design's limitations
- a list of questions designed to challenge design limits, answered by domain experts
The new system is expected to:
- be easy to integrate with timetable v2
- handle batch simulations of a full trip
- handle incremental simulations for STDCM
- follow a schedule during batch simulations
- handle rich train state vectors (pantograph, battery state)
- handle realistic reactions to signaling
- provide a low-level API, usable independantly
- integrate a pluggable margin algorithm API
In the long-term, this system is expected to:
- be used to drive multi-train simulations
- handling switching rolling stock at stops
- integrate driver behavior properties
The current implementation has a number of shortcomings make it pretty much impossible to evolve to meet current system requirements. It also has a number of less severe flaws, such as the over-reliance on floating point, especially for input and output.
The previous implementation cannot be changed to:
- react to signaling
- handle rich train state vectors
- be usable for both incremental simualtion and batch
These limitations are the primary reasons for this redesign.
-
are defined as post-processing filter passes on simulation results. This has a number of undesirable side effects:
-
the burden of producing correct results lays on margin algorithms, which makes the implementation brittle
-
because margins are applied after the simulation, the simulation can't adjust to impossible margin values
-
margin algorithms have no choice but to piece together results of different simulations:
- this can only be done if the train state is entirely described by its location and speed, otherwise simulation results cannot be pieced together.
- piecing together simulation results very hard to execute reliably, as there are many corner cases to be considered. the end result is quite brittle.
-
-
engineering margins are defined such that their effect has to be entirely contained within their bounds. even though it's a desirable property, it means that simulations become a multi-pass affair, with no obvious way of keeping train behavior consistent accross passes and boundaries.
-
how much time should be lost and where isn't defined in a way that makes scheduled points implementation easy
-
when a transition between two margin values occurs, slow downs occur before value changes, and speed ups after value changes. This is nice in theory, because it makes the graphs look nicer. The downside is that it makes margin values interdependant at each slow-down, as how much speed needs to be lost affects the time lost in the section.
With the previous implementation, the simulation takes sequence of constraint position and speed curves as an input (continuous in position, can be discontinuous in speed), and produces a continuous curve.
The output is fine, but the input is troublesome:
- braking curves have to be part of constraint curves
- these constraint curves don't have a direct match with actual constraints, such as speed limits, stops, or reaction to signal
- constraints cannot evolve over time, and cannot be interpreted differently depending on when the train reached these constraints
- constraints cannot overlap. the input is pre-processed to filter out obscured constraints
flowchart TD
subgraph Input
InitTrainState[initial train state]
PathPhysicsProps[path physics properties]
AbstractDrivingInstructions[abstract driving instructions]
TargetSchedule[target schedule]
end
DrivingInstructionCompiler([driving instruction compiler])
ConcreteDrivingInstructions[concrete driving instructions]
MarginController([margin controller])
MarginDriver([margin driver])
MarginDrivingInstructions[margin concrete driving instructions]
DrivingInstructionsMerge([set merge])
FinalDrivingInstructions[final concrete driving instructions]
TargetSchedule --> MarginController
MarginController -- adjusts margin speed ceiling --> MarginDriver
AbstractDrivingInstructions --> DrivingInstructionCompiler
PathPhysicsProps --> DrivingInstructionCompiler
MarginDriver ---> MarginDrivingInstructions
ConcreteDrivingInstructions -- "filter by margin section range" --> MarginDriver
DrivingInstructionCompiler ---> ConcreteDrivingInstructions
MarginDrivingInstructions ---> DrivingInstructionsMerge
ConcreteDrivingInstructions ---> DrivingInstructionsMerge
DrivingInstructionsMerge ---> FinalDrivingInstructions
InitTrainState ---> MarginController
MarginController -- tracks train state --> TrainSim
FinalDrivingInstructions --> TrainSim
TrainSim --> SimResults
TrainSim([train simulator])
SimResults[simulation result curve]
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 independant 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: follow this breaking curve and maintain 30km/h.
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
During the simulation, driving instructions are partitionned into 4 sets:
PENDING
instructions may apply at some point in the futureRECEIVED
instructions aren't enforced yet, but will be unless overridenENFORCED
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 themselvesOVERRIDEN
instructions were removed by another instruction
flowchart TD
subgraph disabled
skipped
retired
overriden
end
subgraph active
received
enforced
end
pending --> received
pending --> skipped
received --> enforced
received --> overriden
enforced --> retired
enforced --> overriden
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
OVERRIDEN
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
OVERRIDEN
due to anoverride_on_enforced
operation
- existing instructions may be
- when simulation state exceeds an instruction's retirement position, it becomes
RETIRED
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 overriden by the restart instruction.
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 overriden.
kind: Option<InstructionKindId>, // could be SPACING, SPEED_LIMIT
rank: Option<usize>,
// when the instruction transitions to a given state,
// instructions matching any filter are overriden
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
}
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
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
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 constraint semantics can be expressed by the abstract constraint type
We decided the explore the possibility of keeping constraint types distinct in the external API, but lowering these constraints into an intermediary representation internaly. 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 incrementaly generate internal abstracted constraints to support the incremental API
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. 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.
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
- when a train stops at a red signal, then restarts, it may have to keep applying the driving instruction from the previous signal until the formerly red 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 modelling instructions which disable previous ones. Here are some examples:
- if a driver watches a signal change state, it's 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:
- {{< rejected >}} adding
source
metadata, which was rejected as it does not address the issue of upstream / downstream - {{< rejected >}} adding identifiers to instructions, and overriding specific instructions, which was rejected as it makes instruction generation and processing more complex
- {{< adopted >}} adding some kind of priority / rank field, which was adopted