fram-core 0.0.0__py3-none-any.whl → 0.1.0a1__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.0a1.dist-info/METADATA +41 -0
  2. fram_core-0.1.0a1.dist-info/RECORD +100 -0
  3. {fram_core-0.0.0.dist-info → fram_core-0.1.0a1.dist-info}/WHEEL +1 -2
  4. fram_core-0.1.0a1.dist-info/licenses/LICENSE.md +8 -0
  5. framcore/Base.py +142 -0
  6. framcore/Model.py +73 -0
  7. framcore/__init__.py +9 -0
  8. framcore/aggregators/Aggregator.py +153 -0
  9. framcore/aggregators/HydroAggregator.py +837 -0
  10. framcore/aggregators/NodeAggregator.py +495 -0
  11. framcore/aggregators/WindSolarAggregator.py +323 -0
  12. framcore/aggregators/__init__.py +13 -0
  13. framcore/aggregators/_utils.py +184 -0
  14. framcore/attributes/Arrow.py +305 -0
  15. framcore/attributes/ElasticDemand.py +90 -0
  16. framcore/attributes/ReservoirCurve.py +37 -0
  17. framcore/attributes/SoftBound.py +19 -0
  18. framcore/attributes/StartUpCost.py +54 -0
  19. framcore/attributes/Storage.py +146 -0
  20. framcore/attributes/TargetBound.py +18 -0
  21. framcore/attributes/__init__.py +65 -0
  22. framcore/attributes/hydro/HydroBypass.py +42 -0
  23. framcore/attributes/hydro/HydroGenerator.py +83 -0
  24. framcore/attributes/hydro/HydroPump.py +156 -0
  25. framcore/attributes/hydro/HydroReservoir.py +27 -0
  26. framcore/attributes/hydro/__init__.py +13 -0
  27. framcore/attributes/level_profile_attributes.py +714 -0
  28. framcore/components/Component.py +112 -0
  29. framcore/components/Demand.py +130 -0
  30. framcore/components/Flow.py +167 -0
  31. framcore/components/HydroModule.py +330 -0
  32. framcore/components/Node.py +76 -0
  33. framcore/components/Thermal.py +204 -0
  34. framcore/components/Transmission.py +183 -0
  35. framcore/components/_PowerPlant.py +81 -0
  36. framcore/components/__init__.py +22 -0
  37. framcore/components/wind_solar.py +67 -0
  38. framcore/curves/Curve.py +44 -0
  39. framcore/curves/LoadedCurve.py +155 -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 +490 -0
  44. framcore/expressions/__init__.py +28 -0
  45. framcore/expressions/_get_constant_from_expr.py +483 -0
  46. framcore/expressions/_time_vector_operations.py +615 -0
  47. framcore/expressions/_utils.py +73 -0
  48. framcore/expressions/queries.py +423 -0
  49. framcore/expressions/units.py +207 -0
  50. framcore/fingerprints/__init__.py +11 -0
  51. framcore/fingerprints/fingerprint.py +293 -0
  52. framcore/juliamodels/JuliaModel.py +161 -0
  53. framcore/juliamodels/__init__.py +7 -0
  54. framcore/loaders/__init__.py +10 -0
  55. framcore/loaders/loaders.py +407 -0
  56. framcore/metadata/Div.py +73 -0
  57. framcore/metadata/ExprMeta.py +50 -0
  58. framcore/metadata/LevelExprMeta.py +17 -0
  59. framcore/metadata/Member.py +55 -0
  60. framcore/metadata/Meta.py +44 -0
  61. framcore/metadata/__init__.py +15 -0
  62. framcore/populators/Populator.py +108 -0
  63. framcore/populators/__init__.py +7 -0
  64. framcore/querydbs/CacheDB.py +50 -0
  65. framcore/querydbs/ModelDB.py +34 -0
  66. framcore/querydbs/QueryDB.py +45 -0
  67. framcore/querydbs/__init__.py +11 -0
  68. framcore/solvers/Solver.py +48 -0
  69. framcore/solvers/SolverConfig.py +272 -0
  70. framcore/solvers/__init__.py +9 -0
  71. framcore/timeindexes/AverageYearRange.py +20 -0
  72. framcore/timeindexes/ConstantTimeIndex.py +17 -0
  73. framcore/timeindexes/DailyIndex.py +21 -0
  74. framcore/timeindexes/FixedFrequencyTimeIndex.py +762 -0
  75. framcore/timeindexes/HourlyIndex.py +21 -0
  76. framcore/timeindexes/IsoCalendarDay.py +31 -0
  77. framcore/timeindexes/ListTimeIndex.py +197 -0
  78. framcore/timeindexes/ModelYear.py +17 -0
  79. framcore/timeindexes/ModelYears.py +18 -0
  80. framcore/timeindexes/OneYearProfileTimeIndex.py +21 -0
  81. framcore/timeindexes/ProfileTimeIndex.py +32 -0
  82. framcore/timeindexes/SinglePeriodTimeIndex.py +37 -0
  83. framcore/timeindexes/TimeIndex.py +90 -0
  84. framcore/timeindexes/WeeklyIndex.py +21 -0
  85. framcore/timeindexes/__init__.py +36 -0
  86. framcore/timevectors/ConstantTimeVector.py +135 -0
  87. framcore/timevectors/LinearTransformTimeVector.py +114 -0
  88. framcore/timevectors/ListTimeVector.py +123 -0
  89. framcore/timevectors/LoadedTimeVector.py +104 -0
  90. framcore/timevectors/ReferencePeriod.py +41 -0
  91. framcore/timevectors/TimeVector.py +94 -0
  92. framcore/timevectors/__init__.py +17 -0
  93. framcore/utils/__init__.py +36 -0
  94. framcore/utils/get_regional_volumes.py +369 -0
  95. framcore/utils/get_supported_components.py +60 -0
  96. framcore/utils/global_energy_equivalent.py +46 -0
  97. framcore/utils/isolate_subnodes.py +163 -0
  98. framcore/utils/loaders.py +97 -0
  99. framcore/utils/node_flow_utils.py +236 -0
  100. framcore/utils/storage_subsystems.py +107 -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,293 @@
