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 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