Configuration

Pogona simulations can be configured with YAML files. You can find the available options and Component instances in the API documentation.

Inheriting the Scene

First, let us have a look at the partial configuration file that we already generated in the previous step. The following is what the first couple of lines of may look like:

 1components:
 2  destructor:
 3    rotation:
 4    - -0.0
 5    - 1.570796251296997
 6    - 0.0
 7    scale:
 8    - 0.0015999999595806003
 9    - 0.0015999999595806003
10    - 0.010000000707805157
11    shape: CYLINDER
12    translation:
13    - 0.16022001206874847
14    - 0.0
15    - 0.0
16    visualization_scale:
17    - 1.2000000476837158
18    - 1.2000000476837158
19    - 1.0
20  injector:
21    rotation:
22    - -0.0
23    - 1.570796251296997
24    - 0.0
25    scale:
26    - 0.0015200000489130616
27    - 0.0015200000489130616

At the top level, you can see that below the key components are listed all the Pogona components we added to the Blender scene, and each component has defined a translation, rotation, scale, as well as a shape and visualization scale. These parameters already have the correct names to be passed to instances of pogona.Component.

To configure the remainder of our simulation scenario, we would technically be able to simply edit the file linked-tubes.scene.yaml and pass this configuration file to the Pogona simulator. However, if we ever discover that we might want to adjust the 3D scene of this scenario, we would have to either overwrite this file using the Blender add-on or edit it manually. 1 Instead, create a new file and name it linked-tubes.config.yaml. Keeping the scene separate from the remaining configuration will also help us to reduce configuration clutter.

Start the new file as follows:

1inherit:
2  - linked-tubes.scene.yaml

In lines 1 and 2 we specify that we want to inherit from our scene configuration file. The inherit directive can be used in any Pogona configuration file to include other configuration files with a path relative to the inheriting file.

1

It would technically also be possible to extend the Blender add-on to merge exported configurations with an existing configuration, but since configurations are Python dict-based, this may change the order of items or strip the configuration of comments if not properly implemented.

Configuring the Simulation Kernel

Now extend the configuration as follows:

1inherit:
2  - linked-tubes.scene.yaml
3seed: 42
4sim_time_limit: 15
5base_delta_time: 0.005
6use_adaptive_time_stepping: True
7adaptive_time_max_error_threshold: 1e-7
8movement_predictor:
9    integration_method: RUNGE_KUTTA_FEHLBERG_45

The parameters in lines 3–7 configure the SimulationKernel, with each key corresponding to any class attribute that is a subclass of AbstractProperty:

  • seed specifies a random seed that will be used whenever random values are needed, unless the respective component is configured to use its own random seed.

  • sim_time_limit says that our simulation should end after 15 s.

  • base_delta_time defines the step size. Every 5 ms, all Sensor instances will have a chance to react to all particles. If we do not use adaptive time stepping, base_delta_time also corresponds to the step size with which the positions of particles will be updated.

  • use_adaptive_time_stepping: True specifies that we do want to use adaptive time stepping in this simulation.

  • adaptive_time_max_error_threshold is the maximum integration error we want to tolerate when using adaptive time stepping. The step size will be adjusted accordingly.

Finally, movement_predictor contains the settings for the ‘kernel component’ MovementPredictor. All existing kernel components are listed in the documentation of the SimulationKernel. In this case, we need to override the default setting of the integration_method to an Integration method that supports adaptive time stepping.

Configuring Simulation Components

Next, we need to configure the components that so far only exist in our scene as empty husks. To do this, copy the names of all components defined in linked-tubes.scene.yaml into the components block in linked-tubes.config.yaml:

 1inherit:
 2  - linked-tubes.scene.yaml
 3seed: 42
 4sim_time_limit: 15
 5base_delta_time: 0.005
 6use_adaptive_time_stepping: True
 7adaptive_time_max_error_threshold: 1e-7
 8movement_predictor:
 9  integration_method: RUNGE_KUTTA_FEHLBERG_45
10components:
11  destructor:
12    type: SensorDestructing
13  injector:
14    type: Injector
15  sensor:
16    type: SensorEmpirical
17  tube0:
18    type: ObjectTube
19  tube1:
20    type: ObjectTube

Besides the component names, you will notice that the version of the configuration file above also includes a type for each component, which corresponds to the name of the respective Component subclass. Like the configuration of the SimulationKernel, each component can also be configured using the names of AbstractProperty-based class attributes of each Component subclass. You can find an example of a complete configuration at the bottom of this page.

destructor

This component does not require any additional configuration. By default, any particle entering its region will be removed from the simulation.

injector

The injector needs a number of particles to inject and a reference to the Object that newly spawned particles will be associated with:

injector:
  type: Injector
  injection_amount: 5  # particles in each base time step (cp. `modulation`)
  attached_object: tube0
sensor

SensorEmpirical needs a subfolder name in the output directory where it will store its sensor logs. Additionally, we can specify a sensor model, which in this case maps the relative position of particles inside the sensor to a magnetic susceptibility response. In the scene configuration, we already made sure that the dimensions of our sensor match this sensor model choice.

