Skip to content

Flow model chain#56

Draft
bjarketol wants to merge 34 commits into
EUFLOW:mainfrom
bjarketol:flow-model-chain
Draft

Flow model chain#56
bjarketol wants to merge 34 commits into
EUFLOW:mainfrom
bjarketol:flow-model-chain

Conversation

@bjarketol

@bjarketol bjarketol commented Mar 13, 2026

Copy link
Copy Markdown
Contributor

Persistent branch tracking the FLOW validation model-chain. Changes from here are cherry-picked into stand-alone PRs when needed.

bjarketol and others added 9 commits March 27, 2026 09:33
… deps, and NOJLocalDeficit alias

- Make all wrapped tools (floris, pywake, wayve, foxes) optional dependencies
- Add case-insensitive submodel name lookup across deficit, deflection, turbulence,
  superposition, rotor averaging, and blockage models
- Add NOJLocalDeficit as a deficit model name alias alongside Jensen
- Fix CI: skip floris on Python <3.10, fix numpy 2.x compat in wayve
- Add comprehensive parametrized tests for all submodel configuration functions

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
run_api() was loading the full YAML+netCDF 4 times per simulation:
validate_yaml (load 1, result discarded), load_yaml (load 2), then
the downstream model runner called validate_yaml (load 3) and
load_yaml (load 4) again internally.

Now validate_yaml is called once (its return value is the loaded dict),
and the dict is passed to model runners — which skip their own
validate+load when they receive a dict. Reduces peak transient memory
from ~800 MB to ~200 MB for time-series wind resources.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…y correction

Switch from the default DensityScale (scales power/CT after lookup) to
DensityCompensation (corrects wind speed before power curve lookup) to
match foxes' air density handling approach: ws *= (rho/rho_ref)^(1/3).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Fix _construct_weibull_site() Speedup axis bug: use dim-name lookup
  instead of hardcoded axis=0, and set Speedup dims from input data's
  actual ordering. Previously, flow_model_chain's (wind_direction,
  wind_turbine) ordering caused Speedup to be silently ignored by
  PyWake, removing terrain-induced wind speed inhomogeneity and
  inflating wake losses from ~10% to ~39%.

- Add automatic flow case computation when wind_speed is absent from
  the windIO dict:
  - WS range: 0 to Speedup-adjusted 99.9% Weibull CDF point, 0.5 m/s steps
  - WD sub-sectors: 5 per sector (matching pywasp), letting PyWake's
    probability partitioning handle sector probability distribution

- Handle missing turbulence_intensity gracefully (default 0.06)

- Add test_weibull_speedup_dim_ordering regression test verifying both
  (wind_direction, wind_turbine) and (wind_turbine, wind_direction)
  orderings produce identical AEP

- Update test_heterogeneous_wind_rose_grid baseline to match new
  auto-computed ws range and wd sub-sectors

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
SimpleYawModel(exp=2) was removed in PyWake 2.6 — the exp=2 behavior
is now the default. Use try/except to support both old and new versions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
bjarketol and others added 20 commits March 27, 2026 09:41
… deps, and NOJLocalDeficit alias

- Make all wrapped tools (floris, pywake, wayve, foxes) optional dependencies
- Add case-insensitive submodel name lookup across deficit, deflection, turbulence,
  superposition, rotor averaging, and blockage models
- Add NOJLocalDeficit as a deficit model name alias alongside Jensen
- Fix CI: skip floris on Python <3.10, fix numpy 2.x compat in wayve
- Add comprehensive parametrized tests for all submodel configuration functions

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…uards

Follow windIO's analysis-schema changes (engine-neutral rotor averaging,
single TI flag) on the pyWake path.

- TI reference: read the nested wake_expansion_coefficient.free_stream_ti
  (foxes-compatible) and set use_effective_ti = not free_stream_ti. Lift the
  handling out of the GAUSSIAN_MODELS branch into a TI_CAPABLE set so it also
  covers SuperGaussian/SuperGaussian2023 and TurbOPark (previously ignored).
  The removed top-level use_effective_ti key is no longer read.
