From c439bcd13bbf47d7281fb02b3492f13c63ca0fe7 Mon Sep 17 00:00:00 2001 From: chria Date: Tue, 27 Feb 2024 11:46:33 +0100 Subject: [PATCH] Unified result handling - enabled all result handlers for the master algorithm --- src/common/io.py | 32 ++++++++++++++ src/pyfmi/fmi_algorithm_drivers.py | 67 +++++++++--------------------- src/pyfmi/fmi_util.pyx | 2 + src/pyfmi/master.pyx | 40 +++++++++--------- tests/test_fmi_master.py | 54 ++++++++++++++++++++++++ 5 files changed, 129 insertions(+), 66 deletions(-) diff --git a/src/common/io.py b/src/common/io.py index c0eb2538..0690e8e5 100644 --- a/src/common/io.py +++ b/src/common/io.py @@ -22,6 +22,7 @@ import re import sys import os +import logging as logging_module from functools import reduce import numpy as N @@ -2723,3 +2724,34 @@ def set_options(self, options): method. """ self.options = options + + +def get_result_handler(model, opts): + result_handler = None + + if opts["result_handling"] == "file": + result_handler = ResultHandlerFile(model) + elif opts["result_handling"] == "binary": + if "sensitivities" in opts and opts["sensitivities"]: + logging_module.warning('The binary result file do not currently support storing of sensitivity results. Switching to textual result format.') + result_handler = ResultHandlerFile(model) + else: + result_handler = ResultHandlerBinaryFile(model) + elif opts["result_handling"] == "memory": + result_handler = ResultHandlerMemory(model) + elif opts["result_handling"] == "csv": + result_handler = ResultHandlerCSV(model, delimiter=",") + elif opts["result_handling"] == "custom": + result_handler = opts["result_handler"] + if result_handler is None: + raise fmi.FMUException("The result handler needs to be specified when using a custom result handling.") + if not isinstance(result_handler, ResultHandler): + raise fmi.FMUException("The result handler needs to be a subclass of ResultHandler.") + elif (opts["result_handling"] is None) or (opts["result_handling"] == 'none'): #No result handling (for performance) + if opts["result_handling"] == 'none': ## TODO: Future; remove this + logging_module.warning("result_handling = 'none' is deprecated. Please use None instead.") + result_handler = ResultHandlerDummy(model) + else: + raise fmi.FMUException("Unknown option to result_handling.") + + return result_handler diff --git a/src/pyfmi/fmi_algorithm_drivers.py b/src/pyfmi/fmi_algorithm_drivers.py index 923c8a01..d6abfc57 100644 --- a/src/pyfmi/fmi_algorithm_drivers.py +++ b/src/pyfmi/fmi_algorithm_drivers.py @@ -29,7 +29,7 @@ import pyfmi.fmi_extended as fmi_extended from pyfmi.common.diagnostics import setup_diagnostics_variables from pyfmi.common.algorithm_drivers import AlgorithmBase, OptionBase, InvalidAlgorithmOptionException, InvalidSolverArgumentException, JMResultBase -from pyfmi.common.io import ResultHandlerFile, ResultHandlerBinaryFile, ResultHandlerMemory, ResultHandler, ResultHandlerDummy, ResultHandlerCSV +from pyfmi.common.io import get_result_handler from pyfmi.common.core import TrajectoryLinearInterpolation from pyfmi.common.core import TrajectoryUserFunction @@ -486,31 +486,7 @@ def _set_result_handler(self): """ Helper functions that sets result_handler. """ - if self.options["result_handling"] == "file": - self.result_handler = ResultHandlerFile(self.model) - elif self.options["result_handling"] == "binary": - if self.options["sensitivities"]: - logging_module.warning('The binary result file do not currently support storing of sensitivity results. Switching to textual result format.') - self.result_handler = ResultHandlerFile(self.model) - else: - self.result_handler = ResultHandlerBinaryFile(self.model) - elif self.options["result_handling"] == "memory": - self.result_handler = ResultHandlerMemory(self.model) - elif self.options["result_handling"] == "csv": - self.result_handler = ResultHandlerCSV(self.model, delimiter=",") - elif self.options["result_handling"] == "custom": - self.result_handler = self.options["result_handler"] - if self.result_handler is None: - raise fmi.FMUException("The result handler needs to be specified when using a custom result handling.") - if not isinstance(self.result_handler, ResultHandler): - raise fmi.FMUException("The result handler needs to be a subclass of ResultHandler.") - elif (self.options["result_handling"] is None) or (self.options["result_handling"] == 'none'): #No result handling (for performance) - if self.options["result_handling"] == 'none': ## TODO: Future; remove this - logging_module.warning("result_handling = 'none' is deprecated. Please use None instead.") - self.result_handler = ResultHandlerDummy(self.model) - else: - raise fmi.FMUException("Unknown option to result_handling.") - + self.result_handler = get_result_handler(self.model, self.options) def _set_options(self): """ @@ -1032,26 +1008,7 @@ def _set_result_handler(self): """ Helper functions that sets result_handler. """ - if self.options["result_handling"] == "file": - self.result_handler = ResultHandlerFile(self.model) - elif self.options["result_handling"] == "binary": - self.result_handler = ResultHandlerBinaryFile(self.model) - elif self.options["result_handling"] == "memory": - self.result_handler = ResultHandlerMemory(self.model) - elif self.options["result_handling"] == "csv": - self.result_handler = ResultHandlerCSV(self.model, delimiter=",") - elif self.options["result_handling"] == "custom": - self.result_handler = self.options["result_handler"] - if self.result_handler is None: - raise fmi.FMUException("The result handler needs to be specified when using a custom result handling.") - if not isinstance(self.result_handler, ResultHandler): - raise fmi.FMUException("The result handler needs to be a subclass of ResultHandler.") - elif (self.options["result_handling"] is None) or (self.options["result_handling"] == 'none'): #No result handling (for performance) - if self.options["result_handling"] == 'none': - logging_module.warning("result_handling = 'none' is deprecated. Please use None instead.") - self.result_handler = ResultHandlerDummy(self.model) - else: - raise fmi.FMUException("Unknown option to result_handling.") + self.result_handler = get_result_handler(self.model, self.options) def _set_options(self): """ @@ -1263,7 +1220,7 @@ def __init__(self, # set options self._set_options() - self.result_handler = ResultHandlerCSV(self.model) + self.result_handler = get_result_handler(self.model, self.options) self.result_handler.set_options(self.options) self.result_handler.initialize_complete() @@ -1419,11 +1376,27 @@ class SciEstAlgOptions(OptionBase): result_file_name can also be set to a stream that supports 'write', 'tell' and 'seek'. Default: Empty string + + result_handling -- + Specifies how the result should be handled. Either stored to + file (txt or binary) or stored in memory. One can also use a + custom handler. + Available options: "file", "binary", "memory", "csv", "custom", None + Default: "csv" + + result_handler -- + The handler for the result. Depending on the option in + result_handling this either defaults to ResultHandlerFile + or ResultHandlerMemory. If result_handling custom is chosen + This MUST be provided. + Default: None """ def __init__(self, *args, **kw): _defaults= {"tolerance": 1e-6, 'result_file_name':'', + 'result_handling':'csv', + 'result_handler':None, 'filter':None, 'method': 'Nelder-Mead', 'scaling': 'Default', diff --git a/src/pyfmi/fmi_util.pyx b/src/pyfmi/fmi_util.pyx index 65c4e764..49f721f4 100644 --- a/src/pyfmi/fmi_util.pyx +++ b/src/pyfmi/fmi_util.pyx @@ -1362,3 +1362,5 @@ def read_name_list(file_name, int file_position, int nbr_variables, int max_leng FMIL.free(tmp) return data + + diff --git a/src/pyfmi/master.pyx b/src/pyfmi/master.pyx index 82d14633..2caf8cf5 100644 --- a/src/pyfmi/master.pyx +++ b/src/pyfmi/master.pyx @@ -17,7 +17,7 @@ import pyfmi.fmi as fmi from pyfmi.common.algorithm_drivers import OptionBase, InvalidAlgorithmOptionException, AssimuloSimResult -from pyfmi.common.io import ResultDymolaTextual, ResultHandlerFile, ResultHandlerDummy, ResultHandlerBinaryFile, ResultDymolaBinary +from pyfmi.common.io import get_result_handler from pyfmi.common.core import TrajectoryLinearInterpolation from pyfmi.common.core import TrajectoryUserFunction @@ -1186,19 +1186,21 @@ cdef class Master: def initialize_result_objects(self, opts): i = 0 for model in self.models_dict.keys(): - if opts["result_handling"] == "binary": - result_object = ResultHandlerBinaryFile(model) - elif opts["result_handling"] == "file": - result_object = ResultHandlerFile(model) - elif opts["result_handling"] == "none": - result_object = ResultHandlerDummy(model) - else: - raise fmi.FMUException("Currently only writing result to file (txt and binary) and none is supported.") + result_object = get_result_handler(model, opts) + if not isinstance(opts["result_file_name"], dict): raise fmi.FMUException("The result file names needs to be stored in a dict with the individual models as key.") + from pyfmi.fmi_algorithm_drivers import FMICSAlgOptions local_opts = FMICSAlgOptions() - prefix = "txt" if opts["result_handling"] == "file" else "mat" + + if opts["result_handling"] == "file": + prefix = "txt" + elif opts["result_handling"] == "csv": + prefix = "csv" + else: + prefix = "mat" + try: if opts["result_file_name"][model] is None: local_opts["result_file_name"] = model.get_identifier()+'_'+str(i)+'_result.'+prefix @@ -1206,6 +1208,7 @@ cdef class Master: local_opts["result_file_name"] = opts["result_file_name"][model] except KeyError: raise fmi.FMUException("Incorrect definition of the result file name option. No result file name found for model %s"%model.get_identifier()) + local_opts["filter"] = opts["filter"][model] result_object.set_options(local_opts) @@ -1519,15 +1522,14 @@ cdef class Master: res = {} #Load data for i,model in enumerate(self.models): - if opts["result_handling"] == "file" or opts["result_handling"] == "binary": - stored_res = self.models_dict[model]["result"].get_result() - res[i] = AssimuloSimResult(model, self.models_dict[model]["result"].file_name, None, stored_res, None) - res[model] = res[i] - elif opts["result_handling"] == "none": - res[model] = None - else: - raise fmi.FMUException("Currently only writing result to file/binary or none is supported.") - + stored_res = self.models_dict[model]["result"].get_result() + try: + file_name = self.models_dict[model]["result"].file_name + except AttributeError: + file_name = "" + res[i] = AssimuloSimResult(model, file_name, None, stored_res, None) + res[model] = res[i] + return res def print_statistics(self, opts): diff --git a/tests/test_fmi_master.py b/tests/test_fmi_master.py index 6d5bb2aa..82e9a523 100644 --- a/tests/test_fmi_master.py +++ b/tests/test_fmi_master.py @@ -25,6 +25,7 @@ import pyfmi.fmi as fmi from pyfmi import Master from pyfmi.tests.test_util import Dummy_FMUModelME2, Dummy_FMUModelCS2 +from pyfmi.common.io import ResultHandler file_path = os.path.dirname(os.path.abspath(__file__)) @@ -206,6 +207,59 @@ def test_basic_simulation_txt_file_naming_exists(self): assert os.path.isfile("Test1.txt"), "Test1.txt does not exists" assert os.path.isfile("Test2.txt"), "Test2.txt does not exists" + + @testattr(stddist = True) + def test_basic_simulation_csv_file_naming_exists(self): + models, connections = self._load_basic_simulation() + + opts = {"result_handling":"csv", "result_file_name": {models[0]: "Test1.csv", models[1]: "Test2.csv"}} + + res = self._sim_basic_simulation(models, connections, opts) + + assert os.path.isfile("Test1.csv"), "Test1.csv does not exists" + assert os.path.isfile("Test2.csv"), "Test2.csv does not exists" + + @testattr(stddist = True) + def test_basic_simulation_none_result(self): + models, connections = self._load_basic_simulation() + + opts = {"result_handling":None} + + master = Master(models, connections) + + opts["step_size"] = 0.0005 + res = master.simulate(options=opts) + + assert res[models[0]]._result_data == None, "Result is not none" + assert res[models[1]]._result_data == None, "Result is not none" + + @testattr(stddist = True) + def test_custom_result_handler(self): + models, connections = self._load_basic_simulation() + + + class A: + pass + class B(ResultHandler): + def get_result(self): + return None + + opts = {} + opts["result_handling"] = "hejhej" + nose.tools.assert_raises(Exception, self._sim_basic_simulation, models, connections, opts) + opts["result_handling"] = "custom" + nose.tools.assert_raises(Exception, self._sim_basic_simulation, models, connections, opts) + opts["result_handler"] = A() + nose.tools.assert_raises(Exception, self._sim_basic_simulation, models, connections, opts) + opts["result_handler"] = B() + + master = Master(models, connections) + + opts["step_size"] = 0.0005 + res = master.simulate(options=opts) + + assert res[models[0]]._result_data == None, "Result is not none" + assert res[models[1]]._result_data == None, "Result is not none" @testattr(stddist = True) def test_basic_simulation_with_block_initialization(self):