Creating your first Simulation#
Let’s create our first simulation. This is a basic example on how to setup and run a simulation in Movici, but it will cover most of the important details that have to do with running simulations.
In order to run a simulation, we create a file squares.py and start editing it. We first need to
instantiate a Simulation
object:
from movici_simulation_core import Simulation
sim = Simulation()
Now, we need to add some models and some (initial) data. We start with the data. Movici datasets, or more specifically, entity based data (see also: Movici Data Format), have a specific format. Let’s create a dataset with two entities of a certain type, and give them some attribute values
dataset = {
"figures": {
"square_entities": {
"id": [1, 2],
"shape.edge_length": [10.0, 20.0]
}
}
}
We now have defined two entities of type square_entities
in our figures
dataset. The first
entity has id=1
and a single attribute shape.edge_length=10.0
. Our second entity has an id=2
and
shape.edge_length=20.0
. Since our Movici data format is array-oriented, we concatenate the attribute
values of every entity of the same type into a single array per attribute. Every index in these
array refers to a specific entity. In our case, position 0 is allocated for our entity with id=1
and position 1 for entity id=2
.
In order to make use of this dataset in a simulation, we must store it to disk:
import json
from pathlib import Path
from tempfile import mkdtemp
input_dir = mkdtemp(prefix='movici-input-')
output_dir = mkdtemp(prefix='movici-output-')
Path(input_dir).joinpath('figures.json').write_text(json.dumps(dataset))
We’ve created two temporary directories, one for input data and one for simulation results. We then stored our dataset in a json file with the name of the dataset.
Note
In Movici, datasets are identified by their filename. Dataset files must have a base name equal to their dataset name, and this basename must be unique within a input data dir
Also, in general you’d want to fill your data_dir with your datasets running your simulation, so that you don’t recreate the datasets every time you run a simulation
We can now tell our Simulation
to look for its input data in the given directory. It is also
recommended to tell our Simulation
that there exists an attribute called shape.edge_length
and
that it has values of type float
. This is done by registring an AttributeSpec
Note
It is not required to specify every attribute using an AttributeSpec
. If an attribute is
encountered that does not have an associated specification, the simulation core will do its best
to infer the data type from the data. This does however, create an performance overhead, and is
not foolproof. It works for simple data types, but can make mistakes for more complex types. It
is therefore recommended to always register your attributes
Our squares.py
now looks as following:
import json
from pathlib import Path
from tempfile import mkdtemp
from movici_simulation_core import AttributeSpec, Simulation
input_dir = mkdtemp(prefix='movici-input-')
output_dir = mkdtemp(prefix='movici-output-')
dataset = {
"figures": {
"square_entities": {
"id": [1, 2],
"shape.edge_length": [10.0, 20.0]
}
}
}
Path(input_dir).joinpath('figures.json').write_text(json.dumps(dataset))
sim = Simulation(data_dir=input_dir, storage_dir=output_dir)
sim.register_attributes([AttributeSpec("shape.edge_length", data_type=float)])
Now that we have data, we can add and configure our models. In our dataset, we have
square_entities
that have an shape.edge_length
but no shape.area
yet. We are going to let
a model calculate these. For this, we’ll make use of the included UDFModel
. UDF
stands for
User Defined Function and thi model can do basic arithmetic operations on attributes. We add the
UDFModel
as following
from movici_simulation_core.models import UDFModel
sim.add_model("square_maker", UDFModel( {
"entity_group": [["figures", "square_entities"]],
"inputs": {"length": [None, "shape.edge_length"]},
"functions": [
{
"expression": "length * length",
"output": [None, "shape.area"],
},
],
}))
sim.register_attributes([AttributeSpec("shape.area", data_type=float)])
We’ve created an instance of UDFModel
and given it a unique name in the Simulation
:
"square_maker"
. We’ve configured the model with its required parameters. We point it to a
specific entity group inside our dataset and refer to certain input attributes (which we can give
a working name). In this case we have one input attribute shape.edge_length
, which we temporarily
call "length"
, we can then create an expression with the temporary name as a variable name,
and store the expression result under an output attribute in the same entity group. For
completeness, we also register the output attribute to the simulation.
Now, we have a single model that does a calculation. However, the results of this calculation are
not going anywhere, currently, they stay in the simulation, and disappear as soon as the simulation
is completed. In order to save the results, we need to add a second, special model called
DataCollectorModel
. This model takes all updates that other models produce, and stores them in the
output directory storage_dir
.
from movici_simulation_core.models import DataCollectorModel
sim.add_model("data_collector", DataCollectorModel({}))
There, we are now ready to run our first simulation. The final squares.py
looks like this:
import json
from pathlib import Path
from tempfile import mkdtemp
from movici_simulation_core import AttributeSpec, Simulation
from movici_simulation_core.models import DataCollectorModel, UDFModel
input_dir = mkdtemp(prefix='movici-input-')
output_dir = mkdtemp(prefix='movici-output-')
dataset = {
"figures": {
"square_entities": {
"id": [1, 2],
"shape.edge_length": [10.0, 20.0]
}
}
}
Path(input_dir).joinpath('figures.json').write_text(json.dumps(dataset))
sim = Simulation(data_dir=input_dir, storage_dir=output_dir)
sim.register_attributes(
[
AttributeSpec("shape.edge_length", data_type=float),
AttributeSpec("shape.area", data_type=float)
]
)
sim.add_model("square_maker", UDFModel({
"entity_group": [["figures", "square_entities"]],
"inputs": {"length": [None, "shape.edge_length"]},
"functions": [
{
"expression": "length * length",
"output": [None, "shape.area"],
},
],
}
)
)
sim.add_model("data_collector", DataCollectorModel({}))
sim.run()
output_file = Path(output_dir).joinpath("t0_0_figures.json")
print(output_file.read_text())
After we’ve succesfully run our simulation, the output directory contains one file:
t0_0_figures.json
. Its filename is made up of the following components:
t0
means timestamp 0 in the simulation. Every simulation starts att=0
0
The second0
marks the iteration number. At every timestamp, there may be multiple updates calculated. Every update in a single timestamp must have a unique increasing, iteration numberfigures
This is to indicate to which dataset the update file belongs to.
When we open this file, we see that it contains the following data:
{
"figures":{
"square_entities":{
"id": [1, 2],
"shape.area": [100.0, 400.0]
}
}
}
The model has succesfully calculated the area for all of our squares, yay! You are now ready to read further about the various aspects of programming with Movici, or take a deep dive and start creating your own Models.