Support FieldTimeSeries targets in Relaxation#5575
Conversation
Adds a discrete-form materialization path for `Relaxation(target=fts)`:
`materialize_forcing` wraps the FTS in an internal `FieldTimeSeriesTarget`
that carries the simulation-side location and the integer index of the
forced field in `model_fields`. The new kernel callable
(`Relaxation{R,M,<:FieldTimeSeriesTarget}`) reads `ϕ` from
`model_fields[index][i,j,k]` and obtains the reference value via
`interpolate(X, Time(t), fts, ...)`, so the FTS can live on a different
grid than the simulation as long as its extent brackets the simulation
grid in x, y, and z. Mismatch throws `ArgumentError` at materialize time.
The existing callable / `Number` `target` path is untouched.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Materialization wires the FTS into a `FieldTimeSeriesTarget` with the forced field's location and the right `model_fields` index; analytical convergence after one Euler step matches `c_ref·(1 − exp(−Δt/τ))` to ~1e-6 relative error; smaller-FTS extent throws `ArgumentError`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
glwagner
left a comment
There was a problem hiding this comment.
just a few minor aesthetic points
|
why is this draft? |
|
Do we need a new type? I think we can achieve the same thing in a simpler and more general way by extending DiscreteRelaxation = Relaxation{<:Any, <:Any, <:Union{AbstractArray, FlavorOfFTS}}
@inline get_target(i, j, k, grid, a::AbstactArray, clock) = @inbounds a[i, j, k]
@inline get_target(i, j, k, grid, a::FlavorOfFTS, clock) = @inbounds a[i, j, k, Time(clock)]
function (r::DiscreteRelaxation)(i, j, k, grid, clock, fields, params)
target = get_target(i, j, k, grid, r.target, clock)
φ = @inbounds fields[p.field_name][i, j, k]
X = node(i, j, k, grid, location(p.fts)...)
return r.rate * r.mask(X...) * (target - φ)
end
function materialize_forcing(forcing::DiscreteRelaxation, field, field_name, model_field_names)
discrete_relaxation = DiscreteForcing(forcing; parameters=(; field_name))
return materialize_forcing(discrete_relaxation, field, field_name, model_field_names)
endbut probably better to think a bit more about it such that is a bit more general. |
|
but maybe to do this we need to pass the |
@glwagner I was looking into optimizing the interpolation by caching the fractional indices but I think we should keep it simple. |
The new target type is needed to support interpolation of the |
|
I do think we could use
Switching to this design does preclude the future possibility of an optimization that embeds the field index as a type parameter (so the compiler can infer The |
I think this points to a flaw in the |
|
@simone-silvestri I really appreciate your time in reviewing this and providing feedback. For the time being, I'd like to move forward with what's currently in the PR to provide some additional flexibility as use cases evolve—I'd rather not refactor to DiscreteForcing now and then undo it later. Happy to revisit and refactor down the road as necessary. |
|
I would argue that this is not a general solution. The general solution would be fixing the technical dept that require us to go in hoops to pass the field name in the forcing. This solution layers on top of this flaw, so the best course of action would be to solve this issue. What I propose would be a more substantial refactor but would prove a little more solid in the future. I understand, however if you need this feature immediately. We can refactor after this. |
|
Note that passing the forced field in the forcing function signature is basically what ContinuousForcing is already doing (through a slightly more convoluted path) this is why we don't need all this extra boilerplate to have a simple relaxation in that case. |
|
I think we should refactor in a future PR to remove the ContinuousForcing dependency. Another thing we want to add is to allow transformation of |
|
sadly GPU tests are now broken... |
Summary
Relaxation(rate=…, target=fts)now accepts aFieldTimeSeriesas the relaxation target.FieldTimeSeriesTarget{L,F}carrying the simulation-side location (instantiated_location(field)) and the integer index of the forced field inmodel_fields. TheRelaxationstruct itself is unchanged (stillRelaxation{R,M,T}); the target field's type just becomesFieldTimeSeriesTargetafter materialization.Relaxation{R,M,<:FieldTimeSeriesTarget}readsϕ = model_fields[index][i,j,k]and obtainsϕᵣviainterpolate(X, Time(clock.time), fts, instantiated_location(fts), fts.grid)(spatial + temporal interpolation), so the FTS may live on a different grid as long as its extent brackets the simulation grid inx,y, andz.ArgumentErrorat materialize time naming the offending axis and both intervals.The existing
Relaxation(target=callable)/Relaxation(target=number)path is untouched — multiple dispatch routes those cases through the originalContinuousForcingwrapping.Why
This is the first slice of a small series that generalizes
Relaxationfor real-data use cases — driving an LES or regional run with prescribed large-scale state from a host model (ERA5, GFS, a parent simulation) through grid nudging and/or open boundaries with a fringe region. Scope of this PR is intentionally narrow: 3D mode only, FTS targets only. "Profile mode" (mode=:profile+ column reference, see NumericalEarth/Breeze.jl#562) and other target types are deferred to follow-up PRs.Design notes
Forcing(fts::FlavorOfFTS)materializes into a discrete forcing (forcing.jl:184-186);Relaxationalready mutates the user's recipe into a kernel-ready form atmaterialize_forcing(relaxation.jl:75-78). This PR keeps theRelaxationidentity through materialization (model.forcing.c isa Relaxationstaystrue), only swapping thetargetfield's type.Int, matching the precedent inContinuousForcing.field_dependencies_indices(user_function_arguments.jl:1-19) rather than encoding as aVal{N}type param.Forcing(fts)doesn't enforce location matching (flagged latent constraint); this PR sidesteps that by usinginterpolate, so location/grid alignment between FTS and forced field is no longer required.Test plan
Relaxationsmoke tests (GaussianMask,PiecewiseLinearMask) still passOut of scope (follow-ups)
FractionalIndicesarrays inFieldTimeSeriesTargetat materialize time.mode=:profile, column-averaged relaxation toward 1×1×Nz reference). Requires the newupdate_forcing!hook in each model'supdate_state!.Field(time-invariant) targets.column_field_time_seriesutility for extracting a column reference from a 3D reanalysis FTS.