From 3782adf10147913765a2562cae1b41cdf40a7b7e Mon Sep 17 00:00:00 2001 From: BaptisteDE Date: Mon, 6 Oct 2025 17:13:06 +0200 Subject: [PATCH 1/4] =?UTF-8?q?=E2=9C=A8=20Sample=20=5F=5Frepr=5F=5F=20met?= =?UTF-8?q?hod?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- corrai/sampling.py | 20 ++++++++++++++++++++ tests/test_sampling.py | 15 +++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/corrai/sampling.py b/corrai/sampling.py index 4f1c66f..ae4093d 100644 --- a/corrai/sampling.py +++ b/corrai/sampling.py @@ -133,6 +133,26 @@ class Sample: values: pd.DataFrame = field(init=False) results: pd.Series = field(default_factory=lambda: pd.Series(dtype=object)) + def __repr__(self): + if not self.results.empty: + if not self.results[0].empty: + indicators_name = ( + self.results[0].columns + if self.is_dynamic + else self.results[0].index + ) + else: + indicators_name = None + else: + indicators_name = None + + return ( + f"is dynamic: {self.is_dynamic} \n" + f"n computed sample: {len(self.results)} \n" + f"parameters: {[par.name for par in self.parameters]} \n" + f"indicators: {[None] if indicators_name is None else list(indicators_name)}" + ) + def __post_init__(self): self.values = pd.DataFrame(columns=[par.name for par in self.parameters]) diff --git a/tests/test_sampling.py b/tests/test_sampling.py index 96ea613..787e245 100644 --- a/tests/test_sampling.py +++ b/tests/test_sampling.py @@ -48,6 +48,14 @@ class TestSample: def test_sample_methods(self): # Dynamic sample sample = Sample(REAL_PARAM) + + assert sample.__repr__() == ( + "is dynamic: True \n" + "n computed sample: 0 \n" + "parameters: ['param_1', 'param_2', 'param_3'] \n" + "indicators: [None]" + ) + assert sample.values.shape == (0, 3) pd.testing.assert_series_equal(sample.results, pd.Series()) @@ -168,6 +176,13 @@ def test_sample_methods(self): pd.DataFrame({"res": {0: np.nan, 1: 2.0}}), ) + assert sample.__repr__() == ( + "is dynamic: True \n" + "n computed sample: 2 \n" + "parameters: ['param_1', 'param_2', 'param_3'] \n" + "indicators: ['res']" + ) + def test_plot_sample(self): t = pd.date_range("2025-01-01 00:00:00", periods=2, freq="h") df1 = pd.DataFrame({"res": [1.0, 2.0]}, index=t) From 9b3389907c41bfd381036f9ea07a3fe3a02c2732 Mon Sep 17 00:00:00 2001 From: BaptisteDE Date: Mon, 6 Oct 2025 17:19:01 +0200 Subject: [PATCH 2/4] =?UTF-8?q?=F0=9F=92=A5=20renamed=20get=5Fscore=5Fdf?= =?UTF-8?q?=20method=20argument?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- corrai/sampling.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/corrai/sampling.py b/corrai/sampling.py index ae4093d..f61c745 100644 --- a/corrai/sampling.py +++ b/corrai/sampling.py @@ -446,7 +446,7 @@ def get_score_df( reference_time_series: pd.Series, scoring_methods: list[str | Callable] = None, resample_rule: str | pd.Timedelta | dt.timedelta = None, - agg_method: str = "mean", + resample_agg_method: str = "mean", ) -> pd.DataFrame: """ Compute scoring metrics for a given indicator across all sample results. @@ -476,7 +476,7 @@ def get_score_df( Examples: ``"D"`` (daily), ``"h"`` (hourly), ``"ME"`` (month end). If None, no resampling is performed. Default is None. - agg_method : str, optional + resample_agg_method : str, optional Aggregation method to use when resampling. Common values include: ``"mean"``, ``"sum"``, ``"min"``, ``"max"``, ``"median"``. Default is ``"mean"``. @@ -539,7 +539,7 @@ def get_score_df( ... reference_time_series=reference, ... scoring_methods=["r2", "rmse", "mae"], ... resample_rule="D", - ... agg_method="sum", + ... resample_agg_method="sum", ... ) >>> print(scores) r2_score rmse mae @@ -573,10 +573,10 @@ def get_score_df( for idx, sample_res in self.results.items(): data = sample_res[indicator] if resample_rule: - data = data.resample(resample_rule).agg(agg_method) + data = data.resample(resample_rule).agg(resample_agg_method) reference_time_series = reference_time_series.resample( resample_rule - ).agg(agg_method) + ).agg(resample_agg_method) for method in method_func: scores.loc[idx, method.__name__] = method(reference_time_series, data) From ce876654bd653a97ba9285f6fdd8bc080365021b Mon Sep 17 00:00:00 2001 From: BaptisteDE Date: Tue, 7 Oct 2025 15:03:54 +0200 Subject: [PATCH 3/4] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20SampleMethodsMixin=20t?= =?UTF-8?q?o=20carry=20Sample=20method?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- corrai/optimize.py | 83 ++--------------------- corrai/sampling.py | 151 ++++++++++++++++++++++++++---------------- corrai/sensitivity.py | 101 ++-------------------------- 3 files changed, 105 insertions(+), 230 deletions(-) diff --git a/corrai/optimize.py b/corrai/optimize.py index 1b9a0ce..9674dd9 100644 --- a/corrai/optimize.py +++ b/corrai/optimize.py @@ -1,6 +1,5 @@ from abc import ABC from typing import Callable -from functools import wraps import numpy as np import pandas as pd @@ -10,12 +9,10 @@ from pymoo.core.problem import ElementwiseProblem from pymoo.core.variable import Integer, Real, Choice, Binary -import plotly.graph_objects as go - from corrai.base.math import METHODS from corrai.base.model import Model from corrai.base.utils import check_indicators_configs -from corrai.sampling import Sample +from corrai.sampling import Sample, SampleMethodsMixin from corrai.base.parameter import Parameter @@ -577,7 +574,7 @@ def _evaluate(self, x, out, *args, **kwargs): self._post_evaluate(pairs, out) -class SciOptimizer: +class SciOptimizer(SampleMethodsMixin): """ Optimization wrapper for models using SciPy. @@ -635,6 +632,10 @@ def __init__( def parameters(self): return self.model_evaluator.parameters + @property + def sample(self): + return self.model_evaluator.sample + @property def values(self): return self.model_evaluator.sample.values @@ -787,75 +788,3 @@ def diff_evo_minimize( rng=rng, workers=workers, ) - - @wraps(Sample.plot_sample) - def plot_sample( - self, - indicator: str | None, - reference_timeseries: pd.Series | None = None, - title: str | None = None, - y_label: str | None = None, - x_label: str | None = None, - alpha: float = 0.5, - show_legends: bool = False, - round_ndigits: int = 2, - quantile_band: float = 0.75, - type_graph: str = "area", - ) -> go.Figure: - return self.model_evaluator.sample.plot_sample( - indicator=indicator, - reference_timeseries=reference_timeseries, - title=title, - y_label=y_label, - x_label=x_label, - alpha=alpha, - show_legends=show_legends, - round_ndigits=round_ndigits, - quantile_band=quantile_band, - type_graph=type_graph, - ) - - @wraps(Sample.plot_pcp) - def plot_pcp( - self, - indicators_configs: list[str] - | list[tuple[str, str | Callable] | tuple[str, str | Callable, pd.Series]], - color_by: str | None = None, - title: str | None = "Parallel Coordinates — Samples", - html_file_path: str | None = None, - ) -> go.Figure: - return self.model_evaluator.sample.plot_pcp( - indicators_configs=indicators_configs, - color_by=color_by, - title=title, - html_file_path=html_file_path, - ) - - @wraps(Sample.plot_hist) - def plot_hist( - self, - indicator: str, - method: str = "mean", - unit: str = "", - agg_method_kwarg: dict = None, - reference_time_series: pd.Series = None, - bins: int = 30, - colors: str = "orange", - reference_value: int | float = None, - reference_label: str = "Reference", - show_rug: bool = False, - title: str = None, - ): - return self.model_evaluator.sample.plot_hist( - indicator=indicator, - method=method, - unit=unit, - agg_method_kwarg=agg_method_kwarg, - reference_time_series=reference_time_series, - bins=bins, - colors=colors, - reference_value=reference_value, - reference_label=reference_label, - show_rug=show_rug, - title=title, - ) diff --git a/corrai/sampling.py b/corrai/sampling.py index f61c745..610d08a 100644 --- a/corrai/sampling.py +++ b/corrai/sampling.py @@ -980,7 +980,99 @@ def plot_pcp( ) -class Sampler: +class SampleMethodsMixin: + """Mixin to expose Sample plotting methods to classes that contain a Sample object.""" + + sample: Sample + + @wraps(Sample.get_aggregated_time_series) + def get_sample_aggregated_time_series( + self, + indicator: str, + method: str = "mean", + agg_method_kwarg: dict = None, + reference_time_series: pd.Series = None, + freq: str | pd.Timedelta | dt.timedelta = None, + prefix: str = "aggregated", + ): + return self.sample.get_aggregated_time_series( + indicator, method, agg_method_kwarg, reference_time_series, freq, prefix + ) + + @wraps(Sample.plot_sample) + def plot_sample( + self, + indicator: str | None, + reference_timeseries: pd.Series | None = None, + title: str | None = None, + y_label: str | None = None, + x_label: str | None = None, + alpha: float = 0.5, + show_legends: bool = False, + round_ndigits: int = 2, + quantile_band: float = 0.75, + type_graph: str = "area", + ) -> go.Figure: + return self.sample.plot_sample( + indicator=indicator, + reference_timeseries=reference_timeseries, + title=title, + y_label=y_label, + x_label=x_label, + alpha=alpha, + show_legends=show_legends, + round_ndigits=round_ndigits, + quantile_band=quantile_band, + type_graph=type_graph, + ) + + @wraps(Sample.plot_pcp) + def plot_pcp( + self, + indicators_configs: list[str] + | list[tuple[str, str | Callable] | tuple[str, str | Callable, pd.Series]], + color_by: str | None = None, + title: str | None = "Parallel Coordinates — Samples", + html_file_path: str | None = None, + ) -> go.Figure: + return self.sample.plot_pcp( + indicators_configs=indicators_configs, + color_by=color_by, + title=title, + html_file_path=html_file_path, + ) + + @wraps(Sample.plot_hist) + def plot_hist( + self, + indicator: str, + method: str = "mean", + unit: str = "", + agg_method_kwarg: dict = None, + reference_time_series: pd.Series = None, + bins: int = 30, + colors: str = "orange", + reference_value: int | float = None, + reference_label: str = "Reference", + show_rug: bool = False, + title: str = None, + ): + return self.sample.plot_hist( + indicator, + method, + unit, + agg_method_kwarg, + reference_time_series, + bins, + colors, + reference_value, + reference_label, + show_rug, + title, + ) + + +class Sampler(SampleMethodsMixin): """ Abstract base class for parameter samplers. @@ -1180,63 +1272,6 @@ def simulate_pending(self, n_cpu: int = 1, simulation_kwargs: dict = None): unsimulated_idx = self.sample.get_pending_index() self.simulate_at(unsimulated_idx, n_cpu, simulation_kwargs) - @wraps(Sample.plot_sample) - def plot_sample( - self, - indicator: str | None, - reference_timeseries: pd.Series | None = None, - title: str | None = None, - y_label: str | None = None, - x_label: str | None = None, - alpha: float = 0.5, - show_legends: bool = False, - round_ndigits: int = 2, - quantile_band: float = 0.75, - type_graph: str = "area", - ) -> go.Figure: - return self.sample.plot_sample( - indicator=indicator, - reference_timeseries=reference_timeseries, - title=title, - y_label=y_label, - x_label=x_label, - alpha=alpha, - show_legends=show_legends, - round_ndigits=round_ndigits, - quantile_band=quantile_band, - type_graph=type_graph, - ) - - @wraps(Sample.get_aggregated_time_series) - def get_sample_aggregated_time_series( - self, - indicator: str, - method: str = "mean", - agg_method_kwarg: dict = None, - reference_time_series: pd.Series = None, - freq: str | pd.Timedelta | dt.timedelta = None, - prefix: str = "aggregated", - ): - return self.sample.get_aggregated_time_series( - indicator, method, agg_method_kwarg, reference_time_series, freq, prefix - ) - - @wraps(Sample.plot_pcp) - def plot_pcp( - self, - indicators_configs: list[str] - | list[tuple[str, str | Callable] | tuple[str, str | Callable, pd.Series]], - color_by: str | None = None, - title: str | None = "Parallel Coordinates — Samples", - html_file_path: str | None = None, - ) -> go.Figure: - return self.sample.plot_pcp( - indicators_configs=indicators_configs, - color_by=color_by, - title=title, - html_file_path=html_file_path, - ) - class RealSampler(Sampler): """ diff --git a/corrai/sensitivity.py b/corrai/sensitivity.py index a68d53d..a61ce20 100644 --- a/corrai/sensitivity.py +++ b/corrai/sensitivity.py @@ -1,7 +1,5 @@ import warnings from abc import ABC, abstractmethod -from functools import wraps -from typing import Callable import datetime as dt import numpy as np @@ -10,7 +8,7 @@ from SALib.analyze import morris, sobol, fast, rbd_fast from corrai.base.parameter import Parameter from corrai.sampling import ( - Sample, + SampleMethodsMixin, SobolSampler, MorrisSampler, FASTSampler, @@ -19,7 +17,7 @@ from corrai.base.model import Model -class Sanalysis(ABC): +class Sanalysis(ABC, SampleMethodsMixin): """ Abstract base class for Sensitivity Analysis workflows. @@ -76,6 +74,10 @@ def __init__( self.analyser = self._set_analyser() self.x_needed = x_needed + @property + def sample(self): + return self.sampler.sample + @property def parameters(self): return self.sampler.parameters @@ -323,97 +325,6 @@ def salib_plot_dynamic_metric( return plot_dynamic_metric(metrics, sensitivity_metric, unit, title, stacked) - @wraps(Sample.get_aggregated_time_series) - def get_sample_aggregated_time_series( - self, - indicator: str, - method: str = "mean", - agg_method_kwarg: dict = None, - reference_time_series: pd.Series = None, - freq: str | pd.Timedelta | dt.timedelta = None, - prefix: str = "aggregated", - ) -> pd.DataFrame: - return self.sampler.sample.get_aggregated_time_series( - indicator, - method, - agg_method_kwarg, - reference_time_series, - freq, - prefix, - ) - - @wraps(Sample.plot_hist) - def plot_sample_hist( - self, - indicator: str, - method: str = "mean", - unit: str = "", - agg_method_kwarg: dict = None, - reference_time_series: pd.Series = None, - bins: int = 30, - colors: str = "orange", - reference_value: int | float = None, - reference_label: str = "Reference", - show_rug: bool = False, - title: str = None, - ): - return self.sampler.sample.plot_hist( - indicator=indicator, - method=method, - unit=unit, - agg_method_kwarg=agg_method_kwarg, - reference_time_series=reference_time_series, - bins=bins, - colors=colors, - reference_value=reference_value, - reference_label=reference_label, - show_rug=show_rug, - title=title, - ) - - @wraps(Sample.plot_sample) - def plot_sample( - self, - indicator: str | None, - reference_timeseries: pd.Series | None = None, - title: str | None = None, - y_label: str | None = None, - x_label: str | None = None, - alpha: float = 0.5, - show_legends: bool = False, - round_ndigits: int = 2, - quantile_band: float = 0.75, - type_graph: str = "area", - ) -> go.Figure: - return self.sampler.sample.plot_sample( - indicator=indicator, - reference_timeseries=reference_timeseries, - title=title, - y_label=y_label, - x_label=x_label, - alpha=alpha, - show_legends=show_legends, - round_ndigits=round_ndigits, - quantile_band=quantile_band, - type_graph=type_graph, - ) - - @wraps(Sample.plot_pcp) - def plot_pcp( - self, - indicators_configs: list[str] - | list[tuple[str, str | Callable] | tuple[str, str | Callable, pd.Series]], - color_by: str | None = None, - title: str | None = "Parallel Coordinates — Samples", - html_file_path: str | None = None, - ) -> go.Figure: - return self.sampler.sample.plot_pcp( - indicators_configs=indicators_configs, - color_by=color_by, - title=title, - html_file_path=html_file_path, - ) - def salib_plot_matrix( self, indicator: str, From b95188b2fcb79ef04c6329f411396c68d57ac74b Mon Sep 17 00:00:00 2001 From: BaptisteDE Date: Tue, 7 Oct 2025 16:37:21 +0200 Subject: [PATCH 4/4] =?UTF-8?q?=F0=9F=93=9D=20rtd=20updates=20!?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/api_reference/base/math.rst | 8 ----- docs/api_reference/base/simulate.rst | 6 ---- docs/api_reference/base/utils.rst | 8 ----- docs/api_reference/index.rst | 11 +++---- docs/api_reference/{base => }/metrics.rst | 0 docs/api_reference/{base => }/model.rst | 0 docs/api_reference/optimize.rst | 12 ++++++-- docs/api_reference/{base => }/parameter.rst | 0 docs/api_reference/sampling.rst | 4 +++ docs/api_reference/surrogate.rst | 4 +++ docs/index.rst | 33 +++++++-------------- 11 files changed, 33 insertions(+), 53 deletions(-) delete mode 100644 docs/api_reference/base/math.rst delete mode 100644 docs/api_reference/base/simulate.rst delete mode 100644 docs/api_reference/base/utils.rst rename docs/api_reference/{base => }/metrics.rst (100%) rename docs/api_reference/{base => }/model.rst (100%) rename docs/api_reference/{base => }/parameter.rst (100%) diff --git a/docs/api_reference/base/math.rst b/docs/api_reference/base/math.rst deleted file mode 100644 index 50834a1..0000000 --- a/docs/api_reference/base/math.rst +++ /dev/null @@ -1,8 +0,0 @@ -Math -==== - -The module provides convinient mathematical functions. - -.. automodule:: corrai.base.math - :members: - :show-inheritance: diff --git a/docs/api_reference/base/simulate.rst b/docs/api_reference/base/simulate.rst deleted file mode 100644 index 2d14c8f..0000000 --- a/docs/api_reference/base/simulate.rst +++ /dev/null @@ -1,6 +0,0 @@ -Simulate Module -================ - -.. automodule:: corrai.base.simulate - :members: - :show-inheritance: diff --git a/docs/api_reference/base/utils.rst b/docs/api_reference/base/utils.rst deleted file mode 100644 index 972ebd3..0000000 --- a/docs/api_reference/base/utils.rst +++ /dev/null @@ -1,8 +0,0 @@ -Utils Module -================ - -This module provides utility functions for data manipulation and transformation. - -.. automodule:: corrai.base.utils - :members: - :show-inheritance: diff --git a/docs/api_reference/index.rst b/docs/api_reference/index.rst index d3bc833..4c31015 100644 --- a/docs/api_reference/index.rst +++ b/docs/api_reference/index.rst @@ -5,14 +5,11 @@ API Reference :maxdepth: 3 :caption: API Reference - base/parameter - base/model - base/simulate + parameter + model sampling sensitivity - fmu optimize surrogate - base/math - base/metrics - base/utils + fmu + metrics diff --git a/docs/api_reference/base/metrics.rst b/docs/api_reference/metrics.rst similarity index 100% rename from docs/api_reference/base/metrics.rst rename to docs/api_reference/metrics.rst diff --git a/docs/api_reference/base/model.rst b/docs/api_reference/model.rst similarity index 100% rename from docs/api_reference/base/model.rst rename to docs/api_reference/model.rst diff --git a/docs/api_reference/optimize.rst b/docs/api_reference/optimize.rst index e035539..b340eb3 100644 --- a/docs/api_reference/optimize.rst +++ b/docs/api_reference/optimize.rst @@ -1,10 +1,18 @@ Optimize Module ================== -.. autoclass:: corrai.optimize.Problem +.. autoclass:: corrai.optimize.SciOptimizer :members: :show-inheritance: -.. autoclass:: corrai.optimize.ObjectiveFunction +.. autoclass:: corrai.optimize.ModelEvaluator :members: :show-inheritance: + +.. autoclass:: corrai.optimize.RealContinuousProblem + :members: + :show-inheritance: + +.. autoclass:: corrai.optimize.MixedProblem + :members: + :show-inheritance: \ No newline at end of file diff --git a/docs/api_reference/base/parameter.rst b/docs/api_reference/parameter.rst similarity index 100% rename from docs/api_reference/base/parameter.rst rename to docs/api_reference/parameter.rst diff --git a/docs/api_reference/sampling.rst b/docs/api_reference/sampling.rst index 1355dbb..a83a112 100644 --- a/docs/api_reference/sampling.rst +++ b/docs/api_reference/sampling.rst @@ -9,6 +9,10 @@ Sampling Module :members: :show-inheritance: +.. autoclass:: corrai.sampling.RealSampler + :members: + :show-inheritance: + .. autoclass:: corrai.sampling.MorrisSampler :members: :show-inheritance: diff --git a/docs/api_reference/surrogate.rst b/docs/api_reference/surrogate.rst index 9750850..77e5848 100644 --- a/docs/api_reference/surrogate.rst +++ b/docs/api_reference/surrogate.rst @@ -8,3 +8,7 @@ Surrogate Module .. autoclass:: corrai.surrogate.MultiModelSO :members: :show-inheritance: + +.. autoclass:: corrai.surrogate.StaticScikitModel + :members: + :show-inheritance: \ No newline at end of file diff --git a/docs/index.rst b/docs/index.rst index 32a7b0b..1a06b29 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -60,18 +60,10 @@ Quick Example .. code-block:: python - import pandas as pd - from corrai.base.parameter import Parameter - from corrai.sensitivity import SobolSanalysis, MorrisSanalysis + from corrai.sensitivity import SobolSanalysis from corrai.base.model import Ishigami - SIMULATION_OPTIONS = { - "start": "2009-01-01 00:00:00", - "end": "2009-01-01 05:00:00", - "timestep": "h", - } - PARAMETER_LIST = [ Parameter("par_x1", (-3.14159265359, 3.14159265359), model_property="x1"), Parameter("par_x2", (-3.14159265359, 3.14159265359), model_property="x2"), @@ -82,30 +74,27 @@ Quick Example sobol = SobolSanalysis( parameters=PARAMETER_LIST, model=Ishigami(), - simulation_options=SIMULATION_OPTIONS, ) # Draw sample, and run simulations - sobol.add_sample(15**2, simulate=True, n_cpu=1, calc_second_order=True) + sobol.add_sample(2**10, simulate=True, n_cpu=1, calc_second_order=True) - # Corrai works for models that returns time series - # Ishigami model here will return the same value for the given parameters - # from START to END at 1h timestep - sobol.analyze('res', method="mean")["mean_res"] + # Corrai works for models that returns either time series or series + # depending on their static or dynamic nature + # Ishigami is a static model (see is_dynamic attribute) + sobol.analyze("res") # Default aggregation method is mean value of the timeseries - sobol.plot_bar('res') + sobol.plot_bar("res", sensitivity_metric="ST") # Display 2nd order matrix for parameters interaction - sobol.plot_s2_matrix('res') + sobol.plot_s2_matrix("res") # Display mean output values of the sample as hist - sobol.sampler.sample.plot_hist('res') - - # Compute dynamic sensitivity analisys at plot - # Obviously, in this example indexes value do not vary - sobol.plot_dynamic_metric('res', sensitivity_metric="ST", freq="h") + sobol.sampler.sample.plot_hist("res") + # Display parallel coordinate plot + sobol.plot_pcp(["res"]) .. toctree::