Skip to content
Draft
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
37 changes: 34 additions & 3 deletions src/pyfmi/fmi_algorithm_drivers.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,15 @@
PYFMI_JACOBIAN_LIMIT = 10
PYFMI_JACOBIAN_SPARSE_SIZE_LIMIT = 100
PYFMI_JACOBIAN_SPARSE_NNZ_LIMIT = 0.15 #In percentage
PYFMI_JACOBIAN_SOLVERS = ("CVode", "Radau5ODE", "RodasODE") # get with_jacobian by default (see below)
PYFMI_SPARSE_JACOBIAN_SOLVERS = ("CVode", "Radau5ODE") # ... and support linear_solver = "SPARSE"

# Radau5ODE recomputes the Jacobian after every step in which Newton's contraction rate
# exceeded 'thet' (Hairer's THET, Assimulo default 1e-3, i.e. after nearly every step).
# With the PyFMI Jacobian (100-240 rhs evaluations each on 140-320 state FMUs) a looser
# threshold is worth it: measured -42 % solver time on a 141-state vehicle model and
# -10 % on a 226-state plant, correct events, unchanged accuracy, neutral on small models.
PYFMI_RADAU5_THET_WITH_JACOBIAN = 0.1

class FMIResult(JMResultBase):
def __init__(self, model=None, result_file_name=None, solver=None,
Expand Down Expand Up @@ -207,6 +216,19 @@ class AssimuloFMIAlgOptions(OptionBase):
iter --
The iteration method. Can be either 'Newton' or 'FixedPoint'
Default: 'Newton'

Options for Radau5ODE::

rtol, atol, maxh --
As for CVode.

thet --
Newton contraction rate above which the Jacobian is recomputed
after an accepted step (0 < thet < 1). Assimulo's default 1e-3
recomputes it after almost every step; with the PyFMI Jacobian
('with_jacobian' in effect) the default is 0.1, which halves the
Jacobian evaluations on models where Newton converges slowly.
Default: "Default" (0.1 with the PyFMI Jacobian, else Assimulo's 1e-3)
"""
def __init__(self, *args, **kw):
_defaults= {
Expand All @@ -229,7 +251,7 @@ def __init__(self, *args, **kw):
'extra_equations':None,
'CVode_options':{'discr':'BDF','iter':'Newton',
'atol':"Default",'rtol':"Default","maxh":"Default",'external_event_detection':False},
'Radau5ODE_options':{'atol':"Default",'rtol':"Default","maxh":"Default"},
'Radau5ODE_options':{'atol':"Default",'rtol':"Default","maxh":"Default","thet":"Default"},
'RungeKutta34_options':{'atol':"Default",'rtol':"Default"},
'Dopri5_options':{'atol':"Default",'rtol':"Default", "maxh":"Default"},
'RodasODE_options':{'atol':"Default",'rtol':"Default", "maxh":"Default"},
Expand Down Expand Up @@ -547,9 +569,12 @@ def _set_options(self):
self.with_jacobian = True
else:
fnbr, _ = self.model.get_ode_sizes()
if fnbr >= PYFMI_JACOBIAN_LIMIT and solver == "CVode":
# Solvers that evaluate the Jacobian themselves by dense finite differences
# (nx rhs calls per Jacobian) benefit from the structure-aware (coloured)
# Jacobian of FMIODE2 just like CVode; RodasODE needs one every step.
if fnbr >= PYFMI_JACOBIAN_LIMIT and solver in PYFMI_JACOBIAN_SOLVERS:
self.with_jacobian = True
if fnbr >= PYFMI_JACOBIAN_SPARSE_SIZE_LIMIT:
if fnbr >= PYFMI_JACOBIAN_SPARSE_SIZE_LIMIT and solver in PYFMI_SPARSE_JACOBIAN_SOLVERS:
try:
self.solver_options["linear_solver"]
except KeyError:
Expand Down Expand Up @@ -623,6 +648,12 @@ def _set_solver_options(self):
if "usejac" in solver_options and fnbr == 0:
solver_options["usejac"] = False

if "thet" in solver_options and isinstance(solver_options["thet"], str) and solver_options["thet"] == "Default":
if self.with_jacobian:
solver_options["thet"] = PYFMI_RADAU5_THET_WITH_JACOBIAN
else:
del solver_options["thet"] # Assimulo's own default

if "maxh" in solver_options and solver_options["maxh"] == "Default":
if self.options["ncp"] == 0:
solver_options["maxh"] = 0.0
Expand Down
27 changes: 27 additions & 0 deletions tests/test_fmi2.py
Original file line number Diff line number Diff line change
Expand Up @@ -312,7 +312,14 @@ def run_case(expected, default="Default"):
model.get_ode_sizes = lambda: (PYFMI_JACOBIAN_LIMIT+1, 0)
run_case(True)

# solvers with their own dense finite-difference Jacobian get the coloured one too
opts["solver"] = "Radau5ODE"
run_case(True)

opts["solver"] = "RodasODE"
run_case(True)

opts["solver"] = "LSODAR"
run_case(False)

opts["solver"] = "CVode"
Expand All @@ -323,6 +330,26 @@ def run_case(expected, default="Default"):
opts["with_jacobian"] = True
run_case(True, True)

def test_radau5_thet_default(self):
"""Radau5ODE 'thet': 0.1 with the PyFMI Jacobian, Assimulo's default without, user value wins."""
model = Dummy_FMUModelME2([], os.path.join(file_path, "files", "FMUs", "XML", "ME2.0", "NoState.Example1.fmu"), _connect_dll=False)
opts = model.simulate_options()
opts["solver"] = "Radau5ODE"
opts["result_handling"] = None
model.get_ode_sizes = lambda: (PYFMI_JACOBIAN_LIMIT + 1, 0)

def run_case(expected_thet, with_jacobian="Default", thet="Default"):
model.reset()
opts["with_jacobian"] = with_jacobian
opts["Radau5ODE_options"]["thet"] = thet
alg = NoSolveAlg(0.0, 1.0, (), model, opts)
assert alg.simulator.thet == pytest.approx(expected_thet), alg.simulator.thet

run_case(0.1) # with_jacobian by default for this size and solver
run_case(1e-3, with_jacobian=False) # Assimulo's default
run_case(0.5, thet=0.5)
run_case(0.5, with_jacobian=False, thet=0.5)

def test_sparse_option(self):

def run_case(expected_jacobian, expected_sparse, fnbr=0, nnz={}, set_sparse=False):
Expand Down