pyepwmorph 2.2.0__tar.gz → 3.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.
Files changed (36) hide show
  1. pyepwmorph-3.1.0/PKG-INFO +260 -0
  2. pyepwmorph-3.1.0/README.md +221 -0
  3. pyepwmorph-3.1.0/pyepwmorph/__init__.py +8 -0
  4. pyepwmorph-3.1.0/pyepwmorph/data/__init__.py +1 -0
  5. pyepwmorph-3.1.0/pyepwmorph/data/ch2025_monthly.parquet +0 -0
  6. pyepwmorph-3.1.0/pyepwmorph/data/ch2025_stations.parquet +0 -0
  7. {pyepwmorph-2.2.0 → pyepwmorph-3.1.0}/pyepwmorph/models/access.py +13 -19
  8. {pyepwmorph-2.2.0 → pyepwmorph-3.1.0}/pyepwmorph/models/assemble.py +27 -6
  9. pyepwmorph-3.1.0/pyepwmorph/models/ch2025.py +303 -0
  10. {pyepwmorph-2.2.0 → pyepwmorph-3.1.0}/pyepwmorph/models/coordinate.py +7 -38
  11. {pyepwmorph-2.2.0 → pyepwmorph-3.1.0}/pyepwmorph/models/custom.py +0 -1
  12. pyepwmorph-3.1.0/pyepwmorph/morph/procedures.py +614 -0
  13. pyepwmorph-3.1.0/pyepwmorph/tools/cache.py +324 -0
  14. pyepwmorph-3.1.0/pyepwmorph/tools/configuration.py +400 -0
  15. {pyepwmorph-2.2.0 → pyepwmorph-3.1.0}/pyepwmorph/tools/io.py +47 -27
  16. pyepwmorph-3.1.0/pyepwmorph/tools/psychrometrics.py +185 -0
  17. pyepwmorph-3.1.0/pyepwmorph/tools/solar.py +268 -0
  18. pyepwmorph-3.1.0/pyepwmorph/tools/utilities.py +407 -0
  19. pyepwmorph-3.1.0/pyepwmorph/tools/workflow.py +599 -0
  20. pyepwmorph-3.1.0/pyproject.toml +94 -0
  21. pyepwmorph-2.2.0/PKG-INFO +0 -238
  22. pyepwmorph-2.2.0/README.md +0 -197
  23. pyepwmorph-2.2.0/pyepwmorph/__init__.py +0 -8
  24. pyepwmorph-2.2.0/pyepwmorph/morph/procedures.py +0 -563
  25. pyepwmorph-2.2.0/pyepwmorph/tools/cache.py +0 -271
  26. pyepwmorph-2.2.0/pyepwmorph/tools/configuration.py +0 -213
  27. pyepwmorph-2.2.0/pyepwmorph/tools/ladybug_psychrometrics.py +0 -531
  28. pyepwmorph-2.2.0/pyepwmorph/tools/solar.py +0 -326
  29. pyepwmorph-2.2.0/pyepwmorph/tools/utilities.py +0 -455
  30. pyepwmorph-2.2.0/pyepwmorph/tools/workflow.py +0 -569
  31. pyepwmorph-2.2.0/pyproject.toml +0 -66
  32. {pyepwmorph-2.2.0 → pyepwmorph-3.1.0}/.gitignore +0 -0
  33. {pyepwmorph-2.2.0 → pyepwmorph-3.1.0}/LICENSE +0 -0
  34. {pyepwmorph-2.2.0 → pyepwmorph-3.1.0}/pyepwmorph/models/__init__.py +0 -0
  35. {pyepwmorph-2.2.0 → pyepwmorph-3.1.0}/pyepwmorph/morph/__init__.py +0 -0
  36. {pyepwmorph-2.2.0 → pyepwmorph-3.1.0}/pyepwmorph/tools/__init__.py +0 -0
