Skip to content

Commit 0a08605

Browse files
authored
Merge pull request #15 from SMI-Lab-Inha/input-contract-and-strict
Harden input contract and make the CLI fail closed
2 parents 8cdee4a + 1aa5bf8 commit 0a08605

70 files changed

Lines changed: 801 additions & 151 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CHANGELOG.md

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,49 @@ All notable changes to `pyIECWind` will be documented in this file.
44

55
The format is inspired by Keep a Changelog, adapted to the needs of this project.
66

7+
## [0.2.0] - 2026-05-22
8+
9+
Input-contract and CLI hardening. No changes to the numeric `.wnd` output of any
10+
valid case. Note the changed CLI default below: `pyiecwind run` now fails closed.
11+
12+
### Added
13+
14+
- Atomic generation: `generate_all(..., atomic=True)` (and the matching
15+
`generate_from_input_file(..., atomic=True)`) stage every file in a temporary
16+
directory and only commit them once the whole batch has been generated, so a
17+
mid-batch failure under `strict=True` leaves no partial output behind.
18+
- Optional `! format: <id>` input directive (`openfast-table-v1`, `keyed-v1`,
19+
`legacy-v1`) that pins the layout and overrides auto-detection; auto-detection
20+
is now the documented compatibility fallback. Emitted files (template, wizard)
21+
carry the directive so they are self-describing.
22+
- A formal **Input Format v1** specification in the documentation (lexical rules,
23+
duplicate-field policy, condition-code grammar, unit semantics, and
24+
invalid-file examples), and a per-family traceability table in the theory page
25+
mapping each equation to its code, oracle test, and golden scenarios.
26+
27+
### Changed
28+
29+
- The `run` command (and the no-argument default) now fail closed by default:
30+
an unparseable input or invalid condition aborts with a clean message and
31+
writes no files (atomic + strict). The existing `--continue-on-error` flag now
32+
selects the previous lenient behaviour (generate the valid conditions, skip and
33+
report the rest). The interactive wizard also fails closed.
34+
- Input parsers reject a scalar field defined more than once (directly or via an
35+
alias), reporting both line numbers, instead of silently keeping the last
36+
value.
37+
- Development status classifier moved from Alpha to Beta now that the input
38+
contract and release-package contents are settled.
39+
- The golden corpus stores its version stamp in masked form (`pyIECWind vX`), so
40+
it is version-independent and no longer needs regenerating after a release bump.
41+
42+
### Fixed
43+
44+
- Input files are read with `utf-8-sig`, so a leading byte-order mark (common in
45+
files saved by Windows editors) no longer corrupts the first line.
46+
- A repeated condition code (it maps to the same `.wnd` file) is now generated
47+
once instead of being staged twice, which previously broke `atomic=True` with a
48+
`FileNotFoundError` and left partial output behind.
49+
750
## [0.1.2] - 2026-05-22
851

952
Packaging, release-process, and documentation fixes. No changes to generated

CITATION.cff

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ authors:
1313
given-names: Jae Hoon
1414
affiliation: "SMI Lab, Inha University"
1515
# orcid: "https://orcid.org/0000-0000-0000-0000" # add when available
16-
version: "0.1.2"
16+
version: "0.2.0"
1717
date-released: "2026-05-22"
1818
license: MIT
1919
repository-code: "https://github.com/SMI-Lab-Inha/pyIECWind"

docs/data_sources.rst

Lines changed: 199 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,177 @@
1-
Input files
2-
===========
1+
Input file format (v1)
2+
======================
33

44
``pyIECWind`` reads a single plain-text input file describing the turbine and the
5-
conditions to generate. Three layouts are auto-detected by
6-
:func:`~pyiecwind.parse_input_file`; the **OpenFAST-style table** is recommended
7-
for new work.
5+
conditions to generate. This page is the normative specification of that format,
6+
**Input Format v1**. :func:`~pyiecwind.parse_input_file` implements it; anything
7+
not described here is unsupported and may be rejected.
88

9-
OpenFAST-style table (recommended)
10-
----------------------------------
9+
The format provides three interchangeable *layouts* that all produce the same
10+
validated :class:`~pyiecwind.IECParameters`:
1111

