diff --git a/CHANGELOG b/CHANGELOG index 8f876710..341e2a79 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -2,6 +2,7 @@ --- PyFMI-NEXT_VERSION --- * Removed utilities related to the obsolete FMUX model interface. * Removed no longer required dependency on lxml. + * Unified result handling and allowed the master algorithm to use all available result handlers --- PyFMI-2.11.0 --- * Refactored result handling for dynamic_diagnostics. It is now possible use dynamic_diagnostics with a custom result handler. 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..ea2ee930 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 @@ -297,7 +297,7 @@ def __init__(self, else: raise InvalidAlgorithmOptionException(options) - self._set_result_handler() # sets self.results_handler + self.result_handler = get_result_handler(self.model, self.options) self._set_options() # set options #time_start = timer() @@ -482,36 +482,6 @@ def __init__(self, self.simulator = self.solver(self.probl) self._set_solver_options() - 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.") - - def _set_options(self): """ Helper function that sets options for AssimuloFMI algorithm. @@ -987,8 +957,7 @@ def __init__(self, #time_start = timer() - self._set_result_handler() ## set self.results_handler - + self.result_handler = get_result_handler(self.model, self.options) self.result_handler.set_options(self.options) time_end = timer() @@ -1028,31 +997,6 @@ def __init__(self, self.timings["initializing_result"] = timer() - time_start - time_res_init - 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.") - def _set_options(self): """ Helper function that sets options for FMICS algorithm. @@ -1263,7 +1207,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 +1363,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..eded5424 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__)) @@ -167,6 +168,11 @@ def test_basic_simulation_mat_file(self): opts = {"result_handling":"binary"} self._basic_simulation(opts) + @testattr(stddist = True) + def test_basic_simulation_memory(self): + opts = {"result_handling":"memory"} + self._basic_simulation(opts) + @testattr(stddist = True) def test_basic_simulation_mat_file_naming(self): from pyfmi.common.algorithm_drivers import UnrecognizedOptionError @@ -206,6 +212,66 @@ 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_invalid(self): + models, connections = self._load_basic_simulation() + + class A: + pass + + 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) + + @testattr(stddist = True) + def test_custom_result_handler_valid(self): + models, connections = self._load_basic_simulation() + + class B(ResultHandler): + def get_result(self): + return None + + opts = {} + opts["result_handling"] = "custom" + opts["result_handler"] = B() + opts["step_size"] = 0.0005 + + master = Master(models, connections) + + 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):