Skip to content

Commit ac77418

Browse files
authored
Merge pull request #4 from SMI-Lab-Inha/academic-polish
Academic polish: docs encoding, provenance, validation framing, supply-chain hardening
2 parents 8bbb12f + c4b3ef4 commit ac77418

16 files changed

Lines changed: 435 additions & 123 deletions

.github/dependabot.yml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
version: 2
2+
updates:
3+
# Python dependencies declared in pyproject.toml (runtime + extras).
4+
- package-ecosystem: "pip"
5+
directory: "/"
6+
schedule:
7+
interval: "weekly"
8+
open-pull-requests-limit: 5
9+
10+
# GitHub Actions used by the workflows.
11+
- package-ecosystem: "github-actions"
12+
directory: "/"
13+
schedule:
14+
interval: "weekly"
15+
open-pull-requests-limit: 5

.github/workflows/ci.yml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,30 @@ jobs:
6868
- name: Run tests with coverage gate
6969
run: pytest --cov=pyiecwind --cov-report=term-missing
7070

71+
audit:
72+
name: Dependency audit
73+
runs-on: ubuntu-latest
74+
steps:
75+
- name: Check out repository
76+
uses: actions/checkout@v4
77+
78+
- name: Set up Python
79+
uses: actions/setup-python@v5
80+
with:
81+
python-version: "3.12"
82+
83+
- name: Install package and pip-audit
84+
run: |
85+
python -m pip install --upgrade pip
86+
python -m pip install -e . pip-audit
87+
88+
- name: Audit dependencies for known vulnerabilities
89+
# Skip the local (editable) pyiecwind package, which is not on PyPI and
90+
# cannot be audited. --strict is intentionally omitted so the skipped
91+
# local package is not itself treated as a failure; pip-audit still exits
92+
# non-zero when a real vulnerability is found in a dependency.
93+
run: pip-audit --skip-editable
94+
7195
docs:
7296
name: Build documentation
7397
needs: lint