sensor:
  type: SensorEmpirical
  log_folder: sensor_data  # relative to `--results-dir`
  use_known_sensor: ERLANGEN_20200310
tube0, tube1

As explained in the chapter for the scene configuration, we cannot use the regular scale parameter for vector fields such as our tube0 and tube1, because vector fields typically already have the correct scale. However, the ObjectTube class still allows us to choose the length of the tube. Internally, the instance of this class will then load the smallest available tube vector field that our tube would fit into. Additionally, we need to define the flow rate and mesh resolution, which are used to compose the final OpenFOAM simulation results path from which the vector field will be loaded.

tube0:
  type: ObjectTube
  radius: 0.00076  # in m
  length: 0.09  # in m
  flow_rate: 10  # in ml/min
  mesh_resolution: 11  # radius cells in OpenFOAM mesh definition
tube1:
  type: ObjectTube
  radius: 0.00076  # in m
  length: 0.09  # in m
  flow_rate: 10  # in ml/min
  mesh_resolution: 11  # radius cells in OpenFOAM mesh definition

With all components from our 3D scene taken care of, there are a few further, dimensionless components we need to add.

  • We already have an Injector, but we have not yet specified when and how injections should take place. We can use on-off keying, using a ModulationOOK instance, to transmit a bit sequence from a BitstreamGenerator.

    modulation:
      type: ModulationOOK
      pause_duration: 1.5  # in s
      attached_injector: injector
      attached_pump: pump
      # Inject new particles in every time step while active,
      # rather than all at once:
      use_burst: False
    bitstream_generator:
      type: BitstreamGenerator
      start_time: 0  # in s
      bit_sequence: "101001"
      attached_modulation: modulation
    
  • When using vector fields in a Pogona simulation, each particle will typically always be associated with exactly one Object. When particles leave tube0, we need to make sure that their respective association is handed over to tube1. This is done with a SensorTeleporting, that checks if a particle is within the ObjectTube's outlet_zone. Furthermore, each Object defines its own set of inlets and outlets, since some objects may have multiple of each. We have to specify the name of the inlet and outlet for the link between our two tubes. This is not critical in the present example, but it is required if we have a simulation scenario in which the flow rate might change in tube0 and if this change should propagate to tube1. If we had a separate inlet for the particle injections in which we wanted to stop the flow whenever the pump is inactive, we would need another teleporter from the pump to the respective tube.

    teleport_tube0_to_tube1:
      type: SensorTeleporting
      source_object: tube0
      target_object: tube1
      source_outlet_name: outlet
      target_inlet_name: inlet
    
  • Lastly, we want to log all particle positions in each base time step for later visualization, and we want see some status information like the current number of particles in the terminal output:

    plotter_terminal:
      type: PlotterTerminal
    plotter_csv:
      type: PlotterCSV
      folder: particle_positions  # relative to --results-dir
      write_interval: 1  # in number of time steps
    

Complete Configuration

 1inherit:
 2  - linked-tubes.scene.yaml
 3seed: 42
 4sim_time_limit: 15
 5base_delta_time: 0.005
 6use_adaptive_time_stepping: True
 7adaptive_time_max_error_threshold: 1e-7
 8movement_predictor:
 9  integration_method: RUNGE_KUTTA_FEHLBERG_45
10components:
11  destructor:
12    type: SensorDestructing
13  injector:
14    type: Injector
15    injection_amount: 5
16    attached_object: tube0
17  sensor:
18    type: SensorEmpirical
19    log_folder: sensor_data  # relative to `--results-dir`
20    use_known_sensor: ERLANGEN_20200310
21  tube0:
22    type: ObjectTube
23    radius: 0.00076  # in m
24    length: 0.09  # in m
25    flow_rate: 10  # in ml/min
26    mesh_resolution: 11  # radius cells in OpenFOAM mesh definition
27  tube1:
28    type: ObjectTube
29    radius: 0.00076  # in m
30    length: 0.09  # in m
31    flow_rate: 10  # in ml/min
32    mesh_resolution: 11  # radius cells in OpenFOAM mesh definition
33  modulation:
34    type: ModulationOOK
35    pause_duration: 1.5  # in s
36    attached_injector: injector
37    attached_pump: pump
38    # Inject new particles in every time step while active,
39    # rather than all at once:
40    use_burst: False
41  bitstream_generator:
42    type: BitstreamGenerator
43    start_time: 0  # in s
44    bit_sequence: "101001"
45    attached_modulation: modulation
46  pump:
47    type: ObjectPumpTimed
48    flow_rate: 10
49    injection_flow_mlpmin: 10  # Don't change flow rate on injection in this scenario.
50    pump_duration_s: 0.1
51  teleport_tube0_to_tube1:
52    type: SensorTeleporting
53    source_object: tube0
54    target_object: tube1
55    source_outlet_name: outlet
56    target_inlet_name: inlet
57  plotter_terminal:
58    type: PlotterTerminal
59  plotter_csv:
60    type: PlotterCSV
61    folder: particle_positions
62    write_interval: 1  # in number of time steps

Having now configured our simulation scenario, we can go ahead and run it.