Model Train-related Notes Blog -- these are personal notes and musings on the subject of model train control, automation, electronics, or whatever I find interesting. I also have more posts in a blog dedicated to the maintenance of the Randall Museum Model Railroad.

2019-06-20 - Conductor 2: Simulations & Expectations for Occupancy

Category Rtac

Starting to implement the occupancy manager in Conductor 2 immediately showed an issue in the default route mapping: the default idle block of one route cannot be part of another route.

So let’s backup and analyze this.

First let’s define what is a “sanitized” route. This is a route, composed of blocks, where one and only one block is marked as occupied at a given time. This is the ideal non-ambiguous situation that makes everything easier. At startup it means we can positively identify where the one engine is. When running, given the known position of the train, we can either assert the train is moving through the route as expected or during simulation we can determine the next block.

We define an “idle” block as the expected resting block of the train once it has finished its programming. For a given route, this block is typically a constant. This is not currently codified in the script, although it could (e.g. to validate starting conditions).

One limitation comes from overlapping routes. Let’s say route A is a super-set of route B. Both have “idle” blocks. If route B is purely a subset of route A, it means route A has two occupied blocks when both trains are idle. This is contradictory as it means train A cannot possibly run without colliding with train B. In reality that condition could happen yet be physically safe if the idle block B is a turnout that triggers as occupied once thrown for route B, such as a branch spur.

It does not work from a graph perspective, so we’ll posit these rules:

  • The idle block of a route cannot be part of another overlapping route.
  • Routes should be defined such that there is only one occupied block per route when they are all idle.

There are 3 runtime scenarios we must consider:

  • The case of the initial loading of the script, either for automation or simulation. We want to be able to compute the initial state of the route and determine if it is “safe” in a non-ambiguous way.
  • The case of an active route during automation.
  • The case of an active route during simulation.

Routes currently do not have a proper state variable.

There are two kinds of states we can associate with a route.

A first state would be the “quality” or “error” state of the route:

  • A “good” state is a “sane” idealized route with a single block occupied.
  • An “invalid” state is an ambiguous route that has either zero or more than one block occupied. This can be a transient state, for example if two routes share a crossing block.
  • An “error” state is when a route has been identified as being in error by the script, typically because a moving train failed to reach the next checkpoint within a certain time. That typically denotes a permanent error where the script determines that a human verification of the state is required before doing a full reset.
  • A “paused” route is an active route (with a running train) where that train has been stopped due to a possible transient and rectifiable error situation.

Orthogonal to this, there’s a “programmatic” state (needs a better name!):

  • An empty route -- no block is known to be occupied. It has no trains as far as the “blind” detectors allow us to determine. Such a route cannot be started.
  • An “idle” route -- this is a “sane” idealized route with a single block occupied, where the said block is the expected idle block for the route. As far as the automation knows, there is one train at the expected location, thus we’re going to assume it is the proper train (e.g. with the expected DCC address defined for that route).
    • Next state: Active, or Locked.
  • An “active” route is a route where movement has started. There can be more than one active route in the script as long as they do not overlap.
    • Next state: Idle.
  • A “locked” route is an idle route that cannot run as it currently overlaps with an active route.
    • Next state: Idle.

It is important to indicate that here we deal with “absolute locked” routes. As indicated before, there is some value in having different route management strategies. Another obvious strategy is a “window block occupancy” that would automatically lock blocks as a train advances and release them behind. The current manager would not have to deal with that.

In the current Conductor 1 script, the route programmatic state is not explicit. It is implicit via the script’s internal enum states. As such the mainline has two overlapping routes and which one is running is determined by the AM vs SP enum states. There is some value in making that explicit.

The script would be structured as such:

  • When both AM & SP routes are in a good + idle state, and motion is triggered, then the AM or SP route is set to active (alternatively).
  • Once the AM route is active do all its conditions/actions.
  • Same for the SP route.
  • At the end, set the active route to idle.

The semantic difference is that:

  • Checking the route is in a “good state” validates only one block is active.
  • Checking the route is in the “idle” state validates only that the idle block is active.

This removes a bunch of confusing checks in the script.