@@ -0,0 +1,260 @@
1
+ Metadata-Version: 2.4
2
+ Name: pyepwmorph
3
+ Version: 3.1.0
4
+ Summary: A python package to enable simple and easy gathering of climate model data and morphing of EPW files
5
+ Project-URL: Homepage, https://github.com/justinfmccarty/pyepwmorph
6
+ Project-URL: Issues, https://github.com/justinfmccarty/pyepwmorph/issues
7
+ Project-URL: Changelog, https://github.com/justinfmccarty/pyepwmorph/blob/main/CHANGELOG.md
8
+ Author-email: Justin McCarty <mccarty.justin.f@gmail.com>
9
+ License: MIT
10
+ License-File: LICENSE
11
+ Classifier: Intended Audience :: Science/Research
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.9
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: Scientific/Engineering :: Atmospheric Science
21
+ Requires-Python: >=3.9
22
+ Requires-Dist: dask>=2023.5.0
23
+ Requires-Dist: gcsfs>=2023.5.0
24
+ Requires-Dist: intake-esm>=2023.6.0
25
+ Requires-Dist: intake>=0.6.0
26
+ Requires-Dist: numpy>=1.24
27
+ Requires-Dist: pandas>=2.2
28
+ Requires-Dist: pvlib<1.0.0,>=0.13.0
29
+ Requires-Dist: pyarrow>=13.0.0
30
+ Requires-Dist: xarray>=2022.3.0
31
+ Provides-Extra: dev
32
+ Requires-Dist: build>=0.10; extra == 'dev'
33
+ Requires-Dist: ipykernel>=6.20.0; extra == 'dev'
34
+ Requires-Dist: pytest-cov>=4.0; extra == 'dev'
35
+ Requires-Dist: pytest>=7.0; extra == 'dev'
36
+ Requires-Dist: ruff>=0.4.0; extra == 'dev'
37
+ Requires-Dist: twine>=4.0; extra == 'dev'
38
+ Description-Content-Type: text/markdown
39
+
40
+ # pyepwmorph
41
+
42
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
43
+ [![Python 3.9+](https://img.shields.io/badge/python-3.9%2B-blue.svg)](https://www.python.org/downloads/)
44
+ [![PyPI](https://img.shields.io/pypi/v/pyepwmorph.svg)](https://pypi.org/project/pyepwmorph/)
45
+
46
+ A Python package for morphing EnergyPlus Weather (EPW) files with climate model data. Supports CMIP6 projections from Google Cloud and custom CSV-based model data for both future and historical scenarios.
47
+
48
+ ## Overview
49
+
50
+ `pyepwmorph` enables building performance analysts and researchers to create morphed weather files by applying scientifically validated procedures (Belcher et al. 2005, Jentsch et al. 2013) to existing EPW files. The package can morph EPWs using:
51
+
52
+ - **CMIP6 data** fetched automatically from Google Cloud (Pangeo)
53
+ - **Custom CSV data** from any climate model, including historical reconstructions
54
+ - **CH2025 station scenarios** for Switzerland, shipped with the package and indexed by global warming level
55
+
56
+ ## Installation
57
+
58
+ ### With uv (recommended)
59
+
60
+ ```bash
61
+ uv add pyepwmorph
62
+ ```
63
+
64
+ ### Development setup
65
+
66
+ ```bash
67
+ git clone https://github.com/justinfmccarty/pyepwmorph.git
68
+ cd pyepwmorph
69
+ uv sync --extra dev
70
+ ```
71
+
72
+ ### With pip
73
+
74
+ ```bash
75
+ pip install pyepwmorph
76
+ ```
77
+
78
+ ## Quick start
79
+
80
+ ### CMIP6 workflow (future projections)
81
+
82
+ ```python
83
+ import pyepwmorph.tools.workflow as workflow
84
+
85
+ results = workflow.morphing_workflow(
86
+ project_name="MyBuilding_Future",
87
+ epw_file="weather.epw",
88
+ user_variables=["Temperature", "Humidity", "Clouds and Radiation"],
89
+ user_pathways=["Middle of the Road"], # ssp245
90
+ percentiles=[50],
91
+ target_years=[2050],
92
+ output_directory="output/",
93
+ )
94
+ ```
95
+
96
+ ### Custom CSV workflow (historical or any model)
97
+
98
+ ```python
99
+ import pyepwmorph.tools.workflow as workflow
100
+
101
+ custom_data = {
102
+ "reference": {
103
+ "tas": "data/reference_tas.csv",
104
+ "tasmax": "data/reference_tasmax.csv",
105
+ "tasmin": "data/reference_tasmin.csv",
106
+ },
107
+ "target_1990s": {
108
+ "tas": "data/target_tas.csv",
109
+ "tasmax": "data/target_tasmax.csv",
110
+ "tasmin": "data/target_tasmin.csv",
111
+ },
112
+ }
113
+
114
+ results = workflow.morphing_workflow(
115
+ project_name="Historical_Morph",
116
+ epw_file="weather.epw",
117
+ user_variables=["Temperature"],
118
+ user_pathways=["target_1990s"],
119
+ percentiles=[50],
120
+ target_years=[1990],
121
+ output_directory="output/",
122
+ data_source="custom",
123
+ custom_data=custom_data,
124
+ reference_scenario="reference",
125
+ )
126
+ ```
127
+
128
+ Custom CSVs should have a `date` column (parseable by pandas) and a column named after the CMIP6 variable (e.g. `tas`, `tasmax`). Rows should be monthly.
129
+
130
+ The reference and target scenarios must cover **different years**, the same way the CMIP6 `historical` and `sspXXX` experiments do. The two series are concatenated before the baseline and target periods are sliced out, so overlapping years get averaged together and weaken the climate signal. Keep `baseline_range` inside the years the reference scenario covers.
131
+
132
+ ### Switzerland (CH2025 warming levels)
133
+
134
+ For an EPW inside Switzerland, `data_source="ch2025"` morphs from MeteoSwiss CH2025 station scenarios. There is no target year: each pathway is a global warming level relative to 1991-2020, and the run is offline.
135
+
136
+ ```python
137
+ results = workflow.morphing_workflow(
138
+ project_name="Zurich_GWL2",
139
+ epw_file="zurich.epw",
140
+ user_variables=["Temperature", "Humidity", "Wind", "Radiation", "Dew Point"],
141
+ user_pathways=["GWL 2.0"],
142
+ percentiles=[50],
143
+ output_directory="output/",
144
+ data_source="ch2025",
145
+ )
146
+ morphed = results["gwl2.0"]["50"]
147
+ ```
148
+
149
+ The site is matched to the nearest station that has the requested variables (or, with `ch2025_full_coverage=True`, the nearest station that has every CH2025 variable), with an elevation penalty so a much higher station is not chosen just because it is close on the map. Supported variables are temperature, humidity, dew point, wind, and radiation (global, diffuse, and direct). Pressure and cloud cover are not in CH2025; requesting them raises an error. Sky cover is left at the EPW's baseline values when radiation is morphed, so longwave sky temperature in EnergyPlus does not follow the shortwave change.
150
+
151
+ The signal is the change from the 1991-2020 reference climate to the chosen warming level. Each model chain's change is computed first, over the chains that reach that warming level, and the percentiles are taken of those changes. If the EPW's years fall outside 1991-2020, a `UserWarning` recommends a TMY built from 1991-2020 data and the signal is still applied to the file as it stands. The same caveats are listed in `MorphConfig.ch2025_notes` and the matched station in `MorphConfig.ch2025_station`.
152
+
153
+ CH2025 data: MeteoSwiss & ETH Zurich (2025), Climate CH2025 - Daily Datasets, CC-BY 4.0, https://doi.org/10.18751/climate/scenarios/ch2025/data/1.0/
154
+
155
+ ## Climate scenarios
156
+
157
+ | Scenario | SSP | Description | Expected warming |
158
+ | --------------------- | ------ | --------------------------------------- | ---------------- |
159
+ | Best Case Scenario | ssp126 | Strong mitigation, renewable transition | ~1.8 C by 2100 |
160
+ | Middle of the Road | ssp245 | Moderate mitigation efforts | ~2.7 C by 2100 |
161
+ | Upper Middle Scenario | ssp370 | Regional rivalry, slow convergence | ~3.6 C by 2100 |
162
+ | Worst Case Scenario | ssp585 | Fossil-fueled development | ~4.4 C by 2100 |
163
+
164
+ ## Morphing variables
165
+
166
+ - **Temperature** -- dry bulb temperature (shift + stretch)
167
+ - **Humidity** -- relative humidity, stretched in specific humidity space
168
+ - **Pressure** -- atmospheric pressure (shift)
169
+ - **Wind** -- wind speed (stretch)
170
+ - **Radiation** -- global/diffuse/direct radiation, without changing sky cover (all sources; the only radiation option for CH2025)
171
+ - **Clouds and Radiation** -- global/diffuse/direct radiation and sky cover (CMIP6 and custom data)
172
+ - **Dew Point** -- recalculated from morphed temperature and humidity
173
+
174
+ ### Variable dependencies
175
+
176
+ Some variables cannot be morphed on their own:
177
+
178
+ - **Humidity** requires Temperature and Pressure, except with CH2025, where relative humidity is stretched directly
179
+ - **Dew Point** requires Temperature, Humidity, and Pressure (Temperature and Humidity only with CH2025)
180
+
181
+ Dependencies are added automatically and are **written to the output file**. Asking for `Dew Point` alone therefore returns an EPW with morphed pressure, temperature, relative humidity, and dew point, which keeps the file internally consistent. `MorphConfig.resolved_variables` shows exactly what will be written, in the order it is computed.
182
+
183
+ ## Caching
184
+
185
+ Climate model data is cached locally after the first download, and the cache is consulted before the remote catalogue is opened so a hit costs no network traffic.
186
+
187
+ ```python
188
+ import pyepwmorph.models.access as access
189
+
190
+ stats = access.get_cmip6_cache_stats()
191
+ access.clear_cmip6_cache()
192
+ ```
193
+
194
+ Cache entries live in the per-user cache directory (`~/Library/Caches/pyepwmorph` on macOS, `~/.cache/pyepwmorph` on Linux, `%LOCALAPPDATA%\pyepwmorph` on Windows) and are keyed by location, pathway, variable, model sources, and time slices. Two environment variables override the defaults:
195
+
196
+ | Variable | Purpose | Default |
197
+ | ------------------------- | ------------------------------ | ------- |
198
+ | `PYEPWMORPH_CACHE_DIR` | Where cache files are written | per-user cache directory |
199
+ | `PYEPWMORPH_CACHE_MAX_MB` | Size cap before old files go | 500 |
200
+
201
+ ## Available climate models
202
+
203
+ ```python
204
+ from pyepwmorph.tools.utilities import available_models
205
+
206
+ models = available_models()
207
+ ```
208
+
209
+ ## Development
210
+
211
+ ```bash
212
+ git clone https://github.com/justinfmccarty/pyepwmorph.git
213
+ cd pyepwmorph
214
+ uv sync --extra dev
215
+
216
+ # Run tests (fully offline)
217
+ uv run pytest
218
+
219
+ # Run with coverage
220
+ uv run pytest --cov=pyepwmorph
221
+
222
+ # Lint
223
+ uv run ruff check pyepwmorph tests gui
224
+ ```
225
+
226
+ ### Releases
227
+
228
+ ```bash
229
+ ./release.sh [patch|minor|major]
230
+ ```
231
+
232
+ The script refuses to run on a dirty tree, off `main`, with failing lint or tests, or without a matching `CHANGELOG.md` section. It bumps the version, tags, and pushes; creating the GitHub Release then triggers the PyPI publish workflow.
233
+
234
+ ```bash
235
+ gh release create v3.0.0 \
236
+ --title "v3.0.0" \
237
+ --notes "See CHANGELOG.md."
238
+ ```
239
+
240
+ ## Changes
241
+
242
+ See [CHANGELOG.md](CHANGELOG.md) for the full history. The most recent release corrects several morphing calculations, so morphed humidity, dew point, wind speed, cloud cover, and direct/diffuse radiation all differ from files produced by earlier versions.
243
+
244
+ ## Requirements
245
+
246
+ - Python >= 3.9
247
+ - pandas >= 2.2
248
+ - Internet connection (for CMIP6 data download; the custom CSV and CH2025 workflows run offline)
249
+
250
+ ## License
251
+
252
+ MIT License. See [LICENSE](LICENSE).
253
+
254
+ ## Citation
255
+
256
+ ```text
257
+ McCarty, J. (2026). pyepwmorph: A Python package for climate-informed
258
+ EPW file morphing. Version 3.0.0.
259
+ https://github.com/justinfmccarty/pyepwmorph
260
+ ```
@@ -0,0 +1,221 @@
1
+ # pyepwmorph
2
+
3
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
4
+ [![Python 3.9+](https://img.shields.io/badge/python-3.9%2B-blue.svg)](https://www.python.org/downloads/)
5
+ [![PyPI](https://img.shields.io/pypi/v/pyepwmorph.svg)](https://pypi.org/project/pyepwmorph/)
6
+
7
+ A Python package for morphing EnergyPlus Weather (EPW) files with climate model data. Supports CMIP6 projections from Google Cloud and custom CSV-based model data for both future and historical scenarios.
8
+
9
+ ## Overview
10
+
11
+ `pyepwmorph` enables building performance analysts and researchers to create morphed weather files by applying scientifically validated procedures (Belcher et al. 2005, Jentsch et al. 2013) to existing EPW files. The package can morph EPWs using:
12
+
13
+ - **CMIP6 data** fetched automatically from Google Cloud (Pangeo)
14
+ - **Custom CSV data** from any climate model, including historical reconstructions
15
+ - **CH2025 station scenarios** for Switzerland, shipped with the package and indexed by global warming level
16
+
17
+ ## Installation
18
+
19
+ ### With uv (recommended)
20
+
21
+ ```bash
22
+ uv add pyepwmorph
23
+ ```
24
+
25
+ ### Development setup
26
+
27
+ ```bash
28
+ git clone https://github.com/justinfmccarty/pyepwmorph.git
29
+ cd pyepwmorph
30
+ uv sync --extra dev
31
+ ```
32
+
33
+ ### With pip
34
+
35
+ ```bash
36
+ pip install pyepwmorph
37
+ ```
38
+
39
+ ## Quick start
40
+
41
+ ### CMIP6 workflow (future projections)
42
+
43
+ ```python
44
+ import pyepwmorph.tools.workflow as workflow
45
+
46
+ results = workflow.morphing_workflow(
47
+ project_name="MyBuilding_Future",
48
+ epw_file="weather.epw",
49
+ user_variables=["Temperature", "Humidity", "Clouds and Radiation"],
50
+ user_pathways=["Middle of the Road"], # ssp245
51
+ percentiles=[50],
52
+ target_years=[2050],
53
+ output_directory="output/",
54
+ )
55
+ ```
56
+
57
+ ### Custom CSV workflow (historical or any model)
58
+
59
+ ```python
60
+ import pyepwmorph.tools.workflow as workflow
61
+
62
+ custom_data = {
63
+ "reference": {
64
+ "tas": "data/reference_tas.csv",
65
+ "tasmax": "data/reference_tasmax.csv",
66
+ "tasmin": "data/reference_tasmin.csv",
67
+ },
68
+ "target_1990s": {
69
+ "tas": "data/target_tas.csv",
70
+ "tasmax": "data/target_tasmax.csv",
71
+ "tasmin": "data/target_tasmin.csv",
72
+ },
73
+ }
74
+
75
+ results = workflow.morphing_workflow(
76
+ project_name="Historical_Morph",
77
+ epw_file="weather.epw",
78
+ user_variables=["Temperature"],
79
+ user_pathways=["target_1990s"],
80
+ percentiles=[50],
81
+ target_years=[1990],
82
+ output_directory="output/",
83
+ data_source="custom",
84
+ custom_data=custom_data,
85
+ reference_scenario="reference",
86
+ )
87
+ ```
88
+
89
+ Custom CSVs should have a `date` column (parseable by pandas) and a column named after the CMIP6 variable (e.g. `tas`, `tasmax`). Rows should be monthly.
90
+
91
+ The reference and target scenarios must cover **different years**, the same way the CMIP6 `historical` and `sspXXX` experiments do. The two series are concatenated before the baseline and target periods are sliced out, so overlapping years get averaged together and weaken the climate signal. Keep `baseline_range` inside the years the reference scenario covers.
92
+
93
+ ### Switzerland (CH2025 warming levels)
94
+
95
+ For an EPW inside Switzerland, `data_source="ch2025"` morphs from MeteoSwiss CH2025 station scenarios. There is no target year: each pathway is a global warming level relative to 1991-2020, and the run is offline.
96
+
97
+ ```python
98
+ results = workflow.morphing_workflow(
99
+ project_name="Zurich_GWL2",
100
+ epw_file="zurich.epw",
101
+ user_variables=["Temperature", "Humidity", "Wind", "Radiation", "Dew Point"],
102
+ user_pathways=["GWL 2.0"],
103
+ percentiles=[50],
104
+ output_directory="output/",
105
+ data_source="ch2025",
106
+ )
107
+ morphed = results["gwl2.0"]["50"]
108
+ ```
109
+
110
+ The site is matched to the nearest station that has the requested variables (or, with `ch2025_full_coverage=True`, the nearest station that has every CH2025 variable), with an elevation penalty so a much higher station is not chosen just because it is close on the map. Supported variables are temperature, humidity, dew point, wind, and radiation (global, diffuse, and direct). Pressure and cloud cover are not in CH2025; requesting them raises an error. Sky cover is left at the EPW's baseline values when radiation is morphed, so longwave sky temperature in EnergyPlus does not follow the shortwave change.
111
+
112
+ The signal is the change from the 1991-2020 reference climate to the chosen warming level. Each model chain's change is computed first, over the chains that reach that warming level, and the percentiles are taken of those changes. If the EPW's years fall outside 1991-2020, a `UserWarning` recommends a TMY built from 1991-2020 data and the signal is still applied to the file as it stands. The same caveats are listed in `MorphConfig.ch2025_notes` and the matched station in `MorphConfig.ch2025_station`.
113
+
114
+ CH2025 data: MeteoSwiss & ETH Zurich (2025), Climate CH2025 - Daily Datasets, CC-BY 4.0, https://doi.org/10.18751/climate/scenarios/ch2025/data/1.0/
115
+
116
+ ## Climate scenarios
117
+
118
+ | Scenario | SSP | Description | Expected warming |
119
+ | --------------------- | ------ | --------------------------------------- | ---------------- |
120
+ | Best Case Scenario | ssp126 | Strong mitigation, renewable transition | ~1.8 C by 2100 |
121
+ | Middle of the Road | ssp245 | Moderate mitigation efforts | ~2.7 C by 2100 |
122
+ | Upper Middle Scenario | ssp370 | Regional rivalry, slow convergence | ~3.6 C by 2100 |
123
+ | Worst Case Scenario | ssp585 | Fossil-fueled development | ~4.4 C by 2100 |
124
+
125
+ ## Morphing variables
126
+
127
+ - **Temperature** -- dry bulb temperature (shift + stretch)
128
+ - **Humidity** -- relative humidity, stretched in specific humidity space
129
+ - **Pressure** -- atmospheric pressure (shift)
130
+ - **Wind** -- wind speed (stretch)
131
+ - **Radiation** -- global/diffuse/direct radiation, without changing sky cover (all sources; the only radiation option for CH2025)
132
+ - **Clouds and Radiation** -- global/diffuse/direct radiation and sky cover (CMIP6 and custom data)
133
+ - **Dew Point** -- recalculated from morphed temperature and humidity
134
+
135
+ ### Variable dependencies
136
+
137
+ Some variables cannot be morphed on their own:
138
+
139
+ - **Humidity** requires Temperature and Pressure, except with CH2025, where relative humidity is stretched directly
140
+ - **Dew Point** requires Temperature, Humidity, and Pressure (Temperature and Humidity only with CH2025)
141
+
142
+ Dependencies are added automatically and are **written to the output file**. Asking for `Dew Point` alone therefore returns an EPW with morphed pressure, temperature, relative humidity, and dew point, which keeps the file internally consistent. `MorphConfig.resolved_variables` shows exactly what will be written, in the order it is computed.
143
+
144
+ ## Caching
145
+
146
+ Climate model data is cached locally after the first download, and the cache is consulted before the remote catalogue is opened so a hit costs no network traffic.
147
+
148
+ ```python
149
+ import pyepwmorph.models.access as access
150
+
151
+ stats = access.get_cmip6_cache_stats()
152
+ access.clear_cmip6_cache()
153
+ ```
154
+
155
+ Cache entries live in the per-user cache directory (`~/Library/Caches/pyepwmorph` on macOS, `~/.cache/pyepwmorph` on Linux, `%LOCALAPPDATA%\pyepwmorph` on Windows) and are keyed by location, pathway, variable, model sources, and time slices. Two environment variables override the defaults:
156
+
157
+ | Variable | Purpose | Default |
158
+ | ------------------------- | ------------------------------ | ------- |
159
+ | `PYEPWMORPH_CACHE_DIR` | Where cache files are written | per-user cache directory |
160
+ | `PYEPWMORPH_CACHE_MAX_MB` | Size cap before old files go | 500 |
161
+
162
+ ## Available climate models
163
+
164
+ ```python
165
+ from pyepwmorph.tools.utilities import available_models
166
+
167
+ models = available_models()
168
+ ```
169
+
170
+ ## Development
171
+
172
+ ```bash
173
+ git clone https://github.com/justinfmccarty/pyepwmorph.git
174
+ cd pyepwmorph
175
+ uv sync --extra dev
176
+
177
+ # Run tests (fully offline)
178
+ uv run pytest
179
+
180
+ # Run with coverage
181
+ uv run pytest --cov=pyepwmorph
182
+
183
+ # Lint
184
+ uv run ruff check pyepwmorph tests gui
185
+ ```
186
+
187
+ ### Releases
188
+
189
+ ```bash
190
+ ./release.sh [patch|minor|major]
191
+ ```
192
+
193
+ The script refuses to run on a dirty tree, off `main`, with failing lint or tests, or without a matching `CHANGELOG.md` section. It bumps the version, tags, and pushes; creating the GitHub Release then triggers the PyPI publish workflow.
194
+
195
+ ```bash
196
+ gh release create v3.0.0 \
197
+ --title "v3.0.0" \
198
+ --notes "See CHANGELOG.md."
199
+ ```
200
+
201
+ ## Changes
202
+
203
+ See [CHANGELOG.md](CHANGELOG.md) for the full history. The most recent release corrects several morphing calculations, so morphed humidity, dew point, wind speed, cloud cover, and direct/diffuse radiation all differ from files produced by earlier versions.
204
+
205
+ ## Requirements
206
+
207
+ - Python >= 3.9
208
+ - pandas >= 2.2
209
+ - Internet connection (for CMIP6 data download; the custom CSV and CH2025 workflows run offline)
210
+
211
+ ## License
212
+
213
+ MIT License. See [LICENSE](LICENSE).
214
+
215
+ ## Citation
216
+
217
+ ```text
218
+ McCarty, J. (2026). pyepwmorph: A Python package for climate-informed
219
+ EPW file morphing. Version 3.0.0.
220
+ https://github.com/justinfmccarty/pyepwmorph
221
+ ```
@@ -0,0 +1,8 @@
1
+ """pyepwmorph -- Climate model data gathering and EPW file morphing."""
2
+
3
+ from importlib.metadata import PackageNotFoundError, version
4
+
5
+ try:
6
+ __version__ = version("pyepwmorph")
7
+ except PackageNotFoundError: # running from a source tree that was never installed
8
+ __version__ = "0.0.0+unknown"
@@ -0,0 +1 @@
1
+ """Shipped climate datasets."""
@@ -1,16 +1,11 @@
1
- # coding=utf-8
2
1
  """
3
2
  various scripts for accessing different flavors of climate models
4
3
  """
5
4
 
6
- import warnings
7
-
8
5
  from pyepwmorph.tools import cache
9
6
 
10
- warnings.filterwarnings("ignore")
11
-
12
7
  __author__ = "Justin McCarty"
13
- __copyright__ = "Copyright 2023"
8
+ __copyright__ = "Copyright 2023-2026"
14
9
  __credits__ = ["Justin McCarty"]
15
10
  __license__ = "MIT"
16
11
 
@@ -46,12 +41,12 @@ def access_cmip6_data(models, pathway, variable):
46
41
  import intake
47
42
 
48
43
  # NOTE: No caching at this level because the data contains lazy dask arrays
49
- # that reference the full global grid. Caching happens in coordinate.py after
44
+ # that reference the full global grid. Caching happens in workflow.py once
50
45
  # the data has been spatially selected and computed for a specific location.
51
-
46
+
52
47
  table_id = 'Amon' # atmospheric variables (A) saved at monthly resolution (mon)
53
48
  member_id = 'r1i1p1f1'
54
-
49
+
55
50
  # Fetch from Google Cloud
56
51
  gcsfs.GCSFileSystem(token='anon')
57
52
  # datastore_json = 'pangeo-cmip6.json'
@@ -64,7 +59,7 @@ def access_cmip6_data(models, pathway, variable):
64
59
 
65
60
  # convert data catalog into a dictionary of xarray datasets
66
61
  dataset_dict = model_search.to_dataset_dict(zarr_kwargs={'consolidated': True, 'decode_times': False})
67
-
62
+
68
63
  return dataset_dict
69
64
 
70
65
 
@@ -84,19 +79,18 @@ def build_accessible_data_list():
84
79
  import gcsfs
85
80
  import intake
86
81
  gcsfs.GCSFileSystem(token='anon')
87
- # datastore_json = 'pangeo-cmip6.json'
88
82
  esm_data = intake.open_esm_datastore("https://storage.googleapis.com/cmip6/pangeo-cmip6.json")
89
- # return esm_data.df['source_id'].unique().tolist()
83
+ return sorted(esm_data.df['source_id'].unique().tolist())
90
84
 
91
85
 
92
86
  def clear_cmip6_cache():
93
87
  """
94
88
  Clear all cached CMIP6 data.
95
-
89
+
96
90
  This clears location-specific cached data that has been processed and stored
97
- for faster repeated access. This is useful for development or when you want
91
+ for faster repeated access. This is useful for development or when you want
98
92
  to free up disk space.
99
-
93
+
100
94
  Examples
101
95
  --------
102
96
  >>> clear_cmip6_cache()
@@ -107,11 +101,11 @@ def clear_cmip6_cache():
107
101
  def get_cmip6_cache_stats():
108
102
  """
109
103
  Get statistics about the CMIP6 data cache.
110
-
104
+
111
105
  Returns information about cache size, number of files, and individual
112
106
  file details for development and monitoring purposes. The cache stores
113
107
  location-specific processed data.
114
-
108
+
115
109
  Returns
116
110
  -------
117
111
  dict
@@ -122,10 +116,10 @@ def get_cmip6_cache_stats():
122
116
  - max_size_mb: Maximum allowed cache size
123
117
  - usage_percent: Percentage of max size used
124
118
  - files: List of individual file details
125
-
119
+
126
120
  Examples
127
121
  --------
128
122
  >>> stats = get_cmip6_cache_stats()
129
123
  >>> print(f"Cache using {stats['total_size_mb']} MB ({stats['usage_percent']}%)")
130
124
  """
131
- return cache.get_cache_stats()
125
+ return cache.get_cache_stats()
@@ -1,17 +1,14 @@
1
- # coding=utf-8
2
1
  """
3
2
  This module creates ensembles from multiple model inputs downloaded for a single pathway and variable.
4
3
  This is what enables the slicing of the data from a percentile point of view.
5
4
  """
6
5
  import pandas as pd
7
6
  import xarray as xr
8
- from pyepwmorph.tools import utilities
9
- import warnings
10
7
 
11
- warnings.filterwarnings("ignore")
8
+ from pyepwmorph.tools import utilities
12
9
 
13
10
  __author__ = "Justin McCarty"
14
- __copyright__ = "Copyright 2023"
11
+ __copyright__ = "Copyright 2023-2026"
15
12
  __credits__ = ["Justin McCarty"]
16
13
  __license__ = "MIT"
17
14
 
@@ -111,4 +108,28 @@ def calc_model_climatologies(baseline_range, future_range, baseline_data, future
111
108
  future_means = utilities.monthly_means(future_data).rename(variable)
112
109
 
113
110
 
114
- return baseline_means, future_means
111
+ return baseline_means, future_means
112
+
113
+
114
+ def calc_gwl_climatologies(baseline_data, future_data, variable):
115
+ """Return CH2025 monthly climatologies unchanged.
116
+
117
+ A warming-level state is already a stationary 30-year sample, so there
118
+ is no year range to slice the way ``calc_model_climatologies`` does for
119
+ a transient CMIP6 run.
120
+
121
+ Parameters
122
+ ----------
123
+ baseline_data : pd.Series
124
+ Twelve monthly values for the reference state.
125
+ future_data : pd.Series
126
+ Twelve monthly values for the warming-level state.
127
+ variable : str
128
+ Name used to rename both series.
129
+
130
+ Returns
131
+ -------
132
+ tuple
133
+ ``(baseline_data, future_data)``, each renamed to *variable*.
134
+ """
135
+ return baseline_data.rename(variable), future_data.rename(variable)