Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions agentlib/models/fmu_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,12 +99,14 @@ def extract_fmu(self):
def do_step(self, *, t_start, t_sample=None):
if t_sample is None:
t_sample = self.dt
# Write current values to system
while not self._variables_to_write.empty():
self.__write_value(self._variables_to_write.get_nowait())
t_samples = self._create_time_samples(t_sample=t_sample) + t_start
try:
for _idx, _t_sample in enumerate(t_samples[:-1]):
# Write current values to system
while not self._variables_to_write.empty():
self.__write_value(self._variables_to_write.get_nowait())

# do step
self.system.doStep(
currentCommunicationPoint=_t_sample,
communicationStepSize=t_samples[_idx + 1] - _t_sample,
Expand Down
149 changes: 91 additions & 58 deletions agentlib/modules/simulation/simulator.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"""

import os
import warnings
from dataclasses import dataclass
from math import inf
from pathlib import Path
Expand Down Expand Up @@ -65,8 +66,8 @@ def __init__(self, variables: List[ModelVariable]):
self.data = []

def initialize(
self,
time: float,
self,
time: float,
):
"""Adds the first row to the data"""

Expand Down Expand Up @@ -104,17 +105,38 @@ class SimulatorConfig(BaseModuleConfig):
outputs: AgentVariables = []
states: AgentVariables = []
shared_variable_fields: List[str] = ["outputs"]
model: Dict

t_start: Union[float, int] = Field(
title="t_start", default=0.0, ge=0, description="Simulation start time"
)
t_stop: Union[float, int] = Field(
title="t_stop", default=inf, ge=0, description="Simulation stop time"
)
t_sample: Union[float, int] = Field(
title="t_sample", default=1, ge=0, description="Simulation sample time"
title="t_sample",
default=1,
ge=0,
description="Deprecated option."
)
t_sample_communication: Union[float, int] = Field(
title="t_sample",
default=1,
ge=0,
description="Sample time of a full simulation step relevant for communication, including:"
"1. if update_inputs_on_callback=False update model inputs,"
"2. Perform simulation with t_sample_simulation"
"3. Update model results and send output values to other Agents or Modules."
)
t_sample_simulation: Union[float, int] = Field(
title="t_sample_simulation",
default=1,
ge=0,
description="Sample time of the simulation itself. "
"If update_inputs_on_callback=True, the inputs of the models "
"may be updated every other t_sample_simulation, as long as the "
"model supports this. Used to override dt of the model."
)
model: Dict

# Model results
save_results: bool = Field(
title="save_results",
Expand All @@ -130,19 +152,19 @@ class SimulatorConfig(BaseModuleConfig):
title="result_filename",
default=None,
description="If not None, results are stored in that filename."
"Needs to be a .csv file",
"Needs to be a .csv file",
)
result_sep: str = Field(
title="result_sep",
default=",",
description="Separator in the .csv file. Only relevant if "
"result_filename is passed",
"result_filename is passed",
)
result_causalities: List[Causality] = Field(
title="result_causalities",
default=[Causality.input, Causality.output],
description="List of causalities to store. Default stores "
"only inputs and outputs",
"only inputs and outputs",
)
write_results_delay: Optional[float] = Field(
title="Write Results Delay",
Expand All @@ -155,23 +177,23 @@ class SimulatorConfig(BaseModuleConfig):
title="update_inputs_on_callback",
default=True,
description="If True, model inputs are updated if they are updated in data_broker."
"Else, the model inputs are updated before each simulation.",
"Else, the model inputs are updated before each simulation.",
)
measurement_uncertainty: Union[Dict[str, float], float] = Field(
title="measurement_uncertainty",
default=0,
description="Either pass a float and add the percentage uncertainty "
"to all measurements from the model."
"Or pass a Dict and specify the model variable name as key"
"and the associated uncertainty as a float",
"to all measurements from the model."
"Or pass a Dict and specify the model variable name as key"
"and the associated uncertainty as a float",
)
validate_incoming_values: Optional[bool] = Field(
default=False, # we overwrite the default True in base, to be more efficient
title="Validate Incoming Values",
description="If true, the validator of the AgentVariable value is called when "
"receiving a new value from the DataBroker. In the simulator, this "
"is False by default, as we expect to receive a lot of measurements"
" and want to be efficient.",
"receiving a new value from the DataBroker. In the simulator, this "
"is False by default, as we expect to receive a lot of measurements"
" and want to be efficient.",
)

@field_validator("result_filename")
Expand Down Expand Up @@ -215,17 +237,30 @@ def check_t_stop(cls, t_stop, info: FieldValidationInfo):
assert t_stop > t_start, "t_stop must be greater than t_start"
return t_stop

@field_validator("t_sample")
@field_validator("t_sample_communication", "t_sample_simulation")
@classmethod
def check_t_sample(cls, t_sample, info: FieldValidationInfo):
"""Check if t_sample is smaller than stop-start time"""
t_start = info.data.get("t_start")
t_stop = info.data.get("t_stop")
t_sample_old = info.data.get("t_sample")
if t_sample_old is not None:
t_sample = t_sample_old
assert (
t_start + t_sample <= t_stop
t_start + t_sample <= t_stop
), "t_stop-t_start must be greater than t_sample"
return t_sample

@field_validator("t_sample")
@classmethod
def deprecate_t_sample(cls, t_sample, info: FieldValidationInfo):
"""Check if t_sample is smaller than stop-start time"""
warnings.warn(
"t_sample is deprecated, use t_sample_communication, "
"t_sample_simulation for a concise separation of the two.",
)
return t_sample

@field_validator("write_results_delay")
@classmethod
def set_default_t_sample(cls, write_results_delay, info: FieldValidationInfo):
Expand All @@ -249,6 +284,14 @@ def check_model(cls, model, info: FieldValidationInfo):
inputs = info.data.get("inputs")
outputs = info.data.get("outputs")
states = info.data.get("states")
dt = info.data.get("t_sample_simulation")
if "dt" in model and dt != model["dt"]:
warnings.warn(
f"Given model {model['dt']=} differs from {dt=} of simulator. "
f"Using models dt, consider switching to t_sample_simulation."
)
else:
model["dt"] = dt
if "type" not in model:
raise KeyError(
"Given model config does not " "contain key 'type' (type of the model)."
Expand Down Expand Up @@ -362,10 +405,10 @@ def _register_input_callbacks(self):
# Outputs and states are always the result of the model
# "Complicated" double for-loop to avoid boilerplate code
for _type, model_var_names, ag_vars, callback in zip(
["input", "parameter"],
[self.model.get_input_names(), self.model.get_parameter_names()],
[self.config.inputs, self.config.parameters],
[self._callback_update_model_input, self._callback_update_model_parameter],
["input", "parameter"],
[self.model.get_input_names(), self.model.get_parameter_names()],
[self.config.inputs, self.config.parameters],
[self._callback_update_model_input, self._callback_update_model_parameter],
):
for var in ag_vars:
if var.name in model_var_names:
Expand Down Expand Up @@ -397,52 +440,43 @@ def _callback_update_model_parameter(self, par: AgentVariable, name: str):

def process(self):
"""
This function creates a endless loop for the single simulation step event.
The do_step() function needs to return a generator.
"""
self._update_result_outputs(self.env.time)
while True:
self.do_step()
yield self.env.timeout(self.config.t_sample)
self.update_module_vars()

def do_step(self):
"""
Generator function to perform a simulation step,
update inputs, outputs and model results.
This function creates a endless loop for the single simulation step event,
updating inputs, simulating, model results and then outputs.

In a simulation step following happens:
1. Update inputs (only necessary if self.update_inputs_on_callback = False)
2. Specify the end time of the simulation from the agents perspective.
**Important note**: The agents use unix-time as a timestamp and start
the simulation with the current datetime (represented by self.env.time),
the model starts at 0 seconds (represented by self.env.now).
3. Directly after the simulation we send the updated output values
to other modules and agents by setting them the data_broker.
Even though the environment time is not already at the end time specified above,
we explicitly add the timestamp to the variables.
This way other agents and communication has the maximum time possible to
process the outputs and send input signals to the simulation.
4. Call the timeout in the environment,
3. Directly after the simulation we store the results with
the output time and then call the timeout in the environment,
hence actually increase the environment time.
4. Once the environment time reached the simulation time,
we send the updated output values to other modules and agents by setting
them the data_broker.
"""
if not self.config.update_inputs_on_callback:
# Update inputs manually
self.update_model_inputs()
# Simulate
self.model.do_step(
t_start=(self.env.now + self.config.t_start), t_sample=self.config.t_sample
)
# Update the results and outputs
self._update_results()
self._update_result_outputs(self.env.time)
while True:
if not self.config.update_inputs_on_callback:
# Update inputs manually
self.update_model_inputs()
# Simulate
self.model.do_step(
t_start=(self.env.now + self.config.t_start), t_sample=self.config.t_sample_communication
)
# Update the results and outputs
self._update_results()
yield self.env.timeout(self.config.t_sample_communication)
self.update_module_vars()

def update_model_inputs(self):
"""
Internal method to write current data_broker to simulation model.
Only update values, not other module_types.
"""
model_input_names = (
self.model.get_input_names() + self.model.get_parameter_names()
self.model.get_input_names() + self.model.get_parameter_names()
)
for inp in self.variables:
if inp.name in model_input_names:
Expand All @@ -456,9 +490,9 @@ def update_module_vars(self):
"""
# pylint: disable=logging-fstring-interpolation
for _type, model_get, agent_vars in zip(
["state", "output"],
[self.model.get_state, self.model.get_output],
[self.config.states, self.config.outputs],
["state", "output"],
[self.model.get_state, self.model.get_output],
[self.config.states, self.config.outputs],
):
for var in agent_vars:
mo_var = model_get(var.name)
Expand Down Expand Up @@ -499,15 +533,14 @@ def cleanup_results(self):
return
os.remove(self.config.result_filename)


def _update_results(self):
"""
Adds model variables to the SimulationResult object
at the given timestamp.
"""
if not self.config.save_results:
return
timestamp = self.env.time + self.config.t_sample
timestamp = self.env.time + self.config.t_sample_communication
inp_values = [var.value for var in self._get_result_input_variables()]

# add inputs in the time stamp before adding outputs, as they are active from
Expand All @@ -517,8 +550,8 @@ def _update_results(self):
# above will point to the wrong entry
self._update_result_outputs(timestamp)
if (
self.config.result_filename is not None
and timestamp // (self.config.write_results_delay * self._save_count) > 0
self.config.result_filename is not None
and timestamp // (self.config.write_results_delay * self._save_count) > 0
):
self._save_count += 1
self._result.write_results(self.config.result_filename)
Expand Down
3 changes: 2 additions & 1 deletion examples/multi-agent-systems/room_mas/configs/Room1.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
"path": "models/SimpleRoom.fmu"
},
"measurement_uncertainty": 0.0001,
"t_sample": 50,
"t_sample_communication": 50,
"t_sample_simulation": 50,
"save_results": true,
"overwrite_result_file": true,
"result_filename": "res_room1.csv",
Expand Down
3 changes: 2 additions & 1 deletion examples/multi-agent-systems/room_mas/configs/Room2.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
"type": "fmu",
"path": "models/SimpleRoom.fmu"
},
"t_sample": 50,
"t_sample_communication": 50,
"t_sample_simulation": 50,
"save_results": true,
"measurement_uncertainty": {
"T_air": 0.0001
Expand Down
2 changes: 1 addition & 1 deletion examples/multi-agent-systems/room_mas/room_mas.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ def run_example(until, with_plots=True, log_level=logging.INFO):
# Start by setting the log-level
logging.basicConfig(level=log_level)

env_config = {"rt": False, "t_sample": 60, "clock": True}
env_config = {"rt": True, "factor": 0.005, "t_sample": 60, "clock": True}

# Change the working directly so that relative paths work
os.chdir(os.path.abspath(os.path.dirname(__file__)))
Expand Down