.github/workflows/release.yml

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
name: Release
2+
3+
# Publishes to PyPI when a version tag is pushed, with build provenance
4+
# attestation. Requires a one-time setup on PyPI: configure a Trusted Publisher
5+
# for this project pointing at this repository and the "release.yml" workflow
6+
# (https://docs.pypi.org/trusted-publishers/). No API token is stored.
7+
8+
on:
9+
push:
10+
tags:
11+
- "v*"
12+
13+
permissions:
14+
contents: read
15+
16+
jobs:
17+
release:
18+
name: Build, attest, and publish
19+
runs-on: ubuntu-latest
20+
permissions:
21+
contents: read
22+
id-token: write # OIDC: trusted publishing + attestation signing
23+
attestations: write # build provenance attestation
24+
steps:
25+
- name: Check out repository
26+
uses: actions/checkout@v4
27+
28+
- name: Set up Python
29+
uses: actions/setup-python@v5
30+
with:
31+
python-version: "3.12"
32+
33+
- name: Build sdist and wheel
34+
run: |
35+
python -m pip install --upgrade pip build twine
36+
python -m build
37+
twine check dist/*
38+
39+
- name: Attest build provenance
40+
uses: actions/attest-build-provenance@v1
41+
with:
42+
subject-path: "dist/*"
43+
44+
- name: Publish to PyPI (trusted publishing)
45+
uses: pypa/gh-action-pypi-publish@release/v1

CITATION.cff

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
cff-version: 1.2.0
2+
message: "If you use this software, please cite it as below."
3+
title: "pyIECWind: IEC 61400-1 wind-condition file generation for OpenFAST InflowWind"
4+
abstract: >-
5+
pyIECWind generates IEC 61400-1 wind-condition (.wnd) files for the OpenFAST
6+
InflowWind module, implementing the wind-condition models historically provided
7+
by NREL IECWind behind a typed, validated, and regression-locked Python API and
8+
command-line interface.
9+
type: software
10+
authors:
11+
- family-names: Seo
12+
given-names: Jae Hoon
13+
affiliation: "SMI Lab, Inha University"
14+
# orcid: "https://orcid.org/0000-0000-0000-0000" # add when available
15+
version: "0.1.0"
16+
date-released: "2026-05-22"
17+
license: MIT
18+
repository-code: "https://github.com/SMI-Lab-Inha/pyIECWind"
19+
url: "https://github.com/SMI-Lab-Inha/pyIECWind"
20+
keywords:
21+
- wind energy
22+
- wind turbine
23+
- IEC 61400-1
24+
- IECWind
25+
- OpenFAST
26+
- InflowWind
27+
# identifiers:
28+
# - type: doi
29+
# value: "10.5281/zenodo.XXXXXXX" # add a Zenodo DOI after archiving a release

README.md

Lines changed: 84 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -7,76 +7,97 @@
77
![OpenFAST](https://img.shields.io/badge/OpenFAST-InflowWind-0A7E8C)
88

99
`pyIECWind` generates IEC 61400-1 wind-condition `.wnd` files for the OpenFAST
10-
*InflowWind* module. It reproduces the wind-condition models of the legacy NREL
11-
*IECWind* tool behind a typed, validated Python API, a reproducible command-line
12-
interface, and a regression-locked test suite.
13-
14-
The six classical condition families — `ECD`, `EWS`, `EOG`, `EDC`, `NWP`, and
15-
`EWM` — are supported across SI and English unit systems and IEC 61400-1
16-
Editions 1 and 3. See [Scope and limitations](docs/limitations.rst) for the
17-
precise boundary of what is implemented.
18-
19-
## Highlights
20-
21-
- **Validated inputs.** `IECParameters` is frozen and validated on construction;
22-
invalid turbine definitions fail immediately rather than producing misleading
23-
files.
24-
- **Fail-closed generation.** The batch API raises on the first invalid condition
25-
by default; partial output is opt-in.
26-
- **Verified output.** A committed golden `.wnd` corpus and an independent oracle
27-
derived from the IEC equations lock numeric results
28-
([Validation](docs/validation.rst)).
29-
- **Three input formats.** An OpenFAST-style table (recommended), a keyed format,
30-
and the legacy positional layout ([Input files](docs/data_sources.rst)).
31-
- **Typed and documented.** `mypy --strict`, NumPy-style docstrings, and a Sphinx
32-
documentation set.
10+
*InflowWind* module. It implements the IEC 61400-1 wind-condition models
11+
historically provided by NREL's *IECWind* tool, behind a typed, validated, and
12+
regression-locked Python API and command-line interface.
13+
14+
It targets compatibility with selected legacy *IECWind*-style workflows. It has
15+
**not** been validated byte-for-byte against NREL *IECWind* output; see
16+
[Validation](#validation) for the exact strength of the current evidence.
3317

3418
## Installation
3519

3620
```bash
3721
python -m pip install pyiecwind
3822
```
3923

40-
For a conda environment or a source/development install, see
41-
[Installation](docs/installation.rst).
24+
Conda and source/development installs are described in
25+
[docs/installation.rst](docs/installation.rst).
4226

4327
## Usage
4428

45-
Generate wind files from an input file:
29+
Command line:
4630

4731
```bash
48-
pyiecwind run examples/sample_case.ipt -o outputs
32+
pyiecwind run examples/sample_case.ipt -o outputs # generate from an input file
33+
pyiecwind template my_case.ipt # write a starter template
34+
pyiecwind wizard -o outputs # guided, interactive build
4935
```
5036

51-
Write a starter template, or use the guided wizard:
37+
Python API:
5238

53-
```bash
54-
pyiecwind template my_case.ipt
55-
pyiecwind wizard -o outputs
39+
```python
40+
from pyiecwind import IECParameters, generate_all
41+
42+
params = IECParameters(
43+
si_unit=True, t1=40.0, wtc=2, catg="B", slope_deg=0.0, iec_edition=3,
44+
hh=80.0, dia=80.0, vin=4.0, vrated=10.0, vout=24.0,
45+
conditions=("ECD+R", "EWSV+12.0", "EWM50"),
46+
)
47+
result = generate_all(params, output_dir="outputs")
48+
print(result.count)
5649
```
5750

58-
From Python:
51+
`IECParameters` validates on construction and is immutable; `generate_all` fails
52+
closed (raises on the first invalid condition) unless `strict=False`.
5953

60-
```python
61-
from pyiecwind import generate_from_input_file
54+
## Supported scope
6255

63-
params, result = generate_from_input_file("examples/sample_case.ipt", output_dir="outputs")
64-
print(f"generated {result.count} files")
65-
```
56+
| Aspect | Supported |
57+
| --- | --- |
58+
| IEC 61400-1 edition | Editions 1 and 3 (power-law shear exponent alpha = 0.2 / 0.14) |
59+
| Turbine class | I, II, III (Vref = 50 / 42.5 / 37.5 m/s) |
60+
| Turbulence category | A, B, C (Iref = 0.16 / 0.14 / 0.12) |
61+
| Condition families | ECD, EWS, EOG, EDC, NWP, EWM |
62+
| Unit systems | SI (m, m/s) and English (ft, ft/s) |
63+
| Inflow inclination | up to 8 deg (larger values warn) |
6664

67-
See [Quickstart](docs/quickstart.rst) for more.
65+
### Limitations and non-goals
6866

69-
## Documentation
67+
- IEC 61400-1 Edition 2 and other editions are rejected (opt-in legacy coercion only).
68+
- Turbine class S and site-specific/measured turbulence are not modelled.
69+
- Speed modifiers are valid only on the rated (`R`) reference.
70+
- This package generates inflow files; it does not run OpenFAST.
71+
72+
Full detail: [docs/limitations.rst](docs/limitations.rst).
73+
74+
## Validation
75+
76+
Output is protected by three layers of differing strength, documented with a
77+
per-scenario reproducibility matrix in [docs/validation.rst](docs/validation.rst):
7078

71-
The full documentation is hosted at
72-
[pyiecwind.readthedocs.io](https://pyiecwind.readthedocs.io/en/latest/). The
73-
source is in [`docs/`](docs/) and builds with Sphinx
74-
(`sphinx-build -W -b html docs docs/_build/html`). Key references:
79+
1. **Golden regression corpus** - committed reference `.wnd` files regenerated and
80+
compared on every run. These are generated by this package, so they detect
81+
*regressions*, not original-formulation errors.
82+
2. **Independent analytical oracle** - each family's headline quantity is
83+
recomputed directly from the IEC 61400-1 equations (reference values
84+
transcribed independently) and compared to the output.
85+
3. **Property/invariant tests** - durations, row counts, unit scaling, grammar,
86+
and direction-ramp monotonicity (Hypothesis).
7587

76-
- [Theory](docs/theory.rst) — the IEC equations behind each case family
77-
- [Input files](docs/data_sources.rst) and [Units](docs/units.rst)
78-
- [API reference](docs/api.rst) and [API contract](docs/api_contract.rst)
79-
- [Validation](docs/validation.rst) and [Scope and limitations](docs/limitations.rst)
88+
**Tolerance policy:** header/comment lines are compared exactly (version stamp
89+
masked); numeric data rows are compared column-by-column within +/- 5e-4, half of
90+
the rendered three-decimal precision. **No external byte-for-byte comparison
91+
against NREL IECWind binaries has been performed**; such an external reference
92+
layer is future work.
93+
94+
## Documentation
95+
96+
Hosted at [pyiecwind.readthedocs.io](https://pyiecwind.readthedocs.io/en/latest/)
97+
(source in [`docs/`](docs/), built with Sphinx). Highlights:
98+
[Theory](docs/theory.rst) (IEC equations), [API reference](docs/api.rst),
99+
[API contract](docs/api_contract.rst), [Input files](docs/data_sources.rst),
100+
[Units](docs/units.rst).
80101

81102
## Development
82103

@@ -87,8 +108,22 @@ mypy
87108
pytest --cov=pyiecwind
88109
```
89110

90-
Contribution guidelines and the full quality-gate list are in
91-
[Contributing](docs/contributing.rst).
111+
Quality gates and contribution guidelines: [docs/contributing.rst](docs/contributing.rst).
112+
113+
## Citation
114+
115+
If you use `pyIECWind` in academic work, please cite it. Citation metadata is in
116+
[`CITATION.cff`](CITATION.cff); GitHub renders a ready-to-use citation from that
117+
file under *Cite this repository*.
118+
119+
## References
120+
121+
- IEC 61400-1, *Wind turbines - Part 1: Design requirements* (Editions 1 and 3).
122+
- B. J. Jonkman and M. L. Buhl, *IECWind*, National Renewable Energy Laboratory.
123+
- OpenFAST *InflowWind* module documentation, National Renewable Energy Laboratory.
124+
125+
See [docs/theory.rst](docs/theory.rst) for the equations and their mapping to the
126+
implementation.
92127

93128
## License
94129

benchmarks/bench_generation.py

Lines changed: 42 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,20 @@
66
77
Usage::
88
9-
python benchmarks/bench_generation.py [repeats]
9+
python benchmarks/bench_generation.py [repeats] # human-readable
10+
python benchmarks/bench_generation.py [repeats] --json # machine-readable
11+
12+
The ``--json`` output is suitable for recording a baseline and comparing against
13+
later runs. See the threshold policy in ``docs/contributing.rst`` -- CI runs this
14+
as a smoke test (must complete without error); wall-clock comparison is done
15+
offline against a recorded baseline rather than gated in CI, because shared
16+
runners are too noisy for a stable threshold.
1017
"""
1118

1219
from __future__ import annotations
1320

21+
import argparse
22+
import json
1423
import sys
1524
import tempfile
1625
import time
@@ -35,22 +44,42 @@ def run_once() -> int:
3544
return total
3645

3746

38-
def main(argv: list[str]) -> int:
39-
repeats = int(argv[1]) if len(argv) > 1 else 5
47+
def measure(repeats: int) -> dict[str, float | int]:
48+
"""Run the workload ``repeats`` times (after a warm-up) and summarise timings."""
4049

4150
files = run_once() # warm up imports and the filesystem
42-
samples = []
43-
for _ in range(repeats):
44-
start = time.perf_counter()
45-
run_once()
46-
samples.append(time.perf_counter() - start)
47-
51+
samples = [(_timed_run()) for _ in range(repeats)]
4852
best = min(samples)
49-
mean = sum(samples) / len(samples)
50-
print(f"scenarios={len(SCENARIOS)} files/run={files} repeats={repeats}")
51-
print(f"best={best * 1e3:.1f} ms mean={mean * 1e3:.1f} ms per-file={best / files * 1e6:.0f} us")
53+
return {
54+
"scenarios": len(SCENARIOS),
55+
"files_per_run": files,
56+
"repeats": repeats,
57+
"best_ms": round(best * 1e3, 3),
58+
"mean_ms": round(sum(samples) / len(samples) * 1e3, 3),
59+
"per_file_us": round(best / files * 1e6, 1),
60+
}
61+
62+
63+
def _timed_run() -> float:
64+
start = time.perf_counter()
65+
run_once()
66+
return time.perf_counter() - start
67+
68+
69+
def main(argv: list[str] | None = None) -> int:
70+
parser = argparse.ArgumentParser(description="Benchmark wind-file generation.")
71+
parser.add_argument("repeats", nargs="?", type=int, default=5)
72+
parser.add_argument("--json", action="store_true", help="Emit machine-readable JSON.")
73+
args = parser.parse_args(argv)
74+
75+
result = measure(args.repeats)
76+
if args.json:
77+
print(json.dumps(result))
78+
else:
79+
print(f"scenarios={result['scenarios']} files/run={result['files_per_run']} repeats={result['repeats']}")
80+
print(f"best={result['best_ms']} ms mean={result['mean_ms']} ms per-file={result['per_file_us']} us")
5281
return 0
5382

5483

5584
if __name__ == "__main__":
56-
raise SystemExit(main(sys.argv))
85+
raise SystemExit(main())

docs/api_contract.rst

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ Two modules are supported import facades:
2626
``gen_ewm``) and the shared constants.
2727

2828
Every module declares an explicit ``__all__``. **Any name not listed in a
29-
module's ``__all__`` in particular underscore-prefixed helpers is internal
29+
module's ``__all__`` - in particular underscore-prefixed helpers - is internal
3030
and may change or disappear without notice.** Importing internals from their
3131
defining modules is unsupported.
3232

@@ -46,8 +46,8 @@ Validated, immutable parameters
4646
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
4747

4848
:class:`~pyiecwind.IECParameters` is a frozen dataclass that validates on
49-
construction. Every public path the parser, the wizard, and direct
50-
construction goes through the same validation, so an object that violates the
49+
construction. Every public path - the parser, the wizard, and direct
50+
construction - goes through the same validation, so an object that violates the
5151
IEC class/category/edition, geometry, or speed-ordering rules cannot exist. The
5252
``conditions`` field is stored as a tuple; instances cannot be mutated into an
5353
invalid state afterwards.
@@ -112,5 +112,5 @@ Concurrency and thread-safety
112112
Generating different conditions, or the same conditions into different output
113113
directories, is safe to run concurrently.
114114
* The one shared resource is the filesystem. Writing the **same** output path
115-
from multiple workers at once is **not** safe give each worker a distinct
115+
from multiple workers at once is **not** safe - give each worker a distinct
116116
``output_dir``. The batch helpers already write one file per condition code.

0 commit comments

Comments
 (0)