12-
One parameter per row as ``value key - comment``, with a ``Cases`` section:
12+
* **openfast-table-v1** -- one parameter per row, ``value key - comment``
13+
(recommended for new work; this is what the tool emits).
14+
* **keyed-v1** -- ``key = value`` or ``key: value`` pairs.
15+
* **legacy-v1** -- the historical fixed-line IECWind positional layout.
16+
17+
By default the layout is auto-detected. A file may pin its layout explicitly with
18+
a format directive (see `Pinning the layout`_); doing so is recommended for
19+
archival and review, and makes auto-detection a compatibility fallback rather
20+
than the primary contract.
21+
22+
Lexical rules
23+
-------------
24+
25+
These rules apply to the ``openfast-table-v1`` and ``keyed-v1`` layouts. The
26+
``legacy-v1`` layout is positional and is described separately below.
27+
28+
Encoding and lines
29+
~~~~~~~~~~~~~~~~~~~
30+
31+
* Files are UTF-8 text. Both ``\n`` and ``\r\n`` line endings are accepted.
32+
* The file is processed line by line; line numbers in error messages are
33+
1-based and count every physical line, including blanks and comments.
34+
35+
Whitespace
36+
~~~~~~~~~~
37+
38+
* Leading and trailing whitespace on a line is ignored.
39+
* Blank lines are ignored anywhere.
40+
* In ``openfast-table-v1`` rows, fields are separated by a run of **two or more**
41+
spaces. A single space is treated as part of a field, so column alignment is
42+
free-form as long as at least two spaces separate ``value``, ``key``, and the
43+
optional comment.
44+
* In ``keyed-v1`` lines, whitespace around the ``=`` / ``:`` separator and around
45+
the value is ignored.
46+
47+
Comments
48+
~~~~~~~~
49+
50+
* A line whose first non-whitespace character is ``!`` or ``#`` is a comment and
51+
is ignored (except for the directives below).
52+
* In ``openfast-table-v1`` rows, a trailing ``- comment`` after the key is
53+
ignored. Everything from the first ``-`` that follows the key to end of line is
54+
commentary.
55+
56+
Pinning the layout
57+
~~~~~~~~~~~~~~~~~~~
58+
59+
A comment line of the form::
60+
61+
! format: <id>
62+
63+
(equivalently ``# format = <id>`` or ``! format_version: <id>``; the keyword and
64+
id are case-insensitive) pins the layout and overrides auto-detection. Recognised
65+
ids are ``openfast-table-v1``, ``keyed-v1``, and ``legacy-v1`` (the short forms
66+
``openfast``, ``keyed``, ``legacy`` are accepted as aliases). An unrecognised id
67+
is an error. Files emitted by ``pyiecwind template`` and the wizard carry
68+
``! format: openfast-table-v1`` so they are self-describing.
69+
70+
Scalar fields
71+
-------------
72+
73+
Every input must define the eleven scalar fields below exactly once. Length and
74+
speed fields are interpreted in the file's own unit system (see
75+
`Unit semantics`_).
76+
77+
================ ==================================================== ===============
78+
Field Meaning Allowed values
79+
================ ==================================================== ===============
80+
``si_unit`` Unit system (see :doc:`units`) boolean token
81+
``t1`` Transient start time [s] finite float
82+
``wtc`` Wind turbine class 1, 2, 3
83+
``catg`` Turbulence category A, B, C
84+
``slope_deg`` Inflow inclination angle [deg] finite float
85+
``iec_edition`` Edition for the shear exponent 1, 3
86+
``hh`` Hub height > ``dia`` / 2
87+
``dia`` Rotor diameter > 0
88+
``vin`` Cut-in wind speed 0 < ``vin``
89+
``vrated`` Rated wind speed > ``vin``
90+
``vout`` Cut-out wind speed > ``vrated``
91+
================ ==================================================== ===============
92+
93+
Key names are case-insensitive and accept common aliases (for example
94+
``turbine_class`` or ``turbine class`` for ``wtc``; ``rotor_diameter`` for
95+
``dia``). Hyphens and spaces in a key are normalised to underscores.
96+
97+
Duplicate-field policy
98+
~~~~~~~~~~~~~~~~~~~~~~~
99+
100+
Each scalar field may appear **at most once**. Defining a field twice -- directly
101+
or through a different alias for the same field -- is rejected with both line
102+
numbers, rather than silently keeping the last value. This catches typos such as
103+
two conflicting ``wtc`` lines. (Condition rows are additive and are *not*
104+
subject to this rule.)
105+
106+
Boolean and unit tokens
107+
~~~~~~~~~~~~~~~~~~~~~~~~
108+
109+
``si_unit`` is parsed strictly. The following tokens are accepted
110+
(case-insensitive); any other token is an error reported with line context:
111+
112+
* **SI / True:** ``True``, ``T``, ``.TRUE.``, ``yes``, ``y``, ``SI``,
113+
``metric``, ``1``.
114+
* **English / False:** ``False``, ``F``, ``.FALSE.``, ``no``, ``n``,
115+
``English``, ``imperial``, ``us``, ``0``.
116+
117+
Unit semantics
118+
--------------
119+
120+
``si_unit`` controls how length and speed fields are *read from* and *written to*
121+
files; values are always stored internally in SI (m, m/s). With ``si_unit = True``
122+
they are metres and m/s; with ``si_unit = False`` they are feet and ft/s. The
123+
full unit model, including the conversion factor, is in :doc:`units`. Two rules
124+
deserve emphasis because they are easy to get wrong:
125+
126+
* The speed embedded in an ``NWP<speed>`` code is **always in m/s**, regardless
127+
of ``si_unit`` -- this matches the historical IECWind convention.
128+
* The EWS hub speed ``U`` and the ``du`` speed modifiers (ECD/EOG/EDC) are in the
129+
file's user units; a modifier's magnitude must not exceed 2 in those units.
130+
131+
Condition codes
132+
---------------
133+
134+
Conditions are specified as *case rows* in ``openfast-table-v1`` or under a
135+
``conditions:`` block in ``keyed-v1``. Each row expands into one or more
136+
condition codes drawn from the grammar below. ``U`` denotes a wind speed in user
137+
units and ``du`` a speed modifier (magnitude <= 2 in user units, valid only on
138+
the rated ``R`` reference). The physics behind each family is in :doc:`theory`.
139+
140+
============ ========================================== ==========================================
141+
Family Code grammar Example codes
142+
============ ========================================== ==========================================
143+
ECD ``ECD[+/-]R[[+/-]du]`` ``ECD+R``, ``ECD-R+1.5``
144+
EWS ``EWS[V/H][+/-]U`` ``EWSV+12.0``, ``EWSH-12.0``
145+
EOG ``EOG{I,O,R}[[+/-]du]`` (``du`` on R only) ``EOGI``, ``EOGR+2.0``
146+
EDC ``EDC[+/-]{I,O,R}[[+/-]du]`` (``du`` on R) ``EDC+R``, ``EDC-I``
147+
NWP ``NWP<speed_in_m_per_s>`` ``NWP10.0``, ``NWP23.7``
148+
EWM ``EWM{50,01}`` ``EWM50``, ``EWM01``
149+
============ ========================================== ==========================================
150+
151+
Codes outside this grammar -- a modifier above 2, a modifier on a non-rated
152+
reference, an EWS speed outside ``[vin, vout]``, or a malformed code -- are
153+
rejected (see :doc:`limitations`).
154+
155+
Case rows (openfast-table-v1)
156+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
157+
158+
Each case-family row is ``case_type use_case options_array``:
159+
160+
* ``use_case`` is ``True`` (generate the listed options), ``False`` (skip the
161+
row), or ``None`` (placeholder, ignored).
162+
* ``options_array`` is a bracketed, comma-separated list expanded into individual
163+
condition codes (for example ``[+R, -R]`` becomes ``ECD+R`` and ``ECD-R``).
164+
165+
Layouts
166+
-------
167+
168+
openfast-table-v1 (recommended)
169+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
13170