- rotor_averaging: add the new windIO names grid -> GridRotorAvg,
  gaussian_overlap -> GaussianOverlapAvgModel, area_overlap ->
  AreaOverlapAvgModel; keep avg_deficit as a deprecated alias.
- Guard: WeightedSum/CumulativeWakeSum with a non-node rotor-averaging model
  now raises a clear ValueError instead of PyWake's deep AssertionError.
- Vector superposition (foxes-only) raises an explicit NotImplementedError.
- Tests: free_stream_ti polarity across all TI-capable deficits + exclusions,
  the three rotor-averaging names, the Weighted/Cumulative node guard, and
  Vector rejection; update prior tests off the removed use_effective_ti key.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…into flow-model-chain-submodels

# Conflicts:
#	pyproject.toml
#	tests/test_pywake_submodels.py
#	wifa/pywake_api.py
NOJLocalDeficit accepts use_effective_ti and, with a=[k_a, k_b], references
effective TI — so a no-turbulence Jensen/PARK config (turbulence_model=None)
hit PyWake's "TI_eff requires a turbulence model" assertion. Add jensen/
nojlocaldeficit to TI_CAPABLE so free_stream_ti=True selects ambient TI and
the config runs without an added-turbulence model. Tests updated accordingly.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
These superpositions also require a ConvectionDeficitModel-based deficit
(PyWake asserts isinstance(deficit, ConvectionDeficitModel)). SuperGaussian,
SuperGaussian2023 and GCL are not convection models, so pairing them with
Weighted/Cumulative raised a bare AssertionError deep in a run. Extend the
existing node-rotor-avg guard to also check the deficit type and raise a
clear ValueError naming the offending model.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
windIO's wake_expansion_coefficient has no scalar `k` field (validation
rejects it), so the free-stream NOJDeficit path now also reads k_b. Lets
jensen1983 use the faithful free-stream Jensen_1983 deficit with k via k_b.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Use per-turbine inflow already at hub height (wind_turbine dim, no height
  profile) via dict_to_site instead of averaging across turbines
- Restore and fix vertical-profile support for mixed hub heights: build the
  height-indexed XRSite unconditionally (was only built when TI present,
  leaving site=None and crashing), use turbine-averaged reference inflow
  (was the last interpolated height only), and interpolate density profiles
- Add vector (sin/cos) wind-direction interpolation so the 0/360 wrap is
  handled correctly
- Raise on the ambiguous height-profile + wind_turbine combination
- Add golden-equivalence tests (per-turbine and vertical-profile) matching
  hand-built pyWake to rtol 1e-6, plus a guard test

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The flow_model_chain integration branch tracks the persistent windIO
flow-model-chain branch so the transitive and direct windIO pins agree.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Skip the all-True [cases_idx] subset copy when no time subset is requested
- Use np.asarray so an array-backed resource is not re-copied (enables Phase 2)
- Reuse site.ds in the Weibull TI path instead of a second dict_to_netcdf build
- Add tests/mem_bench.py to measure site-construction memory

Measured (timeseries 4000x100): with today's dict-of-lists input the
construct_site peak is unchanged (~35 MB) because the removed copies are
transient; with an array-backed resource the same path drops to ~19 MB and
the input dict from ~59 MB to ~16 MB, previewing the Phase 2 win.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- pyWake + foxes readers call load_yaml(..., nc_data="array") and validate
  structure-only (array_data=True), keeping a large included wind_resource.nc
  as numpy arrays instead of nested Python lists
- guard the height-profile checks with an explicit has_heights boolean (an
  array-backed "height" has an ambiguous truth value)
- mem_bench: add the real-netCDF load comparison

