1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
//! A.3 Robin BC partial-additive port (math.md §3.5.tris, ADR-0098).
//!
//! Standalone sibling of v2.8 `ReflectedHeatChernoff` (ADR-0072); REUSES `σ_R`
//! via the `ReflectingRegion<F>` trait (no new geometric-reflection trait —
//! the geometry is shared with Neumann; only the scalar mixing coefficient
//! `r(α, β, τ)` differs from Neumann's `r ≡ 1`).
//!
//! ## Order
//!
//! `RobinHeatChernoff::order() = 1`. The skew extension `1_R + r · 1_R ∘ σ_R`
//! has a NONZERO commutator with self-adjoint L for `r ≠ 1` (math §3.5.tris.5).
//! Proposition 25.1 (Neumann order-preservation) does NOT extend to Robin.
//!
//! ## Kernel (Carslaw-Jaeger 1959 §14.2 eq 5)
//!
//! The exact Robin heat kernel on `[0, ∞)` is:
//!
//! ```text
//! K^Robin(x, y; t) = K(x,y,t) + K(x,-y,t)
//! - (α/β)·exp((α/β)(x+y) + (α/β)²t)·erfc((x+y)/(2√t) + (α/β)√t)
//! ```
//!
//! (Factor is `(α/β)`, NOT `2·(α/β)` — see ADR-0098 Amendment 1.)
//! Satisfies: `α·K(0,y;t) − β·∂_x K(0,y;t) = 0` (outward-normal form;
//! `∂_n = −∂_x` at x=0 for `[0, ∞)`).
//!
//! ## Citations
//! - Carslaw-Jaeger 1959 §3.4 (image-method skew-r derivation) + §14.2 eq 5
//! - Walsh 1986 §3.4 (image-method generalisation to skew reflection)
//! - Engel-Nagel 2000 Ch. VI §6 (Robin generator characterisation)
//! - research1.md Part B Vector 4 (partial-additivity verdict; Dynamic BC unsettled)
use core::marker::PhantomData;
use crate::{
boundary::BoundaryPolicy,
chernoff::{ChernoffFunction, Growth},
diffusion::DiffusionChernoff,
error::SemiflowError,
float::SemiflowFloat,
grid_fn::GridFn1D,
reflection::{HalfSpaceRegion, ReflectingRegion},
scratch::ScratchPool,
};
// ---------------------------------------------------------------------------
// RobinRegion<F> — sub-trait of ReflectingRegion<F>
// ---------------------------------------------------------------------------
/// Robin region — sub-trait of `ReflectingRegion<F>` that adds the scalar
/// Robin coefficients `(α, β)` to the geometric-reflection contract.
///
/// v4.6 stores `(alpha, beta)` constant across ∂R; per-cell varying coefficients
/// deferred to v5.x via a future `robin_coeffs_at(point: &[F]) -> (F, F)` method.
pub trait RobinRegion<F: SemiflowFloat>: ReflectingRegion<F> {
/// Return the scalar Robin coefficients `(α, β)` for the region.
fn robin_coeffs(&self) -> (F, F);
}
// ---------------------------------------------------------------------------
// HalfSpaceRobin<F, D> — Robin BC on a half-space
// ---------------------------------------------------------------------------
/// Half-space Robin region: wraps `HalfSpaceRegion<F, D>` with scalar `(α, β)`.
///
/// Construction validates `‖normal‖₂ = 1` (delegated to `HalfSpaceRegion::new`)
/// and `alpha ≥ 0 ∧ beta > 0` (well-posedness of static Robin BC).
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct HalfSpaceRobin<F: SemiflowFloat = f64, const D: usize = 1> {
/// Underlying half-space geometry (origin + unit outward normal).
pub half_space: HalfSpaceRegion<F, D>,
/// Coefficient α on u(x) at the boundary; α ≥ 0.
pub alpha: F,
/// Coefficient β on ∂_n u(x) at the boundary; β > 0.
pub beta: F,
}
impl<F: SemiflowFloat, const D: usize> HalfSpaceRobin<F, D> {
/// Construct with validated unit normal and Robin coefficients.
///
/// # Errors
/// - `DomainViolation` if `‖normal‖₂ ≠ 1` (via `HalfSpaceRegion::new`).
/// - `DomainViolation` if `alpha < 0` or `beta ≤ 0` or any value non-finite.
pub fn new(origin: [F; D], normal: [F; D], alpha: F, beta: F) -> Result<Self, SemiflowError> {
let half_space = HalfSpaceRegion::new(origin, normal)?;
if !alpha.is_finite() || !beta.is_finite() {
return Err(SemiflowError::DomainViolation {
what: "HalfSpaceRobin: alpha and beta must be finite",
value: f64::NAN,
});
}
if alpha < F::zero() || beta <= F::zero() {
return Err(SemiflowError::DomainViolation {
what: "HalfSpaceRobin: require alpha ≥ 0 and beta > 0 (well-posed Robin BC)",
value: alpha.to_f64().unwrap_or(f64::NAN),
});
}
Ok(Self {
half_space,
alpha,
beta,
})
}
}
impl<F: SemiflowFloat, const D: usize> ReflectingRegion<F> for HalfSpaceRobin<F, D> {
fn dim(&self) -> usize {
self.half_space.dim()
}
fn is_inside(&self, point: &[F]) -> bool {
self.half_space.is_inside(point)
}
fn reflect_in_place(
&self,
dst: &mut GridFn1D<F>,
src: &GridFn1D<F>,
) -> Result<(), SemiflowError> {
self.half_space.reflect_in_place(dst, src)
}
}
impl<F: SemiflowFloat, const D: usize> RobinRegion<F> for HalfSpaceRobin<F, D> {
fn robin_coeffs(&self) -> (F, F) {
(self.alpha, self.beta)
}
}
// ---------------------------------------------------------------------------
// RobinHeatChernoff<C, R, F> — wrapper
// ---------------------------------------------------------------------------
/// Chernoff wrapper for Robin (mixed) BCs via skew image method (math §3.5.tris.3).
///
/// Per-step: `F_Robin(τ)f = C(τ)f + r(α, β, τ) · C(τ)(f ∘ σ_R)`
/// where `r(α, β, τ) = (β − α·√(2τ)) / (β + α·√(2τ))`.
///
/// `order()` = 1 (skew extension breaks Proposition 25.1 order-preservation;
/// the commutator `[1_R ∘ σ_R, L]` is nonzero for `r ≠ 1`).
#[derive(Debug, Clone)]
pub struct RobinHeatChernoff<C, R, F = f64>
where
C: ChernoffFunction<F, S = GridFn1D<F>>,
R: RobinRegion<F>,
F: SemiflowFloat,
{
inner: C,
/// The Robin region providing geometry + (α, β) coefficients.
pub region: R,
_f: PhantomData<F>,
}
impl<C, R, F> RobinHeatChernoff<C, R, F>
where
C: ChernoffFunction<F, S = GridFn1D<F>>,
R: RobinRegion<F>,
F: SemiflowFloat,
{
/// Wrap `inner` Chernoff function with Robin region `region`.
///
/// # Errors
/// Currently infallible; returns `Ok` for API consistency.
pub fn new(inner: C, region: R) -> Result<Self, SemiflowError> {
Ok(Self {
inner,
region,
_f: PhantomData,
})
}
}
// ---------------------------------------------------------------------------
// Concrete ChernoffFunction<f64> impl
// ---------------------------------------------------------------------------
impl ChernoffFunction<f64>
for RobinHeatChernoff<DiffusionChernoff<f64>, HalfSpaceRobin<f64, 1>, f64>
{
type S = GridFn1D<f64>;
/// Option A skew image, ADR-0098 Am.2.
///
/// Mirrors the proven-working `ReflectedHeatChernoff` (G27/Neumann) pattern:
/// set `src.grid.boundary = BoundaryPolicy::Robin { alpha, beta }` so the
/// inner `DiffusionChernoff` stencil samples ghost nodes via the exponential
/// skew image `u_{-d} = exp(-2(α/β)·d·dx)·u_{d}`. α=0 ⟹ weight 1 = even
/// reflection = Neumann (G27 cannot regress).
fn apply_into(
&self,
tau: f64,
src: &Self::S,
dst: &mut Self::S,
scratch: &mut ScratchPool<f64>,
) -> Result<(), SemiflowError> {
let (alpha, beta) = self.region.robin_coeffs();
let mut src_robin = src.clone();
src_robin.grid = src_robin
.grid
.with_boundary(BoundaryPolicy::Robin { alpha, beta });
self.inner.apply_into(tau, &src_robin, dst, scratch)
}
/// Order = 1 (skew extension breaks Proposition 25.1; math §3.5.tris.5).
fn order(&self) -> u32 {
1
}
/// Growth: same as inner (skew adds bounded scalar; `r ∈ [−1, +1]`).
fn growth(&self) -> Growth<f64> {
self.inner.growth()
}
}
// ---------------------------------------------------------------------------
// Unit tests
// ---------------------------------------------------------------------------
#[cfg(test)]
mod tests {
use super::{HalfSpaceRobin, RobinHeatChernoff, RobinRegion};
use crate::{
diffusion::DiffusionChernoff, error::SemiflowError, grid::Grid1D, ChernoffFunction,
};
#[test]
fn half_space_robin_construction_ok() {
let r = HalfSpaceRobin::<f64, 1>::new([0.0], [1.0], 1.0, 1.0);
assert!(r.is_ok());
}
#[test]
fn half_space_robin_negative_alpha_err() {
let err = HalfSpaceRobin::<f64, 1>::new([0.0], [1.0], -1.0, 1.0).unwrap_err();
assert!(matches!(err, SemiflowError::DomainViolation { .. }));
}
#[test]
fn half_space_robin_zero_beta_err() {
let err = HalfSpaceRobin::<f64, 1>::new([0.0], [1.0], 1.0, 0.0).unwrap_err();
assert!(matches!(err, SemiflowError::DomainViolation { .. }));
}
#[test]
fn robin_heat_chernoff_order_is_1() {
let grid = Grid1D::new(0.0_f64, 10.0, 16).unwrap();
let inner = DiffusionChernoff::new(|_| 1.0, |_| 0.0, |_| 0.0, 1.0, grid);
let region = HalfSpaceRobin::<f64, 1>::new([0.0], [1.0], 1.0, 1.0).unwrap();
let wrapper = RobinHeatChernoff::new(inner, region).unwrap();
assert_eq!(
wrapper.order(),
1,
"Robin order is 1 (skew commutator non-vanishing)"
);
}
#[test]
fn robin_alpha_zero_skew_weight_recovers_neumann() {
// v6.2.3: apply_into uses the skew image weight w^(d)=exp(-2(α/β)·d·dx),
// NOT the removed scalar r-formula. At α=0 the weight is 1 = even
// reflection = Neumann (math §3.5.tris.3, ADR-0098 Amendment 2).
let alpha = 0.0_f64;
let beta = 1.0_f64;
let dx = 0.1_f64;
let depth = 5.0_f64;
let w = libm::exp(-2.0 * (alpha / beta) * depth * dx); // e^0 == 1
assert!(
(w - 1.0).abs() < 1e-14,
"alpha=0 skew weight must be 1 (Neumann limit), got {w}"
);
}
#[test]
fn robin_region_coeffs_round_trip() {
let region = HalfSpaceRobin::<f64, 1>::new([0.0], [1.0], 2.5, 0.7).unwrap();
let (a, b) = region.robin_coeffs();
assert!((a - 2.5).abs() < 1e-15);
assert!((b - 0.7).abs() < 1e-15);
}
}