fram-core 0.0.0__py3-none-any.whl → 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. fram_core-0.1.0.dist-info/METADATA +42 -0
  2. fram_core-0.1.0.dist-info/RECORD +100 -0
  3. {fram_core-0.0.0.dist-info → fram_core-0.1.0.dist-info}/WHEEL +1 -2
  4. fram_core-0.1.0.dist-info/licenses/LICENSE.md +8 -0
  5. framcore/Base.py +161 -0
  6. framcore/Model.py +90 -0
  7. framcore/__init__.py +10 -0
  8. framcore/aggregators/Aggregator.py +172 -0
  9. framcore/aggregators/HydroAggregator.py +849 -0
  10. framcore/aggregators/NodeAggregator.py +530 -0
  11. framcore/aggregators/WindSolarAggregator.py +315 -0
  12. framcore/aggregators/__init__.py +13 -0
  13. framcore/aggregators/_utils.py +184 -0
  14. framcore/attributes/Arrow.py +307 -0
  15. framcore/attributes/ElasticDemand.py +90 -0
  16. framcore/attributes/ReservoirCurve.py +23 -0
  17. framcore/attributes/SoftBound.py +16 -0
  18. framcore/attributes/StartUpCost.py +65 -0
  19. framcore/attributes/Storage.py +158 -0
  20. framcore/attributes/TargetBound.py +16 -0
  21. framcore/attributes/__init__.py +63 -0
  22. framcore/attributes/hydro/HydroBypass.py +49 -0
  23. framcore/attributes/hydro/HydroGenerator.py +100 -0
  24. framcore/attributes/hydro/HydroPump.py +178 -0
  25. framcore/attributes/hydro/HydroReservoir.py +27 -0
  26. framcore/attributes/hydro/__init__.py +13 -0
  27. framcore/attributes/level_profile_attributes.py +911 -0
  28. framcore/components/Component.py +136 -0
  29. framcore/components/Demand.py +144 -0
  30. framcore/components/Flow.py +189 -0
  31. framcore/components/HydroModule.py +371 -0
  32. framcore/components/Node.py +99 -0
  33. framcore/components/Thermal.py +208 -0
  34. framcore/components/Transmission.py +198 -0
  35. framcore/components/_PowerPlant.py +81 -0
  36. framcore/components/__init__.py +22 -0
  37. framcore/components/wind_solar.py +82 -0
  38. framcore/curves/Curve.py +44 -0
  39. framcore/curves/LoadedCurve.py +146 -0
  40. framcore/curves/__init__.py +9 -0
  41. framcore/events/__init__.py +21 -0
  42. framcore/events/events.py +51 -0
  43. framcore/expressions/Expr.py +591 -0
  44. framcore/expressions/__init__.py +30 -0
  45. framcore/expressions/_get_constant_from_expr.py +477 -0
  46. framcore/expressions/_utils.py +73 -0
  47. framcore/expressions/queries.py +416 -0
  48. framcore/expressions/units.py +227 -0
  49. framcore/fingerprints/__init__.py +11 -0
  50. framcore/fingerprints/fingerprint.py +292 -0
  51. framcore/juliamodels/JuliaModel.py +171 -0
  52. framcore/juliamodels/__init__.py +7 -0
  53. framcore/loaders/__init__.py +10 -0
  54. framcore/loaders/loaders.py +405 -0
  55. framcore/metadata/Div.py +73 -0
  56. framcore/metadata/ExprMeta.py +56 -0
  57. framcore/metadata/LevelExprMeta.py +32 -0
  58. framcore/metadata/Member.py +55 -0
  59. framcore/metadata/Meta.py +44 -0
  60. framcore/metadata/__init__.py +15 -0
  61. framcore/populators/Populator.py +108 -0
  62. framcore/populators/__init__.py +7 -0
  63. framcore/querydbs/CacheDB.py +50 -0
  64. framcore/querydbs/ModelDB.py +34 -0
  65. framcore/querydbs/QueryDB.py +45 -0
  66. framcore/querydbs/__init__.py +11 -0
  67. framcore/solvers/Solver.py +63 -0
  68. framcore/solvers/SolverConfig.py +272 -0
  69. framcore/solvers/__init__.py +9 -0
  70. framcore/timeindexes/AverageYearRange.py +27 -0
  71. framcore/timeindexes/ConstantTimeIndex.py +22 -0
  72. framcore/timeindexes/DailyIndex.py +33 -0
  73. framcore/timeindexes/FixedFrequencyTimeIndex.py +814 -0
  74. framcore/timeindexes/HourlyIndex.py +33 -0
  75. framcore/timeindexes/IsoCalendarDay.py +33 -0
  76. framcore/timeindexes/ListTimeIndex.py +277 -0
  77. framcore/timeindexes/ModelYear.py +23 -0
  78. framcore/timeindexes/ModelYears.py +27 -0
  79. framcore/timeindexes/OneYearProfileTimeIndex.py +29 -0
  80. framcore/timeindexes/ProfileTimeIndex.py +43 -0
  81. framcore/timeindexes/SinglePeriodTimeIndex.py +37 -0
  82. framcore/timeindexes/TimeIndex.py +103 -0
  83. framcore/timeindexes/WeeklyIndex.py +33 -0
  84. framcore/timeindexes/__init__.py +36 -0
  85. framcore/timeindexes/_time_vector_operations.py +689 -0
  86. framcore/timevectors/ConstantTimeVector.py +131 -0
  87. framcore/timevectors/LinearTransformTimeVector.py +131 -0
  88. framcore/timevectors/ListTimeVector.py +127 -0
  89. framcore/timevectors/LoadedTimeVector.py +97 -0
  90. framcore/timevectors/ReferencePeriod.py +51 -0
  91. framcore/timevectors/TimeVector.py +108 -0
  92. framcore/timevectors/__init__.py +17 -0
  93. framcore/utils/__init__.py +35 -0
  94. framcore/utils/get_regional_volumes.py +387 -0
  95. framcore/utils/get_supported_components.py +60 -0
  96. framcore/utils/global_energy_equivalent.py +63 -0
  97. framcore/utils/isolate_subnodes.py +172 -0
  98. framcore/utils/loaders.py +97 -0
  99. framcore/utils/node_flow_utils.py +236 -0
  100. framcore/utils/storage_subsystems.py +106 -0
  101. fram_core-0.0.0.dist-info/METADATA +0 -5
  102. fram_core-0.0.0.dist-info/RECORD +0 -4
  103. fram_core-0.0.0.dist-info/top_level.txt +0 -1
@@ -0,0 +1,108 @@
1
+ """Populator API, for creating a system of Components, TimeVectors and Curves (and Expr) for a Model object."""
2
+
3
+ from abc import ABC, abstractmethod
4
+
5
+ from framcore import Base, Model
6
+ from framcore.components import Component
7
+ from framcore.curves import Curve
8
+ from framcore.expressions import Expr
9
+ from framcore.timevectors import TimeVector
10
+
11
+
12
+ class Populator(Base, ABC):
13
+ """Populate a model with data from a data source."""
14
+
15
+ def __init__(self) -> None:
16
+ """
17
+ Set up ID and reference registration containers.
18
+
19
+ These are used to check if IDs and references actually exist in the system.
20
+ """
21
+ super().__init__()
22
+
23
+ self._registered_ids: dict[str, list[object]] = {}
24
+ self._registered_refs: dict[str, set[str]] = {}
25
+
26
+ def populate(self, model: Model) -> None:
27
+ """
28
+ Add data objects from a database to an input Model.
29
+
30
+ These data objects shall be of class Component, TimeVector, and Curve.
31
+ The method _populate should be overwritten in a subclass of Populator.
32
+ In this way, it is used to create objects from any database.
33
+
34
+ Args:
35
+ model (Model): Model which will have the objects added to it.
36
+
37
+ """
38
+ self._check_type(model, Model)
39
+ new_data = self._populate()
40
+
41
+ # check that the new_data dict complies with the type hints of _populate?
42
+ for existing_id in model.get_data():
43
+ self._register_id(existing_id, model)
44
+ errors = list(self._check_duplicate_ids())
45
+ model.get_data().update(new_data)
46
+ errors += list(self._check_references(model.get_data()))
47
+ self._report_errors(errors)
48
+
49
+ @abstractmethod
50
+ def _populate(self) -> dict[str, Component | TimeVector | Curve | Expr]:
51
+ """Create and return Components, TimeVectors and Curves. Possibly also Exprs."""
52
+ pass
53
+
54
+ def _check_duplicate_ids(self) -> dict[str, list[object]]:
55
+ """
56
+ Retrieve dictionary with ids of duplicated objects and their corresponding source.
57
+
58
+ Returns:
59
+ dict[str, list[object]]: keys are ids and values are lists of sources.
60
+
61
+ """
62
+ return {f"Duplicate ID found: '{duplicate_id}' in sources {sources}" for duplicate_id, sources in self._registered_ids.items() if len(sources) > 1}
63
+
64
+ def _check_references(self, data: dict[str, Component | TimeVector | Curve | Expr]) -> set:
65
+ errors = set()
66
+ for ref, referencers in self._registered_refs.items():
67
+ if ref not in data:
68
+ msg = f"References to an invalid ID found. ID '{ref}' is not connected to any data."
69
+ try:
70
+ sources = {source_id: data[source_id] for source_id in referencers}
71
+ except KeyError:
72
+ errors.add(
73
+ msg + f" Sub Components referencing the faulty ID: {referencers}",
74
+ )
75
+ else:
76
+ errors.add(
77
+ msg + f" Components referencing the faulty ID: {sources}",
78
+ )
79
+ return errors
80
+
81
+ def _report_errors(self, errors: list[str]) -> None:
82
+ if errors:
83
+ n = len(errors)
84
+ s = "s" if n > 1 else ""
85
+ error_str = "\n".join(errors)
86
+ message = f"Found {n} error{s}:\n{error_str}"
87
+ raise RuntimeError(message)
88
+
89
+ def _register_id(self, new_id: str, source: object) -> None:
90
+ """
91
+ Register an id and its source.
92
+
93
+ Args:
94
+ new_id (str): New id to be registered.
95
+ source (object): Source of the new id.
96
+
97
+ """
98
+ if new_id in self._registered_ids:
99
+ self._registered_ids[new_id].append(source)
100
+ else:
101
+ self._registered_ids[new_id] = [source]
102
+
103
+ def _register_references(self, component_id: str, references: set) -> None:
104
+ for ref in references:
105
+ if ref in self._registered_refs:
106
+ self._registered_refs[ref].add(component_id)
107
+ else:
108
+ self._registered_refs[ref] = {component_id}
@@ -0,0 +1,7 @@
1
+ # framcore/populators/__init__.py
2
+
3
+ from framcore.populators.Populator import Populator
4
+
5
+ __all__ = [
6
+ "Populator",
7
+ ]
@@ -0,0 +1,50 @@
1
+ from framcore import Model
2
+ from framcore.querydbs import QueryDB
3
+
4
+
5
+ class CacheDB(QueryDB):
6
+ """Stores models and precomputed values."""
7
+
8
+ def __init__(self, model: Model, *models: tuple[Model]) -> None:
9
+ """
10
+ Initialize CacheDB with one or more Model instances.
11
+
12
+ Args:
13
+ model (Model): The primary Model instance.
14
+ *models (tuple[Model]): Additional Model instances.
15
+
16
+ """
17
+ self._models: tuple[Model] = (model, *models)
18
+ self._cache = dict()
19
+ self._min_elapsed_seconds = 0.01
20
+
21
+ def set_min_elapsed_seconds(self, value: float) -> None:
22
+ """Values that takes below this threshold to compute, does not get cached."""
23
+ self._check_type(value, float)
24
+ self._check_float(value=value, lower_bound=0.0, upper_bound=None)
25
+ self._min_elapsed_seconds = value
26
+
27
+ def get_min_elapsed_seconds(self) -> float:
28
+ """Values that takes below this threshold to compute, does not get cached."""
29
+ return self._min_elapsed_seconds
30
+
31
+ def _get(self, key: object) -> object:
32
+ if key in self._cache:
33
+ return self._cache[key]
34
+ for m in self._models:
35
+ data = m.get_data()
36
+ if key in data:
37
+ return data[key]
38
+ message = f"Key '{key}' not found."
39
+ raise KeyError(message)
40
+
41
+ def _has_key(self, key: object) -> bool:
42
+ return key in self._cache or any(key in m.get_data() for m in self._models)
43
+
44
+ def _put(self, key: object, value: object, elapsed_seconds: float) -> None:
45
+ if elapsed_seconds < self._min_elapsed_seconds:
46
+ return
47
+ self._cache[key] = value
48
+
49
+ def _get_data(self) -> dict:
50
+ return self._models[0].get_data()
@@ -0,0 +1,34 @@
1
+ from framcore import Model
2
+ from framcore.querydbs import QueryDB
3
+
4
+
5
+ class ModelDB(QueryDB):
6
+ """A database-like interface for querying multiple Model instances."""
7
+
8
+ def __init__(self, model: Model, *models: tuple[Model]) -> None:
9
+ """
10
+ Initialize ModelDB with one or more Model instances.
11
+
12
+ Args:
13
+ model (Model): The primary Model instance.
14
+ *models (tuple[Model]): Additional Model instances.
15
+
16
+ """
17
+ self._models: tuple[Model] = (model, *models)
18
+
19
+ def _get(self, key: object) -> object:
20
+ for m in self._models:
21
+ data = m.get_data()
22
+ if key in data:
23
+ return data[key]
24
+ message = f"Key '{key}' not found."
25
+ raise KeyError(message)
26
+
27
+ def _has_key(self, key: object) -> bool:
28
+ return any(key in m.get_data() for m in self._models)
29
+
30
+ def _put(self, key: object, value: object, elapsed_seconds: float) -> None:
31
+ return None
32
+
33
+ def _get_data(self) -> dict:
34
+ return self._models[0].get_data()
@@ -0,0 +1,45 @@
1
+ from abc import ABC, abstractmethod
2
+
3
+ from framcore import Base
4
+
5
+
6
+ class QueryDB(Base, ABC):
7
+ """
8
+ Abstract base class for database queries.
9
+
10
+ Provides an interface for getting, putting, and checking keys in a database.
11
+ Subclasses must implement the _get, _put, and _has_key methods.
12
+
13
+ """
14
+
15
+ def get(self, key: object) -> object:
16
+ """Get value behind key from db."""
17
+ return self._get(key)
18
+
19
+ def put(self, key: object, value: object, elapsed_seconds: float) -> None:
20
+ """Put value in db behind key (maybe, depending on implementation)."""
21
+ self._put(key, value, elapsed_seconds)
22
+
23
+ def has_key(self, key: str) -> bool:
24
+ """Return True if db has value behind key."""
25
+ return self._has_key(key)
26
+
27
+ def get_data(self) -> dict:
28
+ """Return output of get_data called on first underlying model."""
29
+ return self._get_data()
30
+
31
+ @abstractmethod
32
+ def _get(self, key: object) -> object:
33
+ pass
34
+
35
+ @abstractmethod
36
+ def _put(self, key: object, value: object, elapsed_seconds: float) -> None:
37
+ pass
38
+
39
+ @abstractmethod
40
+ def _has_key(self, key: object) -> bool:
41
+ pass
42
+
43
+ @abstractmethod
44
+ def _get_data(self) -> dict:
45
+ pass
@@ -0,0 +1,11 @@
1
+ # framcore/querydbs/__init__.py
2
+
3
+ from framcore.querydbs.QueryDB import QueryDB
4
+ from framcore.querydbs.ModelDB import ModelDB
5
+ from framcore.querydbs.CacheDB import CacheDB
6
+
7
+ __all__ = [
8
+ "CacheDB",
9
+ "ModelDB",
10
+ "QueryDB",
11
+ ]
@@ -0,0 +1,63 @@
1
+ import pickle
2
+ from abc import ABC, abstractmethod
3
+ from copy import deepcopy
4
+ from pathlib import Path
5
+
6
+ from framcore import Base, Model
7
+ from framcore.solvers import SolverConfig
8
+
9
+
10
+ class Solver(Base, ABC):
11
+ """
12
+ Solver inteface class.
13
+
14
+ In FRAM we call energy market models for Solvers. They take a populated Model and configurations from a SolverConfig,
15
+ and transfers this to the solver software. Then it solves the energy market model, and writes results back to the Model.
16
+ """
17
+
18
+ _FILENAME_MODEL = "model.pickle"
19
+ _FILENAME_SOLVER = "solver.pickle"
20
+
21
+ def solve(self, model: Model) -> None:
22
+ """
23
+ Inititiate the solve.
24
+
25
+ It takes the populated Model and configurations from self.SolverConfig, and transfers this to the solver software.
26
+ Then it solves the energy market model, and writes results back to the Model.
27
+
28
+ At the end of the solve, the Model (now with results) and the Solver object (with configurations) are pickled to the solve folder.
29
+ - model.pickle can be used to inspect results later.
30
+ - solver.pickle allows reuse of the same solver configurations (with solve_folder set to None to avoid overwriting).
31
+ TODO: Could also pickle the Model before solving, to have a record of the input model.
32
+
33
+ """
34
+ self._check_type(model, Model)
35
+
36
+ config = self.get_config()
37
+
38
+ folder = config.get_solve_folder()
39
+
40
+ if folder is None:
41
+ raise ValueError("A folder for the Solver has not been set yet. Use Solver.get_config().set_solve_folder(folder)")
42
+
43
+ Path.mkdir(folder, parents=True, exist_ok=True)
44
+
45
+ self._solve(folder, model)
46
+
47
+ with Path.open(folder / self._FILENAME_MODEL, "wb") as f:
48
+ pickle.dump(model, f)
49
+
50
+ c = deepcopy(self)
51
+ c.get_config().set_solve_folder(None)
52
+ with Path.open(folder / self._FILENAME_SOLVER, "wb") as f:
53
+ pickle.dump(c, f)
54
+
55
+ @abstractmethod
56
+ def get_config(self) -> SolverConfig:
57
+ """Return the solver's config object."""
58
+ pass
59
+
60
+ @abstractmethod
61
+ def _solve(self, folder: Path, model: Model) -> None:
62
+ """Solve the model inplace. Write to folder. Must be implemented by specific solvers."""
63
+ pass
@@ -0,0 +1,272 @@
1
+ """Definition of SolverConfig interface."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from abc import ABC
6
+ from pathlib import Path
7
+
8
+ from framcore import Base
9
+ from framcore.expressions import is_convertable
10
+ from framcore.timeindexes import TimeIndex
11
+
12
+
13
+ class SolverConfig(Base, ABC):
14
+ """SolverConfig inteface class."""
15
+
16
+ _SIMULATION_MODE_SERIAL = "serial"
17
+ _SIMULATION_MODE_FORECAST = "forecast"
18
+
19
+ _DIFF_POLICY_ERROR = "error"
20
+ _DIFF_POLICY_IGNORE = "ignore"
21
+ _DIFF_POLICY_BACKUP = "backup"
22
+
23
+ def __init__(self) -> None:
24
+ """Create internal variables with default values."""
25
+ self._simulation_mode: str | None = None
26
+ self._diff_policy: str = self._DIFF_POLICY_ERROR
27
+ self._show_screen_output: bool = False
28
+ self._currency: str | None = None
29
+ self._num_cpu_cores: int = 1
30
+ self._is_float32 = True
31
+ self._first_weather_year: int | None = None
32
+ self._num_weather_years: int | None = None
33
+ self._first_simulation_year: int | None = None
34
+ self._num_simulation_years: int | None = None
35
+ self._data_period: TimeIndex | None = None
36
+ self._commodity_unit_flow_default: str | None = None
37
+ self._commodity_unit_stock_default: str | None = None
38
+ self._commodity_unit_flows: dict[str, str] = {}
39
+ self._commodity_unit_stocks: dict[str, str] = {}
40
+ self._solve_folder: Path | None = None
41
+
42
+ def set_solve_folder(self, folder: Path | str | None) -> None:
43
+ """Set folder where solve related files will be written."""
44
+ self._check_type(folder, (str, Path, type(None)))
45
+ if isinstance(folder, str):
46
+ folder = Path(folder)
47
+ self._solve_folder = folder
48
+
49
+ def get_solve_folder(self) -> Path | None:
50
+ """Get folder where solve related files will be written."""
51
+ return self._solve_folder
52
+
53
+ def set_commodity_units(
54
+ self,
55
+ commodity: str,
56
+ stock_unit: str,
57
+ flow_unit: str | None = None,
58
+ is_default: bool | None = None,
59
+ ) -> None:
60
+ """
61
+ Set the stock and flow units for a commodity.
62
+
63
+ Parameters
64
+ ----------
65
+ commodity : str
66
+ The name of the commodity.
67
+ stock_unit : str
68
+ The unit for the commodity stock.
69
+ flow_unit : str or None, optional
70
+ The unit for the commodity flow, representing the rate of change of the stock unit over time.
71
+ is_default : bool or None, optional
72
+ If True, set these units as the default for all commodities.
73
+
74
+ Raises
75
+ ------
76
+ ValueError
77
+ If the flow unit is incompatible with the stock unit.
78
+
79
+ """
80
+ self._check_type(commodity, str)
81
+ self._check_type(stock_unit, str)
82
+ self._check_type(flow_unit, (str, type(None)))
83
+ self._check_type(is_default, (bool, type(None)))
84
+ if flow_unit:
85
+ candidate = f"{stock_unit}/s"
86
+ if not is_convertable(candidate, flow_unit):
87
+ message = (
88
+ f"Incompatible units for commodity '{commodity}': stock_unit '{stock_unit}' flow_unit '{flow_unit}'"
89
+ "The flow_unit must represent the rate of change of the stock_unit over time."
90
+ )
91
+ raise ValueError(message)
92
+ if is_default:
93
+ self._warn_if_changed_defaults(stock_unit, flow_unit)
94
+ self._commodity_unit_stock_default = stock_unit
95
+ if flow_unit:
96
+ self._commodity_unit_flow_default = flow_unit
97
+ else:
98
+ self._commodity_unit_stocks[commodity] = stock_unit
99
+ self._commodity_unit_flows[commodity] = flow_unit
100
+
101
+ def get_unit_stock(self, commodity: str) -> str:
102
+ """
103
+ Get the stock unit for a given commodity.
104
+
105
+ Parameters
106
+ ----------
107
+ commodity : str
108
+ The name of the commodity.
109
+
110
+ Returns
111
+ -------
112
+ str
113
+ The stock unit for the commodity.
114
+
115
+ Raises
116
+ ------
117
+ ValueError
118
+ If no stock unit is set for the commodity.
119
+
120
+ """
121
+ if commodity not in self._commodity_unit_stocks and not self._commodity_unit_stock_default:
122
+ message = f"No stock unit set for '{commodity}'."
123
+ raise ValueError(message)
124
+ return self._commodity_unit_stocks.get(commodity, self._commodity_unit_stock_default)
125
+
126
+ def get_unit_flow(self, commodity: str) -> str | None:
127
+ """
128
+ Get the flow unit for a given commodity.
129
+
130
+ Parameters
131
+ ----------
132
+ commodity : str
133
+ The name of the commodity.
134
+
135
+ Returns
136
+ -------
137
+ str or None
138
+ The flow unit for the commodity, or None if not set.
139
+
140
+ """
141
+ return self._commodity_unit_flows.get(commodity, self._commodity_unit_flow_default)
142
+
143
+ def _warn_if_changed_defaults(self, stock_unit: str, flow_unit: str) -> None:
144
+ if self._commodity_unit_flow_default and flow_unit != self._commodity_unit_flow_default:
145
+ message = f"Replacing flow default from {self._commodity_unit_flow_default} to {flow_unit}. Usually default is only set once."
146
+ self.send_warning_event(message)
147
+ if self._commodity_unit_stock_default and stock_unit != self._commodity_unit_stock_default:
148
+ message = f"Replacing stock default from {self._commodity_unit_stock_default} to {stock_unit}. Usually default is only set once."
149
+ self.send_warning_event(message)
150
+
151
+ def get_num_cpu_cores(self) -> int:
152
+ """Return number of cpu cores the Solver can use."""
153
+ return self._num_cpu_cores
154
+
155
+ def set_num_cpu_cores(self, n: int) -> int:
156
+ """Set number of cpu cores the Solver can use."""
157
+ self._num_cpu_cores = n
158
+
159
+ def set_currency(self, currency: str) -> None:
160
+ """Set currency."""
161
+ self._check_type(currency, str)
162
+ self._currency = currency
163
+
164
+ def get_currency(self) -> str | None:
165
+ """Get currency."""
166
+ return self._currency
167
+
168
+ def set_screen_output_on(self) -> None:
169
+ """Print output from Solver to stdout and logfile."""
170
+ self._show_screen_output = True
171
+
172
+ def set_screen_output_off(self) -> None:
173
+ """Only print output from Solver to logfile."""
174
+ self._show_screen_output = False
175
+
176
+ def show_screen_output(self) -> bool:
177
+ """Return True if screen output is set to be shown."""
178
+ return self._show_screen_output
179
+
180
+ def set_diff_policy_error(self) -> None:
181
+ """Error if non-empty diff during solve."""
182
+ self._diff_policy = self._DIFF_POLICY_ERROR
183
+
184
+ def set_diff_policy_ignore(self) -> None:
185
+ """Ignore if non-empty diff during solve."""
186
+ self._diff_policy = self._DIFF_POLICY_IGNORE
187
+
188
+ def set_diff_policy_backup(self) -> None:
189
+ """Copy existing folder to folder/backup_[timestamp] folder if non-empty diff during solve."""
190
+ self._diff_policy = self._DIFF_POLICY_BACKUP
191
+
192
+ def is_diff_policy_error(self) -> bool:
193
+ """Return True if error diff policy."""
194
+ return self._diff_policy == self._DIFF_POLICY_ERROR
195
+
196
+ def is_diff_policy_ignore(self) -> bool:
197
+ """Return True if ignore diff policy."""
198
+ return self._diff_policy == self._DIFF_POLICY_IGNORE
199
+
200
+ def is_diff_policy_backup(self) -> bool:
201
+ """Return True if backup diff policy."""
202
+ return self._diff_policy == self._DIFF_POLICY_BACKUP
203
+
204
+ def set_simulation_mode_serial(self) -> None:
205
+ """Activate serial simulation mode."""
206
+ self._simulation_mode = self._SIMULATION_MODE_SERIAL
207
+
208
+ def is_simulation_mode_serial(self) -> bool:
209
+ """Return True if serial simulation mode."""
210
+ return self._simulation_mode == self._SIMULATION_MODE_SERIAL
211
+
212
+ def set_data_period(self, period: TimeIndex) -> None:
213
+ """Set period used in level value queries."""
214
+ self._check_type(period, TimeIndex)
215
+ self._data_period = period
216
+
217
+ def get_data_period(self) -> TimeIndex | None:
218
+ """Get period used in level value queries."""
219
+ return self._data_period
220
+
221
+ def set_simulation_years(self, first_year: int, num_years: int) -> None:
222
+ """Set subset of scenario years. For serial simulation."""
223
+ self._check_type(first_year, int)
224
+ self._check_type(num_years, int)
225
+ self._check_int(first_year, lower_bound=0, upper_bound=None)
226
+ self._check_int(num_years, lower_bound=1, upper_bound=None)
227
+ self._first_simulation_year = first_year
228
+ self._num_simulation_years = num_years
229
+
230
+ def get_simulation_years(self) -> tuple[int, int]:
231
+ """
232
+ Get simulation years (first_year, num_years).
233
+
234
+ Return weather years as fallback if serial simulation.
235
+ """
236
+ if (self._first_simulation_year is None or self._num_simulation_years is None) and self.is_simulation_mode_serial():
237
+ first_weather_year, num_weather_years = self.get_weather_years()
238
+ if first_weather_year is not None and num_weather_years is not None:
239
+ return first_weather_year, num_weather_years
240
+
241
+ if self._first_simulation_year is None or self._num_simulation_years is None:
242
+ message = "Simulation years not set."
243
+ raise ValueError(message)
244
+ return (self._first_simulation_year, self._num_simulation_years)
245
+
246
+ def set_weather_years(self, first_year: int, num_years: int) -> None:
247
+ """Set weather scenario period used in profiles."""
248
+ self._check_type(first_year, int)
249
+ self._check_type(num_years, int)
250
+ self._check_int(first_year, lower_bound=0, upper_bound=None)
251
+ self._check_int(num_years, lower_bound=1, upper_bound=None)
252
+ self._first_weather_year = first_year
253
+ self._num_weather_years = num_years
254
+
255
+ def get_weather_years(self) -> tuple[int, int]:
256
+ """Get weather scenario period (first_year, num_years) used in profiles."""
257
+ if self._first_weather_year < 0 or self._num_weather_years < 0:
258
+ message = "Scenario years not set."
259
+ raise ValueError(message)
260
+ return (self._first_weather_year, self._num_weather_years)
261
+
262
+ def use_float32(self) -> None:
263
+ """Use single precision floating point numbers in data management."""
264
+ self._is_float32 = True
265
+
266
+ def use_float64(self) -> None:
267
+ """Use double precision floating point numbers in data management."""
268
+ self._is_float32 = False
269
+
270
+ def is_float32(self) -> bool:
271
+ """Return if single precision in data management, else double precision."""
272
+ return self._is_float32
@@ -0,0 +1,9 @@
1
+ # framcore/solvers/__init__.py
2
+
3
+ from framcore.solvers.Solver import Solver
4
+ from framcore.solvers.SolverConfig import SolverConfig
5
+
6
+ __all__ = [
7
+ "Solver",
8
+ "SolverConfig",
9
+ ]
@@ -0,0 +1,27 @@
1
+ from datetime import datetime
2
+
3
+ from framcore.timeindexes.SinglePeriodTimeIndex import SinglePeriodTimeIndex # NB! full import path needed for inheritance to work
4
+
5
+
6
+ class AverageYearRange(SinglePeriodTimeIndex):
7
+ """AverageYearRange represents a period over a range of years. No extrapolation and represents full iso calendar years."""
8
+
9
+ def __init__(self, start_year: int, num_years: int) -> None:
10
+ """
11
+ Initialize AverageYearRange with a year range. No extrapolation and represents full iso calendar years.
12
+
13
+ Args:
14
+ start_year (int): First year in the range.
15
+ num_years (int): Number of years in the range.
16
+
17
+ """
18
+ start_time = datetime.fromisocalendar(start_year, 1, 1)
19
+ end_time = datetime.fromisocalendar(start_year + num_years, 1, 1)
20
+ period_duration = end_time - start_time
21
+ super().__init__(
22
+ start_time=start_time,
23
+ period_duration=period_duration,
24
+ is_52_week_years=False,
25
+ extrapolate_first_point=False,
26
+ extrapolate_last_point=False,
27
+ )
@@ -0,0 +1,22 @@
1
+ from datetime import datetime, timedelta
2
+
3
+ from framcore.timeindexes.SinglePeriodTimeIndex import SinglePeriodTimeIndex # NB! full import path needed for inheritance to work
4
+
5
+
6
+ class ConstantTimeIndex(SinglePeriodTimeIndex):
7
+ """
8
+ ConstantTimeIndex that is constant over time. For use in ConstantTimeVector.
9
+
10
+ Represents a period of 52 weeks starting from the iso calendar week 1 of 1985. Extrapolates both first and last point.
11
+
12
+ """
13
+
14
+ def __init__(self) -> None:
15
+ """Initialize ConstantTimeIndex."""
16
+ super().__init__(
17
+ start_time=datetime.fromisocalendar(1985, 1, 1),
18
+ period_duration=timedelta(weeks=52),
19
+ is_52_week_years=True,
20
+ extrapolate_first_point=True,
21
+ extrapolate_last_point=True,
22
+ )