smhi2epw 1.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 (90) hide show
  1. smhi2epw-1.1.0/.readthedocs.yaml +18 -0
  2. smhi2epw-1.1.0/LICENSE +21 -0
  3. smhi2epw-1.1.0/MANIFEST.in +9 -0
  4. smhi2epw-1.1.0/PKG-INFO +502 -0
  5. smhi2epw-1.1.0/README.md +455 -0
  6. smhi2epw-1.1.0/docs/api.rst +48 -0
  7. smhi2epw-1.1.0/docs/bibliography.rst +36 -0
  8. smhi2epw-1.1.0/docs/cli.rst +68 -0
  9. smhi2epw-1.1.0/docs/conf.py +66 -0
  10. smhi2epw-1.1.0/docs/energyplus.rst +106 -0
  11. smhi2epw-1.1.0/docs/index.rst +48 -0
  12. smhi2epw-1.1.0/docs/installation.rst +74 -0
  13. smhi2epw-1.1.0/docs/internals.rst +80 -0
  14. smhi2epw-1.1.0/docs/limitations.rst +63 -0
  15. smhi2epw-1.1.0/docs/notebooks/00_getting_started.nblink +1 -0
  16. smhi2epw-1.1.0/docs/notebooks/01_inspect_an_epw.nblink +1 -0
  17. smhi2epw-1.1.0/docs/notebooks/02_compare_locations.nblink +1 -0
  18. smhi2epw-1.1.0/docs/notebooks/03_compare_years.nblink +1 -0
  19. smhi2epw-1.1.0/docs/notebooks/04_identify_heatwaves.nblink +1 -0
  20. smhi2epw-1.1.0/docs/notebooks/05_compare_amy_to_tmy.nblink +1 -0
  21. smhi2epw-1.1.0/docs/notebooks/06_data_quality_and_gap_filling.nblink +1 -0
  22. smhi2epw-1.1.0/docs/notebooks/07_solar_components.nblink +1 -0
  23. smhi2epw-1.1.0/docs/notebooks/08_batch_generation.nblink +1 -0
  24. smhi2epw-1.1.0/docs/notebooks/09_run_energyplus.nblink +3 -0
  25. smhi2epw-1.1.0/docs/pipeline.rst +109 -0
  26. smhi2epw-1.1.0/docs/provenance.rst +105 -0
  27. smhi2epw-1.1.0/docs/python_api.rst +73 -0
  28. smhi2epw-1.1.0/docs/quickstart.rst +75 -0
  29. smhi2epw-1.1.0/docs/troubleshooting.rst +74 -0
  30. smhi2epw-1.1.0/docs/tutorials.rst +33 -0
  31. smhi2epw-1.1.0/docs/weather_recovery.rst +371 -0
  32. smhi2epw-1.1.0/examples/00_getting_started.ipynb +151 -0
  33. smhi2epw-1.1.0/examples/01_inspect_an_epw.ipynb +215 -0
  34. smhi2epw-1.1.0/examples/02_compare_locations.ipynb +171 -0
  35. smhi2epw-1.1.0/examples/03_compare_years.ipynb +168 -0
  36. smhi2epw-1.1.0/examples/04_identify_heatwaves.ipynb +154 -0
  37. smhi2epw-1.1.0/examples/05_compare_amy_to_tmy.ipynb +204 -0
  38. smhi2epw-1.1.0/examples/06_data_quality_and_gap_filling.ipynb +166 -0
  39. smhi2epw-1.1.0/examples/07_solar_components.ipynb +190 -0
  40. smhi2epw-1.1.0/examples/08_batch_generation.ipynb +289 -0
  41. smhi2epw-1.1.0/examples/09_run_energyplus.ipynb +218 -0
  42. smhi2epw-1.1.0/examples/README.md +77 -0
  43. smhi2epw-1.1.0/examples/data/completeness_2026-10-05.json +316 -0
  44. smhi2epw-1.1.0/examples/data/pvlib_validation_2026-10-06.json +1183 -0
  45. smhi2epw-1.1.0/examples/data/solar_validation_2026-10-06.json +839 -0
  46. smhi2epw-1.1.0/examples/support/energyplus.py +187 -0
  47. smhi2epw-1.1.0/examples/support/single_zone.idf +460 -0
  48. smhi2epw-1.1.0/pyproject.toml +92 -0
  49. smhi2epw-1.1.0/scripts/install_energyplus.sh +50 -0
  50. smhi2epw-1.1.0/setup.cfg +4 -0
  51. smhi2epw-1.1.0/src/smhi2epw/__init__.py +46 -0
  52. smhi2epw-1.1.0/src/smhi2epw/automatic.py +445 -0
  53. smhi2epw-1.1.0/src/smhi2epw/cli.py +203 -0
  54. smhi2epw-1.1.0/src/smhi2epw/compiler.py +651 -0
  55. smhi2epw-1.1.0/src/smhi2epw/constants.py +140 -0
  56. smhi2epw-1.1.0/src/smhi2epw/errors.py +45 -0
  57. smhi2epw-1.1.0/src/smhi2epw/export.py +524 -0
  58. smhi2epw-1.1.0/src/smhi2epw/gap_recovery.py +415 -0
  59. smhi2epw-1.1.0/src/smhi2epw/ingestion.py +1083 -0
  60. smhi2epw-1.1.0/src/smhi2epw/processing.py +951 -0
  61. smhi2epw-1.1.0/src/smhi2epw/provenance.py +79 -0
  62. smhi2epw-1.1.0/src/smhi2epw/py.typed +0 -0
  63. smhi2epw-1.1.0/src/smhi2epw/reader.py +339 -0
  64. smhi2epw-1.1.0/src/smhi2epw/reanalysis.py +120 -0
  65. smhi2epw-1.1.0/src/smhi2epw/solar.py +222 -0
  66. smhi2epw-1.1.0/src/smhi2epw.egg-info/PKG-INFO +502 -0
  67. smhi2epw-1.1.0/src/smhi2epw.egg-info/SOURCES.txt +88 -0
  68. smhi2epw-1.1.0/src/smhi2epw.egg-info/dependency_links.txt +1 -0
  69. smhi2epw-1.1.0/src/smhi2epw.egg-info/entry_points.txt +2 -0
  70. smhi2epw-1.1.0/src/smhi2epw.egg-info/requires.txt +30 -0
  71. smhi2epw-1.1.0/src/smhi2epw.egg-info/top_level.txt +1 -0
  72. smhi2epw-1.1.0/tests/fixtures/__init__.py +1 -0
  73. smhi2epw-1.1.0/tests/fixtures/provider_replay.py +326 -0
  74. smhi2epw-1.1.0/tests/test_automatic.py +379 -0
  75. smhi2epw-1.1.0/tests/test_automatic_physics.py +366 -0
  76. smhi2epw-1.1.0/tests/test_cli_cache.py +78 -0
  77. smhi2epw-1.1.0/tests/test_cloud_units.py +158 -0
  78. smhi2epw-1.1.0/tests/test_documentation.py +228 -0
  79. smhi2epw-1.1.0/tests/test_energyplus.py +157 -0
  80. smhi2epw-1.1.0/tests/test_gap_recovery.py +494 -0
  81. smhi2epw-1.1.0/tests/test_ingestion_metadata.py +124 -0
  82. smhi2epw-1.1.0/tests/test_network.py +100 -0
  83. smhi2epw-1.1.0/tests/test_pipeline.py +717 -0
  84. smhi2epw-1.1.0/tests/test_provenance.py +230 -0
  85. smhi2epw-1.1.0/tests/test_provider_replay.py +103 -0
  86. smhi2epw-1.1.0/tests/test_reader.py +260 -0
  87. smhi2epw-1.1.0/tests/test_solar_decomposition.py +93 -0
  88. smhi2epw-1.1.0/tests/test_solar_geometry.py +103 -0
  89. smhi2epw-1.1.0/tests/test_solar_sources.py +183 -0
  90. smhi2epw-1.1.0/tests/test_target_pressure.py +103 -0
