LBNL-UCB-STI / beam

The Framework for Modeling Behavior, Energy, Autonomy, and Mobility in Transportation Systems
https://transportation.lbl.gov/beam
Other
145 stars 57 forks source link

Refactor and Clean PersonAgent/PersonData and Other Code #5

Closed sfwatergit closed 7 years ago

sfwatergit commented 7 years ago

PersonData is not easy to create for purposes other than the current demonstration code.

This makes it difficult to write tests and modularize the code.

colinsheppard commented 7 years ago

So after much thinking, planning, and experimentation, I have an approach to code modularization that I think will work. I'm going to lay it out here for the team to review and consider. My hope is to implement this plan in the next week, so please chime in ASAP if anyone has feedback.

The Problem

We want our agents to be extensible and modular. We want to layer in behavior that is relevant to different categories of agents and we want to re-use our code when behavior is applicable across different types of agents.

So for example, all Persons can be a passenger in a vehicle, but only Adults can drive. Children and Elderly/Infirmed cannot drive, but Elderly perhaps can use some modes that children can't (e.g. maybe we believe Children can use an autonomous vehicle but not public transit).

At the same time, we are modeling our agents as FSM's where we need to define all possible states and the corresponding transitions to accomplish urban mobility across any mode. One approach would be to just add all possible states to a single PersonAgent class, but limit what branches of that FSM that can be accessed. But this approach would smell since it requires our PersonAgent class to know about everything that could possibly happen which could easily become bloated as more and more mobility options are added to our framework.

Proposed Solution - Composed Polymorphic Finite State Machines

Scala gives us a powerful tool for polymorphism and composition, the trait. Traits can hold concrete data and methods and/or define interfaces for implementing classes. We can then mix in the appropriate traits into a particular agent depending on the agent's demography. The challenge is how to we do this within the Akka FSM frameork?

The first step in solving this problem is relatively easy. For a trait we want to define to enable certain behavior (e.g. CanUseTaxi), we can simply create the BeamAgentStates that are needed for that behavior in the trait and we can add new condititions to the "when" block of the Akka FSM actor by adding a when block inside the trait. Under the hood, Akka FSM uses the orElse method to extend the definition of the PartialFunction to cover any undefined areas of the PF's domain. So if you introduce a new Trigger type (e.g. EnterTaxiTrigger) you can make a PersonAgent respond to this trigger from an existing BeamAgentState very easily. E.g. if the PersonAgent is in "ChoosingMode" and we send the EnterTaxiTrigger, the PersonAgent class doesn't need to know about this kind of trigger at all if it has been extended with CanUseTaxi which has an appropriate reponse block in the trait using "when(ChoosingMode)".

But this doesn't give us everything we need. We ideally could extend the response to any Trigger (or more generally, any message) from any BeamAgentState from any trait that has been mixed in. There unforutnately is no native way to do this with Akka FSM's. So I'm proposing to add some new capabilities to BeamAgent that effectively accomplish this.

During either construction or initialization, we register handlers (in the form of FSM.StateFunctions) to the BeamAgent for chained responses to messages. The chaining is going to be similar to the "transform" capability in Akka FSM or to the andThen method for partial functions, but it will have some extra capabilities to ensure that when multiple traits all add responses, we have error checking to ensure that only one state transition happens at a time.

So we use some new keyword that works a lot like "when", maybe "chainedWhen" or some better name which then is passed a StateFunction (a partial function that maps FSM.Event to FSM.State). Then during runtime, when a message is received and an event is created, all of the matching Partials that have been registered to the BeamAgentState are executed serially.

The result of each execution in the chain is an FSM.State which contains the BeamAgentInfo, BeamAgentState, and any replies that were created. The BeamAgentState and replies are stored for use at the end of the chain. Meanwhile, a new event is created and passed to the next Partial in the chain with the original message and the new BeamAgentInfo.

At the end of the chain, we look for consensus on the next state to transition to. We introduce an Abstain state (which is never actually visited) so that some behaviors can execute code but without weighing in on where to transition next (i.e. code can execute but then abstain from deciding where to go next by saying "goto(Abstain)"). For all non-Abstaining BeamAgentStates, we require consensus, otherwise we throw an error (and we also require at least one non-Abstaining outcome). This agreed upon state is what is ultimately passed back to the Akka-FSM for the transition along with all of the replies that were accumulated along way. Also note that we will do an error check at this point to prevent multiple traits from send back a CompletionNotice because that should only happen once.

Once we've done this, we can now finally layer in behavior from traits allowing us to enrich the actions that happen in the PersonAgent as well as give us the basis for branching off into other regions of the composed FSM.

Feedback appreciated!

Coming soon will be an outline on how we use this new framework to implement mode choice.