pyghe 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.
- pyghe/__init__.py +46 -0
- pyghe/_base.py +278 -0
- pyghe/_case.py +536 -0
- pyghe/_cli.py +396 -0
- pyghe/batch.py +267 -0
- pyghe/borehole_resistance.py +66 -0
- pyghe/building_loads.py +52 -0
- pyghe/errors.py +56 -0
- pyghe/ground_heat_exchanger.py +111 -0
- pyghe/gshp_system.py +34 -0
- pyghe/heat_carrier_fluid.py +13 -0
- pyghe/heat_pump_performance.py +96 -0
- pyghe/project.py +644 -0
- pyghe/results.py +474 -0
- pyghe-0.1.0.dist-info/METADATA +154 -0
- pyghe-0.1.0.dist-info/RECORD +19 -0
- pyghe-0.1.0.dist-info/WHEEL +5 -0
- pyghe-0.1.0.dist-info/licenses/LICENSE +21 -0
- pyghe-0.1.0.dist-info/top_level.txt +1 -0
pyghe/__init__.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""Python interface to the GHE module of Analysis Suite.
|
|
2
|
+
|
|
3
|
+
import pyghe as ga
|
|
4
|
+
p = ga.Project()
|
|
5
|
+
p.ground_heat_exchanger.ground_thermal_conductivity = 2.5
|
|
6
|
+
...
|
|
7
|
+
result = p.simulate()
|
|
8
|
+
|
|
9
|
+
Names and units follow the interface of GHE Analysis (SI): each attribute of a Project is a
|
|
10
|
+
node of the Analysis Tree. Every number is computed by the application's kernel through
|
|
11
|
+
AnalysisSuite-cli.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from ._base import ByMode
|
|
15
|
+
from ._case import DEFAULT_SERIES, SERIES
|
|
16
|
+
from ._cli import close_servers, configure, find_cli
|
|
17
|
+
from .batch import run_many, sweep
|
|
18
|
+
from .borehole_resistance import Coaxial, DoubleULoopA, DoubleULoopD, SingleULoop, StandingColumn
|
|
19
|
+
from .building_loads import (HourlyBuildingLoadsCombined, HourlyBuildingLoadsSeparate, HourlyGroundLoadsCombined,
|
|
20
|
+
MonthlyBuildingLoadsSeparate)
|
|
21
|
+
from .errors import (CliError, CliNotFoundError, ConvergenceWarning, PygheError, InvalidInputError,
|
|
22
|
+
KernelError, KernelWarning, LicenseError, NotConvergedError, OutsideInterfaceWarning,
|
|
23
|
+
SeriesNotRequested)
|
|
24
|
+
from .ground_heat_exchanger import Coordinates, GroundHeatExchanger
|
|
25
|
+
from .gshp_system import SYSTEM_TYPES, GSHPSystem
|
|
26
|
+
from .heat_carrier_fluid import FLUIDS, HeatCarrierFluid
|
|
27
|
+
from .heat_pump_performance import HeatPumpPerformance, ManufacturerData, PerformanceConstant, PerformanceTemperature
|
|
28
|
+
from .project import PLUME_READINGS, Project, ProjectDescription, SizingTool, fluid_properties
|
|
29
|
+
from .results import (BoreholeResistanceResult, FluidProperties, GFunctionResult, Result, SimulationResult,
|
|
30
|
+
SizingResult, ThermalPlume2DResult, ThermalPlume3DResult, load_result)
|
|
31
|
+
|
|
32
|
+
__version__ = "0.1.0"
|
|
33
|
+
|
|
34
|
+
__all__ = [
|
|
35
|
+
"BoreholeResistanceResult", "ByMode", "CliError", "CliNotFoundError", "Coaxial",
|
|
36
|
+
"ConvergenceWarning", "Coordinates", "DEFAULT_SERIES", "DoubleULoopA", "DoubleULoopD",
|
|
37
|
+
"FLUIDS", "FluidProperties", "GFunctionResult", "GSHPSystem", "PygheError",
|
|
38
|
+
"GroundHeatExchanger", "HeatCarrierFluid", "HeatPumpPerformance",
|
|
39
|
+
"HourlyBuildingLoadsCombined", "HourlyBuildingLoadsSeparate", "HourlyGroundLoadsCombined",
|
|
40
|
+
"InvalidInputError", "KernelError", "KernelWarning", "LicenseError", "ManufacturerData",
|
|
41
|
+
"MonthlyBuildingLoadsSeparate", "NotConvergedError", "OutsideInterfaceWarning", "PLUME_READINGS",
|
|
42
|
+
"PerformanceConstant", "PerformanceTemperature", "Project", "ProjectDescription", "Result", "SERIES", "SYSTEM_TYPES",
|
|
43
|
+
"SeriesNotRequested", "SimulationResult", "SingleULoop", "SizingResult", "SizingTool",
|
|
44
|
+
"StandingColumn", "ThermalPlume2DResult", "ThermalPlume3DResult", "close_servers", "configure",
|
|
45
|
+
"find_cli", "fluid_properties", "load_result", "run_many", "sweep",
|
|
46
|
+
]
|
pyghe/_base.py
ADDED
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
"""Common machinery of the project's nodes: slots, validation on assignment, units, repr."""
|
|
2
|
+
|
|
3
|
+
import dataclasses
|
|
4
|
+
import math
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
|
|
8
|
+
from .errors import InvalidInputError
|
|
9
|
+
|
|
10
|
+
HOURS_PER_YEAR = 8760
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def param(unit="", check=None, default=None, factory=None, symbol=""):
|
|
14
|
+
"""A node field: its unit and symbol as the interface shows them (both printed by repr)
|
|
15
|
+
and the check run on every assignment. None always means 'not set' and is never checked."""
|
|
16
|
+
meta = {"unit": unit, "check": check, "symbol": symbol}
|
|
17
|
+
if factory is not None:
|
|
18
|
+
return dataclasses.field(default_factory=factory, metadata=meta)
|
|
19
|
+
return dataclasses.field(default=default, metadata=meta)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def private():
|
|
23
|
+
"""A field the user does not set: not a constructor argument, not compared, not printed."""
|
|
24
|
+
return dataclasses.field(default=None, init=False, metadata={"private": True})
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _public(obj):
|
|
28
|
+
return [f for f in dataclasses.fields(obj) if not f.metadata.get("private")]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
_checks_cache = {}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _checks_of(cls):
|
|
35
|
+
checks = _checks_cache.get(cls)
|
|
36
|
+
if checks is None:
|
|
37
|
+
checks = {f.name: f.metadata.get("check") for f in dataclasses.fields(cls)}
|
|
38
|
+
_checks_cache[cls] = checks
|
|
39
|
+
return checks
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class Node:
|
|
43
|
+
"""Base of every node. Subclasses are dataclasses with slots, so that assigning a
|
|
44
|
+
misspelt attribute raises AttributeError instead of creating a new field."""
|
|
45
|
+
|
|
46
|
+
__slots__ = ()
|
|
47
|
+
|
|
48
|
+
def __setattr__(self, name, value):
|
|
49
|
+
check = _checks_of(type(self)).get(name)
|
|
50
|
+
if check is not None and value is not None:
|
|
51
|
+
value = check(value, f"{type(self).__name__}.{name}")
|
|
52
|
+
object.__setattr__(self, name, value)
|
|
53
|
+
|
|
54
|
+
def __eq__(self, other):
|
|
55
|
+
if type(other) is not type(self):
|
|
56
|
+
return NotImplemented
|
|
57
|
+
for f in _public(self):
|
|
58
|
+
a, b = getattr(self, f.name), getattr(other, f.name)
|
|
59
|
+
if isinstance(a, np.ndarray) or isinstance(b, np.ndarray):
|
|
60
|
+
if a is None or b is None or not np.array_equal(a, b, equal_nan=True):
|
|
61
|
+
return False
|
|
62
|
+
elif a != b:
|
|
63
|
+
return False
|
|
64
|
+
return True
|
|
65
|
+
|
|
66
|
+
__hash__ = None
|
|
67
|
+
|
|
68
|
+
def __repr__(self):
|
|
69
|
+
return "\n".join(self._describe())
|
|
70
|
+
|
|
71
|
+
def _describe(self, indent=""):
|
|
72
|
+
lines = [f"{indent}{type(self).__name__}"]
|
|
73
|
+
for f in _public(self):
|
|
74
|
+
value = getattr(self, f.name)
|
|
75
|
+
symbol = f.metadata.get("symbol", "")
|
|
76
|
+
name = f"{f.name} ({symbol})" if symbol else f.name
|
|
77
|
+
if isinstance(value, Node):
|
|
78
|
+
sub = value._describe(indent + " ")
|
|
79
|
+
lines.append(f"{indent} {name}: {sub[0].strip()}")
|
|
80
|
+
lines.extend(sub[1:])
|
|
81
|
+
elif isinstance(value, ByMode):
|
|
82
|
+
lines.append(f"{indent} {name} = {value!r}")
|
|
83
|
+
else:
|
|
84
|
+
lines.append(f"{indent} {name} = {_show(value, f.metadata.get('unit', ''))}")
|
|
85
|
+
return lines
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _show(value, unit):
|
|
89
|
+
if value is None:
|
|
90
|
+
return "(not set)"
|
|
91
|
+
if isinstance(value, np.ndarray):
|
|
92
|
+
if value.size == 0:
|
|
93
|
+
text = "[] (0 values)"
|
|
94
|
+
else:
|
|
95
|
+
text = f"[{value.min():.6g} … {value.max():.6g}] ({value.size} values)"
|
|
96
|
+
else:
|
|
97
|
+
text = repr(value)
|
|
98
|
+
return f"{text} {unit}".rstrip()
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
# Checks. Each returns the value to store, converted, or raises InvalidInputError naming the
|
|
102
|
+
# attribute. Those taking settings are classes, not closures: a ByMode keeps its check, and
|
|
103
|
+
# pickle (multiprocessing, joblib) refuses local functions.
|
|
104
|
+
|
|
105
|
+
def real(value, where):
|
|
106
|
+
if isinstance(value, bool):
|
|
107
|
+
raise InvalidInputError(f"{where} must be a number, got {value!r}")
|
|
108
|
+
try:
|
|
109
|
+
x = float(value)
|
|
110
|
+
except (TypeError, ValueError):
|
|
111
|
+
raise InvalidInputError(f"{where} must be a number, got {value!r}") from None
|
|
112
|
+
if not math.isfinite(x):
|
|
113
|
+
raise InvalidInputError(f"{where} must be finite, got {x}")
|
|
114
|
+
return x
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def positive(value, where):
|
|
118
|
+
x = real(value, where)
|
|
119
|
+
if x <= 0:
|
|
120
|
+
raise InvalidInputError(f"{where} must be > 0, got {x}")
|
|
121
|
+
return x
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def non_negative(value, where):
|
|
125
|
+
x = real(value, where)
|
|
126
|
+
if x < 0:
|
|
127
|
+
raise InvalidInputError(f"{where} must be >= 0, got {x}")
|
|
128
|
+
return x
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class between:
|
|
132
|
+
def __init__(self, low, high, low_open=False, high_open=False):
|
|
133
|
+
self.low, self.high, self.low_open, self.high_open = low, high, low_open, high_open
|
|
134
|
+
|
|
135
|
+
def __call__(self, value, where):
|
|
136
|
+
x = real(value, where)
|
|
137
|
+
low, high = self.low, self.high
|
|
138
|
+
if (x < low or (self.low_open and x == low)) or (x > high or (self.high_open and x == high)):
|
|
139
|
+
lo = "(" if self.low_open else "["
|
|
140
|
+
hi = ")" if self.high_open else "]"
|
|
141
|
+
raise InvalidInputError(f"{where} must lie in {lo}{low}, {high}{hi}, got {x}")
|
|
142
|
+
return x
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class integer:
|
|
146
|
+
def __init__(self, minimum=None, choices=None):
|
|
147
|
+
self.minimum, self.choices = minimum, choices
|
|
148
|
+
|
|
149
|
+
def __call__(self, value, where):
|
|
150
|
+
if isinstance(value, bool) or not float(real(value, where)).is_integer():
|
|
151
|
+
raise InvalidInputError(f"{where} must be an integer, got {value!r}")
|
|
152
|
+
n = int(value)
|
|
153
|
+
if self.minimum is not None and n < self.minimum:
|
|
154
|
+
raise InvalidInputError(f"{where} must be >= {self.minimum}, got {n}")
|
|
155
|
+
if self.choices is not None and n not in self.choices:
|
|
156
|
+
raise InvalidInputError(f"{where} must be one of {', '.join(map(str, self.choices))}, got {n}")
|
|
157
|
+
return n
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
class one_of:
|
|
161
|
+
def __init__(self, *choices):
|
|
162
|
+
self.choices = choices
|
|
163
|
+
|
|
164
|
+
def __call__(self, value, where):
|
|
165
|
+
if value not in self.choices:
|
|
166
|
+
raise InvalidInputError(f"{where} must be one of {', '.join(map(repr, self.choices))}, got {value!r}")
|
|
167
|
+
return value
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
class instance_of:
|
|
171
|
+
def __init__(self, *classes):
|
|
172
|
+
self.classes = classes
|
|
173
|
+
|
|
174
|
+
def __call__(self, value, where):
|
|
175
|
+
if not isinstance(value, self.classes):
|
|
176
|
+
names = " or ".join(c.__name__ for c in self.classes)
|
|
177
|
+
raise InvalidInputError(f"{where} must be a {names}, got {type(value).__name__}")
|
|
178
|
+
return value
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
class series:
|
|
182
|
+
"""A read-only float64 array. Reassign the attribute to change it: an array edited in place
|
|
183
|
+
would bypass the checks."""
|
|
184
|
+
|
|
185
|
+
def __init__(self, length=None, minimum_length=1, allow_nan=False):
|
|
186
|
+
self.length, self.minimum_length, self.allow_nan = length, minimum_length, allow_nan
|
|
187
|
+
|
|
188
|
+
def __call__(self, value, where):
|
|
189
|
+
try:
|
|
190
|
+
a = np.array(value, dtype=np.float64, copy=True)
|
|
191
|
+
except (TypeError, ValueError):
|
|
192
|
+
raise InvalidInputError(f"{where} must be an array of numbers") from None
|
|
193
|
+
if a.ndim != 1:
|
|
194
|
+
raise InvalidInputError(f"{where} must be one-dimensional, got shape {a.shape}")
|
|
195
|
+
if self.length is not None and a.size != self.length:
|
|
196
|
+
raise InvalidInputError(f"{where} must hold {self.length} values, got {a.size}")
|
|
197
|
+
if a.size < self.minimum_length:
|
|
198
|
+
raise InvalidInputError(f"{where} must hold at least {self.minimum_length} value(s)")
|
|
199
|
+
bad = ~np.isfinite(a) if not self.allow_nan else np.isinf(a)
|
|
200
|
+
if bad.any():
|
|
201
|
+
raise InvalidInputError(f"{where}[{int(np.argmax(bad))}] is not finite")
|
|
202
|
+
a.setflags(write=False)
|
|
203
|
+
return a
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def node(dc=None, kw_only=True):
|
|
207
|
+
"""Decorator: a slotted keyword-only dataclass deriving from Node."""
|
|
208
|
+
def wrap(cls):
|
|
209
|
+
return dataclasses.dataclass(slots=True, kw_only=kw_only, repr=False, eq=False)(cls)
|
|
210
|
+
return wrap(dc) if dc is not None else wrap
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
class ByMode:
|
|
214
|
+
"""A pair of values, one for heating and one for cooling. Assigned to a node, it takes the
|
|
215
|
+
unit and the check of that attribute, which then apply to each of its two values."""
|
|
216
|
+
|
|
217
|
+
__slots__ = ("heating", "cooling", "_check", "_unit", "_where")
|
|
218
|
+
|
|
219
|
+
def __init__(self, heating=None, cooling=None):
|
|
220
|
+
object.__setattr__(self, "_check", None)
|
|
221
|
+
object.__setattr__(self, "_unit", "")
|
|
222
|
+
object.__setattr__(self, "_where", "ByMode")
|
|
223
|
+
self.heating = heating
|
|
224
|
+
self.cooling = cooling
|
|
225
|
+
|
|
226
|
+
def __setattr__(self, name, value):
|
|
227
|
+
if name in ("heating", "cooling") and value is not None and self._check is not None:
|
|
228
|
+
value = self._check(value, f"{self._where}.{name}")
|
|
229
|
+
object.__setattr__(self, name, value)
|
|
230
|
+
|
|
231
|
+
def __eq__(self, other):
|
|
232
|
+
return (isinstance(other, ByMode) and self.heating == other.heating
|
|
233
|
+
and self.cooling == other.cooling)
|
|
234
|
+
|
|
235
|
+
def __deepcopy__(self, memo):
|
|
236
|
+
twin = ByMode()
|
|
237
|
+
for name in self.__slots__:
|
|
238
|
+
object.__setattr__(twin, name, getattr(self, name))
|
|
239
|
+
return twin
|
|
240
|
+
|
|
241
|
+
def __reduce__(self):
|
|
242
|
+
return _restore_by_mode, tuple(getattr(self, name) for name in self.__slots__)
|
|
243
|
+
|
|
244
|
+
def __repr__(self):
|
|
245
|
+
return (f"ByMode(heating={_show(self.heating, self._unit)}, "
|
|
246
|
+
f"cooling={_show(self.cooling, self._unit)})")
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def _restore_by_mode(*values):
|
|
250
|
+
pair = ByMode()
|
|
251
|
+
for name, value in zip(ByMode.__slots__, values):
|
|
252
|
+
object.__setattr__(pair, name, value)
|
|
253
|
+
return pair
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
class by_mode:
|
|
257
|
+
"""Check of an attribute holding a ByMode. Accepts a ByMode, a (heating, cooling) pair or
|
|
258
|
+
a dict with those keys; returns a new ByMode bound to check and unit."""
|
|
259
|
+
|
|
260
|
+
def __init__(self, check, unit=""):
|
|
261
|
+
self.check, self.unit = check, unit
|
|
262
|
+
|
|
263
|
+
def __call__(self, value, where):
|
|
264
|
+
if isinstance(value, ByMode):
|
|
265
|
+
h, c = value.heating, value.cooling
|
|
266
|
+
elif isinstance(value, dict) and set(value) <= {"heating", "cooling"}:
|
|
267
|
+
h, c = value.get("heating"), value.get("cooling")
|
|
268
|
+
elif isinstance(value, (tuple, list)) and len(value) == 2:
|
|
269
|
+
h, c = value
|
|
270
|
+
else:
|
|
271
|
+
raise InvalidInputError(f"{where} must be a ByMode(heating=..., cooling=...)")
|
|
272
|
+
pair = ByMode()
|
|
273
|
+
object.__setattr__(pair, "_check", self.check)
|
|
274
|
+
object.__setattr__(pair, "_unit", self.unit)
|
|
275
|
+
object.__setattr__(pair, "_where", where)
|
|
276
|
+
pair.heating = h
|
|
277
|
+
pair.cooling = c
|
|
278
|
+
return pair
|