14171
.. code-block:: text
15172
173+
! format: openfast-table-v1
174+
16175
! General
17176
True si_unit - True for SI (m, m/s), False for English (ft, ft/s)
18177
40.000 t1 - transient start time [s]
@@ -38,14 +197,16 @@ One parameter per row as ``value key - comment``, with a ``Cases`` section:
38197
39198
A complete working example ships in ``examples/sample_case.ipt``.
40199

41-
Keyed layout
42-
------------
200+
keyed-v1
201+
~~~~~~~~
43202

44203
A simpler ``key = value`` (or ``key: value``) layout, with conditions under a
45-
``conditions:`` block:
204+
``conditions:`` block. Each ``- code`` line adds a condition; a code may be
205+
prefixed with ``True``/``False``/``None`` to toggle it.
46206

47207
.. code-block:: text
48208
209+
! format: keyed-v1
49210
si_unit = True
50211
t1 = 40.0
51212
wtc = 2
@@ -61,45 +222,36 @@ A simpler ``key = value`` (or ``key: value``) layout, with conditions under a
61222
- EWM50
62223
- NWP10.0
63224
64-
Legacy positional layout
65-
------------------------
66-
67-
The historical fixed-line IECWind layout is still accepted for backward
68-
compatibility. It is not recommended for new files.
225+
legacy-v1
226+
~~~~~~~~~
69227

