Skip to content

Commit ededa7f

Browse files
testing downsampling of master algorithm in serial mode; executing maximally sized do_step calls
1 parent 52bd89d commit ededa7f

2 files changed

Lines changed: 232 additions & 12 deletions

File tree

‎src/pyfmi/master.pyx‎

Lines changed: 47 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,31 @@ cdef perform_do_step_serial(list models, dict time_spent, double cur_time, doubl
7575
if status != 0:
7676
raise FMUException("The step failed for model %s at time %f. See the log for more information. Return flag %d."%(model.get_name(), cur_time, status))
7777

78+
cdef perform_do_step_serial_with_downsampling(
79+
long step_number,
80+
list models,
81+
list downsampling_rates,
82+
dict time_spent,
83+
double cur_time,
84+
double final_time,
85+
double step_size,
86+
bool new_step):
87+
"""
88+
Perform a do step on all the models.
89+
"""
90+
cdef double time_start = 0.0
91+
cdef int status = 0
92+
93+
for model, ds_rate in zip(models, downsampling_rates):
94+
# Note: step_number is 0 based
95+
if (step_number % ds_rate) == 0:
96+
time_start = timer()
97+
h = min(ds_rate*step_size, abs(final_time - cur_time)) # TODO: eps adjustments here?
98+
status = model.do_step(cur_time, h, new_step)
99+
time_spent[model] += timer() - time_start
100+
if status != 0:
101+
raise FMUException("The step failed for model %s at time %f. See the log for more information. Return flag %d."%(model.get_name(), cur_time, status))
102+
78103
cdef perform_do_step_parallel(list models, FMIL2.fmi2_import_t** model_addresses, int n, double cur_time, double step_size, int new_step):
79104
"""
80105
Perform a do step on all the models.
@@ -358,6 +383,9 @@ class MasterAlgOptions(OptionBase):
358383
It is not required to have all models as keys,
359384
missing ones take the default value of 1.
360385
Default: {m: 1 for m in models} (no downsampling)
386+
387+
_experimental_serial_downsampling --
388+
TODO
361389
"""
362390
def __init__(self, master, *args, **kw):
363391
_defaults= {
@@ -389,6 +417,7 @@ class MasterAlgOptions(OptionBase):
389417
"num_threads":None,
390418
"result_downsampling_factor": dict((model, 1) for model in master.models),
391419
"step_size_downsampling_factor" : dict((model, 1) for model in master.models),
420+
"_experimental_serial_downsampling": False,
392421
}
393422
super(MasterAlgOptions,self).__init__(_defaults)
394423
# Exceptions to the above types need to handled here, e.g., allowing both
@@ -440,6 +469,7 @@ cdef class Master:
440469
cdef public long long _step_number
441470
cdef public bool _last_step
442471
cdef public dict step_size_downsampling_factor
472+
cdef public bool _uses_step_size_downsampling
443473

444474
def __init__(self, models, connections):
445475
"""
@@ -1400,8 +1430,18 @@ cdef class Master:
14001430
step_size = final_time - tcur
14011431
self.set_current_step_size(step_size)
14021432
self._last_step = True
1403-
1404-
perform_do_step(self.models, self.elapsed_time, self.fmu_adresses, tcur, step_size, True, calling_setting)
1433+
if opts["_experimental_serial_downsampling"]:
1434+
perform_do_step_serial_with_downsampling(
1435+
self._step_number,
1436+
self.models,
1437+
list(self.step_size_downsampling_factor.values()),
1438+
self.elapsed_time,
1439+
tcur,
1440+
final_time,
1441+
step_size,
1442+
True)
1443+
else:
1444+
perform_do_step(self.models, self.elapsed_time, self.fmu_adresses, tcur, step_size, True, calling_setting)
14051445

14061446
if self.opts["store_step_before_update"]:
14071447
time_start = timer()
@@ -1629,7 +1669,9 @@ cdef class Master:
16291669
f"got: '{val}'.")
16301670
self.step_size_downsampling_factor[m] = val
16311671

1632-
if set(self.step_size_downsampling_factor.values()) != {1} and options["logging"]:
1672+
self._uses_step_size_downsampling = set(self.step_size_downsampling_factor.values()) != {1}
1673+
1674+
if self._uses_step_size_downsampling and options["logging"]:
16331675
warnings.warn("Both 'step_size_downsampling_factor' and 'logging' are used. " \
16341676
"Logging of A, B, C, and D matrices will be done on the global step-size." \
16351677
"Actual values may no longer be sensible.")
@@ -1639,10 +1681,9 @@ cdef class Master:
16391681
warnings.warn("Extrapolation of inputs only supported if the individual FMUs support interpolation of inputs.")
16401682
options["extrapolation_order"] = 0
16411683

1642-
uses_step_size_downsampling = set(self.step_size_downsampling_factor.values()) != {1}
1643-
if uses_step_size_downsampling and options["extrapolation_order"] > 0:
1684+
if self._uses_step_size_downsampling and options["extrapolation_order"] > 0:
16441685
raise FMUException("Use of 'step_size_downsampling_factor' with 'extrapolation_order' > 0 not supported.")
1645-
if uses_step_size_downsampling and self.linear_correction:
1686+
if self._uses_step_size_downsampling and self.linear_correction:
16461687
raise FMUException("Use of 'step_size_downsampling_factor' with 'linear_correction' not supported.")
16471688

16481689
if options["num_threads"] and options["execution"] == "parallel":

‎tests/test_fmi_master.py‎

Lines changed: 185 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
import warnings
2222
import re
2323
import scipy.sparse as sps
24+
import dataclasses
25+
from typing import List
2426
from pathlib import Path
2527

2628
from pyfmi import Master
@@ -618,6 +620,7 @@ def test_with_error_control(self):
618620
msg = "Step-size downsampling not supported for error controlled simulation, no downsampling will be performed."
619621
with pytest.warns(UserWarning, match = re.escape(msg)):
620622
self.master.simulate(options = opts)
623+
assert not self.master._uses_step_size_downsampling
621624

622625
def test_extrapolation_order(self):
623626
"""Test combination with the 'extrapolation_order' option."""
@@ -635,6 +638,7 @@ def test_extrapolation_order(self):
635638
err_msg = "Use of 'step_size_downsampling_factor' with 'extrapolation_order' > 0 not supported."
636639
with pytest.raises(FMUException, match = re.escape(err_msg)):
637640
master.simulate(options = opts)
641+
assert not self.master._uses_step_size_downsampling
638642

639643
def test_linear_correction(self):
640644
"""Test combination with the 'linear_correction' option."""
@@ -652,6 +656,7 @@ def test_linear_correction(self):
652656
err_msg = "Use of 'step_size_downsampling_factor' with 'linear_correction' not supported."
653657
with pytest.raises(FMUException, match = re.escape(err_msg)):
654658
master.simulate(options = opts)
659+
assert not self.master._uses_step_size_downsampling
655660

656661
def test_check_invalid_option_input_non_dict_via_simulate_options(self):
657662
"""Test invalid inputs to the option, not a dictionary."""
@@ -694,44 +699,50 @@ def test_partial_input(self):
694699
opts["step_size_downsampling_factor"] = {self.fmu1: 2}
695700
opts["step_size"] = 0.25
696701
self.master.simulate(options = opts)
702+
assert self.master._uses_step_size_downsampling
697703

698704
@pytest.mark.parametrize("input_var_name, output_var_name",
699705
[
700706
("Float64_continuous_input", "Float64_continuous_output"),
701707
("Float64_discrete_input", "Float64_discrete_output"),
702708
]
703709
)
704-
@pytest.mark.parametrize("rate1, rate2, expected_res1, expected_res2",
710+
@pytest.mark.parametrize("rate1, rate2, expected_res1, expected_res2, uses",
705711
[
706712
# Sanity check
707713
(1, 1,
708714
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11],
709715
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11],
716+
False,
710717
),
711718
# external input only sampled every other step
712719
(2, 1,
713720
[1, 1, 3, 3, 5, 5, 7, 7, 9, 9, 11],
714-
[1, 1, 3, 3, 5, 5, 7, 7, 9, 9, 11]
721+
[1, 1, 3, 3, 5, 5, 7, 7, 9, 9, 11],
722+
True,
715723
),
716724
# connection only updated every other step
717725
(1, 2,
718726
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11],
719-
[1, 1, 3, 3, 5, 5, 7, 7, 9, 9, 11]
727+
[1, 1, 3, 3, 5, 5, 7, 7, 9, 9, 11],
728+
True,
720729
),
721730
# non-aligned test 1
722731
(2, 3,
723732
[1, 1, 3, 3, 5, 5, 7, 7, 9, 9, 11],
724-
[1, 1, 1, 3, 3, 3, 7, 7, 7, 9, 9]
733+
[1, 1, 1, 3, 3, 3, 7, 7, 7, 9, 9],
734+
True,
725735
),
726736
# non-aligned test 2
727737
(3, 2,
728738
[1, 1, 1, 4, 4, 4, 7, 7, 7, 10, 10],
729-
[1, 1, 1, 1, 4, 4, 7, 7, 7, 7, 10]
739+
[1, 1, 1, 1, 4, 4, 7, 7, 7, 7, 10],
740+
True,
730741
),
731742
]
732743
)
733744
def test_two_feedthrough_system(self, input_var_name, output_var_name,
734-
rate1, rate2, expected_res1, expected_res2):
745+
rate1, rate2, expected_res1, expected_res2, uses):
735746
"""Test 'step_size_downsampling_factor' for 2 Feedthrough models + 1 external input."""
736747
t_start, t_final = 0, 10
737748
opts = self.master.simulate_options()
@@ -747,6 +758,7 @@ def test_two_feedthrough_system(self, input_var_name, output_var_name,
747758
res = self.master.simulate(t_start, t_final, options = opts, input = input_object)
748759
np.testing.assert_array_equal(res[0][output_var_name], expected_res1)
749760
np.testing.assert_array_equal(res[1][output_var_name], expected_res2)
761+
assert self.master._uses_step_size_downsampling == uses
750762

751763
@pytest.mark.parametrize("input_var_name, output_var_name",
752764
[
@@ -779,6 +791,7 @@ def test_three_feedthrough_system(self, input_var_name, output_var_name):
779791
]
780792

781793
res = master.simulate(t_start, t_final, options = opts, input = input_object)
794+
assert self.master._uses_step_size_downsampling
782795
expected_res1 = np.array([1, 1, 3, 3, 5, 5, 7, 7, 9, 9, 11])
783796
expected_res2 = np.array([1, 1, 1, 3, 3, 3, 7, 7, 7, 9, 9])
784797
expected_res3 = np.array([1, 1, 1, 1, 5, 5, 5, 5, 9, 9, 9])
@@ -801,6 +814,7 @@ def test_with_store_step_before_update(self):
801814
]
802815

803816
res = self.master.simulate(t_start, t_final, options = opts, input = input_object)
817+
assert self.master._uses_step_size_downsampling
804818
np.testing.assert_array_equal(
805819
res[0]["Float64_continuous_output"],
806820
# [1, 1, 1, X, 4, 4, 4, X, 7, 7, 7, X, 10, last = 10], # X = before update value
@@ -843,3 +857,168 @@ def compute_global_D(self):
843857
"Actual values may no longer be sensible."
844858
with pytest.warns(UserWarning, match = re.escape(msg)):
845859
master.simulate(t_start, t_final, options = opts)
860+
assert self.master._uses_step_size_downsampling
861+
862+
863+
class FMUModelCS2DoStepLogging(FMUModelCS2):
864+
def __init__(self, *args, **kwargs):
865+
super().__init__(*args, **kwargs)
866+
self.do_step_history = []
867+
868+
def do_step(self, current_t, step_size, new_step = True):
869+
self.do_step_history.append(step_size)
870+
return super().do_step(current_t, step_size, new_step)
871+
872+
873+
@dataclasses.dataclass
874+
class SerialDownsamplingTestCase():
875+
# inputs
876+
downsampling_rate_1: int
877+
downsampling_rate_2: int
878+
# expected outputs
879+
uses_downsampling: bool
880+
expected_outputs_1: List[float]
881+
expected_outputs_2: List[float]
882+
expected_stepsizes_1: List[float]
883+
expected_stepsizes_2: List[float]
884+
885+
886+
class Test_Master_serial_downsampling():
887+
@pytest.mark.parametrize("test_case", [
888+
SerialDownsamplingTestCase( # trivial case
889+
downsampling_rate_1 = 1,
890+
downsampling_rate_2 = 1,
891+
uses_downsampling = False,
892+
expected_outputs_1 = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11],
893+
expected_outputs_2 = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11],
894+
expected_stepsizes_1 = [1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
895+
expected_stepsizes_2 = [1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
896+
),
897+
SerialDownsamplingTestCase( # external input only sampled every other step
898+
downsampling_rate_1 = 2,
899+
downsampling_rate_2 = 1,
900+
uses_downsampling = True,
901+
expected_outputs_1 = [1, 1, 3, 3, 5, 5, 7, 7, 9, 9, 11],
902+
expected_outputs_2 = [1, 1, 3, 3, 5, 5, 7, 7, 9, 9, 11],
903+
expected_stepsizes_1 = [2, 2, 2, 2, 2],
904+
expected_stepsizes_2 = [1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
905+
),
906+
SerialDownsamplingTestCase( # connection only updated every other step
907+
downsampling_rate_1 = 1,
908+
downsampling_rate_2 = 2,
909+
uses_downsampling = True,
910+
expected_outputs_1 = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11],
911+
expected_outputs_2 = [1, 1, 3, 3, 5, 5, 7, 7, 9, 9, 11],
912+
expected_stepsizes_1 = [1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
913+
expected_stepsizes_2 = [2, 2, 2, 2, 2],
914+
),
915+
SerialDownsamplingTestCase( # non-aligned test 1
916+
downsampling_rate_1 = 2,
917+
downsampling_rate_2 = 3,
918+
uses_downsampling = True,
919+
expected_outputs_1 = [1, 1, 3, 3, 5, 5, 7, 7, 9, 9, 11],
920+
expected_outputs_2 = [1, 1, 1, 3, 3, 3, 7, 7, 7, 9, 9],
921+
expected_stepsizes_1 = [2, 2, 2, 2, 2],
922+
expected_stepsizes_2 = [3, 3, 3, 1],
923+
),
924+
SerialDownsamplingTestCase( # non-aligned test 2
925+
downsampling_rate_1 = 3,
926+
downsampling_rate_2 = 2,
927+
uses_downsampling = True,
928+
expected_outputs_1 = [1, 1, 1, 4, 4, 4, 7, 7, 7, 10, 10],
929+
expected_outputs_2 = [1, 1, 1, 1, 4, 4, 7, 7, 7, 7, 10],
930+
expected_stepsizes_1 = [3, 3, 3, 1],
931+
expected_stepsizes_2 = [2, 2, 2, 2, 2],
932+
),
933+
])
934+
def test_serial_downsampling(self, test_case: SerialDownsamplingTestCase):
935+
"""Test the warning one gets when using 'logging' + 'step_size_downsampling_factor'."""
936+
fmu1 = FMUModelCS2DoStepLogging(os.path.join(FMI2_REF_FMU_PATH, "Feedthrough.fmu"))
937+
fmu2 = FMUModelCS2DoStepLogging(os.path.join(FMI2_REF_FMU_PATH, "Feedthrough.fmu"))
938+
939+
input_variable_name = "Float64_continuous_input"
940+
output_variable_name = "Float64_continuous_output"
941+
942+
models = [fmu1, fmu2]
943+
connections = [
944+
(fmu1, output_variable_name,
945+
fmu2, input_variable_name),
946+
]
947+
master = Master(models, connections)
948+
949+
t_start, t_final = 0, 10
950+
opts = master.simulate_options()
951+
opts["step_size"] = 1
952+
opts["step_size_downsampling_factor"] = {
953+
fmu1: test_case.downsampling_rate_1,
954+
fmu2: test_case.downsampling_rate_2
955+
}
956+
opts["execution"] = "serial"
957+
opts["_experimental_serial_downsampling"] = True
958+
959+
# Generate input
960+
input_object = [
961+
[(fmu1, input_variable_name)],
962+
lambda t: [t + 1] # not starting at zero to test correct values taken with initialization
963+
]
964+
965+
res = master.simulate(t_start, t_final, options = opts, input = input_object)
966+
967+
assert master._uses_step_size_downsampling == test_case.uses_downsampling
968+
# verify output values
969+
np.testing.assert_array_equal(
970+
res[0][output_variable_name],
971+
test_case.expected_outputs_1)
972+
np.testing.assert_array_equal(
973+
res[1][output_variable_name],
974+
test_case.expected_outputs_2)
975+
976+
# verify used step sizes
977+
np.testing.assert_array_equal(
978+
fmu1.do_step_history,
979+
test_case.expected_stepsizes_1)
980+
np.testing.assert_array_equal(
981+
fmu2.do_step_history,
982+
test_case.expected_stepsizes_2)
983+
984+
@pytest.mark.parametrize("factor", [1, 2, 5, 10])
985+
def test_rescale_step_size_and_downsampling(self, factor):
986+
"""Test that rescaling both the step-size and downsampling gives identical results."""
987+
fmu1 = FMUModelCS2DoStepLogging(os.path.join(FMI2_REF_FMU_PATH, "Feedthrough.fmu"))
988+
fmu2 = FMUModelCS2DoStepLogging(os.path.join(FMI2_REF_FMU_PATH, "Feedthrough.fmu"))
989+
990+
input_variable_name = "Float64_continuous_input"
991+
output_variable_name = "Float64_continuous_output"
992+
993+
models = [fmu1, fmu2]
994+
connections = [
995+
(fmu1, output_variable_name,
996+
fmu2, input_variable_name),
997+
]
998+
master = Master(models, connections)
999+
1000+
t_start, t_final = 0, 10
1001+
opts = master.simulate_options()
1002+
opts["step_size"] = 1/factor
1003+
ds_1, ds_2 = 2*factor, 3*factor
1004+
opts["step_size_downsampling_factor"] = {fmu1: ds_1, fmu2: ds_2}
1005+
opts["execution"] = "serial"
1006+
opts["_experimental_serial_downsampling"] = True
1007+
1008+
# Generate input
1009+
input_object = [
1010+
[(fmu1, input_variable_name)],
1011+
lambda t: [t + 1] # not starting at zero to test correct values taken with initialization
1012+
]
1013+
1014+
master.simulate(t_start, t_final, options = opts, input = input_object)
1015+
1016+
assert master._uses_step_size_downsampling
1017+
# verify used step sizes
1018+
np.testing.assert_array_equal(
1019+
fmu1.do_step_history,
1020+
[2, 2, 2, 2, 2])
1021+
np.testing.assert_array_equal(
1022+
fmu2.do_step_history,
1023+
[3, 3, 3, 1])
1024+

0 commit comments

Comments
 (0)