@@ -0,0 +1,18 @@
1
+ version: 2
2
+
3
+ build:
4
+ os: ubuntu-24.04
5
+ tools:
6
+ python: "3.12"
7
+
8
+ sphinx:
9
+ configuration: docs/conf.py
10
+ fail_on_warning: true
11
+
12
+ python:
13
+ install:
14
+ - method: pip
15
+ path: .
16
+ extra_requirements:
17
+ - docs
18
+ - tutorials
smhi2epw-1.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 smhi2epw contributors
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.
@@ -0,0 +1,9 @@
1
+ include .readthedocs.yaml
2
+ recursive-include docs *.nblink *.py *.rst
3
+ recursive-include examples *.ipynb *.md
4
+ include examples/data/completeness_2026-10-05.json
5
+ recursive-include examples/support *.py *.idf
6
+ include scripts/install_energyplus.sh
7
+ include examples/data/solar_validation_2026-10-06.json
8
+ recursive-include tests/fixtures *.py
9
+ include examples/data/pvlib_validation_2026-10-06.json
@@ -0,0 +1,502 @@
1
+ Metadata-Version: 2.4
2
+ Name: smhi2epw
3
+ Version: 1.1.0
4
+ Summary: Build complete-year EnergyPlus weather files from SMHI observations, with documented gap recovery.
5
+ Author: smhi2epw contributors
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/SB-Chalmers/smhi2epw
8
+ Project-URL: Repository, https://github.com/SB-Chalmers/smhi2epw.git
9
+ Project-URL: Issues, https://github.com/SB-Chalmers/smhi2epw/issues
10
+ Keywords: smhi,epw,energyplus,weather,amy,strang,metobs
11
+ Classifier: Programming Language :: Python :: 3.11
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: Programming Language :: Python :: 3.13
14
+ Classifier: Programming Language :: Python :: 3.14
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Topic :: Scientific/Engineering :: Atmospheric Science
17
+ Requires-Python: >=3.11
18
+ Description-Content-Type: text/markdown
19
+ License-File: LICENSE
20
+ Requires-Dist: numpy>=1.24
21
+ Requires-Dist: pandas>=1.5.3
22
+ Requires-Dist: requests>=2.28
23
+ Requires-Dist: pvlib<0.17,>=0.16.1
24
+ Provides-Extra: dev
25
+ Requires-Dist: build>=1.2; extra == "dev"
26
+ Requires-Dist: mypy>=1.10; extra == "dev"
27
+ Requires-Dist: pandas-stubs>=2.2; extra == "dev"
28
+ Requires-Dist: pytest>=8.0; extra == "dev"
29
+ Requires-Dist: ruff>=0.9; extra == "dev"
30
+ Requires-Dist: twine>=5.0; extra == "dev"
31
+ Requires-Dist: types-requests>=2.28; extra == "dev"
32
+ Provides-Extra: test
33
+ Requires-Dist: pytest>=7.0; extra == "test"
34
+ Provides-Extra: docs
35
+ Requires-Dist: nbsphinx>=0.9; extra == "docs"
36
+ Requires-Dist: nbsphinx-link>=1.3; extra == "docs"
37
+ Requires-Dist: sphinx>=7.2; extra == "docs"
38
+ Requires-Dist: sphinx-rtd-theme>=2.0; extra == "docs"
39
+ Provides-Extra: tutorials
40
+ Requires-Dist: ipykernel>=6.25; extra == "tutorials"
41
+ Requires-Dist: ipython>=8.12; extra == "tutorials"
42
+ Requires-Dist: jupyterlab>=4.0; extra == "tutorials"
43
+ Requires-Dist: matplotlib>=3.7; extra == "tutorials"
44
+ Requires-Dist: nbclient>=0.9; extra == "tutorials"
45
+ Requires-Dist: nbformat>=5.9; extra == "tutorials"
46
+ Dynamic: license-file
47
+
48
+ # smhi2epw
49
+
50
+ Build a complete-year EnergyPlus weather file (`.epw`) from Swedish weather
51
+ observations. `smhi2epw` combines SMHI station measurements with STRÅNG solar
52
+ data, checks gaps, and can recover missing hours with nearby stations, bounded
53
+ time profiles, and same-year ERA5 data. It writes a provenance receipt so you
54
+ can see which sources and recovery steps contributed to each file.
55
+
56
+ Use it from the command line or Python for building-energy simulations,
57
+ weather-year comparisons, and reproducible research. It supports completed
58
+ years from 1999 onward and writes 8,760 rows for common years or 8,784 for leap
59
+ years. Automatic recovery is the default; strict mode is available when missing
60
+ source data should stop the build.
61
+
62
+ ## Install
63
+
64
+ Install the latest published version from PyPI:
65
+
66
+ ```bash
67
+ python -m pip install smhi2epw
68
+ ```
69
+
70
+ Python 3.11 or newer is required. The package installs its solar and data
71
+ processing dependencies. Building a new weather file fetches data from SMHI and
72
+ may use Open-Meteo ERA5 when automatic recovery needs it. Reading and analysing
73
+ an existing EPW file works offline.
74
+
75
+ For the notebooks and development tools, clone the repository and install the
76
+ extras:
77
+
78
+ ```bash
79
+ git clone https://github.com/SB-Chalmers/smhi2epw.git
80
+ cd smhi2epw
81
+ python -m pip install -e ".[tutorials]"
82
+ jupyter lab examples/
83
+ ```
84
+
85
+ See the [installation guide](docs/installation.rst) for environment setup and
86
+ the [online documentation](https://smhi2epw.readthedocs.io/) for the full API,
87
+ methods, and tutorials. For contributors, install `.[dev,docs,tutorials]`.
88
+
89
+ ## Usage
90
+
91
+ ### Command line
92
+
93
+ ```bash
94
+ # Explicit station id:
95
+ smhi2epw 2023 gothenburg_2023.epw --station 71420 --city Gothenburg --utc-offset 1
96
+
97
+ # Or auto-select the nearest qualifying station from coordinates:
98
+ smhi2epw 2023 gothenburg_2023.epw --lat 57.7156 --lon 11.9924 --city Gothenburg
99
+
100
+ # Force a fresh fetch, ignoring the on-disk cache:
101
+ smhi2epw 2023 gothenburg_2023.epw --station 71420 --refresh
102
+
103
+ # Retain source-coverage and gap failures:
104
+ smhi2epw 2023 gothenburg_2023.epw --station 71420 --weather-policy strict
105
+
106
+ # Change the 50 km automatic pyranometer limit:
107
+ smhi2epw 2023 gothenburg_2023.epw --station 71420 \
108
+ --radiation-station-max-distance 25
109
+ ```
110
+
111
+ The positional arguments are `year` and `output`. Provide either `--station`,
112
+ or both `--lat` and `--lon` (which also become the STRÅNG solar query point).
113
+
114
+ ### Python API
115
+
116
+ ```python
117
+ from smhi2epw import compile_epw
118
+ from smhi2epw.compiler import EPWConfig
119
+
120
+ result = compile_epw(
121
+ EPWConfig(
122
+ year=2023,
123
+ output_path="gothenburg_2023.epw",
124
+ station_id=71420, # or omit and pass latitude/longitude instead
125
+ city="Gothenburg",
126
+ latitude=57.7156, # nearest-station search + solar query point
127
+ longitude=11.9924,
128
+ utc_offset=1.0, # Local Standard Time; DST ignored
129
+ cache_dir=".smhi_cache",
130
+ refresh=False, # set True to bypass the cache
131
+ weather_policy="automatic", # default; strict disables ERA5 recovery
132
+ )
133
+ )
134
+
135
+ print(result.rows, "rows,", f"{result.interpolated_fraction:.2%} interpolated")
136
+ if result.coordinate_distance_km is not None:
137
+ print(f"{result.coordinate_distance_km:.1f} km from the primary station")
138
+ print("cloud data available:", result.report.cloud_available)
139
+ print(
140
+ "max solar energy-balance residual:",
141
+ result.report.energy_balance_max_residual,
142
+ "W/m^2",
143
+ )
144
+ print("diurnally filled hours:", result.report.diurnal_filled_hours)
145
+ print("recovery warnings:", result.report.warnings)
146
+ # Automatic policy writes gothenburg_2023.epw.json by default.
147
+ ```
148
+
149
+ ### Reading and analysing EPW files
150
+
151
+ ```python
152
+ from smhi2epw import read_epw
153
+
154
+ weather = read_epw("gothenburg_2023.epw")
155
+ print(weather[["dry_bulb", "ghi", "wind_speed"]].describe())
156
+ print(weather.attrs["location"])
157
+ ```
158
+
159
+ `read_epw()` names all 35 fields, recognizes field-specific missing tokens, and
160
+ accepts both chronological AMYs and composite-year TMYs. The original eight
161
+ headers remain available in `weather.attrs["header"]`.
162
+
163
+ ## Tutorials and documentation
164
+
165
+ The [numbered notebook curriculum](examples/README.md) starts with one minimal
166
+ weather file, then covers EPW inspection, location/year comparisons, heat-wave
167
+ detection, AMY-versus-TMY analysis, gap filling, solar components, and batch
168
+ generation. Network and external-data requirements are stated at the top of
169
+ every notebook.
170
+
171
+ ### Build and view the documentation locally
172
+
173
+ From a source checkout, install the documentation and tutorial dependencies
174
+ into your active virtual environment:
175
+
176
+ ```bash
177
+ python -m pip install -e ".[docs,tutorials]"
178
+ ```
179
+
180
+ Build the complete Sphinx site, including the API reference and rendered
181
+ notebooks, with warnings treated as errors:
182
+
183
+ ```bash
184
+ python -m sphinx -W --keep-going -b html docs docs/_build/html
185
+ ```
186
+
187
+ The generated home page is `docs/_build/html/index.html`. The published site is
188
+ available at [smhi2epw.readthedocs.io](https://smhi2epw.readthedocs.io/). For the most reliable
189
+ navigation and search behavior, serve the directory over a local HTTP server:
190
+
191
+ ```bash
192
+ python -m http.server 8000 --directory docs/_build/html
193
+ ```
194
+
195
+ Then open [http://localhost:8000](http://localhost:8000) in a browser. Stop the
196
+ server with <kbd>Ctrl</kbd>+<kbd>C</kbd>. You can also open the HTML file directly
197
+ with `open docs/_build/html/index.html` on macOS, `xdg-open
198
+ docs/_build/html/index.html` on Linux, or `start docs\_build\html\index.html` in
199
+ Windows Command Prompt.
200
+
201
+ Notebook outputs are not executed during the documentation build, so building
202
+ the site does not contact SMHI, Open-Meteo or OneBuilding. The generated
203
+ `docs/_build/` tree is local-only and ignored by Git.
204
+
205
+ ## Pipeline
206
+
207
+ | Layer | Responsibility |
208
+ | --- | --- |
209
+ | Ingestion | Concurrent primary-source retrieval, year-aware station metadata, quality filtering, request retries, buffered hourly UTC grid, local caching, same-year ERA5 fallback |
210
+ | Processing | Short interpolation, assessed donor transfer with optional median bias correction, bounded daily profiles, circular winds, source warnings and physical checks, pressure conversion, dew point and longwave derivation, closed solar components |
211
+ | Export | Exact UTC→LST constant shift, hour 1–24 formatting, standards-compliant headers and 35-field rows, strict range/calendar validation, atomic 8760/8784-row output, recovery comments and JSON provenance |
212
+
213
+ ## Notes
214
+
215
+ - Required gaps of 1–3 hours use interpolation, circularly for wind direction.
216
+ Longer gaps and unfillable short wind gaps first use assessed same-year
217
+ donors. Remaining gaps up to 48 hours use previous/next valid daily profiles
218
+ with endpoint correction and 50/50 mixing, then automatic mode uses ERA5.
219
+ Strict mode permits donors only when explicitly enabled and has no ERA5
220
+ fallback. Neither policy extends temporal filling beyond 48 hours.
221
+ Solar recovery has no donor stage.
222
+ - Observations are filtered by MetObs quality flag; only accepted grades
223
+ (`G`, `Y`) are used, others are treated as gaps.
224
+ - STRÅNG `-999` missing sentinels are removed and back-filled with a
225
+ diurnal-aware (same-hour, day-to-day) interpolation that preserves the solar
226
+ cycle.
227
+ - When total cloud cover (MetObs parameter 16) is available, it populates total
228
+ sky cover and acts as a documented proxy only in the longwave IR calculation.
229
+ Opaque sky cover remains missing because SMHI does not provide it. ERA5 can
230
+ supply missing cloud cover only at hours where it replaces required
231
+ meteorology or GHI; usable cloud values and other hours are preserved.
232
+ - Daylight Savings Time is intentionally ignored to keep solar angles
233
+ continuous. A small UTC buffer is ingested around each year end so the LST
234
+ shift uses real observations at the boundary.
235
+ - Raw payloads are cached on disk (`cache_dir`) so repeated compilations for the
236
+ same station/year are idempotent and avoid redundant API load.
237
+ - STRÅNG parameter semantics are resolved against the live `strang1g` v1 API:
238
+ `117` = global horizontal, `118` = direct *normal*, `121` = direct beam on
239
+ the horizontal plane; diffuse horizontal is derived as `117 − 121`.
240
+ Direct-horizontal parameter 121 starts on 18 April 2017; parameter 118
241
+ (DNI) is requested throughout the supported history from 1999. Before
242
+ parameter 121 is available, horizontal beam is projected from instantaneous
243
+ DNI before adjacent-sample averaging. The continuous Erbs-Driesse form of
244
+ the Erbs (1982) model is used only when usable direct components are absent;
245
+ see the
246
+ [SMHI extraction guide](https://strang.smhi.se/extraction/index.php).
247
+ - STRÅNG values are instantaneous irradiance at the full hour. The pipeline
248
+ converts them to EPW interval-averaged irradiance (preceding-hour mean) by
249
+ averaging adjacent samples. Supplied DNI and horizontal beam retain their
250
+ separate interval means, including below five degrees solar elevation.
251
+ The five-degree guard applies only when DNI must be inferred by dividing
252
+ horizontal radiation by solar geometry. Interval closure is `GHI = DHI +
253
+ horizontal beam`; mean DNI times a midpoint cosine is an approximation.
254
+ - Solar geometry uses pvlib's NREL SPA `nrel_numpy` method and geometric
255
+ (unrefracted) zenith. `delta_t=None` lets pvlib calculate the terrestrial-time
256
+ correction for each UTC year/month. Extraterrestrial irradiance uses pvlib's
257
+ `asce` method with a 1367 W/m² solar constant. Five-minute integration over
258
+ the preceding hour supplies interval geometry and caps. Both weather
259
+ policies use these fixed solar methods.
260
+ - `result.report.solar_source` reports which solar path was used:
261
+ `"strang"` (supplied STRÅNG DNI and supplied/projected horizontal beam),
262
+ `"measured+strang_partition"` (nearby Sol station GHI with the STRÅNG partition),
263
+ `"strang_ghi+erbs"` (Erbs-Driesse on STRÅNG GHI when direct components are absent),
264
+ `"measured+erbs"` (Erbs-Driesse on measured GHI), `"era5"` (ERA5 solar fallback),
265
+ or `"mixed"` (multiple solar sources). The `+erbs` labels retain their existing
266
+ spelling as Erbs-family identifiers. New JSON sidecars include top-level
267
+ `pvlib_version` alongside the package version and source hashes.
268
+ - Automatic pyranometer discovery is limited to 50 km by default. If its data
269
+ cannot satisfy the 48-hour policy, compilation falls back to STRÅNG.
270
+ Explicitly requested radiation stations fail in strict mode; automatic mode
271
+ records the failure and continues solar recovery.
272
+ - Only whole-hour Local Standard Time offsets are supported. This covers the
273
+ Nordic STRÅNG region without silently resampling hourly source data.
274
+ - A warning is logged when the solar query point is outside Sweden (~55–69.5°N,
275
+ 10–24.5°E), where STRÅNG accuracy degrades (RMSD up to 30–40% for GHI).
276
+
277
+ ## Validation of the pvlib solar methods
278
+
279
+ On **6 October 2026**, the rebuilt wheel with pvlib 0.16.1 passed **261 offline
280
+ checks**, including notebook 07, and **7 required EnergyPlus tests**. Notebook 09
281
+ also completed all **8,760 hours with zero engine warnings**. The Python 3.11
282
+ minimum-dependency run passed 257 checks; four optional notebook tests were
283
+ skipped there and executed in the full wheel environment. Static checks and the
284
+ HTML documentation build passed, with 68 documentation doctests and no warnings.
285
+ Explicit timestamp keywords keep the declared pandas 1.5.3 floor working.
286
+
287
+ Six paired engineering cases used identical provider payloads, building models,
288
+ and the pinned EnergyPlus engine. Five cases had identical annual heating,
289
+ cooling and window-solar totals. For historical 2016 data, SPA changed the
290
+ horizontal beam projected from instantaneous DNI: annual heating changed by
291
+ **−0.1154%**, cooling by **−0.1385%**, and window solar by **−0.1235%**. GHI and DNI
292
+ were unchanged; the largest hourly DHI change was 4 Wh/m². Separate tests compare
293
+ classic Erbs and Erbs-Driesse at identical geometry, confirming a maximum
294
+ **0.000429 diffuse-fraction difference**, below the documented 0.0005 bound.
295
+
296
+ Warm annual geometry calls on this machine took about **17 ms** for zenith and
297
+ **224–226 ms** for preceding-hour integration, versus about 0.8 ms and 13 ms
298
+ previously. The existing five-minute integration is retained without caching.
299
+
300
+ The [portable validation summary](examples/data/pvlib_validation_2026-10-06.json)
301
+ records wheel/source/input identities, all six cases, and runtime measurements.
302
+ The rebuilt wheel generated byte-identical weather for the paired comparison;
303
+ its consumer checks were also run separately. These are numerical and consumer
304
+ regressions, not independent evidence of site-weather accuracy. The archived validation summary records the source revision and test evidence;
305
+ GitHub-hosted release checks run again for each release tag.
306
+
307
+
308
+ ## Completeness-run results
309
+
310
+ The EPSM national measurement-year weather runs on **5 October 2026** covered
311
+ requested municipality/year jobs from **2017–2024**:
312
+
313
+ | Run | Weather policy / source commit | Completed | Failed |
314
+ | --- | --- | ---: | ---: |
315
+ | v1 | Historical bounded temporal filling; Git commit unrecorded | 32 / 85 | 53 |
316
+ | v2 | Opt-in raw nearby observations, `4861f9e`; no ERA5 | 124 / 128 | 4 |
317
+ | v3 | Automatic assessed donors + ERA5, `5930c07` | **128 / 128** | **0** |
318
+
319
+ Preparation expanded the job set from 85 to 128; v2 and v3 use the same job
320
+ manifest. V1/v2 figures are retained historical status counts; their EPWs are
321
+ no longer available locally. On 6 October, all 128 v3 files were independently
322
+ rechecked against recorded hashes, exact ordered calendars, 35-field rows,
323
+ finite required fields, export ranges and dew-point consistency. They contain
324
+ 112 common-year and 16 leap-year files: **1,121,664 exported hours**.
325
+
326
+ Completeness includes reconstruction. All 128 v3 files are
327
+ `mixed_reconstructed`: 101 use donor meteorology, 35 use ERA5 meteorology, and
328
+ 126 use temporal filling. These groups overlap. **25 / 128** reconstruct more
329
+ than 5% of required meteorological cells; the fraction ranges from 0.0114% to
330
+ 100%. This statistic uses five meteorological variables on the buffered UTC
331
+ grid and excludes solar; exported-hour source shares are reported separately.
332
+ Five percent is a reporting aid, not a validated acceptance threshold. Review
333
+ source fractions and warnings before calibration or extreme-event analysis.
334
+ Successful export establishes complete weather inputs, not local weather accuracy.
335
+ The archived outputs predate the solar and actual-year header corrections and
336
+ the pvlib implementation. Their counts describe the recorded source revisions.
337
+ Regenerate inputs in a new directory to apply the current solar methods and
338
+ EnergyPlus acceptance checks.
339
+
340
+ The [portable evidence summary](examples/data/completeness_2026-10-05.json)
341
+ includes policies, source identities, artifact hashes, warning counts and
342
+ validation definitions. [Notebook 08](examples/08_batch_generation.ipynb)
343
+ reads it offline and records reconstruction diagnostics for new batches. The
344
+ [recovery guide](docs/weather_recovery.rst) explains the separate earlier
345
+ 52-of-53 failed-job replay and the current run's limitations.
346
+
347
+ ## Recorded pre-pvlib sensitivity check
348
+
349
+ A recorded check on **6 October 2026**, after the solar fixes (`050d195`) and
350
+ before the pvlib implementation, rebuilt references and two outage cases for
351
+ each selected weather-year. All **9 annual
352
+ EnergyPlus simulations** completed with zero warnings; paired models and
353
+ unmasked EPW rows were identical. One fixed, illustrative 100 m² ideal-load
354
+ building was used. Changes below are signed differences from its corrected
355
+ reference, not national uncertainty bounds or HVAC electricity.
356
+
357
+ | Weather-year | 168 h solar outage: annual sensible cooling change | 48 h meteorology outage at temperature maximum: cooling change | Temperature MAE in hidden hours |
358
+ | --- | ---: | ---: | ---: |
359
+ | Gothenburg 2016 | +0.900 kWh/m² (+4.06%) | +0.102% | 0.923 °C |
360
+ | Luleå 2023 | +0.618 kWh/m² (+3.04%) | +0.094% | 1.142 °C |
361
+ | Gothenburg 2024 | +1.259 kWh/m² (+6.79%) | −0.166% | 0.856 °C |
362
+
363
+ Nighttime zeros split the solar outages into daylight gaps filled by daily
364
+ profiles. Meteorology used assessed fixed donors, with circular temporal wind
365
+ recovery where a donor was rejected. None of these six outages selected ERA5;
366
+ its transport and recovery are covered separately by deterministic integration
367
+ tests. The reference solar partition still includes modelled STRÅNG radiation.
368
+ These results describe the selected weather, model and masks; they do not
369
+ independently validate DNI/DHI or establish a universal donor accuracy.
370
+
371
+ The [portable sensitivity summary](examples/data/solar_validation_2026-10-06.json)
372
+ contains assumptions, recovery decisions, source/model hashes and validation
373
+ checks. Notebook 08 reads its results offline alongside completeness evidence.
374
+ Both evidence JSON files and all reported numbers retain their recorded source
375
+ identities. They have not been regenerated with pvlib. The earlier frozen
376
+ campaign is also retained with its original source identity.
377
+
378
+ ## Run the weather in EnergyPlus
379
+
380
+ [Notebook 09](examples/09_run_energyplus.ipynb) runs a generated AMY in a small,
381
+ standalone single-zone model and inspects temperatures, solar gains, ideal loads,
382
+ and engine diagnostics. It uses a local EPW and requires **EnergyPlus 24.2.0
383
+ build 94a887817b**; set `ENERGYPLUS_EXE` to its executable if it is outside `PATH`.
384
+ The engine is a separate optional installation, with official platform archives
385
+ at the [24.2.0 bug-fix release](https://github.com/NatLabRockies/EnergyPlus/releases/tag/v24.2.0a).
386
+ The notebook performs no provider requests and needs no EPSM package.
387
+
388
+ CI requires deterministic provider-to-EnergyPlus tests against the built wheel,
389
+ including common and leap-year calendars and recovery cases. Missing engines,
390
+ severe/fatal errors, unexpected warnings, and incomplete hourly outputs fail the
391
+ gate. Release tags validate the same wheel that is published after all required
392
+ checks succeed. Live provider checks run separately on the weekly schedule.
393
+ See the [EnergyPlus validation guide](docs/energyplus.rst) for the engine pin,
394
+ warning policy, distribution checks, and publication setup. Engine acceptance
395
+ establishes consumer compatibility, not local weather accuracy or calibration.
396
+
397
+ ## Development
398
+
399
+ ```bash
400
+ pip install -e ".[dev]"
401
+ pytest -m "not network and not energyplus" # offline Python suite
402
+ pytest -m energyplus # requires the pinned engine; provider requests are offline
403
+ pytest -m network # live integration tests against weather-source endpoints
404
+ ruff check src tests examples
405
+ ruff format --check src tests examples
406
+ mypy src/smhi2epw
407
+ python -m build && twine check dist/*
408
+ pytest --doctest-modules src/smhi2epw
409
+ python -m sphinx -W --keep-going -b html docs docs/_build/html
410
+ ```
411
+
412
+ Use `pytest -m "not network and not energyplus"` for the Python-only offline
413
+ suite. Plain `pytest` also selects live and engine integration tests; markers
414
+ do not skip them by themselves.
415
+ Provider access and quotas apply to live SMHI and Open-Meteo requests.
416
+
417
+ ## License
418
+
419
+ This project is distributed under the [MIT License](LICENSE).
420
+
421
+ ### Target elevation and pressure correction
422
+
423
+ `EPWConfig.target_elevation_m` / `--target-elevation-m` sets the EPW site height;
424
+ omitting it uses station elevation. SMHI parameter 9 supplies sea-level QFF, so
425
+ the compiler now derives surface pressure using the inverse SMHI reduction.
426
+ `CompileResult` retains target coordinates, target elevation, the pressure method
427
+ and the original observation-station metadata separately. Rebuild weather files
428
+ when adopting this correction; see `docs/provenance.rst` for assumptions.
429
+
430
+
431
+ ### Actual-year EPW calendar headers
432
+
433
+ Exports declare leap-day observation as `Yes` for Gregorian leap years (8784
434
+ hours) and `No` for common years (8760 hours). The data-period start weekday
435
+ matches January 1 of the measurement year. `write_epw` rejects contradictory
436
+ calendar headers before replacing an output file. Downstream workflows do not
437
+ need to patch headers after compilation. Previously exported or pinned study
438
+ files are not modified; regenerate in a fresh directory when migrating.
439
+
440
+
441
+ ### Automatic recovery, warnings and provenance
442
+
443
+ `EPWConfig.weather_policy="automatic"` and CLI `--weather-policy automatic`
444
+ are the defaults. Preserve usable primary data, interpolate gaps up to three
445
+ hours, assess at most three nearby stations within 75 km for longer or unfillable
446
+ short wind gaps, then use bounded daily profiles through 48 hours. Same-year
447
+ ERA5 at the requested point recovers required hours still missing. Source failures
448
+ and recovery decisions are visible in `result.report.warnings` and the CLI summary.
449
+ Valid primary extreme temperatures are retained; physically impossible values are
450
+ recovered, and abrupt source transitions produce warnings. Hourly solar inputs
451
+ outside 0–2,000 W/m² are discarded as broadly implausible; this engineering guard
452
+ does not clip plausible heatwave temperatures. When ERA5 is used, overlap with
453
+ original observations is reported and substantial disagreement produces warnings
454
+ without rejecting otherwise usable primary extremes. Recovery cannot guarantee
455
+ an event's peak intensity or persistence. The sensitivity campaign covered
456
+ 17 weather-years, six regions and six building profiles. Short-gap annual-load
457
+ errors were generally small, while long solar gaps and missing event peaks were
458
+ more sensitive. These are conditional comparisons against the campaign's own
459
+ reference weather: shared solar processing can hide a common error, and multiple
460
+ building profiles do not create independent weather samples. They establish
461
+ neither building-site accuracy nor universal donor superiority. See
462
+ [the method limitations](docs/limitations.rst).
463
+
464
+ Donors are checked against original same-variable overlap near each gap, using
465
+ held-out complete days. A scalar median offset is applied only when it improves
466
+ validation MAE by at least 10%; wind directions are assessed without rotation.
467
+ Insufficient overlap and excessive error reject a donor. These checks establish
468
+ agreement with a station during overlap, not building-site accuracy during the
469
+ outage. Model estimates may miss local microclimates and extreme intensity.
470
+
471
+ Automatic mode writes `OUTPUT.epw.json` beside the EPW. Set
472
+ `EPWConfig.provenance_path` or CLI `--provenance PATH.json` to choose its path.
473
+ The receipt records warnings, sources and recovery fractions, donor assessments,
474
+ reanalysis metadata, configuration, code identity, output checksum and hashes
475
+ of consumed responses. Cached and live payloads follow the same contract;
476
+ receipts are isolated for each compilation even when a client is reused.
477
+ `reanalysis_filled_hours` includes recovered cloud hours; `source_fractions`
478
+ currently describes required meteorology and GHI, excluding optional cloud cover.
479
+ Custom clients without scoped response receipts are marked incomplete. Paths
480
+ must differ and their parent directories must exist. Automatic mode returns the
481
+ valid EPW with a `provenance_write_failed` warning if a later sidecar-write
482
+ filesystem failure prevents saving its audit trail. Keep that warning visible.
483
+
484
+ Use `weather_policy="strict"` or `--weather-policy strict` for the earlier
485
+ failure behavior. The legacy `metobs_gap_fallback=True` / `--metobs-gap-fallback`
486
+ flag enables assessed donors in strict mode; automatic mode already uses them.
487
+ `gap_fallback_max_distance_km` and `gap_fallback_max_stations` limit donor search
488
+ in either policy. Strict mode writes a sidecar only when explicitly requested.
489
+
490
+ Automatic recovery still fails on invalid configuration, unknown coordinates,
491
+ unavailable weather from all sources, incomplete future-year data,
492
+ unrecoverable physical inconsistencies and filesystem errors. It never
493
+ substitutes another year or claims success without a complete valid calendar.
494
+ The Open-Meteo adapter uses the public noncommercial service; check
495
+ [current access limits](https://open-meteo.com/en/pricing) before batch or
496
+ commercial use, and acknowledge Open-Meteo and Copernicus Climate Change Service
497
+ ERA5. See the [Historical Weather API documentation](https://open-meteo.com/en/docs/historical-weather-api)
498
+ for source definitions.
499
+
500
+ See [the weather-recovery guide](docs/weather_recovery.rst) for assessment
501
+ thresholds, source units, diagnostic definitions, strict-mode examples,
502
+ historical replay evidence and limitations.