From db2932ffd526d455d4d38dcdd5fba7f46945981e Mon Sep 17 00:00:00 2001 From: Eneko Martin-Martinez Date: Sat, 23 Dec 2023 14:05:16 +0100 Subject: [PATCH] Include warnings in set_components --- docs/whats_new.rst | 4 +- pysd/py_backend/model.py | 24 +++++ tests/pytest_pysd/pytest_pysd.py | 91 +++++++++++++------ tests/pytest_pysd/pytest_select_submodel.py | 5 +- .../data/pytest_data_with_model.py | 9 +- 5 files changed, 101 insertions(+), 32 deletions(-) diff --git a/docs/whats_new.rst b/docs/whats_new.rst index f1078273..04f20e71 100644 --- a/docs/whats_new.rst +++ b/docs/whats_new.rst @@ -21,13 +21,15 @@ Documentation Performance ~~~~~~~~~~~ -- Improved performace of :py:class:`pysd.py_backend.output.DataFrameHandler` by creating the dataframe at the end of the run (:issue:`374`). (`@easyas314159 `_ and `@enekomartinmartinez `_) +- Improved performace of :py:class:`pysd.py_backend.output.DataFrameHandler` by creating the dataframe at the end of the run (:issue:`374` and :issue:`330`). (`@easyas314159 `_ and `@enekomartinmartinez `_) Internal Changes ~~~~~~~~~~~~~~~~ - Move old :py:meth:`pysd.py_backend.model.Macro.set_components` to :py:meth:`pysd.py_backend.model.Macro._set_components`, and create new method with the same name without the `new` argument. (`@enekomartinmartinez `_) - Move old :py:meth:`pysd.py_backend.model.Macro.set_stateful` to :py:meth:`pysd.py_backend.model.Macro._set_stateful`. (`@enekomartinmartinez `_) - Make integration tests filter only specific warnings. (`@enekomartinmartinez `_) +- Include warnings in :py:meth:`pysd.py_backend.model.Macro.set_components` when changing the behaviour of the component (:issue:`58`). (`@enekomartinmartinez `_) + v3.12.0 (2023/10/02) -------------------- diff --git a/pysd/py_backend/model.py b/pysd/py_backend/model.py index 32bbdbb6..ae0a19b7 100644 --- a/pysd/py_backend/model.py +++ b/pysd/py_backend/model.py @@ -1129,6 +1129,10 @@ def _set_components(self, params, new): obj ).set_values(value) + if not isinstance(value, pd.Series): + warnings.warn( + "Replacing interpolation data with constant values.") + # Update dependencies if func_type == "Data": if isinstance(value, pd.Series): @@ -1140,11 +1144,29 @@ def _set_components(self, params, new): continue + if func_type == "Stateful": + warnings.warn( + "Replacing the value of Stateful variable with " + "an expression. To set initial conditions use " + "`set_initial_condition` instead..." + ) + if isinstance(value, pd.Series): + if func_type == "Constant": + warnings.warn( + "Replacing a constant value with a " + "time-dependent value. The value will be " + "interpolated over time." + ) new_function, deps = self._timeseries_component( value, dims) self._dependencies[func_name] = deps elif callable(value): + if func_type == "Constant": + warnings.warn( + "Replacing a constant value with a callable. " + "The value may not be constant anymore." + ) new_function = value # Using step cache adding time as dependency # TODO it would be better if we can parse the content @@ -1152,6 +1174,8 @@ def _set_components(self, params, new): self._dependencies[func_name] = {"time": 1} else: + if func_type != "Constant": + warnings.warn("Replacing a variable by a constant value.") new_function = self._constant_component(value, dims) self._dependencies[func_name] = {} diff --git a/tests/pytest_pysd/pytest_pysd.py b/tests/pytest_pysd/pytest_pysd.py index f68a76ac..d5cddea8 100644 --- a/tests/pytest_pysd/pytest_pysd.py +++ b/tests/pytest_pysd/pytest_pysd.py @@ -309,11 +309,14 @@ def test_set_timeseries_parameter(self, model): index=timeseries, data=(50 + np.random.rand(len(timeseries)).cumsum()) ) - res = model.run( - params={"room_temperature": temp_timeseries}, - return_columns=["room_temperature"], - return_timestamps=timeseries, - ) + warn_message = "Replacing a constant value with a time-dependent "\ + "value. The value will be interpolated over time." + with pytest.warns(UserWarning, match=warn_message): + res = model.run( + params={"room_temperature": temp_timeseries}, + return_columns=["room_temperature"], + return_timestamps=timeseries, + ) assert (res["room_temperature"] == temp_timeseries).all() @pytest.mark.parametrize("model_path", [test_model]) @@ -323,7 +326,11 @@ def test_set_timeseries_parameter_inline(self, model): index=timeseries, data=(50 + np.random.rand(len(timeseries)).cumsum()) ) - model.components.room_temperature = temp_timeseries + warn_message = "Replacing a constant value with a time-dependent "\ + "value. The value will be interpolated over time." + with pytest.warns(UserWarning, match=warn_message): + model.components.room_temperature = temp_timeseries + res = model.run( return_columns=["room_temperature"], return_timestamps=timeseries, @@ -352,8 +359,10 @@ def test_set_components_warnings(self, model): def test_set_components_with_function(self, model): def test_func(): return 5 - - model.set_components({"Room Temperature": test_func}) + warn_message = "Replacing a constant value with a callable. "\ + "The value may not be constant anymore." + with pytest.warns(UserWarning, match=warn_message): + model.set_components({"Room Temperature": test_func}) res = model.run(return_columns=["Room Temperature"]) assert test_func() == res["Room Temperature"].iloc[0] @@ -549,12 +558,15 @@ def test_set_subscripted_timeseries_parameter_with_constant(self, model): xr_series = [xr.DataArray(val, coords, dims) for val in val_series] temp_timeseries = pd.Series(index=timeseries, data=val_series) - res = model.run( - params={"initial_values": temp_timeseries, "final_time": 10}, - return_columns=["initial_values"], - return_timestamps=timeseries, - flatten_output=False - ) + warn_message = "Replacing a constant value with a time-dependent "\ + "value. The value will be interpolated over time." + with pytest.warns(UserWarning, match=warn_message): + res = model.run( + params={"initial_values": temp_timeseries, "final_time": 10}, + return_columns=["initial_values"], + return_timestamps=timeseries, + flatten_output=False + ) assert np.all( [ @@ -583,8 +595,11 @@ def test_set_subscripted_timeseries_parameter_with_partial_xarray(self, ).cumsum()] temp_timeseries = pd.Series(index=timeseries, data=val_series) out_series = [out_b + val for val in val_series] - model.set_components({"initial_values": temp_timeseries, - "final_time": 10}) + warn_message = "Replacing a constant value with a time-dependent "\ + "value. The value will be interpolated over time." + with pytest.warns(UserWarning, match=warn_message): + model.set_components({"initial_values": temp_timeseries, + "final_time": 10}) res = model.run( return_columns=["initial_values"], flatten_output=False) assert np.all( @@ -610,12 +625,15 @@ def test_set_subscripted_timeseries_parameter_with_xarray(self, model): data=[init_val + rd for rd in np.random.rand(len(timeseries) ).cumsum()], ) - res = model.run( - params={"initial_values": temp_timeseries, "final_time": 10}, - return_columns=["initial_values"], - return_timestamps=timeseries, - flatten_output=False - ) + warn_message = "Replacing a constant value with a time-dependent "\ + "value. The value will be interpolated over time." + with pytest.warns(UserWarning, match=warn_message): + res = model.run( + params={"initial_values": temp_timeseries, "final_time": 10}, + return_columns=["initial_values"], + return_timestamps=timeseries, + flatten_output=False + ) assert np.all( [ @@ -947,10 +965,24 @@ def test_set_initial_value_subscripted_value_with_numpy_error(self, model): with pytest.raises(TypeError): model.set_initial_value(new_time + 2, {'_integ_stock_a': input3}) + @pytest.mark.parametrize("model_path", [test_model]) + def test_replace_stateful(self, model): + warn_message = "Replacing the value of Stateful variable with "\ + "an expression. To set initial conditions use "\ + "`set_initial_condition` instead..." + with pytest.warns(UserWarning, match=warn_message): + model.components.teacup_temperature = 3 + + stocks = model.run() + assert np.all(stocks["Teacup Temperature"] == 3) + @pytest.mark.parametrize("model_path", [test_model]) def test_replace_element(self, model): stocks1 = model.run() - model.components.characteristic_time = lambda: 3 + warn_message = "Replacing a constant value with a callable. "\ + "The value may not be constant anymore." + with pytest.warns(UserWarning, match=warn_message): + model.components.characteristic_time = lambda: 3 stocks2 = model.run() assert stocks1["Teacup Temperature"].loc[10]\ > stocks2["Teacup Temperature"].loc[10] @@ -1341,7 +1373,10 @@ def test_no_crosstalk(self, _root): model_2 = pysd.read_vensim( _root.joinpath("test-models/samples/SIR/SIR.mdl")) - model_1.components.initial_time = lambda: 10 + warn_message = "Replacing a constant value with a callable. "\ + "The value may not be constant anymore." + with pytest.warns(UserWarning, match=warn_message): + model_1.components.initial_time = lambda: 10 assert model_2.components.initial_time != 10 # check that the model time is not shared between the two objects @@ -1480,9 +1515,13 @@ def test_change_constant_pipe(self, model): # we should ensure that the constant_cache is removed # when passing new param - out2 = model.run(params={"constant1": new_var}) + warn_message = "Replacing a constant value with a "\ + "time-dependent value. The value will "\ + "be interpolated over time." + with pytest.warns(UserWarning, match=warn_message): + out2 = model.run(params={"constant1": new_var}) - assert not np.all(out1 - out2 == 0) + assert not np.array_equal(out1, out2) for key in pipe: assert model.cache_type[key] == "step" diff --git a/tests/pytest_pysd/pytest_select_submodel.py b/tests/pytest_pysd/pytest_select_submodel.py index 470fa89d..5001f859 100644 --- a/tests/pytest_pysd/pytest_select_submodel.py +++ b/tests/pytest_pysd/pytest_select_submodel.py @@ -177,7 +177,10 @@ def test_select_submodel(self, model, variables, modules, assert var in str(record[-1].message) assert np.any(np.isnan(model.run())) # redefine dependencies - assert not np.any(np.isnan(model.run(params=dep_vars))) + warn_message = "Replacing a variable by a constant value." + with pytest.warns(UserWarning, match=warn_message): + out = model.run(params=dep_vars) + assert not np.any(np.isnan(out)) # select submodel using contour values model.reload() diff --git a/tests/pytest_types/data/pytest_data_with_model.py b/tests/pytest_types/data/pytest_data_with_model.py index 7f5e8cc6..9f3c02be 100644 --- a/tests/pytest_types/data/pytest_data_with_model.py +++ b/tests/pytest_types/data/pytest_data_with_model.py @@ -120,10 +120,11 @@ def test_get_data_and_run(self, model, expected): expected) def test_modify_data(self, model, expected): - out = model.run(params={ - "var1": pd.Series(index=[1, 3, 7], data=[10, 20, 30]), - "var2": 10 - }) + with pytest.warns(UserWarning, match="Replacing .*"): + out = model.run(params={ + "var1": pd.Series(index=[1, 3, 7], data=[10, 20, 30]), + "var2": 10 + }) assert (out["var2"] == 10).all() assert (