Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Unified result handling - enabled all result handlers for the master alg #223

Merged
merged 3 commits into from
Feb 27, 2024
Merged
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
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:
PeterMeisrimelModelon marked this conversation as resolved.
Show resolved Hide resolved
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"

chria marked this conversation as resolved.
Show resolved Hide resolved
@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"
PeterMeisrimelModelon marked this conversation as resolved.
Show resolved Hide resolved
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)
chria marked this conversation as resolved.
Show resolved Hide resolved

@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"
chria marked this conversation as resolved.
Show resolved Hide resolved
assert res[models[1]]._result_data == None, "Result is not none"

@testattr(stddist = True)
def test_basic_simulation_with_block_initialization(self):
Expand Down
Loading