70-
Field reference
71-
---------------
72-
73-
================ ==================================================== ===============
74-
Field Meaning Allowed values
75-
================ ==================================================== ===============
76-
``si_unit`` Unit system (see :doc:`units`) boolean token
77-
``t1`` Transient start time [s] finite float
78-
``wtc`` Wind turbine class 1, 2, 3
79-
``catg`` Turbulence category A, B, C
80-
``slope_deg`` Inflow inclination angle [deg] finite float
81-
``iec_edition`` Edition for the shear exponent 1, 3
82-
``hh`` Hub height > ``dia`` / 2
83-
``dia`` Rotor diameter > 0
84-
``vin`` Cut-in wind speed 0 < ``vin``
85-
``vrated`` Rated wind speed > ``vin``
86-
``vout`` Cut-out wind speed > ``vrated``
87-
================ ==================================================== ===============
228+
The historical fixed-line IECWind positional layout is still accepted for
229+
backward compatibility. Field meaning is determined by line position rather than
230+
by a key, so the lexical rules above do not apply. It is not recommended for new
231+
files; prefer ``openfast-table-v1``.
88232

89-
Key names are case-insensitive and accept common aliases (for example
90-
``turbine_class`` for ``wtc`` or ``rotor_diameter`` for ``dia``). Boolean tokens
91-
are parsed strictly: ``True``/``False`` (and equivalents such as ``yes``/``no``,
92-
``SI``/``English``) are accepted; any other token is rejected with line context.
233+
Invalid files
234+
-------------
93235

94-
Case rows
95-
---------
236+
The parser fails closed: a malformed file raises :class:`ValueError` with line
237+
context rather than producing a partial or surprising result. Representative
238+
examples and the errors they produce:
96239

97-
Each case-family row is ``case_type use_case options_array``:
240+
================================================ ==========================================================
241+
Input fragment Error
242+
================================================ ==========================================================
243+
``wtc = 2`` then ``wtc = 3`` ``Duplicate field 'wtc' on line N; already set on line M``
244+
``si_unit = maybe`` ``Cannot interpret si_unit value 'maybe' on line N``
245+
``mystery = 7`` ``Unknown input key on line N``
246+
(omitting ``vout``) ``Missing required input field(s): vout``
247+
``! format: bogus-v9`` ``Unknown format directive 'bogus-v9'``
248+
``EWS True [V+99.0]`` with ``vout = 24`` ``EWS wind speed ... must be between Vin ... and Vout``
249+
================================================ ==========================================================
98250

