Skip to content

Commit

Permalink
Unified result handling - enabled all result handlers for the master …
Browse files Browse the repository at this point in the history
…alg (#223)

* Unified result handling - enabled all result handlers for the master algorithm

* Fixes after review

* Updated changelog

---------

Co-authored-by: chria <[email protected]>
  • Loading branch information
2 people authored and modelonrobinandersson committed Apr 25, 2024
1 parent 0aa4af9 commit 7c030ef
Show file tree
Hide file tree
Showing 6 changed files with 142 additions and 79 deletions.
1 change: 1 addition & 0 deletions CHANGELOG
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
32 changes: 32 additions & 0 deletions src/common/io.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import re
import sys
import os
import logging as logging_module
from functools import reduce

import numpy as N
Expand Down Expand Up @@ -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
80 changes: 20 additions & 60 deletions src/pyfmi/fmi_algorithm_drivers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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()

Expand Down Expand Up @@ -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',
Expand Down
2 changes: 2 additions & 0 deletions src/pyfmi/fmi_util.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -1362,3 +1362,5 @@ def read_name_list(file_name, int file_position, int nbr_variables, int max_leng
FMIL.free(tmp)

return data


40 changes: 21 additions & 19 deletions src/pyfmi/master.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -1186,26 +1186,29 @@ 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
else:
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)
Expand Down Expand Up @@ -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):
Expand Down
66 changes: 66 additions & 0 deletions tests/test_fmi_master.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__))

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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):
Expand Down

0 comments on commit 7c030ef

Please sign in to comment.