2121import warnings
2222import re
2323import scipy .sparse as sps
24+ import dataclasses
25+ from typing import List
2426from pathlib import Path
2527
2628from 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