Measured on a 16 MB wind_resource.nc: load peak 76 MB (lists) -> 19 MB
(arrays), ~3.9x lower; construct_site peak 35 MB -> 19 MB. Requires a windIO
build with the nc_data option.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Asserts an included wind_resource.nc loaded with nc_data="array" peaks well
below the dict-of-lists default, and that the loader actually keeps ndarrays
while the default stays list-backed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two faithfulness fixes on the pyWake path so the configured models reproduce
py_wake.literature exactly:

- Fix A: map analysis.axial_induction_model to ct2a (1D -> ct2a_mom1d,
  Madsen -> ct2a_madsen) and set it on every deficit that accepts a ct2a
  parameter. Previously the field was ignored and every deficit kept its
  ct2a_madsen default, so a "1D" request was silently dropped. Closes the
  Bastankhah2014 gap vs Bastankhah_PorteAgel_2014.

- Fix B: build TurbOPark with the canonical Nygaard (2022) recipe — a Mirror
  ground model and ctlim=0.96 (constructor args) plus WS_key='WS_jlk' (set
  post-construction). run_simulation now takes groundModel from deficit_args
  instead of hardcoding None, and applies deficit_post_attrs. Closes the
  TurbOPark gap vs Nygaard_2022.

_configure_deficit_model now returns (class, args, post_attrs); configure_wake_model
exposes deficit_post_attrs. Adds unit tests for both fixes.

Verified against py_wake.literature: Bastankhah2014 and TurbOPark now match to
0.000% per-turbine power.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The flow-model-chain branch carried three test files (mem_bench.py,
test_memory.py, test_pywake.py) that predate the pinned pre-commit hooks and
failed the black/isort CI check. Reformat them with the repo-pinned versions so
the pipeline is green; no logic changes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Honor axial_induction_model + TurbOPark literature recipe (faithfulness)
…edSum, Zong eps_coeff

Closes the Niayifar (2016) and Zong (2020) gaps vs py_wake.literature:

- Fix C: CrespoHernandez honors a `c` coefficient array from the turbulence
  config; when given it builds CrespoHernandez(c=..., ct2a=ct2a_mom1d,
  addedTurbulenceSuperpositionModel=SqrMaxSum()) — the literature recipe.
  Without `c`, the PyWake default is unchanged.
- Fix D: a "none" rotor-averaging option returns None, and the WeightedSum
  guard now accepts None (rotor centre, which PyWake allows) in addition to
  node models. Zong (2020) uses rotorAvgModel=None; forcing GridRotorAvg was a
  ~24% error.
- ceps now maps to Zong's `eps_coeff` (PyWake's name for its near-wake epsilon),
  instead of being dropped.

Verified against py_wake.literature: Niayifar2016 and Zong2020 now match to
0.000% per-turbine power. Adds unit tests for all three.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Phase 2 faithfulness: CrespoHernandez c, None rotor for WeightedSum, Zong eps_coeff
- Honor the windIO use_effective_ws flag instead of hardcoding True. The flag
  was silently ignored on the pyWake path, so free-stream models (notably GCL's
  original Larsen variant) could not be expressed. NOJDeficit still pops it.
- Add GCL to TI_CAPABLE: GCLDeficit accepts use_effective_ti (GCLLocal sets it),
  so free_stream_ti is now honored for GCL.

