# Model Construction
(model_construction)=

Momba provides *append-only* APIs for incremental model construction, i.e., one can define a model incrementally but one cannot change already defined parts of a model.
For instance, it is possible to add a location to an already defined automaton but it is not possible to remove a location from the automaton.
Thereby, the provided APIs ensure that the model is *valid* at all times.

Here, we construct a model of a simple [jump 'n' run](https://en.wikipedia.org/wiki/Platform_game) game where the player has to avoid obstacles while moving forward at a constant speed.
To avoid obstacles the player can move left or right.
The goal is to reach the end of the *track* without crashing into an obstacle.

With the help of a little ASCII art, a track may be represented as follows:

```{jupyter-execute}
TRACK = (
    "       x         xxxxx     x      ",
    "           xxx          x      xx ",
    " xxxxxxx          xx              ",
)
```

Every `x` represents an obstacle and the player starts on the left and moves forward to the right.
The player wins, if and only if it moves beyond the *finishing line*, i.e., reaches the end of the track.
Now, given such a track, we would like to create a model of the game.
Note that this goes beyond what is possible with mere parametrization of a model.

First, we create a more formal representation of the scenario to be modeled.
To this end, we define a class `Track` holding all the information of the track:

```{jupyter-execute}
import dataclasses as d
import typing as t


@d.dataclass(frozen=True)
class Cell:
    x: int
    y: int


@d.dataclass(frozen=True)
class Track:
    width: int
    height: int
    obstacles: t.Set[Cell]

    @classmethod
    def from_ascii(cls, ascii: t.Tuple[str, ...]) -> "Track":
        width = len(ascii[0])
        height = len(ascii)
        obstacles = set()
        for y, line in enumerate(ascii):
            for x, symbol in enumerate(line):
                if symbol == "x":
                    obstacles.add(Cell(x, y))
        return cls(width, height, obstacles)


track = Track.from_ascii(TRACK)
track
```

With this in place, we are now ready to construct the model.


## Modeling with Momba

The package [`momba.model`](../reference/model/index) provides the core modeling API for model construction.
At the heart of every model is a *modeling context* ({class}`~momba.model.Context`) of a specific *model type* ({class}`~momba.model.ModelType`).
Every modeling context can hold several automata and automata networks comprised of these automata.
Also, global variables, constants, functions, and properties are declared on the modeling context.
To construct a model, we first have to create a modeling context of the respective type:

```{jupyter-execute}
from momba import model

# creates a *Markov Decision Process* (MDP) modeling context
ctx = model.Context(model.ModelType.MDP)
ctx
```

In our case, the model will be a *Markov Decision Process* (MDP).

To represent the position of the car, we define two global variables, `pos_x` and `pos_y`:

```{jupyter-execute}
ctx.global_scope.declare_variable("pos_x", typ=model.types.INT, initial_value=0)
ctx.global_scope.declare_variable("pos_y", typ=model.types.INT, initial_value=0)
```

To control the game, we create three *action types*:

```{jupyter-execute}
left_action = ctx.create_action_type("left")
right_action = ctx.create_action_type("right")
stay_action = ctx.create_action_type("stay")
```

Each of these action types corresponds to a distance moved on the {math}`y`-axis:

```{jupyter-execute}
moves = {left_action: -1, right_action: 1, stay_action: 0}
```

Next, we define an automaton modeling the *environment*:

```{jupyter-execute}
environment_automaton = ctx.create_automaton(name="Environment")
```

This automaton will have one location:

```{jupyter-execute}
ready_location = environment_automaton.create_location("ready", initial=True)
```

To keep track of whether the player reached the goal or crashed into an obstacle or a wall, we declare two local variables `has_won` and `has_crashed` for the environment automaton:

```{jupyter-execute}
environment_automaton.scope.declare_variable(
    "has_won", typ=model.types.BOOL, initial_value=False
)
environment_automaton.scope.declare_variable(
    "has_crashed", typ=model.types.BOOL, initial_value=False
)
```

With the help of Momba's [*syntax-aware macros*](../incubator/moml/macros) we now define two functions computing whether the player *has crashed* or *has finished* depending on the position:

```{jupyter-execute}
from momba.moml import expr


def has_finished(x: model.Expression, track: Track) -> model.Expression:
    return expr("$x >= $width", x=x, width=track.width)


def has_crashed(
    x: model.Expression, y: model.Expression, track: Track
) -> model.Expression:
    out_of_bounds = expr("$y >= $height or $y < 0", y=y, height=track.height)
    on_obstacle = model.expressions.logic_any(
        *(
            expr(
                "$x == $obstacle_x and $y == $obstacle_y",
                x=x,
                y=y,
                obstacle_x=obstacle.x,
                obstacle_y=obstacle.y,
            )
            for obstacle in track.obstacles
        )
    )
    return model.expressions.logic_or(out_of_bounds, on_obstacle)
```

Momba makes it easy to define functions operating on [JANI-model](https://jani-spec.org) expressions.
In particular, the function {func}`~momba.moml.expr` can be used to build expressions with a succinct and intuitive syntax.
We use the same approach to define an expression indicating whether a move can be performed:

```{jupyter-execute}
can_move = expr("not has_won and not has_crashed")
```

Now, with these definitions in place, we create the edges of the automaton:

```{jupyter-execute}
for action_type, delta in moves.items():
    new_pos_x = expr("pos_x + 1")
    new_pos_y = expr("pos_y + $delta", delta=delta)

    environment_automaton.create_edge(
        ready_location,
        destinations={
            model.create_destination(
                ready_location,
                probability=expr("0.6"),
                assignments={
                    "pos_x": new_pos_x,
                    "pos_y": new_pos_y,
                    "has_won": has_finished(new_pos_x, track),
                    "has_crashed": has_crashed(new_pos_x, new_pos_y, track),
                },
            ),
            model.create_destination(
                ready_location,
                probability=expr("1 - 0.6"),
                assignments={
                    "pos_x": new_pos_x,
                    "has_won": has_finished(new_pos_x, track),
                    "has_crashed": has_crashed(new_pos_x, expr("pos_y"), track),
                },
            ),
        },
        action_pattern=action_type.create_pattern(),
        guard=can_move,
    )
```

For each of the previously defined action types, an edge is created.
In this case, the model introduces probabilistic noise and a move might fail, i.e., not change the {math}`y` coordinate, with a probability of {math}`0.4`.
When taking an edge, the variables get updated accordingly.


Finally, we create an *instance* of the automaton:

```{jupyter-execute}
environment = environment_automaton.create_instance()
```

The instance is then added to an automaton network and a *synchronization link* for each action type is created which allows the resulting composition to perform the respective action:

```{jupyter-execute}
network = ctx.create_network()
network.add_instance(environment)
for action_type in moves.keys():
    network.create_link(
        {environment: action_type.create_pattern()},
        result=action_type.create_pattern(),
    ) 
```

You can find the full source code of this example [here](https://github.com/koehlma/momba/blob/master/examples/guide/momba_guide/model.py).