99-
* ``use_case`` is ``True`` (generate the listed options), ``False`` (skip the
100-
row), or ``None`` (placeholder, ignored).
101-
* ``options_array`` is a bracketed, comma-separated list expanded into individual
102-
condition codes (for example ``[+R, -R]`` becomes ``ECD+R`` and ``ECD-R``).
251+
Format stability
252+
----------------
103253

104-
The available codes for each family are documented in :doc:`theory`; values
105-
outside the supported grammar are rejected (see :doc:`limitations`).
254+
This specification is **v1**. Backward-incompatible changes (new required fields,
255+
changed grammar, or removed aliases) will be released under a new id
256+
(``...-v2``) so that a pinned ``! format: ...-v1`` file keeps parsing as written.
257+
Additive, backward-compatible refinements may be made within v1.

docs/theory.rst

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,61 @@ The axis (``V``/``H``) selects whether :math:`s(t)` is written to the vertical o
180180
horizontal linear-shear column, and the sign sets its direction. Implemented by
181181
:func:`~pyiecwind.gen_ews`.
182182

183+
Traceability
184+
------------
185+
186+
Each condition family maps to one implementing function, one independent oracle
187+
check that recomputes its headline quantity from the equations on this page (see
188+
:doc:`validation`), and one or more golden scenarios that lock its byte output.
189+
The standard reference is IEC 61400-1, Clause 6 (wind conditions), for both
190+
Edition 1 and Edition 3; the exact sub-clause and table numbering differs between
191+
editions and is not reproduced here (consult the standard directly).
192+
193+
.. list-table::
194+
:header-rows: 1
195+
:widths: 10 26 18 26 22
196+
197+
* - Family
198+
- Governing equation (this page)
199+
- Code
200+
- Independent oracle test
201+
- Golden scenarios
202+
* - NWP
203+
- Power-law profile; :math:`\alpha` per ``iec_edition``
204+
- :func:`~pyiecwind.gen_nwp`
205+
- ``test_oracle.test_nwp_steady_speed``
206+
- si_baseline, english_baseline, edition1, slope8
207+
* - EWM
208+
- :math:`V_\mathrm{e50}=1.4V_\mathrm{ref}`, :math:`V_\mathrm{e1}=0.8V_\mathrm{e50}`; fixed :math:`\alpha=0.11`
209+
- :func:`~pyiecwind.gen_ewm`
210+
- ``test_oracle.test_ewm_extreme_wind_speeds``
211+
- all six scenarios
212+
* - EOG
213+
- Gust amplitude :math:`V_\mathrm{gust}` over :math:`T=10.5` s
214+
- :func:`~pyiecwind.gen_eog`
215+
- ``test_oracle.test_eog_gust_amplitude``
216+
- si_baseline, english_baseline, edition1, class1_cat_a, class3_cat_c, slope8
217+
* - EDC
218+
- Direction amplitude :math:`\theta_e` over :math:`T=6` s
219+
- :func:`~pyiecwind.gen_edc`
220+
- ``test_oracle.test_edc_direction``
221+
- si_baseline, english_baseline, edition1, class1_cat_a, class3_cat_c, slope8
222+
* - ECD
223+
- :math:`V_\mathrm{cg}=15` m/s and :math:`\theta_\mathrm{cg}` over :math:`T=10` s
224+
- :func:`~pyiecwind.gen_ecd`
225+
- ``test_oracle.test_ecd_direction_and_gust``
226+
- si_baseline, english_baseline, edition1, class1_cat_a, class3_cat_c, slope8
227+
* - EWS
228+
- Peak shear :math:`s_\mathrm{max}` over :math:`T=12` s
229+
- :func:`~pyiecwind.gen_ews`
230+
- ``test_oracle.test_ews_shear``
231+
- si_baseline, english_baseline, edition1, class1_cat_a, class3_cat_c, slope8
232+
233+
Only the power-law shear exponent :math:`\alpha` depends on ``iec_edition``
234+
(0.2 for Edition 1, 0.14 for Edition 3, and a fixed 0.11 for EWM); the transient
235+
gust, shear, and direction-change magnitudes above are edition-independent in
236+
this implementation.
237+
183238
Provenance and scope
184239
--------------------
185240

0 commit comments

Comments
 (0)