Car arrives after Go request

Similar to the call request, we will specify the action where the car arrives after fulfilling its go request. Note that in this action and the previously described action, we did not specify (s) behind the action name. This is because it does not need to be parameterised for the storey — there’s only one go button, whereas there are n call buttons (where n=2 in our simple elevator system).

ECNextArriveGoRequest ==

/\ carTravelStatus # 0

/\ goRequest = 1

/\ carLocation' = IF carLocation = 1 THEN 2 ELSE 1

/\ carTravelStatus' = 0

/\ goRequest' = 0

/\ UNCHANGED << callRequest >>

As the elevator arrives from servicing a go request, the carLocation will flip from either storey 1 to storey 2, or vice versa.

The carTravelStatus will change from moving (not 0) to not moving (0).

The goRequest will be cancelled once the car arrives; callRequests remain unchanged.

Putting it all together

We’ve so far specified an elevator system that has one car, travelling between two storeys, which can be called by waiting passengers, will move to service such a call, and will cancel the call request once it has arrived.

If a passenger enters the elevator car and hits the Go button, the car will move to the other storey, and when it arrives, the ‘go’ request will be cancelled.

The full next-state formula

Given all the individual actions described above, our full next-state formula can be defined as follows:

ECNext ==

\/ \E s \in Storeys :

\/ ECNextCall(s)

\/ ECNextServiceCall(s)

\/ ECNextArriveCall(s)

\/ ECNextGoRequest

\/ ECNextServiceGoRequest

\/ ECNextArriveGoRequest vars == << carLocation, carTravelStatus, callRequest, goRequest >>

Spec == ECInit /\ [][ECNext]_vars

In case you weren’t already aware, the indentation in TLA+ is significant. The above means that our universal next state (ECNext) is a logical disjunction between the three possible actions parameterised by a storey identifier (elevator is called) and three possible actions without a storey identifier (elevator is asked to go by a passenger inside it).

The construct \E s \in Storeys : can be read as “there exists a value s in the set of all Storeys where…”. The TLC model checker will interpret this construct by attempting to fulfil the next state using any valid value from the set Storeys.

Invariants

We also want to describe in a formula all of the things that should remain true in all states of our system. We’ll start simple, as follows:

ECTypeOK ==

/\ carLocation \in Storeys

/\ carTravelStatus \in {-1, 0, 1}

/\ callRequest \in [Storeys -> {0, 1}]

/\ goRequest \in {0, 1}

The above is pretty uncontroversial. We’re checking that the car is always at a valid floor, that it’s always either stationary, going up or going down, that the callRequests are from the valid combination of call requests for our known floors, and that the Go request is either on or off.

These aren’t particularly taxing — we’ll have a go at making those a bit more meaningful later.

Does it work?

We’re ready to give our specification a spin. I won’t share the complete source code, as I think there’s more value in you putting it together yourself and understanding each bit at a time :)

Here’s how your model might look:

When we run this through the model checker, it works! We get 90 states and 32 distinct states. We have a working elevator!

What have we learned?

This was a pretty simple elevator, and only our first iteration.

But despite this, I think that just going through the process of identifying the different significant components of our system, and considering how that translates to variables and state changes (actions), helped me to conceptualise my system better.

We’ve made several important simplifications, such as not modelling doors, and keeping to one car and two storeys. By identifying those simplifications and attempting to codify them in a TLA+ specification, I feel I have a better understanding of the dimensions along which my system is going to get more complex.

Although I didn’t talk you through every step of my thought process, I found the construction of the next-state formulas (“actions”) to be particularly useful. In my first attempt, I started writing the ECNextGoRequest state formula with a behaviour in mind that as soon as the elevator passenger hit the Go button, it would set off for its destination. However, just by starting to write down the conditions of this action formula, I realised that there was a perfectly valid edge case behaviour I’d missed, and this led to the addition of the ECNextServiceGoRequest action.

The very act of trying to describe your system in unambiguous terms is itself a very thought-clarifying process. This is why many advocates of TLA+ consider the journey of formal system specification to be just as important as the destination.

There were other micro-decisions like this that I’ve not documented in this article which helped me conceptualise my system.

There were also decisions I’m not happy with yet: for example, the formulas for servicing/arriving for calls are very similar in nature to those for Go requests, and I wonder if I should refactor them... but then I am also aware that I’m not trying to write clean code here, I’m trying to model a system’s intended behaviours with as much clarity as possible. So the jury’s out on that one! I’m sure the dilemma will evolve as we add complexity to the system.

A couple of gotchas

There were a couple of things I noticed were wrong as I was writing my specification. Did you notice them too?

I’ll let you try to find them. If you spotted them straight away, then well done, you are clearly able to reason about multivariate state machines better than most! But even if you are feeling smug about that, I hope you can also see that spotting anomalous behaviours in a system will only get exponentially harder as we increase the complexity. After all, this is a really, really simple elevator!

If you didn’t spot them, sorry but I’m not going to reveal them yet. I want to see if the next iteration of our specification helps us to tease out these anomalies.

The model checker didn’t spot these gotchas because our model is currently very naive. Our invariant check formula only checks for things that really ought to be true unless something went badly wrong (e.g. we somehow managed to send our elevator to floor 3 even though we don’t have one).

It doesn’t check for more subtle behaviours that might result in our elevator behaving erratically or counterintuitively for the passengers. For these we need more sophisticated checks — and this is where the T in TLA+ comes in. We need temporal checks (i.e. things that we want to be true across an end-to-end behaviour, rather than true simply for all reachable states).

This will be the subject of the next blog post in this series.