pyghe 0.1.0__tar.gz
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-0.1.0/LICENSE +21 -0
- pyghe-0.1.0/PKG-INFO +154 -0
- pyghe-0.1.0/README.md +114 -0
- pyghe-0.1.0/pyproject.toml +40 -0
- pyghe-0.1.0/setup.cfg +4 -0
- pyghe-0.1.0/src/pyghe/__init__.py +46 -0
- pyghe-0.1.0/src/pyghe/_base.py +278 -0
- pyghe-0.1.0/src/pyghe/_case.py +536 -0
- pyghe-0.1.0/src/pyghe/_cli.py +396 -0
- pyghe-0.1.0/src/pyghe/batch.py +267 -0
- pyghe-0.1.0/src/pyghe/borehole_resistance.py +66 -0
- pyghe-0.1.0/src/pyghe/building_loads.py +52 -0
- pyghe-0.1.0/src/pyghe/errors.py +56 -0
- pyghe-0.1.0/src/pyghe/ground_heat_exchanger.py +111 -0
- pyghe-0.1.0/src/pyghe/gshp_system.py +34 -0
- pyghe-0.1.0/src/pyghe/heat_carrier_fluid.py +13 -0
- pyghe-0.1.0/src/pyghe/heat_pump_performance.py +96 -0
- pyghe-0.1.0/src/pyghe/project.py +644 -0
- pyghe-0.1.0/src/pyghe/results.py +474 -0
- pyghe-0.1.0/src/pyghe.egg-info/PKG-INFO +154 -0
- pyghe-0.1.0/src/pyghe.egg-info/SOURCES.txt +38 -0
- pyghe-0.1.0/src/pyghe.egg-info/dependency_links.txt +1 -0
- pyghe-0.1.0/src/pyghe.egg-info/requires.txt +23 -0
- pyghe-0.1.0/src/pyghe.egg-info/top_level.txt +1 -0
- pyghe-0.1.0/tests/test_batch.py +98 -0
- pyghe-0.1.0/tests/test_bounds.py +133 -0
- pyghe-0.1.0/tests/test_case.py +182 -0
- pyghe-0.1.0/tests/test_cli.py +115 -0
- pyghe-0.1.0/tests/test_docs.py +80 -0
- pyghe-0.1.0/tests/test_errors.py +153 -0
- pyghe-0.1.0/tests/test_examples.py +30 -0
- pyghe-0.1.0/tests/test_matrix.py +169 -0
- pyghe-0.1.0/tests/test_nodes.py +140 -0
- pyghe-0.1.0/tests/test_notebooks.py +72 -0
- pyghe-0.1.0/tests/test_pickle.py +72 -0
- pyghe-0.1.0/tests/test_plume.py +148 -0
- pyghe-0.1.0/tests/test_project.py +70 -0
- pyghe-0.1.0/tests/test_results.py +156 -0
- pyghe-0.1.0/tests/test_save.py +117 -0
- pyghe-0.1.0/tests/test_server.py +136 -0
pyghe-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Philippe Pasquier
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
pyghe-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pyghe
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python interface to the simulation kernel of GHE Analysis: ground heat exchangers, g-functions, borehole resistance, sizing
|
|
5
|
+
Author: Philippe Pasquier
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://www.gheanalysis.com
|
|
8
|
+
Project-URL: Documentation, https://pyghe.readthedocs.io
|
|
9
|
+
Project-URL: Repository, https://github.com/ppasquier/pyghe
|
|
10
|
+
Project-URL: Issues, https://github.com/ppasquier/pyghe/issues
|
|
11
|
+
Project-URL: Changelog, https://pyghe.readthedocs.io/en/latest/changelog.html
|
|
12
|
+
Keywords: geothermal,ground heat exchanger,borehole,g-function,heat pump,simulation,thermal plume,groundwater flow
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: Intended Audience :: Science/Research
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Topic :: Scientific/Engineering
|
|
17
|
+
Requires-Python: >=3.10
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
License-File: LICENSE
|
|
20
|
+
Requires-Dist: numpy>=1.24
|
|
21
|
+
Provides-Extra: pandas
|
|
22
|
+
Requires-Dist: pandas>=2; extra == "pandas"
|
|
23
|
+
Provides-Extra: notebooks
|
|
24
|
+
Requires-Dist: matplotlib>=3.8; extra == "notebooks"
|
|
25
|
+
Requires-Dist: pandas>=2; extra == "notebooks"
|
|
26
|
+
Requires-Dist: jupyterlab>=4; extra == "notebooks"
|
|
27
|
+
Provides-Extra: docs
|
|
28
|
+
Requires-Dist: sphinx>=7; extra == "docs"
|
|
29
|
+
Requires-Dist: myst-nb>=1.1; extra == "docs"
|
|
30
|
+
Requires-Dist: numpydoc>=1.7; extra == "docs"
|
|
31
|
+
Requires-Dist: sphinx-rtd-theme>=2; extra == "docs"
|
|
32
|
+
Provides-Extra: test
|
|
33
|
+
Requires-Dist: pytest>=7; extra == "test"
|
|
34
|
+
Requires-Dist: pandas>=2; extra == "test"
|
|
35
|
+
Requires-Dist: matplotlib>=3.8; extra == "test"
|
|
36
|
+
Requires-Dist: nbformat>=5; extra == "test"
|
|
37
|
+
Requires-Dist: nbclient>=0.9; extra == "test"
|
|
38
|
+
Requires-Dist: ipykernel>=6; extra == "test"
|
|
39
|
+
Dynamic: license-file
|
|
40
|
+
|
|
41
|
+
# pyghe
|
|
42
|
+
|
|
43
|
+
[GHE Analysis](https://www.gheanalysis.com/pages/ghe-analysis) is a desktop application for the
|
|
44
|
+
design and simulation of ground heat exchangers. pyghe runs its kernel from Python: borehole
|
|
45
|
+
resistance, g-functions, hourly entering fluid temperatures and the length a borehole field needs.
|
|
46
|
+
It is the same code as the application, so a script and the interface give the same answer.
|
|
47
|
+
|
|
48
|
+
```python skip
|
|
49
|
+
import pyghe as ga
|
|
50
|
+
|
|
51
|
+
p = ga.Project.load("my_project.ghe") # a .ghe file, or a Project built in Python
|
|
52
|
+
p.ground_heat_exchanger.active_length = 180
|
|
53
|
+
result = p.simulate()
|
|
54
|
+
print(result.eft.min()) # entering fluid temperature, °C
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Installing
|
|
58
|
+
|
|
59
|
+
```console
|
|
60
|
+
pip install pyghe
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
You need Python 3.10 or later, numpy, and a Large or Academic licence of Analysis Suite.
|
|
64
|
+
[Installation](docs/install.md) explains how pyghe finds the executable and how to check that
|
|
65
|
+
everything is in place.
|
|
66
|
+
|
|
67
|
+
## Quick start
|
|
68
|
+
|
|
69
|
+
Two heat pumps on four boreholes and a building that heats in winter and cools in summer. The
|
|
70
|
+
script prints the coldest fluid temperature the heat pumps see over 25 years:
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
import numpy as np
|
|
74
|
+
import pyghe as ga
|
|
75
|
+
|
|
76
|
+
p = ga.Project()
|
|
77
|
+
p.simulation_duration = 25
|
|
78
|
+
p.building_loads = ga.HourlyBuildingLoadsCombined(
|
|
79
|
+
combined_demand=30 * np.cos(2 * np.pi * np.arange(8760) / 8760)) # kW, heating +
|
|
80
|
+
p.gshp_system.system_type = "Water-water (zone)"
|
|
81
|
+
p.gshp_system.number_of_heat_pumps = 2
|
|
82
|
+
p.gshp_system.flow_rate_of_pump_a = ga.ByMode(heating=120, cooling=120) # L/min
|
|
83
|
+
p.heat_pump_performance.temperature_limit = ga.ByMode(heating=0.0, cooling=30.0) # °C
|
|
84
|
+
p.heat_pump_performance.performance_data = ga.PerformanceConstant(
|
|
85
|
+
cooling_cap=20, cooling_cop=4.5, heating_cap=15, heating_cop=3.5)
|
|
86
|
+
p.heat_carrier_fluid = ga.HeatCarrierFluid("Propylene glycol", antifreeze_mass_concentration=25)
|
|
87
|
+
p.borehole_resistance = ga.SingleULoop(
|
|
88
|
+
borehole_diameter=0.152, grout_thermal_conductivity=1.25, shank_spacing=0.1,
|
|
89
|
+
pipe_inner_diameter=0.033, pipe_outer_diameter=0.044, pipe_thermal_conductivity=0.39)
|
|
90
|
+
g = p.ground_heat_exchanger
|
|
91
|
+
g.ground_thermal_conductivity, g.ground_volumetric_capacity = 2.5, 2.2
|
|
92
|
+
g.mean_surface_temperature, g.geothermal_heat_flux = 10.0, 60
|
|
93
|
+
g.active_length, g.burial_depth = 150, 2
|
|
94
|
+
g.coordinates = ga.Coordinates.rectangle(nx=2, ny=2, spacing=6.0)
|
|
95
|
+
|
|
96
|
+
print(p.simulate().eft.min()) # °C
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
The nodes are filled in the order of the project tree: loads, heat pumps, fluid, borehole, ground.
|
|
100
|
+
A project saved from a script opens in the application, which recomputes it and shows what a
|
|
101
|
+
script leaves aside. [Getting started](docs/getting-started.md) builds a larger example step by
|
|
102
|
+
step.
|
|
103
|
+
|
|
104
|
+
## Documentation
|
|
105
|
+
|
|
106
|
+
The documentation starts at [docs/index.md](docs/index.md). To build it as HTML:
|
|
107
|
+
|
|
108
|
+
```console
|
|
109
|
+
pip install -e .[docs]
|
|
110
|
+
python -m sphinx -b html docs docs/_build/html
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Citing
|
|
114
|
+
|
|
115
|
+
pyghe is GHE Analysis driven from Python, so please cite the application.
|
|
116
|
+
|
|
117
|
+
> Pasquier, P. (2025). *GHE Analysis – Interactive simulation of ground heat exchangers*. P³
|
|
118
|
+
> Geothermal inc. <https://doi.org/10.5281/zenodo.15660526>
|
|
119
|
+
|
|
120
|
+
[Citing](docs/citing.md) and [Methods](docs/methods.md) list the papers behind each method.
|
|
121
|
+
|
|
122
|
+
## Contact
|
|
123
|
+
|
|
124
|
+
Bug reports and suggestions are welcome as [issues on GitHub](https://github.com/ppasquier/pyghe/issues), or through
|
|
125
|
+
[www.gheanalysis.com](https://www.gheanalysis.com).
|
|
126
|
+
|
|
127
|
+
## Working on the package
|
|
128
|
+
|
|
129
|
+
```console
|
|
130
|
+
git clone https://github.com/ppasquier/pyghe
|
|
131
|
+
cd pyghe
|
|
132
|
+
python -m venv .venv
|
|
133
|
+
.venv/Scripts/python -m pip install -e .[test,docs]
|
|
134
|
+
set PYGHE_CLI=C:\...\AnalysisSuite-cli.exe
|
|
135
|
+
.venv/Scripts/python -m pytest -q
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
The executable is the one of the [download page](https://www.gheanalysis.com/pages/download), unpacked as
|
|
139
|
+
[Installation](docs/install.md) describes. Without `PYGHE_CLI` or the executable where pyghe looks for
|
|
140
|
+
it, the tests that run it are skipped. They need a Large or Academic licence
|
|
141
|
+
activated on the computer.
|
|
142
|
+
|
|
143
|
+
I test the documentation like the code: `tests/test_docs.py` runs every code block of this page
|
|
144
|
+
and of `docs/` against the executable, so an example that stops working fails the build. A block
|
|
145
|
+
fenced `python skip` is shown without being run. `tests/test_notebooks.py` checks the committed
|
|
146
|
+
outputs of the notebooks; setting `PYGHE_TEST_NOTEBOOKS=1` also runs them afresh, which takes
|
|
147
|
+
about a minute.
|
|
148
|
+
|
|
149
|
+
[CONTRIBUTING.md](CONTRIBUTING.md) says how to propose a change.
|
|
150
|
+
|
|
151
|
+
## Licence
|
|
152
|
+
|
|
153
|
+
pyghe is released under the MIT licence ([LICENSE](LICENSE)). It needs `AnalysisSuite-cli`, which is
|
|
154
|
+
distributed with GHE Analysis under its own licence terms.
|
pyghe-0.1.0/README.md
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# pyghe
|
|
2
|
+
|
|
3
|
+
[GHE Analysis](https://www.gheanalysis.com/pages/ghe-analysis) is a desktop application for the
|
|
4
|
+
design and simulation of ground heat exchangers. pyghe runs its kernel from Python: borehole
|
|
5
|
+
resistance, g-functions, hourly entering fluid temperatures and the length a borehole field needs.
|
|
6
|
+
It is the same code as the application, so a script and the interface give the same answer.
|
|
7
|
+
|
|
8
|
+
```python skip
|
|
9
|
+
import pyghe as ga
|
|
10
|
+
|
|
11
|
+
p = ga.Project.load("my_project.ghe") # a .ghe file, or a Project built in Python
|
|
12
|
+
p.ground_heat_exchanger.active_length = 180
|
|
13
|
+
result = p.simulate()
|
|
14
|
+
print(result.eft.min()) # entering fluid temperature, °C
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Installing
|
|
18
|
+
|
|
19
|
+
```console
|
|
20
|
+
pip install pyghe
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
You need Python 3.10 or later, numpy, and a Large or Academic licence of Analysis Suite.
|
|
24
|
+
[Installation](docs/install.md) explains how pyghe finds the executable and how to check that
|
|
25
|
+
everything is in place.
|
|
26
|
+
|
|
27
|
+
## Quick start
|
|
28
|
+
|
|
29
|
+
Two heat pumps on four boreholes and a building that heats in winter and cools in summer. The
|
|
30
|
+
script prints the coldest fluid temperature the heat pumps see over 25 years:
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
import numpy as np
|
|
34
|
+
import pyghe as ga
|
|
35
|
+
|
|
36
|
+
p = ga.Project()
|
|
37
|
+
p.simulation_duration = 25
|
|
38
|
+
p.building_loads = ga.HourlyBuildingLoadsCombined(
|
|
39
|
+
combined_demand=30 * np.cos(2 * np.pi * np.arange(8760) / 8760)) # kW, heating +
|
|
40
|
+
p.gshp_system.system_type = "Water-water (zone)"
|
|
41
|
+
p.gshp_system.number_of_heat_pumps = 2
|
|
42
|
+
p.gshp_system.flow_rate_of_pump_a = ga.ByMode(heating=120, cooling=120) # L/min
|
|
43
|
+
p.heat_pump_performance.temperature_limit = ga.ByMode(heating=0.0, cooling=30.0) # °C
|
|
44
|
+
p.heat_pump_performance.performance_data = ga.PerformanceConstant(
|
|
45
|
+
cooling_cap=20, cooling_cop=4.5, heating_cap=15, heating_cop=3.5)
|
|
46
|
+
p.heat_carrier_fluid = ga.HeatCarrierFluid("Propylene glycol", antifreeze_mass_concentration=25)
|
|
47
|
+
p.borehole_resistance = ga.SingleULoop(
|
|
48
|
+
borehole_diameter=0.152, grout_thermal_conductivity=1.25, shank_spacing=0.1,
|
|
49
|
+
pipe_inner_diameter=0.033, pipe_outer_diameter=0.044, pipe_thermal_conductivity=0.39)
|
|
50
|
+
g = p.ground_heat_exchanger
|
|
51
|
+
g.ground_thermal_conductivity, g.ground_volumetric_capacity = 2.5, 2.2
|
|
52
|
+
g.mean_surface_temperature, g.geothermal_heat_flux = 10.0, 60
|
|
53
|
+
g.active_length, g.burial_depth = 150, 2
|
|
54
|
+
g.coordinates = ga.Coordinates.rectangle(nx=2, ny=2, spacing=6.0)
|
|
55
|
+
|
|
56
|
+
print(p.simulate().eft.min()) # °C
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
The nodes are filled in the order of the project tree: loads, heat pumps, fluid, borehole, ground.
|
|
60
|
+
A project saved from a script opens in the application, which recomputes it and shows what a
|
|
61
|
+
script leaves aside. [Getting started](docs/getting-started.md) builds a larger example step by
|
|
62
|
+
step.
|
|
63
|
+
|
|
64
|
+
## Documentation
|
|
65
|
+
|
|
66
|
+
The documentation starts at [docs/index.md](docs/index.md). To build it as HTML:
|
|
67
|
+
|
|
68
|
+
```console
|
|
69
|
+
pip install -e .[docs]
|
|
70
|
+
python -m sphinx -b html docs docs/_build/html
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Citing
|
|
74
|
+
|
|
75
|
+
pyghe is GHE Analysis driven from Python, so please cite the application.
|
|
76
|
+
|
|
77
|
+
> Pasquier, P. (2025). *GHE Analysis – Interactive simulation of ground heat exchangers*. P³
|
|
78
|
+
> Geothermal inc. <https://doi.org/10.5281/zenodo.15660526>
|
|
79
|
+
|
|
80
|
+
[Citing](docs/citing.md) and [Methods](docs/methods.md) list the papers behind each method.
|
|
81
|
+
|
|
82
|
+
## Contact
|
|
83
|
+
|
|
84
|
+
Bug reports and suggestions are welcome as [issues on GitHub](https://github.com/ppasquier/pyghe/issues), or through
|
|
85
|
+
[www.gheanalysis.com](https://www.gheanalysis.com).
|
|
86
|
+
|
|
87
|
+
## Working on the package
|
|
88
|
+
|
|
89
|
+
```console
|
|
90
|
+
git clone https://github.com/ppasquier/pyghe
|
|
91
|
+
cd pyghe
|
|
92
|
+
python -m venv .venv
|
|
93
|
+
.venv/Scripts/python -m pip install -e .[test,docs]
|
|
94
|
+
set PYGHE_CLI=C:\...\AnalysisSuite-cli.exe
|
|
95
|
+
.venv/Scripts/python -m pytest -q
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
The executable is the one of the [download page](https://www.gheanalysis.com/pages/download), unpacked as
|
|
99
|
+
[Installation](docs/install.md) describes. Without `PYGHE_CLI` or the executable where pyghe looks for
|
|
100
|
+
it, the tests that run it are skipped. They need a Large or Academic licence
|
|
101
|
+
activated on the computer.
|
|
102
|
+
|
|
103
|
+
I test the documentation like the code: `tests/test_docs.py` runs every code block of this page
|
|
104
|
+
and of `docs/` against the executable, so an example that stops working fails the build. A block
|
|
105
|
+
fenced `python skip` is shown without being run. `tests/test_notebooks.py` checks the committed
|
|
106
|
+
outputs of the notebooks; setting `PYGHE_TEST_NOTEBOOKS=1` also runs them afresh, which takes
|
|
107
|
+
about a minute.
|
|
108
|
+
|
|
109
|
+
[CONTRIBUTING.md](CONTRIBUTING.md) says how to propose a change.
|
|
110
|
+
|
|
111
|
+
## Licence
|
|
112
|
+
|
|
113
|
+
pyghe is released under the MIT licence ([LICENSE](LICENSE)). It needs `AnalysisSuite-cli`, which is
|
|
114
|
+
distributed with GHE Analysis under its own licence terms.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "pyghe"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Python interface to the simulation kernel of GHE Analysis: ground heat exchangers, g-functions, borehole resistance, sizing"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
dependencies = ["numpy>=1.24"]
|
|
12
|
+
authors = [{ name = "Philippe Pasquier" }]
|
|
13
|
+
license = "MIT"
|
|
14
|
+
license-files = ["LICENSE"]
|
|
15
|
+
keywords = ["geothermal", "ground heat exchanger", "borehole", "g-function", "heat pump", "simulation", "thermal plume", "groundwater flow"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 3 - Alpha",
|
|
18
|
+
"Intended Audience :: Science/Research",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Topic :: Scientific/Engineering",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
[project.urls]
|
|
24
|
+
Homepage = "https://www.gheanalysis.com"
|
|
25
|
+
Documentation = "https://pyghe.readthedocs.io"
|
|
26
|
+
Repository = "https://github.com/ppasquier/pyghe"
|
|
27
|
+
Issues = "https://github.com/ppasquier/pyghe/issues"
|
|
28
|
+
Changelog = "https://pyghe.readthedocs.io/en/latest/changelog.html"
|
|
29
|
+
|
|
30
|
+
[project.optional-dependencies]
|
|
31
|
+
pandas = ["pandas>=2"]
|
|
32
|
+
notebooks = ["matplotlib>=3.8", "pandas>=2", "jupyterlab>=4"]
|
|
33
|
+
docs = ["sphinx>=7", "myst-nb>=1.1", "numpydoc>=1.7", "sphinx-rtd-theme>=2"]
|
|
34
|
+
test = ["pytest>=7", "pandas>=2", "matplotlib>=3.8", "nbformat>=5", "nbclient>=0.9", "ipykernel>=6"]
|
|
35
|
+
|
|
36
|
+
[tool.setuptools.packages.find]
|
|
37
|
+
where = ["src"]
|
|
38
|
+
|
|
39
|
+
[tool.pytest.ini_options]
|
|
40
|
+
testpaths = ["tests"]
|
pyghe-0.1.0/setup.cfg
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
|
+
]
|
|
@@ -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
|