Together these let the GCL experiment reproduce py_wake.deficit_models.gcl.GCL
(use_effective_ws=False) — verified to 0.000%. Existing twins are unaffected
(they use use_effective_ws=True; TurbOPark's WS_key makes it moot). Adds tests;
moves GCL out of the "free_stream_ti ignored" test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Phase 3: honor use_effective_ws + enable GCL effective TI
bjarketol and others added 5 commits June 9, 2026 11:07
The Weibull/distributions path auto-generates a reference wind-speed grid
starting at 0 m/s. A ws=0 flow case carries zero energy for every model
but is degenerate for the WeightedSum superposition (Zong 2020), whose
convection-velocity iteration divides by the convection speed and is
undefined at zero wind speed. Including ws=0 silently corrupted the
WeightedSum AEP, collapsing the apparent wake loss: on the Twin Groves
distributions case Zong fell to 3.5% while the near-identical LinearSum
Niayifar stayed at 8.0% (and Zong's own time-series value was 8.2%).
LinearSum and the other superpositions were unaffected.

Start the auto ws grid at the first nonzero bin (0.5 m/s); dropping ws=0
is energy-neutral for all models and fixes WeightedSum. After the fix the
Zong distributions wake loss recovers to 8.2%, matching its time-series
value.

- _construct_weibull_site: ws grid now np.arange(0.5, ...).
- Add regression test test_weibull_ws_grid_excludes_zero_for_weightedsum
  (guards both ws[0] > 0 and Weighted-vs-Linear agreement).
- Update test_heterogeneous_wind_rose_grid's reference grid to match.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fix WeightedSum collapse on the distributions path (drop ws=0 bin)
Replace the hardcoded Fuga LUT stub with per-farm LUT generation so Fuga
works for any validation case, not just the one offshore D80 turbine the
stub's hand-built filename happened to encode.

- The old branch hardcoded z0=1e-5, zi=500, zlow=zhigh=70 and constructed a
  LUT_path (z69.2-72.8...dx44.575) that could never match what get_luts
  actually writes (single hub level -> _z70.0_; dx=D/4) -> FileNotFoundError
  for any real farm.
- Derive the LUT atmosphere from the site: roughness z0 from the mean TI
  (Fuga has no TI input; z0 = zhub*exp(-1/TI), the inversion PyWake uses),
  inversion height zi from ABL_height, neutral stability. All overridable
  via wind_deficit_model.fuga.{z0,zi,zeta0,nkz0,nbeta,nx,ny,cache_dir}.
- Probe pyfuga.paths.get_luts_path before generating -> a persistent,
  content-addressed cache (~/.cache/wifa/fuga_luts or $WIFA_FUGA_LUT_DIR);
  pyfuga reuses the costly preLUT stage across geometries.
- Pop use_effective_ws for FugaDeficit (it doesn't accept it) and thread
  resource_dat through configure_wake_model/_configure_deficit_model as an
  optional trailing kwarg (existing callers/tests unaffected).

Validated on Twin Groves TS (V82, D82/hub78): pywake_fuga = 9.7% wake loss,
mid-pack and physically sane (auto-derived z0=0.033 ~ farmland roughness).

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…#8)

Make the Fuga LUT path TI-faithful. Fuga reads ambient turbulence off the LUT
roughness, so a single mean-TI LUT evaluates the wake at loss(mean TI) and
misses the low-TI tail that drives the deepest wakes (same convexity as the
GCL free-stream-TI gap).

- Generate a z0 SWEEP across the site TI distribution (n_z0=5 by default) and
  pass the list to FugaDeficit, which interpolates z0 = z0(TI) per flow case
  at run time -> the farm loss integrates over the TI distribution. All LUTs
  share the costly preLUT, so extra z0 values are cheap.
- Clamp the TI band to [0.03, 0.18] so the neutral inversion z0 stays physical
  (~[1e-5, 0.3] m); TI 0.30 would map to z0 ~2.8 m, outside Fuga's regime. The
  high-TI end saturates to shallow wakes and clamps via bounds='limit'.
- Build one LUT set per turbine geometry and thread turbine_geometries through
  configure_wake_model so mixed-turbine farms interpolate over d_h.
- Add fast unit tests for _fuga_atmosphere / _fuga_z0_sweep (no LUT gen).

Twin Groves TS: pywake_fuga 9.7% (single mean-TI LUT) -> 11.2% (TI-faithful
multi-LUT), now level with bastankhah2014.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
When wind_deficit_model.fuga.z0 is a list (an explicit z0 sweep, handled by
_fuga_z0_sweep), _fuga_atmosphere crashed on float(z0) while computing the
scalar fallback. Ignore a list z0 there (fall back to TI-derived/default), so a
configured z0 sweep works end to end. Add a regression test.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant