fram-core 0.0.0__py3-none-any.whl → 0.1.0a2__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.
- fram_core-0.1.0a2.dist-info/METADATA +42 -0
- fram_core-0.1.0a2.dist-info/RECORD +100 -0
- {fram_core-0.0.0.dist-info → fram_core-0.1.0a2.dist-info}/WHEEL +1 -2
- fram_core-0.1.0a2.dist-info/licenses/LICENSE.md +8 -0
- framcore/Base.py +142 -0
- framcore/Model.py +73 -0
- framcore/__init__.py +9 -0
- framcore/aggregators/Aggregator.py +153 -0
- framcore/aggregators/HydroAggregator.py +837 -0
- framcore/aggregators/NodeAggregator.py +495 -0
- framcore/aggregators/WindSolarAggregator.py +323 -0
- framcore/aggregators/__init__.py +13 -0
- framcore/aggregators/_utils.py +184 -0
- framcore/attributes/Arrow.py +305 -0
- framcore/attributes/ElasticDemand.py +90 -0
- framcore/attributes/ReservoirCurve.py +37 -0
- framcore/attributes/SoftBound.py +19 -0
- framcore/attributes/StartUpCost.py +54 -0
- framcore/attributes/Storage.py +146 -0
- framcore/attributes/TargetBound.py +18 -0
- framcore/attributes/__init__.py +65 -0
- framcore/attributes/hydro/HydroBypass.py +42 -0
- framcore/attributes/hydro/HydroGenerator.py +83 -0
- framcore/attributes/hydro/HydroPump.py +156 -0
- framcore/attributes/hydro/HydroReservoir.py +27 -0
- framcore/attributes/hydro/__init__.py +13 -0
- framcore/attributes/level_profile_attributes.py +714 -0
- framcore/components/Component.py +112 -0
- framcore/components/Demand.py +130 -0
- framcore/components/Flow.py +167 -0
- framcore/components/HydroModule.py +330 -0
- framcore/components/Node.py +76 -0
- framcore/components/Thermal.py +204 -0
- framcore/components/Transmission.py +183 -0
- framcore/components/_PowerPlant.py +81 -0
- framcore/components/__init__.py +22 -0
- framcore/components/wind_solar.py +67 -0
- framcore/curves/Curve.py +44 -0
- framcore/curves/LoadedCurve.py +155 -0
- framcore/curves/__init__.py +9 -0
- framcore/events/__init__.py +21 -0
- framcore/events/events.py +51 -0
- framcore/expressions/Expr.py +490 -0
- framcore/expressions/__init__.py +28 -0
- framcore/expressions/_get_constant_from_expr.py +483 -0
- framcore/expressions/_time_vector_operations.py +615 -0
- framcore/expressions/_utils.py +73 -0
- framcore/expressions/queries.py +423 -0
- framcore/expressions/units.py +207 -0
- framcore/fingerprints/__init__.py +11 -0
- framcore/fingerprints/fingerprint.py +293 -0
- framcore/juliamodels/JuliaModel.py +161 -0
- framcore/juliamodels/__init__.py +7 -0
- framcore/loaders/__init__.py +10 -0
- framcore/loaders/loaders.py +407 -0
- framcore/metadata/Div.py +73 -0
- framcore/metadata/ExprMeta.py +50 -0
- framcore/metadata/LevelExprMeta.py +17 -0
- framcore/metadata/Member.py +55 -0
- framcore/metadata/Meta.py +44 -0
- framcore/metadata/__init__.py +15 -0
- framcore/populators/Populator.py +108 -0
- framcore/populators/__init__.py +7 -0
- framcore/querydbs/CacheDB.py +50 -0
- framcore/querydbs/ModelDB.py +34 -0
- framcore/querydbs/QueryDB.py +45 -0
- framcore/querydbs/__init__.py +11 -0
- framcore/solvers/Solver.py +48 -0
- framcore/solvers/SolverConfig.py +272 -0
- framcore/solvers/__init__.py +9 -0
- framcore/timeindexes/AverageYearRange.py +20 -0
- framcore/timeindexes/ConstantTimeIndex.py +17 -0
- framcore/timeindexes/DailyIndex.py +21 -0
- framcore/timeindexes/FixedFrequencyTimeIndex.py +762 -0
- framcore/timeindexes/HourlyIndex.py +21 -0
- framcore/timeindexes/IsoCalendarDay.py +31 -0
- framcore/timeindexes/ListTimeIndex.py +197 -0
- framcore/timeindexes/ModelYear.py +17 -0
- framcore/timeindexes/ModelYears.py +18 -0
- framcore/timeindexes/OneYearProfileTimeIndex.py +21 -0
- framcore/timeindexes/ProfileTimeIndex.py +32 -0
- framcore/timeindexes/SinglePeriodTimeIndex.py +37 -0
- framcore/timeindexes/TimeIndex.py +90 -0
- framcore/timeindexes/WeeklyIndex.py +21 -0
- framcore/timeindexes/__init__.py +36 -0
- framcore/timevectors/ConstantTimeVector.py +135 -0
- framcore/timevectors/LinearTransformTimeVector.py +114 -0
- framcore/timevectors/ListTimeVector.py +123 -0
- framcore/timevectors/LoadedTimeVector.py +104 -0
- framcore/timevectors/ReferencePeriod.py +41 -0
- framcore/timevectors/TimeVector.py +94 -0
- framcore/timevectors/__init__.py +17 -0
- framcore/utils/__init__.py +36 -0
- framcore/utils/get_regional_volumes.py +369 -0
- framcore/utils/get_supported_components.py +60 -0
- framcore/utils/global_energy_equivalent.py +46 -0
- framcore/utils/isolate_subnodes.py +163 -0
- framcore/utils/loaders.py +97 -0
- framcore/utils/node_flow_utils.py +236 -0
- framcore/utils/storage_subsystems.py +107 -0
- fram_core-0.0.0.dist-info/METADATA +0 -5
- fram_core-0.0.0.dist-info/RECORD +0 -4
- 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,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(lower_bound=0.0)
|
|
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,48 @@
|
|
|
1
|
+
"""Definition of Solver interface."""
|
|
2
|
+
|
|
3
|
+
import pickle
|
|
4
|
+
from abc import ABC, abstractmethod
|
|
5
|
+
from copy import deepcopy
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from framcore import Base, Model
|
|
9
|
+
from framcore.solvers import SolverConfig
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Solver(Base, ABC):
|
|
13
|
+
"""Solver inteface class."""
|
|
14
|
+
|
|
15
|
+
_FILENAME_MODEL = "model.pickle"
|
|
16
|
+
_FILENAME_SOLVER = "solver.pickle"
|
|
17
|
+
|
|
18
|
+
def solve(self, model: Model) -> None:
|
|
19
|
+
"""Solve the models. Use folder to write results."""
|
|
20
|
+
self._check_type(model, Model)
|
|
21
|
+
|
|
22
|
+
config = self.get_config()
|
|
23
|
+
|
|
24
|
+
folder = config.get_solve_folder()
|
|
25
|
+
|
|
26
|
+
assert folder is not None, "use Solver.get_config().set_solve_folder(folder)"
|
|
27
|
+
|
|
28
|
+
Path.mkdir(folder, parents=True, exist_ok=True)
|
|
29
|
+
|
|
30
|
+
self._solve(folder, model)
|
|
31
|
+
|
|
32
|
+
with Path.open(folder / self._FILENAME_MODEL, "wb") as f:
|
|
33
|
+
pickle.dump(model, f)
|
|
34
|
+
|
|
35
|
+
c = deepcopy(self)
|
|
36
|
+
c.get_config().set_solve_folder(None)
|
|
37
|
+
with Path.open(folder / self._FILENAME_SOLVER, "wb") as f:
|
|
38
|
+
pickle.dump(c, f)
|
|
39
|
+
|
|
40
|
+
@abstractmethod
|
|
41
|
+
def get_config(self) -> SolverConfig:
|
|
42
|
+
"""Return the solver's config object."""
|
|
43
|
+
pass
|
|
44
|
+
|
|
45
|
+
@abstractmethod
|
|
46
|
+
def _solve(self, folder: Path, model: Model) -> None:
|
|
47
|
+
"""Solve the model inplace. Write to folder."""
|
|
48
|
+
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 JulES 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 JulES 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 JulES to stdout and logfile."""
|
|
170
|
+
self._show_screen_output = True
|
|
171
|
+
|
|
172
|
+
def set_screen_output_off(self) -> None:
|
|
173
|
+
"""Only print output from JulES 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,20 @@
|
|
|
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 an average over a range of years."""
|
|
8
|
+
|
|
9
|
+
def __init__(self, start_year: int, num_years: int) -> None:
|
|
10
|
+
"""Initialize AverageYearRange with a year range."""
|
|
11
|
+
start_time = datetime.fromisocalendar(start_year, 1, 1)
|
|
12
|
+
end_time = datetime.fromisocalendar(start_year + num_years, 1, 1)
|
|
13
|
+
period_duration = end_time - start_time
|
|
14
|
+
super().__init__(
|
|
15
|
+
start_time=start_time,
|
|
16
|
+
period_duration=period_duration,
|
|
17
|
+
is_52_week_years=False,
|
|
18
|
+
extrapolate_first_point=False,
|
|
19
|
+
extrapolate_last_point=False,
|
|
20
|
+
)
|
|
@@ -0,0 +1,17 @@
|
|
|
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
|
+
"""Used in ConstantTimeVector."""
|
|
8
|
+
|
|
9
|
+
def __init__(self) -> None:
|
|
10
|
+
"""Represent a specified year."""
|
|
11
|
+
super().__init__(
|
|
12
|
+
start_time=datetime.fromisocalendar(1985, 1, 1),
|
|
13
|
+
period_duration=timedelta(weeks=52),
|
|
14
|
+
is_52_week_years=True,
|
|
15
|
+
extrapolate_first_point=True,
|
|
16
|
+
extrapolate_last_point=True,
|
|
17
|
+
)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from datetime import timedelta
|
|
2
|
+
|
|
3
|
+
from framcore.timeindexes.ProfileTimeIndex import ProfileTimeIndex # NB! full import path needed for inheritance to work
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class DailyIndex(ProfileTimeIndex):
|
|
7
|
+
"""One or more whole years with daily resolution."""
|
|
8
|
+
|
|
9
|
+
def __init__(
|
|
10
|
+
self,
|
|
11
|
+
start_year: int,
|
|
12
|
+
num_years: int,
|
|
13
|
+
is_52_week_years: bool = True,
|
|
14
|
+
) -> None:
|
|
15
|
+
"""One or more whole years with daily resolution."""
|
|
16
|
+
super().__init__(
|
|
17
|
+
start_year=start_year,
|
|
18
|
+
num_years=num_years,
|
|
19
|
+
period_duration=timedelta(days=1),
|
|
20
|
+
is_52_week_years=is_52_week_years,
|
|
21
|
+
)
|