We do not explicitly need to check against locked routes for now. That dependency can be computed on the fly, or pre-computed and changed when a route becomes active or inactive.

The route occupancy strategy is going to differ slightly between automation and simulation.

During automation, trains move and thus trigger the “next” block. If the route is well defined, this should be the block just “after” the one currently occupied. The notion of “next block” depends on the route definition, and not on the engine running direction -- even for a shuttle operation, the blocks are present twice in the route. In other words the current block moves from index N to index N+1. This matters since by definition a shuttle route will have every block appearing twice in the route, but at different indexes corresponding to different running directions.

There may be a period of time when both the “active” block and the “next” one are going to be activated as the engine transitions from one to the other. That period of time will be longer if the train has for example a powered rear engine or car. This matters because in that case we don’t want the route to transition from a good state to an invalid one, so we need a way to mark the block as being occupied by a “trailing” portion of the train, ideally with a sanity check as we expect that status to be cleared within a certain time (unless the train needs to stop on a two block boundary, which is not a very desirable scenario).

In the previous post from 2018-12-08, I did define that a “Block Reservation model” with the values (free, occupied, or trailing). This is circling back to exactly that same model.

There is an important detail in the block reservation model so I’ll put this on bold here:

An occupied block must not be conflated with an active sensor/block.

An “active” block (or sensor) is a physical state. An “occupied” block is a modeled state. They must be allowed to sometimes differ.

The reservation model is especially useful for an ABS running strategy with a train controlled by looking at N blocks ahead. However we can generalize it for all kinds of running strategies:

  • The train is on a given block N, which is the occupied block.
  • As the train moves to the next block in the route, block N becomes “trailing” and block N+1 becomes “occupied”. From a sensor perspective, block N (the exited block) may be either off or active, so it’s important here to not conflate block sensor activation and block occupancy.
  • In this scheme, the “previous” block is always reserved as a trailing block as long as the train is moving.
  • Thus it’s only when the train moves to block N+2 that block N becomes free, block N+1 becomes trailing, and block N+2 becomes occupied.

There are however two problems with this scheme:

  • There’s a deadlock situation in a shuttle route when the train reaches the reversal point. By definition the N-1 block would never be free, and the train could never return.
    • ⇒ This can trivially be solved by indicating the “trailing” block is freed when the train stops voluntarily, or by having an explicit “route recompute” action in the script.
  • This scheme does not work for a route that has only two blocks.
    • ⇒ Unless that happens to be a shuttle route since the reversal action will reset the trailing block which is the only other block in the route.

How does this scheme work in a simulation mode?

It should work pretty much the same way. The only difference is that for each block we need an implicit timer to advance to the block N+1. This would be done by simulating a sensor activation for N+1 and a sensor inactivation for N.

How does this scheme work with Virtual Blocks?

First they need to work the same work for automation and simulation.

The previous reasoning was to automatically trigger them with a simple rule:

  • If a block N becomes occupied in the route and N+1 is a virtual block, we automatically make block N+1 occupied too. Virtual Block N+1 is reset when trains enter block N+2.

One issue with that is that we now have 2 occupied blocks on the route, which is contrary to our definition of a “sane” route. It is also wrong, from a physical point of view, for a train to simultaneously occupy blocks N and N+1 at the same time when entering a single block, since the virtual blocks are here to represent non-ambiguous physical blocks that have no sensor.

What matters is that the train entered and left the block before the virtual block. So let’s try this:

  • Give N a sensor-backed block, N+1 a virtual block, and N+2 a sensor-backed block,
  • If a block N was occupied and changes from active to non-active, then virtual block N+1 is made active, which changes it to occupied, and block N changes to a trailing state as usual.
  • When block N+2 becomes active, the manager proceeds into making N+2 occupied and N+1 a trailing block, and N becomes free per the usual rules,

This has the advantage that the only difference between a virtual and sensor block is the activation. The rest uses the usual management.

For simulation, no change is needed. The simulation would make virtual block N+1 active and deactivate N, which the virtual block manager would pickup and basically do the same thing, so it’s an idempotent operation.


 Generated on 2024-11-21 by Rig4j 0.1-Exp-f2c0035