diff --git a/docs/sphinx/source/changelog/pending.rst b/docs/sphinx/source/changelog/pending.rst index ac91954f..3341e4d6 100644 --- a/docs/sphinx/source/changelog/pending.rst +++ b/docs/sphinx/source/changelog/pending.rst @@ -29,6 +29,16 @@ Requirements * Updated pytz version in requirements.txt from 2024.1 to 2025.2 for python 3.13 compatibility. +Enhancements +------------ +* Modified ``TrendAnalysis._filter()`` to allow ``clip_filter`` to use ``pv_energy`` + when ``pv_power`` is not available. This enables clipping detection for energy-based + analyses with sub-hourly data. +* Added frequency validation for ``clip_filter`` in ``TrendAnalysis._filter()`` that + raises a ``ValueError`` if the time series has a median time step greater than 60 + minutes, as clipping detection requires higher resolution data. + + Warnings -------- * Added filter to ignore deprecation warning related to IPyNbFile in setup.cfg. \ No newline at end of file diff --git a/rdtools/analysis_chains.py b/rdtools/analysis_chains.py index 9492badf..77c5c323 100644 --- a/rdtools/analysis_chains.py +++ b/rdtools/analysis_chains.py @@ -571,15 +571,17 @@ def _call_clearsky_filter(filter_string): f = filtering.tcell_filter(cell_temp, **self.filter_params["tcell_filter"]) filter_components["tcell_filter"] = f if "clip_filter" in self.filter_params: - if self.pv_power is None: - raise ValueError( - "PV power (not energy) is required for the clipping filter. " - "Either omit the clipping filter, provide PV power at " - "instantiation, or explicitly assign TrendAnalysis.pv_power." - ) - f = filtering.clip_filter( - self.pv_power, **self.filter_params["clip_filter"] - ) + # Check that the time series frequency is 60 minutes or less + clip_data = self.pv_power if self.pv_power is not None else self.pv_energy + if clip_data is not None and len(clip_data) > 1: + median_freq = pd.Series(clip_data.index).diff().median() + if median_freq > pd.Timedelta(minutes=60): + raise ValueError( + f"clip_filter requires time series frequency of 60 minutes or less. " + f"Median time step is {median_freq}." + ) + + f = filtering.clip_filter(clip_data, **self.filter_params["clip_filter"]) filter_components["clip_filter"] = f if "hour_angle_filter" in self.filter_params: if not hasattr(self, "pvlib_location"): diff --git a/rdtools/test/analysis_chains_test.py b/rdtools/test/analysis_chains_test.py index 78376bbe..85433c78 100644 --- a/rdtools/test/analysis_chains_test.py +++ b/rdtools/test/analysis_chains_test.py @@ -768,6 +768,8 @@ def soiling_parameters(basic_parameters, soiling_normalized_daily, cs_input): def soiling_analysis_sensor(soiling_parameters): soiling_analysis = TrendAnalysis(**soiling_parameters) np.random.seed(1977) + # Disable clip_filter for daily data (doesn't apply to aggregated data) + del soiling_analysis.filter_params["clip_filter"] soiling_analysis.sensor_analysis(analyses=["srr_soiling"], srr_kwargs={"reps": 10}) return soiling_analysis @@ -778,6 +780,8 @@ def soiling_analysis_clearsky(soiling_parameters, cs_input): soiling_analysis.set_clearsky(**cs_input) np.random.seed(1977) soiling_analysis.filter_params["clearsky_filter"] = {"model": "csi"} + # Disable clip_filter for daily data (doesn't apply to aggregated data) + del soiling_analysis.filter_params["clip_filter"] with pytest.warns(UserWarning, match="20% or more of the daily data"): soiling_analysis.clearsky_analysis( analyses=["srr_soiling"], srr_kwargs={"reps": 10} @@ -791,15 +795,15 @@ def test_srr_soiling(soiling_analysis_sensor): ci = srr_results["sratio_confidence_interval"] renorm_factor = srr_results["calc_info"]["renormalizing_factor"] print(f"soiling ci:{ci}") - assert 0.965 == pytest.approx( + assert 0.967 == pytest.approx( sratio, abs=1e-3 ), "Soiling ratio different from expected value in TrendAnalysis.srr_soiling" - assert [0.96, 0.97] == pytest.approx( + assert [0.966, 0.968] == pytest.approx( ci, abs=1e-2 ), "Soiling confidence interval different from expected value in TrendAnalysis.srr_soiling" assert pytest.approx( renorm_factor, abs=1e-3 - ) == 0.977, "Renormalization factor different from expected value in TrendAnalysis.srr_soiling" + ) == 0.982, "Renormalization factor different from expected value in TrendAnalysis.srr_soiling" def test_plot_degradation(sensor_analysis): @@ -865,6 +869,27 @@ def test_errors(sensor_parameters, clearsky_analysis): clearsky_analysis._clearsky_preprocess() +def test_clip_filter_frequency_error(basic_parameters): + # Test that clip_filter raises an error when data frequency > 60 minutes + times = pd.date_range("2019-01-01", "2022-01-01", freq="2h", tz="UTC") + pv = pd.Series(1.0, index=times) + poa_global = pd.Series(1000.0, index=times) + temperature_ambient = pd.Series(25.0, index=times) + + rd_analysis = TrendAnalysis( + pv, + poa_global=poa_global, + temperature_ambient=temperature_ambient, + pv_input="energy", + **basic_parameters, + ) + rd_analysis.filter_params = {"clip_filter": {}} + with pytest.raises( + ValueError, match="clip_filter requires time series frequency of 60 minutes" + ): + rd_analysis.sensor_analysis(analyses=["yoy_degradation"]) + + @pytest.mark.parametrize( "method_name", [