1212
1313import jax .numpy as jnp
1414
15- from lcm .typing import Float1D , FloatND , ScalarFloat , ScalarInt
15+ from lcm .typing import BoolND , Float1D , FloatND , ScalarFloat , ScalarInt
1616
1717
1818def interp_on_padded_grid (
@@ -170,31 +170,32 @@ def interp_on_prepared_grid(
170170 return jnp .where (valid_length == 0 , jnp .nan , result )
171171
172172
173- def interp_right_derivative_on_padded_grid (
173+ def interp_right_germ_on_padded_grid (
174174 * ,
175175 x_query : FloatND ,
176176 xp : Float1D ,
177177 fp : Float1D ,
178178 fp_slopes : Float1D ,
179- ) -> FloatND :
180- """Compute the right derivative of the Hermite value read on a padded row.
179+ ) -> tuple [ BoolND , FloatND , FloatND , FloatND ] :
180+ """Compute the right germ of the Hermite value read on a padded row.
181181
182182 Same row contract as `interp_on_padded_grid`; see
183- `interp_right_derivative_on_prepared_grid ` for the derivative semantics.
183+ `interp_right_germ_on_prepared_grid ` for the germ semantics.
184184
185185 Args:
186- x_query: Points at which to evaluate the right derivative ; any shape.
186+ x_query: Points at which to evaluate the right germ ; any shape.
187187 xp: Weakly ascending grid row with NaNs only in the tail.
188188 fp: Function values on `xp`, NaN-padded in lockstep with `xp`.
189189 fp_slopes: Derivatives of `fp` with respect to `xp` at the `xp` nodes,
190190 NaN-padded in lockstep.
191191
192192 Returns:
193- Right derivatives with the shape of `x_query`.
193+ Tuple of the right-finiteness flag and the first, second, and third
194+ right derivatives, each with the shape of `x_query`.
194195
195196 """
196197 search_grid , valid_length = prepare_padded_grid (xp )
197- return interp_right_derivative_on_prepared_grid (
198+ return interp_right_germ_on_prepared_grid (
198199 x_query = x_query ,
199200 search_grid = search_grid ,
200201 valid_length = valid_length ,
@@ -204,37 +205,40 @@ def interp_right_derivative_on_padded_grid(
204205 )
205206
206207
207- def interp_right_derivative_on_prepared_grid (
208+ def interp_right_germ_on_prepared_grid (
208209 * ,
209210 x_query : FloatND ,
210211 search_grid : Float1D ,
211212 valid_length : ScalarInt ,
212213 xp : Float1D ,
213214 fp : Float1D ,
214215 fp_slopes : Float1D ,
215- ) -> FloatND :
216- """Compute the right derivative of the Hermite value read at each query.
217-
218- This is the one-sided derivative of the *value interpolant* that
219- `interp_on_prepared_grid` evaluates with `fp_slopes` — the local slope
220- governing the read immediately to the right of the query — not a read of
221- the slope row itself. The two differ exactly where a tie-owner decision
222- needs the truth: the Fritsch-Carlson limiter may cap a node's raw slope,
223- and the edge clamps flatten the read outside the valid range. Semantics:
224-
225- - Strictly inside a bracket: the derivative of that bracket's limited
226- cubic Hermite (the secant where the correction is inapplicable — the
227- linear fallback).
228- - Exactly on a node: the derivative at the left edge of the node's *right*
229- bracket (the bracket search is right-continuous), i.e. the node's
230- limited slope with respect to that bracket's secant.
231- - Strictly below the first node, and at or above the last valid node: zero
232- — the read clamps to a constant there.
233- - A bracket with a non-finite value difference (e.g. a `-inf` endpoint):
234- zero, matching the linear rule's flat, clamp-like behavior there.
216+ ) -> tuple [BoolND , FloatND , FloatND , FloatND ]:
217+ """Compute the right germ of the Hermite value read at each query.
218+
219+ The germ is the complete local description of the *value interpolant* that
220+ `interp_on_prepared_grid` evaluates with `fp_slopes` immediately to the
221+ right of the query — not a read of the slope row itself. Each local piece
222+ is a cubic (or a constant clamp), so the germ is finite-dimensional: a
223+ right-finiteness flag plus the first, second, and third one-sided
224+ derivatives determine the read on a right neighborhood exactly. The germ
225+ differs from the raw slope row exactly where a tie-owner decision needs the
226+ truth: the Fritsch-Carlson limiter may cap a node's raw slope, the edge
227+ clamps flatten the read outside the valid range, and a `-inf` bracket
228+ endpoint kills the read immediately right of a finite node. Semantics:
229+
230+ - Strictly inside a bracket: the derivatives of that bracket's limited
231+ cubic Hermite (the secant and zero curvature where the correction is
232+ inapplicable — the linear fallback).
233+ - Exactly on a node: the derivatives at the left edge of the node's *right*
234+ bracket (the bracket search is right-continuous).
235+ - Strictly below the first node, and at or above the last valid node: the
236+ read clamps to a constant — right-finite with all derivatives zero.
237+ - A bracket with a non-finite endpoint value: not right-finite (the read
238+ is `-inf` on the bracket's interior), derivatives zero.
235239
236240 Args:
237- x_query: Points at which to evaluate the right derivative ; any shape.
241+ x_query: Points at which to evaluate the right germ ; any shape.
238242 search_grid: The row's `+inf`-padded search key from
239243 `prepare_padded_grid`.
240244 valid_length: The row's non-NaN prefix length from
@@ -244,12 +248,13 @@ def interp_right_derivative_on_prepared_grid(
244248 fp_slopes: Node derivatives, NaN-padded in lockstep.
245249
246250 Returns:
247- Right derivatives with the shape of `x_query`.
251+ Tuple of the right-finiteness flag and the first, second, and third
252+ right derivatives, each with the shape of `x_query`.
248253
249254 """
250255 # Identical bracket location to `interp_on_prepared_grid`: `side="right"`
251256 # puts an on-node query into the node's right bracket, which is exactly the
252- # bracket whose derivative the right-continuous read needs.
257+ # bracket whose germ the right-continuous read needs.
253258 upper = jnp .clip (
254259 jnp .searchsorted (search_grid , x_query , side = "right" ),
255260 1 ,
@@ -274,37 +279,52 @@ def interp_right_derivative_on_prepared_grid(
274279 safe_df = jnp .where (jnp .isfinite (df ), df , 0.0 )
275280 secant = safe_df / safe_width
276281
277- # The same limiter as `_hermite_correction`, so this derivative is the
278- # derivative of exactly the polynomial the value read evaluates.
282+ # The same limiter as `_hermite_correction`, so these derivatives are the
283+ # derivatives of exactly the polynomial the value read evaluates. In the
284+ # bracket's local coordinate `t` the read is
285+ # `p(t) = f_l + Δf t + c_l t + (c_u - 2 c_l) t² + (c_l - c_u) t³`.
279286 def limit (slope : FloatND ) -> FloatND :
280287 same_sign = slope * secant > 0.0
281288 limited = jnp .sign (secant ) * jnp .minimum (jnp .abs (slope ), 3.0 * jnp .abs (secant ))
282289 return jnp .where (same_sign , limited , 0.0 )
283290
284291 coeff_lower = safe_width * limit (slope_lower ) - safe_df
285292 coeff_upper = safe_df - safe_width * limit (slope_upper )
286- hermite_slope = (
293+ hermite_first = (
287294 safe_df
288295 + (1.0 - 2.0 * relative_position )
289296 * ((1.0 - relative_position ) * coeff_lower + relative_position * coeff_upper )
290297 + relative_position * (1.0 - relative_position ) * (coeff_upper - coeff_lower )
291298 ) / safe_width
299+ hermite_second = (
300+ 2.0 * (coeff_upper - 2.0 * coeff_lower )
301+ + 6.0 * (coeff_lower - coeff_upper ) * relative_position
302+ ) / safe_width ** 2
303+ hermite_third = 6.0 * (coeff_lower - coeff_upper ) / safe_width ** 3
292304 applicable = (
293305 (bracket_width > 0.0 )
294306 & jnp .isfinite (fp_lower )
295307 & jnp .isfinite (fp_upper )
296308 & jnp .isfinite (slope_lower )
297309 & jnp .isfinite (slope_upper )
298310 )
299- derivative = jnp .where (applicable , hermite_slope , secant )
311+ first = jnp .where (applicable , hermite_first , secant )
312+ second = jnp .where (applicable , hermite_second , 0.0 )
313+ third = jnp .where (applicable , hermite_third , 0.0 )
300314 # The read clamps to a constant strictly below the first node and at or
301- # above the last valid one; its right derivative there is exactly zero.
302- # (`search_grid` is `+inf` on the pad, so a poisoned all-NaN row lands on
303- # the lower clamp and stays zero .)
315+ # above the last valid one; the germ there is right-finite with all
316+ # derivatives exactly zero. (`search_grid` is `+inf` on the pad, so a
317+ # poisoned all-NaN row lands on the lower clamp .)
304318 first_node = search_grid [0 ]
305319 last_node = search_grid [jnp .maximum (valid_length - 1 , 0 )]
306320 on_clamp_ray = (x_query < first_node ) | (x_query >= last_node )
307- return jnp .where (on_clamp_ray , 0.0 , derivative )
321+ right_finite = on_clamp_ray | (jnp .isfinite (fp_lower ) & jnp .isfinite (fp_upper ))
322+ return (
323+ right_finite ,
324+ jnp .where (on_clamp_ray , 0.0 , first ),
325+ jnp .where (on_clamp_ray , 0.0 , second ),
326+ jnp .where (on_clamp_ray , 0.0 , third ),
327+ )
308328
309329
310330def _interp_between_nodes (
0 commit comments