Flow model chain#56
Draft
bjarketol wants to merge 34 commits into
Draft
Conversation
… 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>
dad9a6e to
64a87c6
Compare
… 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
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Persistent branch tracking the FLOW validation model-chain. Changes from here are cherry-picked into stand-alone PRs when needed.