Repository: oemof/oemof-solph
Pull request #1181: Add a new `Results` class
Author: login: p-snft
Created: 2025-03-18T07:43:07Z | Merged: 2025-03-24T11:20:04Z

Instances of the class allow access to a model's results by converting the model's variables to pandas `DataFrame`s. Minimal design requirements are:

- allow access by variable name via attribute reference or subscription,
- convert to a `DataFrame` lazily, i.e. only convert on first access and
- convert only once.

The current implementation fulfils these requirements.


Diff:
diff --git a/docs/examples/basic_example.rst b/docs/examples/basic_example.rst
deleted file mode 100644
index abcd5e7c2..000000000
--- a/docs/examples/basic_example.rst
+++ /dev/null
@@ -1,7 +0,0 @@
-Basic example
--------------
-
-Using standard oemof-solph components
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-.. automodule:: basic_example.basic_example
diff --git a/docs/examples/index.rst b/docs/examples/index.rst
index 6ff1fdfa3..a063e2929 100644
--- a/docs/examples/index.rst
+++ b/docs/examples/index.rst
@@ -7,10 +7,10 @@ Examples
     :glob:
     :maxdepth: 2
 
-    basic_example.rst
     time_index.rst
     nametuple.rst
     simple_dispatch.rst
+    result_object.rst
     activity_costs.rst
     min_max_runtimes.rst
     startup_costs.rst
diff --git a/docs/examples/result_object.rst b/docs/examples/result_object.rst
new file mode 100644
index 000000000..ddef81179
--- /dev/null
+++ b/docs/examples/result_object.rst
@@ -0,0 +1,7 @@
+Result object
+-------------
+
+Using standard oemof-solph components and view results
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. automodule:: result_object.result_object
diff --git a/examples/basic_example/basic_example.py b/examples/result_object/result_object.py
similarity index 72%
rename from examples/basic_example/basic_example.py
rename to examples/result_object/result_object.py
index 7da9d03a1..a1cc62535 100644
--- a/examples/basic_example/basic_example.py
+++ b/examples/result_object/result_object.py
@@ -30,21 +30,21 @@
 
 Code
 ----
-Download source code: :download:`basic_example.py </../examples/basic_example/basic_example.py>`
+Download source code: :download:`result_object.py </../examples/result_object/result_object.py>`
 
 .. dropdown:: Click to display code
 
-    .. literalinclude:: /../examples/basic_example/basic_example.py
+    .. literalinclude:: /../examples/result_object/result_object.py
         :language: python
         :lines: 61-
 
 Data
 ----
-Download data: :download:`basic_example.csv </../examples/basic_example/basic_example.csv>`
+Download data: :download:`time_series.csv </../examples/result_object/time_series.csv>`
 
 Installation requirements
 -------------------------
-This example requires oemof.solph (v0.5.x), install by:
+This example requires oemof.solph (v0.6.x), install by:
 
 .. code:: bash
 
@@ -73,12 +73,10 @@
 from oemof.solph import components
 from oemof.solph import create_time_index
 from oemof.solph import flows
-from oemof.solph import helpers
 from oemof.solph import processing
+from oemof.solph import Results
 from oemof.solph import views
 
-STORAGE_LABEL = "battery_storage"
-
 
 def get_data_from_file_path(file_path: str) -> pd.DataFrame:
     try:
@@ -102,22 +100,17 @@ def plot_figures_for(element: dict) -> None:
     plt.show()
 
 
-def main(dump_and_restore=False, optimize=True):
-    # For models that need a long time to optimise, saving and loading the
-    # EnergySystem might be advised. By default, we do not do this here. Feel
-    # free to experiment with this once you understood the rest of the code.
-    dump_results = restore_results = dump_and_restore
+def main(optimize=True):
 
     # *************************************************************************
     # ********** PART 1 - Define and optimise the energy system ***************
     # *************************************************************************
 
     # Read data file
-    file_name = "basic_example.csv"
+    file_name = "time_series.csv"
     data = get_data_from_file_path(file_name)
 
     solver = "cbc"  # 'glpk', 'gurobi',....
-    debug = False  # Set number_of_timesteps to 3 to get a readable lp-file.
     number_of_time_steps = len(data)
     solver_verbose = False  # show/hide solver output
 
@@ -227,7 +220,7 @@ def main(dump_and_restore=False, optimize=True):
 
     battery_storage = components.GenericStorage(
         nominal_capacity=nominal_capacity,
-        label=STORAGE_LABEL,
+        label="battery_storage",
         inputs={
             bus_electricity: flows.Flow(nominal_capacity=nominal_capacity)
         },
@@ -256,18 +249,6 @@ def main(dump_and_restore=False, optimize=True):
     # initialise the operational model
     energysystem_model = Model(energysystem)
 
-    # This is for debugging only. It is not(!) necessary to solve the problem
-    # and should be set to False to save time and disc space in normal use. For
-    # debugging the timesteps should be set to 3, to increase the readability
-    # of the lp-file.
-    if debug:
-        file_path = os.path.join(
-            helpers.extend_basic_path("lp_files"), "basic_example.lp"
-        )
-        logging.info(f"Store lp-file in {file_path}.")
-        io_option = {"symbolic_solver_labels": True}
-        energysystem_model.write(file_path, io_options=io_option)
-
     # if tee_switch is true solver messages will be displayed
     logging.info("Solve the optimization problem")
     energysystem_model.solve(
@@ -279,56 +260,35 @@ def main(dump_and_restore=False, optimize=True):
     # The processing module of the outputlib can be used to extract the results
     # from the model transfer them into a homogeneous structured dictionary.
 
-    # add results to the energy system to make it possible to store them.
-    energysystem.results["main"] = processing.results(energysystem_model)
-    energysystem.results["meta"] = processing.meta_results(energysystem_model)
+    results = Results(energysystem_model)
 
-    # The default path is the '.oemof' folder in your $HOME directory.
-    # The default filename is 'es_dump.oemof'.
-    # You can omit the attributes (as None is the default value) for testing
-    # cases. You should use unique names/folders for valuable results to avoid
-    # overwriting.
-    if dump_results:
-        energysystem.dump(dpath=None, filename=None)
+    result_dict = processing.results(energysystem_model)
 
     # *************************************************************************
     # ********** PART 2 - Processing the results ******************************
     # *************************************************************************
 
-    # Saved data can be restored in a second script. So you can work on the
-    # data analysis without re-running the optimisation every time. If you do
-    # so, make sure that you really load the results you want. For example,
-    # if dumping fails, you might exidentially load outdated results.
-    if restore_results:
-        logging.info("**** The script can be divided into two parts here.")
-        logging.info("Restore the energy system and the results.")
-
-        energysystem = EnergySystem()
-        energysystem.restore(dpath=None, filename=None)
-
     # define an alias for shorter calls below (optional)
-    results = energysystem.results["main"]
-    storage = energysystem.groups[STORAGE_LABEL]
+    storage = energysystem.groups["battery_storage"]
 
     # print a time slice of the state of charge
-    start_time = datetime(2012, 2, 25, 8, 0, 0)
-    end_time = datetime(2012, 2, 25, 17, 0, 0)
+    start_time = datetime(2012, 7, 4, 8, 0, 0)
+    end_time = datetime(2012, 7, 4, 17, 0, 0)
 
     print("\n********* State of Charge (slice) *********")
-    print(f"{results[(storage, None)]['sequences'][start_time : end_time]}\n")
+    print(
+        f"{result_dict[(storage, None)]['sequences'][start_time : end_time]}\n"
+    )
+    print(f"{results.storage_content[storage][start_time : end_time]}\n")
 
     # get all variables of a specific component/bus
-    custom_storage = views.node(results, STORAGE_LABEL)
-    electricity_bus = views.node(results, "electricity")
+    custom_storage = views.node(result_dict, "battery_storage")
+    electricity_bus = views.node(result_dict, "electricity")
 
     # plot the time series (sequences) of a specific component/bus
     plot_figures_for(custom_storage)
     plot_figures_for(electricity_bus)
 
-    # print the solver results
-    print("********* Meta results *********")
-    pp.pprint(f"{energysystem.results['meta']}\n")
-
     # print the sums of the flows around the electricity bus
     print("********* Main results *********")
     print(electricity_bus["sequences"].sum(axis=0))
diff --git a/examples/basic_example/basic_example.csv b/examples/result_object/time_series.csv
similarity index 100%
rename from examples/basic_example/basic_example.csv
rename to examples/result_object/time_series.csv
diff --git a/examples/tuple_as_labels/tuple_as_label.py b/examples/tuple_as_labels/tuple_as_label.py
index 638ad9fff..e6006d22b 100644
--- a/examples/tuple_as_labels/tuple_as_label.py
+++ b/examples/tuple_as_labels/tuple_as_label.py
@@ -4,7 +4,7 @@
 General description
 -------------------
 
-You should have grasped the basic_example to understand this one.
+You should have grasped the result_object example to understand this one.
 
 This is an example to show how the label attribute can be used with tuples to
 manage the results of large energy system. Even though, the feature is
@@ -268,7 +268,7 @@ def main(optimize=True):
     # of the lp-file.
     if debug:
         filename = os.path.join(
-            helpers.extend_basic_path("lp_files"), "basic_example.lp"
+            helpers.extend_basic_path("lp_files"), "tuple_as_label.lp"
         )
         logging.info("Store lp-file in {0}.".format(filename))
         model.write(filename, io_options={"symbolic_solver_labels": True})
diff --git a/src/oemof/solph/__init__.py b/src/oemof/solph/__init__.py
index e9c067e1c..d2d863579 100644
--- a/src/oemof/solph/__init__.py
+++ b/src/oemof/solph/__init__.py
@@ -14,6 +14,7 @@
 from ._options import Investment
 from ._options import NonConvex
 from ._plumbing import sequence
+from ._results import Results
 from .buses import Bus  # default Bus (for convenience)
 from .flows import Flow  # default Flow (for convenience)
 
@@ -26,6 +27,7 @@
     "Flow",
     "helpers",
     "processing",
+    "Results",
     "views",
     "EnergySystem",
     "create_time_index",
diff --git a/src/oemof/solph/_results.py b/src/oemof/solph/_results.py
new file mode 100644
index 000000000..2c636ddb7
--- /dev/null
+++ b/src/oemof/solph/_results.py
@@ -0,0 +1,96 @@
+# -*- coding: utf-8 -*-
+
+"""Modules for providing a convenient data structure for solph results.
+
+SPDX-FileCopyrightText: Stephan Günther
+SPDX-FileCopyrightText: Patrik Schönfeldt <patrik.schoenfeldt@dlr.de>
+
+SPDX-License-Identifier: MIT
+
+"""
+
+import warnings
+from functools import cache
+
+import pandas as pd
+from oemof.tools import debugging
+from pyomo.core.base.var import Var
+from pyomo.environ import ConcreteModel
+from pyomo.opt.results.container import ListContainer
+
+
+class Results:
+    # TODO:
+    #   Defer attribute references not present as variables to
+    #   attributes of `model.solver_results` in order to make `Results`
+    #   instances returnable by `model.solve` and still be backwards
+    #   compatible.
+    def __init__(self, model: ConcreteModel):
+        msg = (
+            "The class 'Results' is experimental. Functionality and API can"
+            " be changed without warning during any update."
+        )
+        warnings.warn(msg, debugging.ExperimentalFeatureWarning)
+
+        self._solver_results = model.solver_results
+        self._variables = {}
+        self._model = model
+        for vardata in model.component_data_objects(Var):
+            for variable in [vardata.parent_component()]:
+                key = str(variable).split(".")[-1]
+                if (
+                    key not in self._variables
+                    and key not in self._solver_results
+                ):
+                    self._variables[key] = variable
+                elif self._variables[key] == variable:
+                    # For debugging purposes.
+                    # We should avoid useless iterations.
+                    pass
+                else:
+                    raise ValueError(
+                        f"Variable name defined multiple times: {key}"
+                        + f"(last time in '{variable}')"
+                    )
+
+    def keys(self):
+        return self._solver_results.keys() | self._variables.keys()
+
+    @cache
+    def to_df(self, variable: str) -> pd.DataFrame | pd.Series:
+        # TODO:
+        #   - Figure out why `Results.init_content` is a `pd.Series`.
+        #   - Support `Var`s as arguments?
+        #   - Add column (level) and index names like:
+        #       source, target, timestep etc.
+        """Return a `DataFrame` view of the model's `variable`.
+
+        This is the function that attribute and dictionary access to
+        variables as `DataFrame`s is based on. Use it if you like to be
+        explicit.
+        For convenience you can also replace `results.to_df("variable")`
+        with the equivalent `results.variable` or `results["variable"]`.
+        """
+        variable = self._variables[variable]
+        df = pd.DataFrame(variable.extract_values(), index=[0]).stack(
+            future_stack=True
+        )
+        # overwrite known indexes
+        match tuple(variable.index_set().subsets())[-1].name:
+            case "TIMEPOINTS":
+                df.index = self._model.es.timeindex
+            case "TIMESTEPS":
+                df.index = self._model.es.timeindex[:-1]
+            case _:
+                df.index = df.index.get_level_values(-1)
+        return df
+
+    def __getattr__(self, key: str) -> pd.DataFrame | ListContainer:
+        return self[key]
+
+    def __getitem__(self, key: str) -> pd.DataFrame | ListContainer:
+        # backward-compatibility with returned results object from Pyomo
+        if key in self._solver_results:
+            return self._solver_results[key]
+        else:
+            return self.to_df(key)


Reviews:
user: login: p-snft
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-18T08:32:20Z
user: login: p-snft
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-18T08:49:05Z
user: login: Bachibouzouk
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-18T09:20:09Z
user: login: Bachibouzouk
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-18T09:22:01Z
user: login: p-snft
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-18T14:31:49Z
user: login: p-snft
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-18T15:30:03Z
user: login: github-advanced-security[bot]
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-19T13:02:16Z
user: login: github-advanced-security[bot]
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-19T16:01:43Z
user: login: Bachibouzouk
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T17:26:51Z
user: login: Bachibouzouk
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T17:27:29Z
user: login: github-advanced-security[bot]
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-19T19:03:11Z
user: login: p-snft
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-19T19:04:10Z

Review comments:
diff_hunk: @@ -441,3 +444,46 @@ def relax_problem(self):
         relaxer._apply_to(self)
 
         return self
+
+
+class Results:
+    # TODO:
+    #   Defer attribute references not present as variables to
+    #   attributes of `model.solver_results` in order to make `Results`
+    #   instances returnable by `model.solve` and still be backwards
+    #   compatible.
+    def __init__(self, model: Model):
+        # TODO: Disambiguate colliding variable names.
+        self.variables = {
+            str(variable).split(".")[-1]: variable
+            for vardata in model.component_data_objects(Var)
+            for variable in [vardata.parent_component()]
+        }
path: src/oemof/solph/_models.py
user: login: p-snft
body: I think we could replace the dictionary comprehension by creating an empty dictionary and adding items afterwards. This way, it was possible to find name collisions.

```suggestion
        self.variables = {}
        for vardata in model.component_data_objects(Var):
            for variable in [vardata.parent_component()]:
                key = str(variable).split(".")[-1]
                if key not in self.variables:
                    self.variables[key] = variable
                else:
                    raise ValueError(
                        f"Variable name defined multiple times: {key}"
                        + f"(last time in '{variable}')"
                    )
```
created_at: 2025-03-18T08:32:20Z
author_association: MEMBER
diff_hunk: @@ -441,3 +444,46 @@ def relax_problem(self):
         relaxer._apply_to(self)
 
         return self
+
+
+class Results:
path: src/oemof/solph/_models.py
user: login: p-snft
body: As this in not depending on `solph.Model`, I see this class going to processing.
created_at: 2025-03-18T08:49:04Z
author_association: MEMBER
diff_hunk: @@ -441,3 +444,46 @@ def relax_problem(self):
         relaxer._apply_to(self)
 
         return self
+
+
+class Results:
+    # TODO:
+    #   Defer attribute references not present as variables to
+    #   attributes of `model.solver_results` in order to make `Results`
+    #   instances returnable by `model.solve` and still be backwards
+    #   compatible.
+    def __init__(self, model: Model):
+        # TODO: Disambiguate colliding variable names.
+        self.variables = {
+            str(variable).split(".")[-1]: variable
+            for vardata in model.component_data_objects(Var)
+            for variable in [vardata.parent_component()]
+        }
path: src/oemof/solph/_models.py
user: login: Bachibouzouk
body: I am not a big fan of list/dict comprehension for more than one iteration loop (unless the operation is super obvious and simple), I like the for loop :). On the plus side it makes debugging  easier
created_at: 2025-03-18T09:20:08Z
author_association: CONTRIBUTOR
diff_hunk: @@ -441,3 +444,46 @@ def relax_problem(self):
         relaxer._apply_to(self)
 
         return self
+
+
+class Results:
+    # TODO:
+    #   Defer attribute references not present as variables to
+    #   attributes of `model.solver_results` in order to make `Results`
+    #   instances returnable by `model.solve` and still be backwards
+    #   compatible.
+    def __init__(self, model: Model):
+        # TODO: Disambiguate colliding variable names.
+        self.variables = {
+            str(variable).split(".")[-1]: variable
+            for vardata in model.component_data_objects(Var)
+            for variable in [vardata.parent_component()]
+        }
path: src/oemof/solph/_models.py
user: login: Bachibouzouk
body: What is the usecase where the variable names are defined multiple times ?
created_at: 2025-03-18T09:22:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -441,3 +444,46 @@ def relax_problem(self):
         relaxer._apply_to(self)
 
         return self
+
+
+class Results:
+    # TODO:
+    #   Defer attribute references not present as variables to
+    #   attributes of `model.solver_results` in order to make `Results`
+    #   instances returnable by `model.solve` and still be backwards
+    #   compatible.
+    def __init__(self, model: Model):
+        # TODO: Disambiguate colliding variable names.
+        self.variables = {
+            str(variable).split(".")[-1]: variable
+            for vardata in model.component_data_objects(Var)
+            for variable in [vardata.parent_component()]
+        }
path: src/oemof/solph/_models.py
user: login: p-snft
body: You can have the same variable name defined in multiple blocks. At the dev meeting, we decided to flatten this (e.g. "flow" instead of "SimpleFlowBlock.flow"). However, nobody prevents you from defining "MyOwnBlock.flow" as well, which then collides with the other variable.
created_at: 2025-03-18T14:31:49Z
author_association: MEMBER
diff_hunk: @@ -441,3 +444,46 @@ def relax_problem(self):
         relaxer._apply_to(self)
 
         return self
+
+
+class Results:
+    # TODO:
+    #   Defer attribute references not present as variables to
+    #   attributes of `model.solver_results` in order to make `Results`
+    #   instances returnable by `model.solve` and still be backwards
+    #   compatible.
+    def __init__(self, model: Model):
+        # TODO: Disambiguate colliding variable names.
+        self.variables = {
+            str(variable).split(".")[-1]: variable
+            for vardata in model.component_data_objects(Var)
+            for variable in [vardata.parent_component()]
+        }
path: src/oemof/solph/_models.py
user: login: p-snft
body: The change also revealed another problem: One variable was added thousands of times. (The current version avoids this, but still iterates over the same variable as often as before.)
created_at: 2025-03-18T15:30:03Z
author_association: MEMBER
diff_hunk: @@ -113,11 +111,11 @@
     # *************************************************************************
 
     # Read data file
-    file_name = "basic_example.csv"
+    file_name = "time_series.csv"
     data = get_data_from_file_path(file_name)
 
     solver = "cbc"  # 'glpk', 'gurobi',....
-    debug = False  # Set number_of_timesteps to 3 to get a readable lp-file.
+    debug = True  # Set number_of_timesteps to 3 to get a readable lp-file.
path: examples/result_object/result_object.py
user: login: github-advanced-security[bot]
body: ## Unused local variable

Variable debug is not used.

[Show more details](https://github.com/oemof/oemof-solph/security/code-scanning/253)
created_at: 2025-03-19T13:02:15Z
author_association: NONE
diff_hunk: @@ -279,56 +265,35 @@
     # The processing module of the outputlib can be used to extract the results
     # from the model transfer them into a homogeneous structured dictionary.
 
-    # add results to the energy system to make it possible to store them.
-    energysystem.results["main"] = processing.results(energysystem_model)
-    energysystem.results["meta"] = processing.meta_results(energysystem_model)
+    results = Results(energysystem_model)
path: examples/result_object/result_object.py
user: login: github-advanced-security[bot]
body: ## Unused local variable

Variable results is not used.

[Show more details](https://github.com/oemof/oemof-solph/security/code-scanning/254)
created_at: 2025-03-19T13:02:15Z
author_association: NONE
diff_hunk: @@ -0,0 +1,97 @@
+# -*- coding: utf-8 -*-
+
+"""Modules for providing a convenient data structure for solph results.
+
+SPDX-FileCopyrightText: Stephan Günther
+SPDX-FileCopyrightText: Patrik Schönfeldt <patrik.schoenfeldt@dlr.de>
+
+SPDX-License-Identifier: MIT
+
+"""
+
+import warnings
+from functools import cache
+
+import numpy as np
path: src/oemof/solph/_results.py
user: login: github-advanced-security[bot]
body: ## Unused import

Import of 'np' is not used.

[Show more details](https://github.com/oemof/oemof-solph/security/code-scanning/255)
created_at: 2025-03-19T16:01:43Z
author_association: NONE
diff_hunk: @@ -0,0 +1,97 @@
+# -*- coding: utf-8 -*-
+
+"""Modules for providing a convenient data structure for solph results.
+
+SPDX-FileCopyrightText: Stephan Günther
+SPDX-FileCopyrightText: Patrik Schönfeldt <patrik.schoenfeldt@dlr.de>
+
+SPDX-License-Identifier: MIT
+
+"""
+
+import warnings
+from functools import cache
+
+import numpy as np
path: src/oemof/solph/_results.py
user: login: Bachibouzouk
body: Don't linter already exist, which can catch unused inputs? They probably consume less energy the copilot no? 
created_at: 2025-03-19T17:26:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,97 @@
+# -*- coding: utf-8 -*-
+
+"""Modules for providing a convenient data structure for solph results.
+
+SPDX-FileCopyrightText: Stephan Günther
+SPDX-FileCopyrightText: Patrik Schönfeldt <patrik.schoenfeldt@dlr.de>
+
+SPDX-License-Identifier: MIT
+
+"""
+
+import warnings
+from functools import cache
+
+import numpy as np
path: src/oemof/solph/_results.py
user: login: Bachibouzouk
body: Don't linter already exist, which can catch unused inputs? They probably consume less energy than the copilot no? 
created_at: 2025-03-19T17:27:29Z
author_association: CONTRIBUTOR
diff_hunk: @@ -102,22 +100,17 @@
     plt.show()
 
 
-def main(dump_and_restore=False, optimize=True):
-    # For models that need a long time to optimise, saving and loading the
-    # EnergySystem might be advised. By default, we do not do this here. Feel
-    # free to experiment with this once you understood the rest of the code.
-    dump_results = restore_results = dump_and_restore
+def main(optimize=True):
path: examples/result_object/result_object.py
user: login: github-advanced-security[bot]
body: ## Explicit returns mixed with implicit (fall through) returns

Mixing implicit and explicit returns may indicate an error as implicit returns always return None.

[Show more details](https://github.com/oemof/oemof-solph/security/code-scanning/256)
created_at: 2025-03-19T19:03:11Z
author_association: NONE
diff_hunk: @@ -0,0 +1,97 @@
+# -*- coding: utf-8 -*-
+
+"""Modules for providing a convenient data structure for solph results.
+
+SPDX-FileCopyrightText: Stephan Günther
+SPDX-FileCopyrightText: Patrik Schönfeldt <patrik.schoenfeldt@dlr.de>
+
+SPDX-License-Identifier: MIT
+
+"""
+
+import warnings
+from functools import cache
+
+import numpy as np
path: src/oemof/solph/_results.py
user: login: p-snft
body: Microsoft just pushes Copilot on everybody. We didn't ask for it.
created_at: 2025-03-19T19:04:10Z
author_association: MEMBER

PR comments:
user: login: p-snft
created_at: 2025-03-18T08:47:34Z
body: Should we flag this experimental and add it to dev already before it's fully done?
author_association: MEMBER
user: login: Bachibouzouk
created_at: 2025-03-18T09:25:17Z
body: > Should we flag this experimental and add it to dev already before it's fully done?

I would be for doing this, better incremental small changes as one huge (but time consuming) change
author_association: CONTRIBUTOR
user: login: p-snft
created_at: 2025-03-18T16:03:38Z
body: * Coverage is expected to decrease, as we chose not test this experimental class.
* Docs test fails because of DDos defence at one page.

In my opinion, we can already merge this state.
author_association: MEMBER
user: login: p-snft
created_at: 2025-03-24T11:19:57Z
body: I'll merge the current state to have an alpha release including it. (Branch will persist to continue working on it.)
author_association: MEMBER