Flow model chain - #56
Draft
bjarketol wants to merge 53 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>
bjarketol
force-pushed
the
flow-model-chain
branch
from
March 27, 2026 08:34
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>
- Span each LUT over all hub heights (zlow=min, zhigh=max) and pin the merged z axis to the hub heights via z_lst; merging single-height LUTs at different hub heights turned the FugaMultiLUTDeficit table all-NaN, zeroing production at every turbine waked by another turbine type - Share one x/y grid (min diameter /4, /16) across mixed rotor diameters so LUTs merge without clipping - Dedupe turbine geometries (duplicate d_h coordinates break the merge) - Forward z_lst to the FUGA blockage deficit - Forward fuga.n_cpu to pyfuga (its spawn pools re-JIT numba per worker; n_cpu=1 keeps small-LUT generation in-process) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
windIO defines k_a as the constant expansion and k_b as the coefficient on TI; PyWake (k = a[0]*TI + a[1]) and wayve's Lanzilao (k = ka*TI + kb) order their native parameters the other way around, so the readers must swap. Matches the foxes eu_flow reader, which fixed the same inversion. - pywake: a = [k_b, k_a]; scalar-k deficits (Jensen_1983, Bastankhah2014) take k_a and warn on a nonzero k_b - wayve: Lanzilao(ka=k_b, kb=k_a); a scalar k is a constant -> kb - Update tests and example analysis yamls to the windIO convention Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Verified end-to-end in a wayve env (foxes 1.8.4): the 4-turbine smoke test converges 3/3 flow cases with the swapped Lanzilao mapping. - Assert wake_model_setup maps ka <- k_b (TI multiplier) and kb <- k_a (constant) onto Lanzilao's kwake = ka*TI + kb - Assert a scalar k lands in the constant kb with no TI term Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two independent defects made `wake_tool: foxes` impossible to use: - `wake_tool` was read from `analysis.wake_tool`, but the windIO schema only allows it under `analysis.wm_coupling.wake_tool` and rejects the analysis- level key (the validator injects `additionalProperties: false`). No schema-valid windIO file could therefore select the foxes wake tool. It is now read from `wm_coupling`, falling back to the analysis level so the existing hand-written example yamls keep working. - Once reachable, the foxes path raised `TypeError: Algorithm.__init__() got an unexpected keyword argument 'name'`. foxes' `Dict` takes its own label as `_name`; a plain `name=` kwarg becomes a data key and was forwarded into the foxes `Algorithm` constructor. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ars) Three defects in how run_wayve/flow_io_abl read the windIO input: - `times_run.subset` simulated the wrong states. The subsetted timestamps were re-enumerated from zero, but the resulting index is used to look up the *full* wind-resource arrays in flow_io_abl(). So `subset: [10, 20]` built its ABL from rows 0 and 1 while labelling the output states 10 and 20 — silently wrong data with correct-looking coordinates. The positions into the full arrays are now carried alongside the timestamp labels. - Turbulence intensity was divided by 100 in the single-point (non-profile) resource branch, and was read only when `z0` happened to also be present. windIO carries TI as a fraction everywhere else, including flow_io_abl's own vertical-profile branch and the `TI = 0.04` default two lines above. The effect was to collapse the TI-proportional wake expansion by two orders of magnitude. - `turbine_outputs.output_variables` was ignored: the code read `turbine_output_variables`, while the schema and every other WIFA engine use `output_variables`. The old key is kept as a fallback. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Attach a Mirror ground model to the analytic blockage models (SelfSimilarity/2020, Rathmann, RankineHalfBody, VortexCylinder, VortexDipole, HybridInduction) when ground_mirror is true; they are calibrated without ground effects and otherwise allow flow through the ground - Warn and ignore the flag for FUGA blockage: the Fuga LUTs come from a linearized RANS solver that already includes the ground - Default off (DEFAULTS entry), wake deficit models stay unmirrored - Tests: mirror attached per model, off by default, FUGA warning, and a physics check that mirroring increases the upstream deficit Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Analytic blockage models produce speed-up (negative deficit) regions; pyWake defaults the blockage superposition to the wake superposition model, so any Squared-superposition recipe (e.g. TurbOPark) hit "SquaredSum only works for deficit - not speedups". Potential-flow induction superposes linearly, so pin LinearSum on the blockage deficit (the ground Mirror inherits it for the real+image sum). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
flow_io_abl hard-coded abl.rho = 1.225. The APM velocity solution does not depend on density (wayve's power_turbines docstring), but abl.rho linearly scales the reported turbine power and is forwarded to foxes as FV.RHO — so a wind resource carrying per-state air_density now gets the standard density correction. The Cp curve stays referenced to 1.225 (read_turbine_type), the density the windIO power curve is defined at. Defaults to 1.225 when the resource has no air_density. Note: air_density is not yet in the windIO wind-resource schema (the wayve path skips validation); flagged alongside the capping-inversion keys in EUFLOW#67. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- dict_to_site: build pyWake's joint P from windIO probability — multiplied by sector_probability when present (windkit/WAsP writes probability as the per-sector conditional over wind speed), bare probability taken as already joint (ieawind37 convention) - construct_site: dispatch "probability" resources to a new _construct_histogram_site — ws verbatim from the histogram bin centers (pyWake cannot interpolate ws-dependent P), wd refined to sub-sectors via XRSite's circular P interpolation, operating=(n_wt, 1), TI defaulting to 0.06 when absent - factor the Weibull path's sub-sector computation into _subsector_wind_directions (shared, no behavior change) and add _coord_values for dict-form windIO coordinates - tests: joint-P construction (incl. windkit dim order), dispatch, sub-sector helper, analytic single-turbine AEP, two-turbine wake loss, and a full run_pywake histogram case Enables eu-flow/wp5/flow_model_chain#15. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- EddyViscosity deficit: free-stream-referenced deficits by default (use_effective_ws=False, the MaxSum/WindFarmer convention), TI-capable - QuartonAndAinslie / ModifiedQuartonAndAinslie turbulence -> the Hassan (1992) modified variant the EV bundle uses - simplified_gaussian rotor averaging -> SimplifiedGaussianRotorAverageModel - all imports lazy: released pyWake keeps working unless EV is requested; tests importorskip on the EV branch module Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017wbMsoVker28aF72Xt5VC5
A height-resolved windIO wind resource carrying friction_velocity and boundary_layer_height (mesoscale/reanalysis data such as ERA5) now sets up the ABL via wayve's abl_setup.mesoscale_based() path instead of requiring turbulence profiles: - profiles reaching the stratosphere (>= 12 km) delegate to wayve's mesoscale_based(), including its tropopause two-line fit - truncated profiles run the same core setup in-line (CI from an explicit windIO capping_inversion block or the SERZ fit, the stability-aware stress/eddy-viscosity closure, geostrophic wind from the profile) but skip the tropopause fit, whose unconditional run on truncated data would feed a garbage stratosphere into the ABL; the ABL keeps its defaults (h_strat=10 km, Uinf/Vinf=U3/V3, Ninf=N) - new analysis.abl_setup block (Gmode, dh_max, serz) forwarded to flow_io_abl; Gmode "avg" falls back to "h2" with a warning when the profiles stop below 5 km - turbulence_intensity in profile resources may now be a single value per state (interpolated profile still supported); profile resources without turbulence or mesoscale inputs get a clear error instead of a KeyError Requested by flow_model_chain#27 (ERA5-profile-driven wayve states). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
wayve's gravity-wave machinery and its grid conventions assume the mean flow blows west->east (+x); the wrapper never rotated anything, so non-westerly states solved with the farm footprint (and any anisotropic APM grid) at the wrong angle to the flow. - flow_io_abl builds the ABL in the solver frame by rotating the inputs (scalar branch: Nieuwstadt profile built westerly; profile branch: direction profile unwrapped and shifted so the hub-height wind is westerly, preserving veer; explicit tau_x/tau_y components rotated). Rotating inputs rather than calling ABL.rotate() sidesteps the broken momentum-flux rotation in wayve 2.0.0 and keeps everything derived downstream (Gmode geostrophic wind, stress alignment) consistent. Returns (abl, rotation). - wf_setup rotates the layout by the same angle per state (and no longer mutates farm_dat); run_wayve rebuilds the wind farm inside the state loop. - Flow-field output stays on the requested earth-frame grid: the query points are rotated into the solver frame, evaluated exactly there via the new _xy_plane_points (meshgrid-free mirror of wayve's xy_plane for both wake models), and the velocity vectors rotated back. - Tests: solver-frame alignment, rotational invariance of per-turbine outputs (westerly row vs 90deg-rotated southerly row), earth-frame flow-field direction/wake position, flow-field equivalence under rotation, and exact _xy_plane_points == xy_plane equivalence. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012uPUjtEg3eRVtASSD5KGs7
number_of_fa_layers > 1 selects wayve's NonUniform gravity-wave closure, the only component that consumes the vertical profile structure (N(z), upper-level shear) beyond the two-layer averages. For truncated reanalysis profiles (e.g. ERA5 levels 96-137 stopping near 6 km, or the shipped 1.7 km Twin Groves files) the ABL kept its 10 km default tropopause, so NonUniform would have spread most of its layers across a no-data region where the profile splines hold constants. - flow_io_abl caps the free atmosphere at the top of the profile data (h_strat = zs[-1], Uinf/Vinf from the top of the profile, Ninf = the fitted free-atmosphere N); above the cap sits the semi-infinite radiating layer. - The pressure closure is built per state (_pressure_for_state): NonUniform only when the profile actually has points between the inversion top and the capped free-atmosphere top, otherwise a warned fallback to the bulk Uniform closure (scalar Nieuwstadt states always fall back - their synthetic profile stops at the inversion). - Tests: h_strat capping + solver-frame profile branch, per-state closure selection/fallback, and a non-westerly NonUniform end-to-end run. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012uPUjtEg3eRVtASSD5KGs7
wayve@e87780a overrides background_flow_direction on the foxes coupling with logic identical to the UniDirectionalSelfSimilar base class but a broken lazy import (e_spanwise from wake_models.wake_model_tools instead of forcing_tools), so every foxes-coupled solve crashes with an ImportError. Rebind the base implementation while that import is broken; the shim self-disables once upstream fixes the module path. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012uPUjtEg3eRVtASSD5KGs7
Findings from a four-angle review (rotation math, profile path, tests, integration) of 20c890c..7424429. No correctness bugs were found; this hardens the edges the review flagged: - Tests: rotational invariance at a generic 37 deg angle (90 deg maps the square FFT grid onto itself, so the pre-rotation code already passed the 90 deg pair; 37 deg discriminates at ~4e-4), identical repeated states (guards the per-state wind-farm rebuild against cumulative layout mutation), a foxes twin of the _xy_plane_points drift guard (which also exercises the background_flow_direction shim in CI), an explicit tau_x/tau_y solver-frame rotation test, a no-silent-Uniform- fallback assertion in the NonUniform end-to-end test, and an Ninf assertion on the capped free atmosphere. - flow_io_abl rejects non-ascending height arrays (np.interp would silently derive a wrong solver frame from them). - Flow-field wind_direction is wrapped to [0, 360). - turbine_data and flow_field appends happen together, so a flow-field failure cannot desync the two files' states axes. - The foxes shim now probes the override's own source for the broken import instead of importing the misplaced symbol, so it stays inert if upstream rewrites the method rather than relocating e_spanwise. - The NonUniform fallback warning is state-independent (dedupes on long series) and run_wayve prints a per-run NonUniform/Uniform tally. - black/isort formatting per the repo pre-commit hooks. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012uPUjtEg3eRVtASSD5KGs7
WIFA's wayve adapter now reads the four capping-inversion scalars from the flat windIO energy-resource keys (ABL_height, capping_inversion_thickness, capping_inversion_strength, lapse_rate) in addition to the nested thermal_stratification.capping_inversion block it read before. This is the spelling the windIO schema declares and the one WIFA's own code_saturne adapter already reads; it is also the only one a netCDF !include can carry, since windIO flattens a netCDF's root group. - The three duplicated nested reads (scalar, turbulence-profile and mesoscale branches) collapse into read_capping_inversion; a capping_inversion_spelling validator runs once before the state loop. - Back-compatible: the nested block is still accepted. A file carrying both spellings, or an incomplete block in either, is rejected rather than silently resolved -- capping_inversion_strength alone sets the gravity-wave forcing amplitude, so a partial block is never completed from defaults (a deliberate departure from the code_saturne adapter's per-key fill). The structural check runs before the loop, whose except Exception would otherwise report a malformed file as every state crashing. - tests: flat/nested equivalence, both-spellings and partial-block rejection, no-inversion profile-fit fallback; the _profile_wind_resource fixture gains a spelling= switch and defaults to flat. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…rofile In the mesoscale branch the atmosphere is *fitted*, not supplied: flow_io_abl fits the capping inversion when the resource states none, derives the geostrophic wind through Gmode, and closes the stress profile from surface scalars. None of that reached the outputs, so a run could not report the atmosphere it had solved on, and the fitted inversion could not be checked against an independent boundary-layer height. Write those scalars alongside the turbine outputs. Also let number_of_fa_layers <= 0 through to wayve's NonUniform closure, which reads it as "one sublayer per level of the ABL's own vertical grid". A reanalysis column then resolves the free atmosphere at the resolution it actually has, instead of an arbitrary layer count over the same range. Only exactly 1 now means the bulk Uniform closure. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JWxbyEe8CKC9Nj7BXfo63N
- Namespace them `abl_*`. They share a dataset with the turbine outputs, and `air_density` / `turbulence_intensity` are per-turbine field names in other engines and in windIO's turbine_data schema; a future collision would have silently overwritten a turbine result. - Give every one a `units` attribute, and note that wayve's `utau` carries the surface stress rather than its root in the turbulence-profile branch. - Never let reporting cost a state its physics: a non-scalar or missing value now becomes NaN instead of an exception, which the state loop's `except Exception` would have turned into a dropped state — discarding power that had already been computed. - Take the mixed-layer temperature from the profile in the turbulence-profile branch's explicit-inversion path, as every other branch does. A hard-coded 293.15 K made the buoyancy jump `g*dtheta/th0` disagree with the ABL's own theta profile by up to ~4% at realistic temperatures. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JWxbyEe8CKC9Nj7BXfo63N
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.