Skip to content

Latest commit

 

History

History
362 lines (259 loc) · 14.4 KB

File metadata and controls

362 lines (259 loc) · 14.4 KB
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

System requirements

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

Previous implementation

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.

Margins

  • 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.

Input modeling

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

Design specification

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]

Loading

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 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
Loading

Interpreting driving instructions

During the simulation, driving instructions are partitionned into 4 sets:

  • PENDING instructions may apply at some point in the future
  • RECEIVED instructions aren't enforced yet, but will be unless overriden
  • ENFORCED instructions influence train behavior
  • DISABLED instructions don't ever have to be considered anymore. There are multiple ways instructions can be disabled:
    • SKIPPED instructions were not received
    • RETIRED instructions expired by themselves
    • OVERRIDEN 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
Loading

These sets evolve as follows:

  • when an integration steps overlaps a PENDING instruction's received condition, it is RECEIVED and becomes a candidate to execution
    • existing instructions may be OVERRIDEN due to an override_on_received operation
  • 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 an override_on_enforced operation
  • 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 overriden 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 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
}

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

{{< rejected >}} 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

{{< rejected >}} 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 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

{{< adopted >}} 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. 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
  • 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