1
+ from __future__ import annotations
2
+
3
+ import hashlib
4
+ import json
5
+ import pickle
6
+ from enum import Enum
7
+
8
+
9
+ class FingerprintRef:
10
+ """Refers to another fingerprint."""
11
+
12
+ def __init__(self, key: str) -> None:
13
+ """
14
+ Initialize a FingerprintRef with the given key.
15
+
16
+ Args:
17
+ key (str): The key referencing another fingerprint.
18
+
19
+ """
20
+ self._key = key
21
+
22
+ def get_key(self) -> str:
23
+ """
24
+ Return the key referencing another fingerprint.
25
+
26
+ Returns:
27
+ str: The key referencing another fingerprint.
28
+
29
+ """
30
+ return self._key
31
+
32
+
33
+ class FingerprintDiffType(Enum):
34
+ """Type of difference between two fingerprints."""
35
+
36
+ NEW = "new"
37
+ MODIFIED = "modified"
38
+ DELETED = "deleted"
39
+
40
+
41
+ class FingerprintDiff:
42
+ """Differences between two fingerprints."""
43
+
44
+ def __init__(self) -> None:
45
+ """Initialize an empty FingerprintDiff."""
46
+ self._diffs: dict[str, tuple] = {}
47
+
48
+ def add_diff(
49
+ self,
50
+ key: str,
51
+ diff_type: FingerprintDiffType,
52
+ obj: object,
53
+ ) -> None:
54
+ """
55
+ Add a difference entry for a fingerprint.
56
+
57
+ Args:
58
+ key (str): The key identifying the fingerprint part.
59
+ diff_type (FingerprintDiffType): The type of difference (NEW, MODIFIED, DELETED).
60
+ obj: The object associated with the difference.
61
+
62
+ """
63
+ from framcore.components.Component import Component
64
+ from framcore.curves.Curve import Curve
65
+ from framcore.timevectors.TimeVector import TimeVector
66
+
67
+ # Trenger vi denne sjekken, siden vi filtrerer ut alt som ikke er Fingerprint før vi kjører add_diff()?
68
+ if isinstance(obj, TimeVector | Curve | Component):
69
+ if key in self._diffs:
70
+ message = f"duplicate entry: {key} ({obj})"
71
+ print(message)
72
+
73
+ self._diffs[key] = (obj, diff_type)
74
+
75
+ def get_diffs(self) -> dict[str, tuple]:
76
+ """
77
+ Return the dictionary of differences.
78
+
79
+ Returns:
80
+ dict[str, tuple]: The differences stored in the FingerprintDiff.
81
+
82
+ """
83
+ return self._diffs
84
+
85
+ def is_changed(self) -> bool:
86
+ """Return True if there are any differences."""
87
+ return bool(self._diffs)
88
+
89
+ def update(self, other: FingerprintDiff) -> None:
90
+ """
91
+ Update this FingerprintDiff with differences from another FingerprintDiff.
92
+
93
+ Args:
94
+ other (FingerprintDiff): Another FingerprintDiff whose differences will be added.
95
+
96
+ """
97
+ self._diffs.update(other.get_diffs())
98
+
99
+
100
+ class Fingerprint:
101
+ """Fingerprint of various data structures."""
102
+
103
+ def __init__(self, source: object = None) -> None:
104
+ """
105
+ Initialize a Fingerprint instance.
106
+
107
+ Args:
108
+ source (object, optional): The source object to fingerprint. Defaults to None.
109
+
110
+ """
111
+ self._nested = {}
112
+ self._hash = None
113
+ self._source = source
114
+
115
+ def add(self, key: str, value: object) -> None:
116
+ """
117
+ Add a value to the fingerprint under the specified key.
118
+
119
+ Args:
120
+ key (str): The key to associate with the value.
121
+ value: The value to add, which can be a Fingerprint, FingerprintRef, or other supported types.
122
+
123
+ Returns:
124
+ None
125
+
126
+ """
127
+ assert key not in self._nested
128
+
129
+ if isinstance(value, Fingerprint | FingerprintRef):
130
+ self._nested[key] = value
131
+ elif hasattr(value, "get_fingerprint"):
132
+ self.add(key, value.get_fingerprint())
133
+ elif isinstance(value, list | tuple | set):
134
+ self.add(key, self._fingerprint_from_list(value))
135
+ elif isinstance(value, dict):
136
+ self.add(key, self._fingerprint_from_dict(value))
137
+ else:
138
+ self._nested[key] = _custom_hash(value)
139
+
140
+ self._hash = None
141
+
142
+ def _fingerprint_from_list(self, items: list | tuple | set) -> Fingerprint:
143
+ fingerprint = Fingerprint()
144
+ for index, value in enumerate(items):
145
+ fingerprint.add(f"{index}", value)
146
+ return fingerprint
147
+
148
+ def _fingerprint_from_dict(self, a_dict: dict) -> Fingerprint:
149
+ fingerprint = Fingerprint()
150
+ for key, value in a_dict.items():
151
+ fingerprint.add(f"{key}", value)
152
+ return fingerprint
153
+
154
+ def add_ref(self, prop: str, ref_key: str) -> None:
155
+ """
156
+ Add a FingerprintRef to the fingerprint under the specified property key.
157
+
158
+ Args:
159
+ prop (str): The property key to associate with the reference.
160
+ ref_key (str): The key referencing another fingerprint.
161
+
162
+ Returns:
163
+ None
164
+
165
+ """
166
+ self.add(prop, FingerprintRef(ref_key))
167
+
168
+ def get_parts(self) -> dict:
169
+ """
170
+ Return the dictionary of parts contained in the fingerprint.
171
+
172
+ Returns:
173
+ dict: A dictionary mapping keys to their associated fingerprint parts.
174
+
175
+ """
176
+ return {k: v for k, v in self._nested.items()}
177
+
178
+ def update_ref(self, ref_key: str, fingerprint: Fingerprint) -> None:
179
+ """
180
+ Update the reference at the given key with a new Fingerprint.
181
+
182
+ Args:
183
+ ref_key (str): The key referencing the FingerprintRef to update.
184
+ fingerprint (Fingerprint): The new Fingerprint to set at the reference.
185
+
186
+ Returns:
187
+ None
188
+
189
+ """
190
+ assert ref_key in self._nested
191
+ assert isinstance(self._nested[ref_key], FingerprintRef)
192
+
193
+ self._nested[ref_key] = fingerprint
194
+ self._hash = None
195
+
196
+ def get_hash(self) -> str:
197
+ """
198
+ Return the hash value of the fingerprint.
199
+
200
+ Returns:
201
+ str: The computed hash value representing the fingerprint.
202
+
203
+ """
204
+ self._resolve_total_hash()
205
+ return self._hash
206
+
207
+ def _contains_refs(self) -> bool:
208
+ return any(isinstance(v, FingerprintRef) for v in self._nested.values())
209
+
210
+ def _contains_key(self, key: str) -> bool:
211
+ return key in self._nested
212
+
213
+ def _resolve_total_hash(self) -> None:
214
+ parts = []
215
+ for k, v in self._nested.items():
216
+ if isinstance(v, Fingerprint):
217
+ parts.append((k, v.get_hash()))
218
+ elif isinstance(v, FingerprintRef):
219
+ parts.append((k, f"#ref:{v.get_key()}"))
220
+ else:
221
+ parts.append((k, v))
222
+
223
+ self._hash = _custom_hash(sorted(parts))
224
+
225
+ def diff(self, other: Fingerprint | None) -> FingerprintDiff:
226
+ """Return differences between this and other fingerprint."""
227
+ diff = FingerprintDiff()
228
+
229
+ if other is None:
230
+ for parent_key, parent_value in self.get_parts().items():
231
+ if isinstance(parent_value, Fingerprint):
232
+ diff.add_diff(parent_key, FingerprintDiffType.NEW, parent_value._source) # noqa: SLF001
233
+ diff.update(parent_value.diff(None))
234
+ return diff
235
+
236
+ if self.get_hash() == other.get_hash():
237
+ return diff
238
+
239
+ self_parts: dict[str, Fingerprint] = {
240
+ key: value for key, value in self.get_parts().items() if isinstance(value, Fingerprint)
241
+ }
242
+ other_parts: dict[str, Fingerprint] = {
243
+ key: value for key, value in other.get_parts().items() if isinstance(value, Fingerprint)
244
+ }
245
+
246
+ # Check for new or modified keys
247
+ for key, value in self_parts.items():
248
+ if key not in other_parts:
249
+ diff.add_diff(key, FingerprintDiffType.NEW, value._source) # noqa: SLF001
250
+ diff.update(value.diff(None))
251
+ elif value.get_hash() != other_parts[key].get_hash():
252
+ diff.add_diff(key, FingerprintDiffType.MODIFIED, value._source) # noqa: SLF001
253
+ diff.update(value.diff(other_parts[key]))
254
+
255
+ # Check for deleted keys
256
+ for key in other_parts.keys() - self_parts.keys():
257
+ other_value = other_parts[key]
258
+ diff.add_diff(key, FingerprintDiffType.DELETED, other_value._source) # noqa: SLF001
259
+ source = self # TODO: Is this correct?
260
+ diff.update(Fingerprint(source).diff(other_value))
261
+
262
+ return diff
263
+
264
+ def __eq__(self, other: Fingerprint) -> bool:
265
+ """
266
+ Determine if two Fingerprint instances are equal based on their hash values.
267
+
268
+ Args:
269
+ other (Fingerprint): The other Fingerprint instance to compare.
270
+
271
+ Returns:
272
+ bool: True if the hash values are equal, False otherwise.
273
+
274
+ """
275
+ return self.get_hash() == other.get_hash()
276
+
277
+
278
+ def _custom_hash(value: object) -> str:
279
+ """Return hash of value represented as str."""
280
+ if isinstance(value, int | bool | float | None):
281
+ return str(value)
282
+
283
+ if isinstance(value, str):
284
+ return hashlib.sha1(value.encode()).hexdigest()
285
+
286
+ if isinstance(value, list | tuple | set):
287
+ return _custom_hash(str(sorted([_custom_hash(x) for x in value])))
288
+
289
+ if isinstance(value, dict):
290
+ return _custom_hash([(_custom_hash(k), (_custom_hash(v))) for k, v in value.items()])
291
+
292
+ sha1_hash = hashlib.sha1(pickle.dumps(value))
293
+ return sha1_hash.hexdigest()
@@ -0,0 +1,161 @@
1
+ """Manage Julia environment and usage of juliacall."""
2
+
3
+ import importlib
4
+ import os
5
+ import sys
6
+ from pathlib import Path
7
+ from urllib.parse import urlparse
8
+
9
+ from framcore import Base
10
+
11
+ os.environ["JULIA_SSL_CA_ROOTS_PATH"] = ""
12
+ os.environ["SSL_CERT_FILE"] = ""
13
+
14
+
15
+ def _is_url(url_string: str) -> bool:
16
+ """
17
+ Check if a string is a valid url.
18
+
19
+ Args:
20
+ url_string (str): Strong to be validated.
21
+
22
+ Returns:
23
+ bool: True if valid as a url, False if invalid.
24
+
25
+ """
26
+ try:
27
+ result = urlparse(url_string)
28
+ return all([result.scheme, result.netloc])
29
+ except ValueError:
30
+ return False
31
+
32
+
33
+ class JuliaModel(Base):
34
+ """Class for running julia code with juliacall."""
35
+
36
+ ENV_NAME: str = "julia_env" # Used to let each model define their own project/environment to avoid overwriting.
37
+ _jl = None
38
+
39
+ def __init__(
40
+ self,
41
+ env_path: Path | str | None = None,
42
+ depot_path: Path | str | None = None,
43
+ julia_path: Path | str | None = None,
44
+ dependencies: list[str | tuple[str, str | None]] | None = None,
45
+ skip_install_dependencies: bool = False,
46
+ ) -> None:
47
+ """
48
+ Initialize management of Julia model, environment and dependencies.
49
+
50
+ The three parameters env_path, depot_path and julia_path sets environment variables for locations of your Julia
51
+ environment, packages and language.
52
+ - If user has not specified locations, the default is to use the current python/conda environment.
53
+ - If a system installation of Python is used, the default is set to the current user location.
54
+
55
+ Args:
56
+ env_path (Path | str | None, optional): Path to location of Julia environment. If it doesnt exist it will be
57
+ created. Defaults to None.
58
+ depot_path (Path | str | None, optional): Path to location where JuliaCall shoult install package
59
+ dependencies. Defaults to None.
60
+ julia_path (Path | str | None, optional): Path to Julia language location. Will be installed here if it
61
+ doesnt exist. Defaults to None.
62
+ dependencies (list[str] | None, optional): List of dependencies of the model. The strings in the list can be
63
+ either urls or Julia package names.. Defaults to None.
64
+
65
+ """
66
+ self._check_type(env_path, (Path, str, type(None)))
67
+ self._check_type(depot_path, (Path, str, type(None)))
68
+ self._check_type(julia_path, (Path, str, type(None)))
69
+ self._check_type(dependencies, (list, str, type(None)))
70
+ self._check_type(skip_install_dependencies, bool)
71
+
72
+ self._env_path = env_path
73
+ self._depot_path = depot_path
74
+ self._julia_path = julia_path
75
+ self._dependencies = dependencies if dependencies else []
76
+ self._skip_install_dependencies = skip_install_dependencies
77
+
78
+ self._jlpkg = None
79
+ self._initialize_julia()
80
+
81
+ def _initialize_julia(self) -> None:
82
+ """Initialize Julia language, package depot, and environment with JuliaCall."""
83
+ if self._jl is not None:
84
+ return
85
+
86
+ # figure out what kind of environment we are in
87
+ prefix = sys.prefix if sys.prefix != sys.base_prefix else os.getenv("CONDA_PREFIX")
88
+ # we have python system installation
89
+ project = Path("~/.julia").expanduser() if prefix is None else prefix
90
+
91
+ self._env_path = str(Path(project) / "julia_envs" / self.ENV_NAME) if not self._env_path else str(self._env_path)
92
+ self._depot_path = str(Path(project) / "julia_pkgs") if not self._depot_path else str(self._depot_path)
93
+
94
+ os.environ["PYTHON_JULIAPKG_PROJECT"] = self._env_path
95
+ os.environ["JULIA_DEPOT_PATH"] = self._depot_path
96
+ if self._julia_path: # If Julia path is not set, let JuliaCall handle defaults.
97
+ os.environ["PYTHON_JULIAPKG_EXE"] = str(self._julia_path)
98
+
99
+ juliacall = importlib.import_module("juliacall")
100
+ JuliaModel._jl = juliacall.Main
101
+ self._jlpkg = juliacall.Pkg
102
+
103
+ self._jlpkg.activate(str(self._env_path))
104
+
105
+ if not self._skip_install_dependencies:
106
+ self._install_dependencies()
107
+
108
+ # Print sysimage Julia
109
+ try:
110
+ path_sysimage = self._jl.seval("unsafe_string(Base.JLOptions().image_file)")
111
+ message = f"path_sysimage: {path_sysimage}"
112
+ self.send_debug_event(message)
113
+ except Exception:
114
+ pass
115
+
116
+ def _install_dependencies(self) -> None:
117
+ """Install dependencies."""
118
+ # if (Path(self._env_path) / Path("Manifest.toml")).exists():
119
+ # print("Manifest found, assuming environment is already initialized.")
120
+ # return
121
+
122
+ url_tuples = [p for p in self._dependencies if isinstance(p, tuple) and _is_url(p[0])]
123
+ urls = [p for p in self._dependencies if isinstance(p, str) and _is_url(p)]
124
+ dev_paths = [p for p in self._dependencies if isinstance(p, str) and Path(p).exists()]
125
+ pkg_names = [p for p in self._dependencies if isinstance(p, str) and not _is_url(p) and not Path(p).exists()]
126
+
127
+ unknowns = [p for p in self._dependencies if not (p in url_tuples or p in urls or p in pkg_names or p in dev_paths)]
128
+
129
+ if unknowns:
130
+ messages = []
131
+ for p in unknowns:
132
+ messages.append(
133
+ (
134
+ f"Unsupported julia package definition: '{p}' of type '{type(p)}' is not supported. "
135
+ "Must be defined as either str or tuple[str, str | None]"
136
+ ),
137
+ )
138
+ message = "\n".join(messages)
139
+ raise ValueError(message)
140
+
141
+ self._jl.seval("using Pkg")
142
+
143
+ pkg_spec_vector = self._jl.seval("x = Pkg.PackageSpec[]")
144
+
145
+ for url, rev in url_tuples:
146
+ self._jl.seval(f'push!(x, Pkg.PackageSpec(url="{url}", rev="{rev}"))')
147
+
148
+ for url in urls:
149
+ self._jl.seval(f'push!(x, Pkg.PackageSpec(url="{url}))"')
150
+
151
+ for pkg_name in pkg_names:
152
+ self._jl.seval(f'push!(x, Pkg.PackageSpec(name="{pkg_name}"))')
153
+
154
+ self._jlpkg.add(pkg_spec_vector)
155
+
156
+ for dev_path in dev_paths:
157
+ self._jl.seval(f'Pkg.develop(path="{dev_path}")')
158
+
159
+ def _run(self, julia_code: str) -> None:
160
+ """Run a string of julia code wich is supposed to start running the Julia Model in the given environment."""
161
+ self._jl.seval(julia_code)
@@ -0,0 +1,7 @@
1
+ # framcore/juliamodels/__init__.py
2
+
3
+ from framcore.juliamodels.JuliaModel import JuliaModel
4
+
5
+ __all__ = [
6
+ "JuliaModel",
7
+ ]
@@ -0,0 +1,10 @@
1
+ # framcore/loaders/__init__.py
2
+
3
+ from framcore.loaders.loaders import CurveLoader, FileLoader, Loader, TimeVectorLoader
4
+
5
+ __all__ = [
6
+ "CurveLoader",
7
+ "FileLoader",
8
+ "Loader",
9
+ "TimeVectorLoader",
10
+ ]