Skip to main content

oxilean_std/finite_element_method/
functions.rs

1//! Auto-generated module
2//!
3//! 🤖 Generated with [SplitRS](https://github.com/cool-japan/splitrs)
4
5use oxilean_kernel::{BinderInfo, Declaration, Environment, Expr, Level, Name};
6
7use super::types::{
8    AdaptiveMeshRefinement, AffineTriangleMap, DGMesh1D, DGMethodStiffness, DenseMatrix, FEMesh2D,
9    GalerkinStiffnessMatrix, GaussQuadrature, NitscheData1D, PoissonFESolver, StiffnessMatrix,
10    TriangularMesh2D,
11};
12
13pub fn app(f: Expr, a: Expr) -> Expr {
14    Expr::App(Box::new(f), Box::new(a))
15}
16pub fn app2(f: Expr, a: Expr, b: Expr) -> Expr {
17    app(app(f, a), b)
18}
19pub fn app3(f: Expr, a: Expr, b: Expr, c: Expr) -> Expr {
20    app(app2(f, a, b), c)
21}
22pub fn app4(f: Expr, a: Expr, b: Expr, c: Expr, d: Expr) -> Expr {
23    app(app3(f, a, b, c), d)
24}
25pub fn cst(s: &str) -> Expr {
26    Expr::Const(Name::str(s), vec![])
27}
28pub fn prop() -> Expr {
29    Expr::Sort(Level::zero())
30}
31pub fn type0() -> Expr {
32    Expr::Sort(Level::succ(Level::zero()))
33}
34pub fn pi(bi: BinderInfo, name: &str, dom: Expr, body: Expr) -> Expr {
35    Expr::Pi(bi, Name::str(name), Box::new(dom), Box::new(body))
36}
37pub fn arrow(a: Expr, b: Expr) -> Expr {
38    pi(BinderInfo::Default, "_", a, b)
39}
40pub fn bvar(n: u32) -> Expr {
41    Expr::BVar(n)
42}
43pub fn nat_ty() -> Expr {
44    cst("Nat")
45}
46pub fn real_ty() -> Expr {
47    cst("Real")
48}
49pub fn bool_ty() -> Expr {
50    cst("Bool")
51}
52pub fn fn_ty(dom: Expr, cod: Expr) -> Expr {
53    arrow(dom, cod)
54}
55/// `Triangulation : Nat → Type`
56///
57/// A triangulation T_h of a domain Ω ⊂ ℝⁿ (n = dim) into simplicial elements.
58/// The parameter h denotes the mesh size (largest element diameter).
59pub fn triangulation_ty() -> Expr {
60    arrow(nat_ty(), type0())
61}
62/// `TriangulationElement : Type`
63///
64/// A single element (simplex) in a triangulation: carries vertex indices and
65/// the Jacobian of the affine map from the reference element.
66pub fn triangulation_element_ty() -> Expr {
67    type0()
68}
69/// `MeshVertex : Type`
70///
71/// A vertex in the mesh: its coordinates and global node index.
72pub fn mesh_vertex_ty() -> Expr {
73    type0()
74}
75/// `IsConforming : Triangulation → Prop`
76///
77/// Conformity condition: any two elements share at most a common face, edge, or vertex.
78/// No hanging nodes: every face of an element is either a boundary face or a full face
79/// of exactly one neighbouring element.
80pub fn is_conforming_ty() -> Expr {
81    arrow(cst("Triangulation"), prop())
82}
83/// `IsShapeRegular : Triangulation → Real → Prop`
84///
85/// Shape regularity: ∃ κ > 0 such that for every element K in T_h,
86///   h_K / ρ_K ≤ κ,
87/// where h_K is the element diameter and ρ_K is the radius of the largest inscribed ball.
88pub fn is_shape_regular_ty() -> Expr {
89    arrow(cst("Triangulation"), arrow(real_ty(), prop()))
90}
91/// `IsQuasiuniform : Triangulation → Real → Prop`
92///
93/// Quasi-uniformity: T_h is shape regular AND ∃ σ > 0 such that h ≤ σ h_K for all K.
94/// This is a stronger condition ensuring no element is too small relative to the mesh size.
95pub fn is_quasiuniform_ty() -> Expr {
96    arrow(cst("Triangulation"), arrow(real_ty(), prop()))
97}
98/// `MeshSize : Triangulation → Real`
99///
100/// The global mesh size h = max_{K ∈ T_h} diam(K).
101pub fn mesh_size_ty() -> Expr {
102    arrow(cst("Triangulation"), real_ty())
103}
104/// `NumElements : Triangulation → Nat`
105///
106/// Total number of elements in the triangulation.
107pub fn num_elements_ty() -> Expr {
108    arrow(cst("Triangulation"), nat_ty())
109}
110/// `NumDOF : FiniteElementSpace → Nat`
111///
112/// Number of degrees of freedom in the finite element space.
113pub fn num_dof_ty() -> Expr {
114    arrow(cst("FiniteElementSpace"), nat_ty())
115}
116/// `ReferenceTriangle : Type`
117///
118/// The reference triangle K̂ = {(ξ,η) ∈ ℝ² : ξ ≥ 0, η ≥ 0, ξ+η ≤ 1}.
119/// All physical triangles are images of K̂ under an affine map Fₖ: ξ ↦ Bₖ ξ + bₖ.
120pub fn reference_triangle_ty() -> Expr {
121    type0()
122}
123/// `ReferenceTetrahedron : Type`
124///
125/// The reference tetrahedron K̂ = {(ξ,η,ζ) ∈ ℝ³ : ξ,η,ζ ≥ 0, ξ+η+ζ ≤ 1}.
126pub fn reference_tetrahedron_ty() -> Expr {
127    type0()
128}
129/// `ReferenceHexahedron : Type`
130///
131/// The reference hexahedron (cube) K̂ = [-1,1]³. Used for Q1/Q2 elements.
132pub fn reference_hexahedron_ty() -> Expr {
133    type0()
134}
135/// `AffineMap : ReferenceElement → PhysicalElement → Prop`
136///
137/// The affine map Fₖ: K̂ → K given by Fₖ(x̂) = Bₖ x̂ + bₖ.
138/// Here Bₖ ∈ ℝ^{n×n} is invertible and bₖ ∈ ℝⁿ.
139pub fn affine_map_ty() -> Expr {
140    arrow(type0(), arrow(type0(), prop()))
141}
142/// `JacobianDeterminant : AffineMap → Real`
143///
144/// |det Bₖ|: the absolute value of the determinant of the Jacobian matrix.
145/// Appears in change of variables in integrals: ∫_K f = ∫_{K̂} (f ∘ Fₖ) |det Bₖ| dξ.
146pub fn jacobian_determinant_ty() -> Expr {
147    arrow(prop(), real_ty())
148}
149/// `ShapeFunction : ReferenceTriangle → Nat → (Real → Real → Real)`
150///
151/// The k-th Lagrange P1 shape function on the reference triangle:
152///   φ₁(ξ,η) = 1 - ξ - η,   φ₂(ξ,η) = ξ,   φ₃(ξ,η) = η.
153pub fn shape_function_ty() -> Expr {
154    arrow(
155        type0(),
156        arrow(nat_ty(), fn_ty(real_ty(), fn_ty(real_ty(), real_ty()))),
157    )
158}
159/// `ShapeFunctionGradient : ReferenceTriangle → Nat → (Real → Real → Real × Real)`
160///
161/// Gradient of the k-th shape function on the reference element.
162/// For P1: ∇φ₁ = (-1,-1), ∇φ₂ = (1,0), ∇φ₃ = (0,1).
163pub fn shape_function_gradient_ty() -> Expr {
164    arrow(
165        type0(),
166        arrow(nat_ty(), fn_ty(real_ty(), fn_ty(real_ty(), type0()))),
167    )
168}
169/// `LagrangeP1Space : Triangulation → FiniteElementSpace`
170///
171/// The piecewise linear continuous finite element space V_h:
172///   V_h = { v ∈ H¹(Ω) : v|_K ∈ P₁(K) for all K ∈ T_h }.
173/// Globally continuous with nodal basis {φᵢ} satisfying φᵢ(xⱼ) = δᵢⱼ.
174pub fn lagrange_p1_space_ty() -> Expr {
175    arrow(cst("Triangulation"), type0())
176}
177/// `LagrangeP2Space : Triangulation → FiniteElementSpace`
178///
179/// The piecewise quadratic continuous finite element space:
180///   V_h = { v ∈ H¹(Ω) : v|_K ∈ P₂(K) for all K ∈ T_h }.
181/// Has 6 DOFs per triangle (3 vertices + 3 edge midpoints).
182pub fn lagrange_p2_space_ty() -> Expr {
183    arrow(cst("Triangulation"), type0())
184}
185/// `FiniteElementSpace : Type`  (base declaration)
186pub fn finite_element_space_ty() -> Expr {
187    type0()
188}
189/// `IsSubspace : FiniteElementSpace → H1Space → Prop`
190///
191/// V_h ⊆ V = H¹₀(Ω): the FE space is conforming (subspace of the continuous space).
192pub fn is_subspace_ty() -> Expr {
193    arrow(cst("FiniteElementSpace"), arrow(cst("H1Space"), prop()))
194}
195/// `InterpolationOperator : H1Space → FiniteElementSpace → H1Space`
196///
197/// The nodal interpolant Π_h: H¹(Ω) → V_h mapping u to its Lagrange interpolant.
198pub fn interpolation_operator_ty() -> Expr {
199    arrow(
200        cst("H1Space"),
201        arrow(cst("FiniteElementSpace"), cst("H1Space")),
202    )
203}
204/// `GalerkinProblem : BilinearForm → LinearForm → FiniteElementSpace → Prop`
205///
206/// The Galerkin problem: find u_h ∈ V_h such that
207///   a(u_h, v_h) = f(v_h) for all v_h ∈ V_h.
208/// Encoded as existence and uniqueness of such u_h.
209pub fn galerkin_problem_ty() -> Expr {
210    arrow(
211        cst("BilinearForm"),
212        arrow(cst("LinearForm"), arrow(cst("FiniteElementSpace"), prop())),
213    )
214}
215/// `LinearForm : Type`  (rhs functional f : V → ℝ)
216pub fn linear_form_ty() -> Expr {
217    type0()
218}
219/// `BilinearForm : Type`  (base declaration for a: V×V → ℝ)
220pub fn bilinear_form_base_ty() -> Expr {
221    type0()
222}
223/// `IsCoercive : BilinearForm → Real → Prop`
224///
225/// Coercivity: ∃ α > 0 such that a(u,u) ≥ α ‖u‖²_V for all u ∈ V.
226pub fn is_coercive_ty() -> Expr {
227    arrow(cst("BilinearForm"), arrow(real_ty(), prop()))
228}
229/// `IsBounded : BilinearForm → Real → Prop`
230///
231/// Continuity: ∃ M > 0 such that |a(u,v)| ≤ M ‖u‖ ‖v‖ for all u,v ∈ V.
232pub fn is_bounded_ty() -> Expr {
233    arrow(cst("BilinearForm"), arrow(real_ty(), prop()))
234}
235/// `StiffnessMatrix : FiniteElementSpace → Matrix`
236///
237/// The assembled global stiffness matrix A with entries
238///   A_{ij} = a(φⱼ, φᵢ) = ∫_Ω ∇φⱼ · ∇φᵢ dx.
239pub fn stiffness_matrix_ty() -> Expr {
240    arrow(cst("FiniteElementSpace"), type0())
241}
242/// `MassMatrix : FiniteElementSpace → Matrix`
243///
244/// The assembled global mass matrix M with entries
245///   M_{ij} = (φⱼ, φᵢ)_{L²} = ∫_Ω φⱼ φᵢ dx.
246pub fn mass_matrix_ty() -> Expr {
247    arrow(cst("FiniteElementSpace"), type0())
248}
249/// `CeaLemma : ∀ (a : BilinearForm) (α M : Real) (Vh : FESpace) (u uh : H1),`
250/// `  IsCoercive a α → IsBounded a M → GalerkinSolution a f Vh uh →`
251/// `  ‖u - uh‖_V ≤ (M/α) * inf_{vh ∈ Vh} ‖u - vh‖_V`
252///
253/// Céa's lemma: the Galerkin solution is quasi-optimal in the energy norm.
254/// The error is bounded by the best approximation error, up to the constant M/α.
255pub fn cea_lemma_ty() -> Expr {
256    pi(
257        BinderInfo::Default,
258        "a",
259        cst("BilinearForm"),
260        pi(
261            BinderInfo::Default,
262            "alpha",
263            real_ty(),
264            pi(
265                BinderInfo::Default,
266                "M",
267                real_ty(),
268                pi(
269                    BinderInfo::Default,
270                    "Vh",
271                    cst("FiniteElementSpace"),
272                    arrow(
273                        app2(cst("IsCoercive"), bvar(3), bvar(2)),
274                        arrow(app2(cst("IsBounded"), bvar(4), bvar(2)), prop()),
275                    ),
276                ),
277            ),
278        ),
279    )
280}
281/// `InterpolationError : FiniteElementSpace → Nat → Real → Prop`
282///
283/// Interpolation estimate: for u ∈ H^{k+1}(Ω) and the P_k FE space,
284///   ‖u - Π_h u‖_{H¹} ≤ C h^k |u|_{H^{k+1}}.
285pub fn interpolation_error_ty() -> Expr {
286    arrow(
287        cst("FiniteElementSpace"),
288        arrow(nat_ty(), arrow(real_ty(), prop())),
289    )
290}
291/// `AprioriErrorEstimate : FiniteElementSpace → Nat → Real → Prop`
292///
293/// The a priori error estimate combining Céa's lemma with the interpolation bound:
294///   ‖u - u_h‖_{H¹} ≤ C h^k |u|_{H^{k+1}}
295/// for degree-k Lagrange elements and sufficiently smooth u.
296pub fn apriori_error_estimate_ty() -> Expr {
297    arrow(
298        cst("FiniteElementSpace"),
299        arrow(nat_ty(), arrow(real_ty(), prop())),
300    )
301}
302/// `L2ErrorEstimate : FiniteElementSpace → Nat → Real → Prop`
303///
304/// The Aubin–Nitsche L² error estimate (duality argument):
305///   ‖u - u_h‖_{L²} ≤ C h^{k+1} |u|_{H^{k+1}}
306/// gaining one extra power of h compared to the H¹ estimate.
307pub fn l2_error_estimate_ty() -> Expr {
308    arrow(
309        cst("FiniteElementSpace"),
310        arrow(nat_ty(), arrow(real_ty(), prop())),
311    )
312}
313/// `AubinNitscheTrick : Prop`
314///
315/// The Aubin–Nitsche duality argument: uses the adjoint problem
316///   a(v, φ) = (e_h, v) for all v ∈ V
317/// to bootstrap the H¹ estimate into an L² estimate with one additional power of h.
318pub fn aubin_nitsche_trick_ty() -> Expr {
319    prop()
320}
321/// `FEMWellPosedness : BilinearForm → LinearForm → FiniteElementSpace → Prop`
322///
323/// The discrete Lax–Milgram theorem: if a is coercive and bounded on V_h,
324/// then the Galerkin problem has a unique solution u_h ∈ V_h.
325pub fn fem_well_posedness_ty() -> Expr {
326    arrow(
327        cst("BilinearForm"),
328        arrow(cst("LinearForm"), arrow(cst("FiniteElementSpace"), prop())),
329    )
330}
331/// `LaxMilgramDiscrete : ∀ (a : BilinearForm) (f : LinearForm) (Vh : FESpace),`
332/// `  IsCoercive a → IsBounded a → ∃! uh : H1Space, GalerkinSolution a f Vh uh`
333///
334/// The discrete Lax–Milgram theorem: existence and uniqueness for the Galerkin problem.
335pub fn lax_milgram_discrete_ty() -> Expr {
336    pi(
337        BinderInfo::Default,
338        "a",
339        cst("BilinearForm"),
340        pi(
341            BinderInfo::Default,
342            "f",
343            cst("LinearForm"),
344            pi(
345                BinderInfo::Default,
346                "Vh",
347                cst("FiniteElementSpace"),
348                arrow(
349                    app2(cst("IsCoercive"), bvar(2), real_ty()),
350                    arrow(app2(cst("IsBounded"), bvar(3), real_ty()), prop()),
351                ),
352            ),
353        ),
354    )
355}
356/// `StabilityEstimate : BilinearForm → LinearForm → Prop`
357///
358/// Stability: the solution satisfies ‖u_h‖_V ≤ (1/α) ‖f‖_{V*},
359/// i.e., the solution depends continuously on the data.
360pub fn stability_estimate_ty() -> Expr {
361    arrow(cst("BilinearForm"), arrow(cst("LinearForm"), prop()))
362}
363/// `NitscheBilinearForm : BilinearForm → Real → BilinearForm`
364///
365/// The Nitsche bilinear form incorporating a penalty term γ/h:
366///   a_Nitsche(u,v) = ∫_Ω ∇u·∇v dx - ∫_{∂Ω} ∂_n u · v ds
367///                  - ∫_{∂Ω} u · ∂_n v ds + (γ/h) ∫_{∂Ω} u · v ds.
368/// This weakly enforces Dirichlet BCs while preserving optimal convergence.
369pub fn nitsche_bilinear_form_ty() -> Expr {
370    arrow(cst("BilinearForm"), arrow(real_ty(), cst("BilinearForm")))
371}
372/// `NitscheConsistency : NitscheBilinearForm → Prop`
373///
374/// Consistency: the exact solution u satisfies the Nitsche variational form,
375/// so the method is Galerkin-orthogonal and admits a Céa-type bound.
376pub fn nitsche_consistency_ty() -> Expr {
377    arrow(cst("BilinearForm"), prop())
378}
379/// `NitscheCoercivity : NitscheBilinearForm → Real → Prop`
380///
381/// Coercivity of the Nitsche form for γ > γ₀ (sufficiently large penalty):
382///   a_Nitsche(v_h, v_h) ≥ (α/2) ‖v_h‖²_{H¹}.
383pub fn nitsche_coercivity_ty() -> Expr {
384    arrow(cst("BilinearForm"), arrow(real_ty(), prop()))
385}
386/// `NitscheOptimalConvergence : FiniteElementSpace → Nat → Prop`
387///
388/// Nitsche's method achieves the same convergence rate as the method with
389/// strongly imposed Dirichlet conditions:
390///   ‖u - u_h^N‖_{H¹} ≤ C h^k |u|_{H^{k+1}}.
391pub fn nitsche_optimal_convergence_ty() -> Expr {
392    arrow(cst("FiniteElementSpace"), arrow(nat_ty(), prop()))
393}
394/// `DGSpace : Triangulation → Nat → Type`
395///
396/// The broken polynomial space V_h^{DG} = { v : Ω → ℝ : v|_K ∈ P_k(K) for all K }
397/// without inter-element continuity constraints.
398pub fn dg_space_ty() -> Expr {
399    arrow(cst("Triangulation"), arrow(nat_ty(), type0()))
400}
401/// `InteriorPenaltyForm : DGSpace → Real → BilinearForm`
402///
403/// The symmetric interior penalty DG (SIPG) bilinear form:
404///   a_{IP}(u,v) = Σ_K ∫_K ∇u·∇v dx
405///               - Σ_e ∫_e ({∇u}·[v] + {∇v}·[u]) ds
406///               + Σ_e (σ/h_e) ∫_e [u]·[v] ds,
407/// where {·} and [·] denote averages and jumps across interior edges.
408pub fn interior_penalty_form_ty() -> Expr {
409    arrow(type0(), arrow(real_ty(), cst("BilinearForm")))
410}
411/// `DGCoercivity : InteriorPenaltyForm → Real → Prop`
412///
413/// Coercivity of the IP form in the DG norm ‖v‖²_{DG} = Σ_K ‖∇v‖²_{L²(K)} + Σ_e (σ/h_e) ‖[v]‖²
414/// for σ > σ₀ sufficiently large.
415pub fn dg_coercivity_ty() -> Expr {
416    arrow(cst("BilinearForm"), arrow(real_ty(), prop()))
417}
418/// `DGConsistency : InteriorPenaltyForm → Prop`
419///
420/// Consistency: the exact solution satisfies a_{IP}(u, v_h) = f(v_h) for all v_h ∈ V_h^{DG}.
421pub fn dg_consistency_ty() -> Expr {
422    arrow(cst("BilinearForm"), prop())
423}
424/// `DGErrorEstimate : DGSpace → Nat → Real → Prop`
425///
426/// DG a priori error estimate in the DG norm:
427///   ‖u - u_h‖_{DG} ≤ C h^k |u|_{H^{k+1}}.
428pub fn dg_error_estimate_ty() -> Expr {
429    arrow(type0(), arrow(nat_ty(), arrow(real_ty(), prop())))
430}
431/// `UPGForm : DGSpace → Real → BilinearForm`
432///
433/// The upwind DG (UWDG) form for convection-dominated problems:
434///   a_{upw}(u,v) = -∫_Ω u (b·∇v) dx + ∫_{∂Ω_-} (b·n) u_inflow v ds
435///                 + Σ_e ∫_e (b·n_e)^+ [u] {v} ds.
436pub fn upg_form_ty() -> Expr {
437    arrow(type0(), arrow(real_ty(), cst("BilinearForm")))
438}
439/// `MixedFEProblem : BilinearForm → BilinearForm → LinearForm → LinearForm → Prop`
440///
441/// The mixed (saddle-point) variational problem: find (u,p) ∈ V × Q such that
442///   a(u,v) + b(v,p) = f(v) for all v ∈ V
443///   b(u,q)           = g(q) for all q ∈ Q.
444pub fn mixed_fe_problem_ty() -> Expr {
445    arrow(
446        cst("BilinearForm"),
447        arrow(
448            cst("BilinearForm"),
449            arrow(cst("LinearForm"), arrow(cst("LinearForm"), prop())),
450        ),
451    )
452}
453/// `InfSupCondition : BilinearForm → Real → Prop`
454///
455/// The Ladyzhenskaya–Babuška–Brezzi (LBB) inf-sup condition:
456///   inf_{q ∈ Q} sup_{v ∈ V} b(v,q) / (‖v‖_V ‖q‖_Q) ≥ β > 0.
457/// This is the key compatibility condition for mixed methods.
458pub fn inf_sup_condition_ty() -> Expr {
459    arrow(cst("BilinearForm"), arrow(real_ty(), prop()))
460}
461/// `BrezziSplitting : MixedFEProblem → Prop`
462///
463/// The Brezzi splitting theorem: the saddle-point problem is well-posed iff
464/// (i) a is coercive on ker(B), and (ii) the LBB inf-sup condition holds.
465pub fn brezzi_splitting_ty() -> Expr {
466    arrow(prop(), prop())
467}
468/// `MixedFEMErrorEstimate : BilinearForm → BilinearForm → FiniteElementSpace → Prop`
469///
470/// A priori error estimate for mixed methods: if the discrete inf-sup holds with
471/// constant β_h ≥ β > 0, then ‖u - u_h‖ + ‖p - p_h‖ ≤ C (inf_{vh} ‖u-vh‖ + inf_{qh} ‖p-qh‖).
472pub fn mixed_fem_error_estimate_ty() -> Expr {
473    arrow(
474        cst("BilinearForm"),
475        arrow(
476            cst("BilinearForm"),
477            arrow(cst("FiniteElementSpace"), prop()),
478        ),
479    )
480}
481/// `TaylorHoodElement : Triangulation → FiniteElementSpace`
482///
483/// The Taylor–Hood element (P2/P1): velocity in P2, pressure in P1.
484/// Satisfies the inf-sup condition for the Stokes equations.
485pub fn taylor_hood_element_ty() -> Expr {
486    arrow(cst("Triangulation"), type0())
487}
488/// `MiniElement : Triangulation → FiniteElementSpace`
489///
490/// The MINI element: velocity in P1 + bubble, pressure in P1.
491/// A stable low-order element for Stokes.
492pub fn mini_element_ty() -> Expr {
493    arrow(cst("Triangulation"), type0())
494}
495/// `ResidualEstimator : FiniteElementSpace → H1Space → Real`
496///
497/// The element residual error estimator η_K:
498///   η²_K = h²_K ‖f + Δu_h‖²_{L²(K)} + Σ_{e ∈ ∂K} h_e ‖[∂_n u_h]‖²_{L²(e)}.
499pub fn residual_estimator_ty() -> Expr {
500    arrow(cst("FiniteElementSpace"), arrow(cst("H1Space"), real_ty()))
501}
502/// `ReliabilityBound : ResidualEstimator → Prop`
503///
504/// Global reliability: ‖u - u_h‖_{H¹} ≤ C_rel (Σ_K η²_K)^{1/2}.
505pub fn reliability_bound_ty() -> Expr {
506    arrow(prop(), prop())
507}
508/// `EfficiencyBound : ResidualEstimator → Prop`
509///
510/// Local efficiency: η_K ≤ C_eff (‖u - u_h‖_{H¹(ω_K)} + h.o.t.)
511/// where ω_K is the element patch.
512pub fn efficiency_bound_ty() -> Expr {
513    arrow(prop(), prop())
514}
515pub fn build_fem_env(env: &mut Environment) {
516    let axioms: &[(&str, Expr)] = &[
517        ("FiniteElementSpace", finite_element_space_ty()),
518        ("BilinearForm", bilinear_form_base_ty()),
519        ("LinearForm", linear_form_ty()),
520        ("H1Space", type0()),
521        ("H10Space", type0()),
522        ("L2Space", type0()),
523        ("Matrix", type0()),
524        ("Triangulation", triangulation_ty()),
525        ("TriangulationElement", triangulation_element_ty()),
526        ("MeshVertex", mesh_vertex_ty()),
527        ("IsConforming", is_conforming_ty()),
528        ("IsShapeRegular", is_shape_regular_ty()),
529        ("IsQuasiuniform", is_quasiuniform_ty()),
530        ("MeshSize", mesh_size_ty()),
531        ("NumElements", num_elements_ty()),
532        ("NumDOF", num_dof_ty()),
533        ("ReferenceTriangle", reference_triangle_ty()),
534        ("ReferenceTetrahedron", reference_tetrahedron_ty()),
535        ("ReferenceHexahedron", reference_hexahedron_ty()),
536        ("AffineMap", affine_map_ty()),
537        ("JacobianDeterminant", jacobian_determinant_ty()),
538        ("ShapeFunction", shape_function_ty()),
539        ("ShapeFunctionGradient", shape_function_gradient_ty()),
540        ("LagrangeP1Space", lagrange_p1_space_ty()),
541        ("LagrangeP2Space", lagrange_p2_space_ty()),
542        ("IsSubspace", is_subspace_ty()),
543        ("InterpolationOperator", interpolation_operator_ty()),
544        ("GalerkinProblem", galerkin_problem_ty()),
545        ("IsCoercive", is_coercive_ty()),
546        ("IsBounded", is_bounded_ty()),
547        ("StiffnessMatrix", stiffness_matrix_ty()),
548        ("MassMatrix", mass_matrix_ty()),
549        ("cea_lemma", cea_lemma_ty()),
550        ("InterpolationError", interpolation_error_ty()),
551        ("AprioriErrorEstimate", apriori_error_estimate_ty()),
552        ("L2ErrorEstimate", l2_error_estimate_ty()),
553        ("aubin_nitsche_trick", aubin_nitsche_trick_ty()),
554        ("FEMWellPosedness", fem_well_posedness_ty()),
555        ("lax_milgram_discrete", lax_milgram_discrete_ty()),
556        ("StabilityEstimate", stability_estimate_ty()),
557        ("NitscheBilinearForm", nitsche_bilinear_form_ty()),
558        ("NitscheConsistency", nitsche_consistency_ty()),
559        ("NitscheCoercivity", nitsche_coercivity_ty()),
560        (
561            "nitsche_optimal_convergence",
562            nitsche_optimal_convergence_ty(),
563        ),
564        ("DGSpace", dg_space_ty()),
565        ("InteriorPenaltyForm", interior_penalty_form_ty()),
566        ("DGCoercivity", dg_coercivity_ty()),
567        ("DGConsistency", dg_consistency_ty()),
568        ("DGErrorEstimate", dg_error_estimate_ty()),
569        ("UPGForm", upg_form_ty()),
570        ("MixedFEProblem", mixed_fe_problem_ty()),
571        ("InfSupCondition", inf_sup_condition_ty()),
572        ("brezzi_splitting", brezzi_splitting_ty()),
573        ("MixedFEMErrorEstimate", mixed_fem_error_estimate_ty()),
574        ("TaylorHoodElement", taylor_hood_element_ty()),
575        ("MiniElement", mini_element_ty()),
576        ("ResidualEstimator", residual_estimator_ty()),
577        ("reliability_bound", reliability_bound_ty()),
578        ("efficiency_bound", efficiency_bound_ty()),
579    ];
580    for (name, ty) in axioms {
581        env.add(Declaration::Axiom {
582            name: Name::str(*name),
583            univ_params: vec![],
584            ty: ty.clone(),
585        })
586        .ok();
587    }
588}
589/// The three Lagrange P1 shape functions on the reference triangle K̂.
590///
591/// φ₁(ξ,η) = 1 - ξ - η
592/// φ₂(ξ,η) = ξ
593/// φ₃(ξ,η) = η
594pub fn p1_shape(k: usize, xi: f64, eta: f64) -> f64 {
595    match k {
596        0 => 1.0 - xi - eta,
597        1 => xi,
598        2 => eta,
599        _ => 0.0,
600    }
601}
602/// Gradient of P1 shape functions on the reference triangle (constant).
603///
604/// ∇φ₁ = (-1, -1),  ∇φ₂ = (1, 0),  ∇φ₃ = (0, 1)
605pub fn p1_grad(k: usize) -> [f64; 2] {
606    match k {
607        0 => [-1.0, -1.0],
608        1 => [1.0, 0.0],
609        2 => [0.0, 1.0],
610        _ => [0.0, 0.0],
611    }
612}
613/// Evaluate the six Lagrange P2 shape functions on the reference triangle.
614///
615/// Nodes: v0=(0,0), v1=(1,0), v2=(0,1), m01=(0.5,0), m12=(0.5,0.5), m02=(0,0.5)
616pub fn p2_shape(k: usize, xi: f64, eta: f64) -> f64 {
617    let lam1 = 1.0 - xi - eta;
618    let lam2 = xi;
619    let lam3 = eta;
620    match k {
621        0 => lam1 * (2.0 * lam1 - 1.0),
622        1 => lam2 * (2.0 * lam2 - 1.0),
623        2 => lam3 * (2.0 * lam3 - 1.0),
624        3 => 4.0 * lam1 * lam2,
625        4 => 4.0 * lam2 * lam3,
626        5 => 4.0 * lam1 * lam3,
627        _ => 0.0,
628    }
629}
630/// 3-point quadrature rule on the reference triangle (degree 2 exact).
631///
632/// Points: (1/6, 1/6), (2/3, 1/6), (1/6, 2/3); weights: 1/6 each.
633pub fn reference_triangle_quadrature() -> Vec<([f64; 2], f64)> {
634    vec![
635        ([1.0 / 6.0, 1.0 / 6.0], 1.0 / 6.0),
636        ([2.0 / 3.0, 1.0 / 6.0], 1.0 / 6.0),
637        ([1.0 / 6.0, 2.0 / 3.0], 1.0 / 6.0),
638    ]
639}
640/// Assemble the global P1 stiffness matrix for -Δu = f on a 2D triangular mesh
641/// with homogeneous Dirichlet boundary conditions (boundary DOFs excluded).
642///
643/// Returns the interior stiffness matrix and the load vector for f ≡ 1.
644#[allow(clippy::too_many_arguments)]
645pub fn assemble_p1_stiffness(mesh: &TriangularMesh2D) -> (DenseMatrix, Vec<f64>) {
646    let n_v = mesh.num_vertices();
647    let mut k_global = DenseMatrix::zeros(n_v);
648    let mut f_global = vec![0.0; n_v];
649    let quad = reference_triangle_quadrature();
650    for tri_idx in 0..mesh.num_triangles() {
651        let [i0, i1, i2] = mesh.triangles[tri_idx];
652        let p0 = mesh.vertices[i0];
653        let p1 = mesh.vertices[i1];
654        let p2 = mesh.vertices[i2];
655        let fmap = AffineTriangleMap::new(p0, p1, p2);
656        let det_j = fmap.det_j();
657        let mut ke = [[0.0f64; 3]; 3];
658        let mut fe = [0.0f64; 3];
659        let grads_phys: [[f64; 2]; 3] = [
660            fmap.transform_grad(p1_grad(0)),
661            fmap.transform_grad(p1_grad(1)),
662            fmap.transform_grad(p1_grad(2)),
663        ];
664        let area = det_j / 2.0;
665        for a in 0..3 {
666            for b in 0..3 {
667                let dot = grads_phys[a][0] * grads_phys[b][0] + grads_phys[a][1] * grads_phys[b][1];
668                ke[a][b] = dot * area;
669            }
670            for (pt, w) in &quad {
671                fe[a] += w * p1_shape(a, pt[0], pt[1]) * det_j;
672            }
673        }
674        let local_nodes = [i0, i1, i2];
675        for a in 0..3 {
676            for b in 0..3 {
677                k_global.add(local_nodes[a], local_nodes[b], ke[a][b]);
678            }
679            f_global[local_nodes[a]] += fe[a];
680        }
681    }
682    (k_global, f_global)
683}
684/// Apply Dirichlet boundary conditions by zeroing rows/columns of boundary nodes
685/// and setting the diagonal to 1 with rhs = 0.
686pub fn apply_dirichlet_bc(mat: &mut DenseMatrix, rhs: &mut Vec<f64>, is_boundary: &[bool]) {
687    for i in 0..mat.n {
688        if is_boundary[i] {
689            for j in 0..mat.n {
690                mat.data[i * mat.n + j] = 0.0;
691                mat.data[j * mat.n + i] = 0.0;
692            }
693            mat.data[i * mat.n + i] = 1.0;
694            rhs[i] = 0.0;
695        }
696    }
697}
698/// Solve A x = b using the conjugate gradient method.
699/// Assumes A is symmetric positive definite.
700pub fn conjugate_gradient(mat: &DenseMatrix, rhs: &[f64], tol: f64, max_iter: usize) -> Vec<f64> {
701    let n = mat.n;
702    let mut x = vec![0.0; n];
703    let mut r = rhs.to_vec();
704    let mut p = r.clone();
705    let mut rr = dot(&r, &r);
706    for _ in 0..max_iter {
707        if rr < tol * tol {
708            break;
709        }
710        let ap = mat.matvec(&p);
711        let alpha = rr / dot(&p, &ap);
712        for i in 0..n {
713            x[i] += alpha * p[i];
714            r[i] -= alpha * ap[i];
715        }
716        let rr_new = dot(&r, &r);
717        let beta = rr_new / rr;
718        for i in 0..n {
719            p[i] = r[i] + beta * p[i];
720        }
721        rr = rr_new;
722    }
723    x
724}
725pub fn dot(a: &[f64], b: &[f64]) -> f64 {
726    a.iter().zip(b.iter()).map(|(x, y)| x * y).sum()
727}
728/// Compute element residual error indicators for a 1D P1 FEM solution of -u'' = f.
729///
730/// η_i = h * |f_i + (u'_{i+1} - u'_i) / h|  (simplified residual at interior nodes)
731pub fn element_residuals_1d(u: &[f64], h: f64, f: impl Fn(f64) -> f64) -> Vec<f64> {
732    let n = u.len();
733    if n < 3 {
734        return vec![];
735    }
736    (1..n - 1)
737        .map(|i| {
738            let x_i = i as f64 * h;
739            let laplacian_u = (u[i - 1] - 2.0 * u[i] + u[i + 1]) / (h * h);
740            let residual = f(x_i) + laplacian_u;
741            h * residual.abs()
742        })
743        .collect()
744}
745/// Global a posteriori error bound: η = (Σ_i η_i²)^{1/2}.
746pub fn global_error_estimator(indicators: &[f64]) -> f64 {
747    indicators.iter().map(|e| e * e).sum::<f64>().sqrt()
748}
749/// `PetrovGalerkinProblem : BilinearForm → LinearForm → FESpace → FESpace → Prop`
750///
751/// The Petrov-Galerkin method: find u_h ∈ V_h such that
752///   a(u_h, v_h) = f(v_h) for all v_h ∈ W_h,
753/// where the trial space V_h and test space W_h may differ.
754pub fn petrov_galerkin_problem_ty() -> Expr {
755    arrow(
756        cst("BilinearForm"),
757        arrow(
758            cst("LinearForm"),
759            arrow(
760                cst("FiniteElementSpace"),
761                arrow(cst("FiniteElementSpace"), prop()),
762            ),
763        ),
764    )
765}
766/// `BubnovGalerkinConsistency : BilinearForm → FiniteElementSpace → Prop`
767///
768/// Variational consistency: the Bubnov-Galerkin solution satisfies the
769/// Galerkin orthogonality condition a(u - u_h, v_h) = 0 for all v_h ∈ V_h.
770pub fn bubnov_galerkin_consistency_ty() -> Expr {
771    arrow(
772        cst("BilinearForm"),
773        arrow(cst("FiniteElementSpace"), prop()),
774    )
775}
776/// `GalerkinOrthogonality : BilinearForm → H1Space → FiniteElementSpace → Prop`
777///
778/// Galerkin orthogonality: the error e_h = u - u_h is orthogonal to V_h
779/// in the energy inner product a(e_h, v_h) = 0.
780pub fn galerkin_orthogonality_ty() -> Expr {
781    arrow(
782        cst("BilinearForm"),
783        arrow(cst("H1Space"), arrow(cst("FiniteElementSpace"), prop())),
784    )
785}
786/// `HRefinement : Triangulation → Triangulation → Prop`
787///
788/// h-refinement: T_h' is obtained from T_h by uniformly or adaptively
789/// subdividing elements, reducing the mesh size h by a factor.
790pub fn h_refinement_ty() -> Expr {
791    arrow(cst("Triangulation"), arrow(cst("Triangulation"), prop()))
792}
793/// `PRefinement : FiniteElementSpace → Nat → FiniteElementSpace → Prop`
794///
795/// p-refinement: the polynomial degree is increased from k to k+1
796/// while the mesh remains fixed.
797pub fn p_refinement_ty() -> Expr {
798    arrow(
799        cst("FiniteElementSpace"),
800        arrow(nat_ty(), arrow(cst("FiniteElementSpace"), prop())),
801    )
802}
803/// `HPAdaptivity : Triangulation → FiniteElementSpace → Prop`
804///
805/// hp-adaptivity: combines h-refinement in regions with low regularity
806/// and p-enrichment in smooth regions to achieve exponential convergence.
807pub fn hp_adaptivity_ty() -> Expr {
808    arrow(
809        cst("Triangulation"),
810        arrow(cst("FiniteElementSpace"), prop()),
811    )
812}
813/// `ExponentialConvergence : FiniteElementSpace → Real → Prop`
814///
815/// Exponential convergence of hp-FEM for analytic solutions:
816///   ‖u - u_{hp}‖_{H¹} ≤ C exp(-b √N)
817/// where N is the number of degrees of freedom.
818pub fn exponential_convergence_ty() -> Expr {
819    arrow(cst("FiniteElementSpace"), arrow(real_ty(), prop()))
820}
821/// `DorflerMarking : ResidualEstimator → Real → Subset → Prop`
822///
823/// Dörfler's marking strategy: select a minimal subset M ⊆ T_h such that
824///   Σ_{K ∈ M} η²_K ≥ θ Σ_{K ∈ T_h} η²_K  for θ ∈ (0,1).
825pub fn dorfler_marking_ty() -> Expr {
826    arrow(prop(), arrow(real_ty(), arrow(prop(), prop())))
827}
828/// `NedelecSpace : Triangulation → Nat → Type`
829///
830/// The Nédélec edge element space of degree k:
831///   N_k(T_h) = { v ∈ H(curl, Ω) : v|_K ∈ N_k(K) for all K ∈ T_h }.
832/// DOFs are moments of tangential components on edges.
833pub fn nedelec_space_ty() -> Expr {
834    arrow(cst("Triangulation"), arrow(nat_ty(), type0()))
835}
836/// `RaviartThomasSpace : Triangulation → Nat → Type`
837///
838/// The Raviart-Thomas H(div) space of degree k:
839///   RT_k(T_h) = { v ∈ H(div, Ω) : v|_K ∈ RT_k(K) for all K ∈ T_h }.
840/// DOFs are moments of normal components on faces.
841pub fn raviart_thomas_space_ty() -> Expr {
842    arrow(cst("Triangulation"), arrow(nat_ty(), type0()))
843}
844/// `BrezziDouglasMariniSpace : Triangulation → Nat → Type`
845///
846/// The BDM space: an enhanced RT space with full polynomial degree on faces.
847pub fn bdm_space_ty() -> Expr {
848    arrow(cst("Triangulation"), arrow(nat_ty(), type0()))
849}
850/// `DeRhamComplex : Triangulation → Prop`
851///
852/// The discrete de Rham complex:
853///   H¹_h →^{∇} H(curl)_h →^{∇×} H(div)_h →^{∇·} L²_h
854/// The complex is exact and commutes with the continuous de Rham complex.
855pub fn de_rham_complex_ty() -> Expr {
856    arrow(cst("Triangulation"), prop())
857}
858/// `HCurlConforming : FiniteElementSpace → Prop`
859///
860/// H(curl)-conforming: the tangential trace is continuous across element faces.
861/// Satisfied by Nédélec elements.
862pub fn hcurl_conforming_ty() -> Expr {
863    arrow(cst("FiniteElementSpace"), prop())
864}
865/// `HDivConforming : FiniteElementSpace → Prop`
866///
867/// H(div)-conforming: the normal trace is continuous across element faces.
868/// Satisfied by Raviart-Thomas and BDM elements.
869pub fn hdiv_conforming_ty() -> Expr {
870    arrow(cst("FiniteElementSpace"), prop())
871}
872/// `ZZEstimator : FiniteElementSpace → H1Space → Real`
873///
874/// The Zienkiewicz-Zhu (ZZ) superconvergent patch recovery estimator:
875/// η²_K = ‖G(∇u_h) - ∇u_h‖²_{L²(K)}
876/// where G is the SPR gradient recovery operator.
877pub fn zz_estimator_ty() -> Expr {
878    arrow(cst("FiniteElementSpace"), arrow(cst("H1Space"), real_ty()))
879}
880/// `GoalOrientedEstimator : FiniteElementSpace → LinearForm → Real`
881///
882/// Goal-oriented a posteriori error estimator for a functional J(u):
883///   |J(u) - J(u_h)| ≤ η_goal
884/// using the dual-weighted residual (DWR) method.
885pub fn goal_oriented_estimator_ty() -> Expr {
886    arrow(
887        cst("FiniteElementSpace"),
888        arrow(cst("LinearForm"), real_ty()),
889    )
890}
891/// `AdaptiveConvergence : FiniteElementSpace → Real → Real → Prop`
892///
893/// Convergence of the AFEM loop: after m refinement steps,
894///   ‖u - u_h^m‖_{H¹} ≤ C m^{-s/d}
895/// where s is the approximation class and d the spatial dimension.
896pub fn adaptive_convergence_ty() -> Expr {
897    arrow(
898        cst("FiniteElementSpace"),
899        arrow(real_ty(), arrow(real_ty(), prop())),
900    )
901}
902/// `MultigridPreconditioner : FiniteElementSpace → Type`
903///
904/// A multigrid V-cycle preconditioner:
905/// combines smoothing (Gauss-Seidel or ILU) on each level with
906/// coarse-grid correction for optimal O(N) complexity.
907pub fn multigrid_preconditioner_ty() -> Expr {
908    arrow(cst("FiniteElementSpace"), type0())
909}
910/// `DomainDecompositionMethod : FiniteElementSpace → Nat → Prop`
911///
912/// Domain decomposition with n subdomains: decomposes the problem into
913/// independent subproblems coupled via interface conditions.
914pub fn domain_decomposition_ty() -> Expr {
915    arrow(cst("FiniteElementSpace"), arrow(nat_ty(), prop()))
916}
917/// `FETIPreconditioner : FiniteElementSpace → Type`
918///
919/// FETI (Finite Element Tearing and Interconnecting) preconditioner:
920/// non-overlapping decomposition with Lagrange multipliers at subdomain interfaces.
921pub fn feti_preconditioner_ty() -> Expr {
922    arrow(cst("FiniteElementSpace"), type0())
923}
924/// `BDDCPreconditioner : FiniteElementSpace → Type`
925///
926/// BDDC (Balancing Domain Decomposition by Constraints) preconditioner:
927/// primal formulation of FETI-DP with optimally chosen primal unknowns.
928pub fn bddc_preconditioner_ty() -> Expr {
929    arrow(cst("FiniteElementSpace"), type0())
930}
931/// `MultigridOptimality : MultigridPreconditioner → Prop`
932///
933/// Optimality of multigrid: the condition number of the preconditioned system
934/// κ(M⁻¹A) = O(1) is bounded independently of the mesh size h.
935pub fn multigrid_optimality_ty() -> Expr {
936    arrow(type0(), prop())
937}
938/// `BSplineBasis : Nat → Nat → Type`
939///
940/// B-spline basis of degree p with n + 1 control points.
941/// Supports C^{p-1} continuity across knot spans (C^{p-m_i} at a knot of multiplicity m_i).
942pub fn bspline_basis_ty() -> Expr {
943    arrow(nat_ty(), arrow(nat_ty(), type0()))
944}
945/// `NURBSSurface : BSplineBasis → BSplineBasis → Type`
946///
947/// A NURBS (Non-Uniform Rational B-Spline) surface: the geometry parameterization
948/// used in isogeometric analysis to represent the exact CAD geometry.
949pub fn nurbs_surface_ty() -> Expr {
950    arrow(type0(), arrow(type0(), type0()))
951}
952/// `IsogeometricSpace : NURBSSurface → Nat → Type`
953///
954/// The isogeometric analysis space: uses the same NURBS basis for both
955/// the geometry representation and the FEM solution approximation.
956pub fn isogeometric_space_ty() -> Expr {
957    arrow(type0(), arrow(nat_ty(), type0()))
958}
959/// `IsogeometricConvergence : IsogeometricSpace → Nat → Real → Prop`
960///
961/// IGA optimal convergence: for degree p,
962///   ‖u - u_h‖_{H¹} ≤ C h^p |u|_{H^{p+1}}
963/// matching standard FEM rates but with better per-DOF accuracy.
964pub fn isogeometric_convergence_ty() -> Expr {
965    arrow(type0(), arrow(nat_ty(), arrow(real_ty(), prop())))
966}
967/// `TSplineSpace : Nat → Type`
968///
969/// T-spline space: allows local refinement (T-junctions) while maintaining
970/// watertight NURBS-compatible geometry representations.
971pub fn tspline_space_ty() -> Expr {
972    arrow(nat_ty(), type0())
973}
974/// `SpaceTimeFESpace : FiniteElementSpace → Real → Type`
975///
976/// A space-time finite element space over Ω × (0,T):
977/// uses tensor-product Lagrange elements in space and time simultaneously.
978pub fn space_time_fe_space_ty() -> Expr {
979    arrow(cst("FiniteElementSpace"), arrow(real_ty(), type0()))
980}
981/// `ParabolicStability : SpaceTimeFESpace → Prop`
982///
983/// Energy stability for the heat equation discretization:
984///   ‖u_h(T)‖²_{L²} + Σ_n ∫_{t_n}^{t_{n+1}} ‖∇u_h‖² dt ≤ C ‖u_h(0)‖²_{L²}.
985pub fn parabolic_stability_ty() -> Expr {
986    arrow(type0(), prop())
987}
988/// `WaveEnergyConservation : SpaceTimeFESpace → Prop`
989///
990/// Energy conservation for the wave equation:
991///   ‖∂_t u_h(t)‖²_{L²} + ‖∇u_h(t)‖²_{L²} = const for all t ∈ (0,T).
992pub fn wave_energy_conservation_ty() -> Expr {
993    arrow(type0(), prop())
994}
995/// `SpaceTimeErrorEstimate : SpaceTimeFESpace → Nat → Real → Prop`
996///
997/// Space-time a priori error estimate:
998///   ‖u - u_h‖_{L²(0,T;H¹)} ≤ C (h^k + τ^q) |u|_{W^{q+1,2}(0,T;H^{k+1})}.
999pub fn space_time_error_estimate_ty() -> Expr {
1000    arrow(type0(), arrow(nat_ty(), arrow(real_ty(), prop())))
1001}
1002/// `NewtonRaphsonFEM : BilinearForm → LinearForm → FiniteElementSpace → Prop`
1003///
1004/// Newton-Raphson iteration for nonlinear FEM:
1005///   K(u_h^k) Δu^k = -R(u_h^k),  u_h^{k+1} = u_h^k + Δu^k
1006/// where K is the consistent tangent stiffness and R the residual.
1007pub fn newton_raphson_fem_ty() -> Expr {
1008    arrow(
1009        cst("BilinearForm"),
1010        arrow(cst("LinearForm"), arrow(cst("FiniteElementSpace"), prop())),
1011    )
1012}
1013/// `ArcLengthMethod : BilinearForm → Real → Prop`
1014///
1015/// Arc-length continuation for path-following through limit points:
1016/// constrains the load parameter to trace the load-displacement curve.
1017pub fn arc_length_method_ty() -> Expr {
1018    arrow(cst("BilinearForm"), arrow(real_ty(), prop()))
1019}
1020/// `ConsistentLinearization : BilinearForm → H1Space → BilinearForm`
1021///
1022/// The consistent linearization of a nonlinear bilinear form at u:
1023///   Da(u)[h,v] = lim_{ε→0} (a(u + εh, v) - a(u,v))/ε.
1024pub fn consistent_linearization_ty() -> Expr {
1025    arrow(
1026        cst("BilinearForm"),
1027        arrow(cst("H1Space"), cst("BilinearForm")),
1028    )
1029}
1030/// `QuadraticConvergenceNewton : NewtonRaphsonFEM → Prop`
1031///
1032/// Quadratic convergence of Newton's method near the solution:
1033///   ‖u_h^{k+1} - u*‖ ≤ C ‖u_h^k - u*‖².
1034pub fn quadratic_convergence_newton_ty() -> Expr {
1035    arrow(prop(), prop())
1036}
1037/// `ReissnerMindlinPlate : Triangulation → Nat → Type`
1038///
1039/// The Reissner-Mindlin plate element:
1040/// thickness-shear deformable plate theory (valid for thick plates).
1041/// DOFs: deflection w and rotations (θ_x, θ_y) at each node.
1042pub fn reissner_mindlin_plate_ty() -> Expr {
1043    arrow(cst("Triangulation"), arrow(nat_ty(), type0()))
1044}
1045/// `KirchhoffLovePlate : Triangulation → Nat → Type`
1046///
1047/// The Kirchhoff-Love plate element (Euler-Bernoulli in 2D):
1048/// thin plate theory requiring C¹ continuity.
1049pub fn kirchhoff_love_plate_ty() -> Expr {
1050    arrow(cst("Triangulation"), arrow(nat_ty(), type0()))
1051}
1052/// `ShearLockingFreedom : ReissnerMindlinPlate → Prop`
1053///
1054/// Shear-locking-free property: for thin plates (t→0),
1055/// the Reissner-Mindlin element recovers the Kirchhoff solution
1056/// without spurious shear stiffness (locking).
1057pub fn shear_locking_freedom_ty() -> Expr {
1058    arrow(type0(), prop())
1059}
1060/// `ReducedIntegration : FiniteElementSpace → Nat → Prop`
1061///
1062/// Selective/reduced integration: using fewer quadrature points
1063/// to avoid locking phenomena in nearly incompressible materials and thin structures.
1064pub fn reduced_integration_ty() -> Expr {
1065    arrow(cst("FiniteElementSpace"), arrow(nat_ty(), prop()))
1066}
1067/// `FluidStructureInteraction : BilinearForm → BilinearForm → Prop`
1068///
1069/// Fluid-structure interaction (FSI): coupled system of fluid equations
1070/// (Navier-Stokes) and structural equations (elasticity) on a moving domain.
1071pub fn fluid_structure_interaction_ty() -> Expr {
1072    arrow(cst("BilinearForm"), arrow(cst("BilinearForm"), prop()))
1073}
1074/// `ThermoMechanicalCoupling : BilinearForm → BilinearForm → LinearForm → Prop`
1075///
1076/// Thermo-mechanical coupling: the thermal and mechanical problems are coupled
1077/// via thermal expansion and heat generation by plastic deformation.
1078pub fn thermo_mechanical_coupling_ty() -> Expr {
1079    arrow(
1080        cst("BilinearForm"),
1081        arrow(cst("BilinearForm"), arrow(cst("LinearForm"), prop())),
1082    )
1083}
1084/// `PoroElasticityBiot : BilinearForm → BilinearForm → Prop`
1085///
1086/// Biot's consolidation (poro-elasticity): fully coupled fluid-solid
1087/// interaction in a porous medium (Terzaghi + Darcy flow).
1088pub fn poro_elasticity_biot_ty() -> Expr {
1089    arrow(cst("BilinearForm"), arrow(cst("BilinearForm"), prop()))
1090}
1091/// `OperatorSplitting : CoupledProblem → Prop`
1092///
1093/// Operator splitting for coupled problems: decouples the subsystems
1094/// and solves them sequentially, with correction terms at each time step.
1095pub fn operator_splitting_ty() -> Expr {
1096    arrow(prop(), prop())
1097}
1098/// Register all new FEM axioms (Sections 11-20) into `env`.
1099pub fn build_fem_env_extended(env: &mut Environment) {
1100    let axioms: &[(&str, Expr)] = &[
1101        ("PetrovGalerkinProblem", petrov_galerkin_problem_ty()),
1102        (
1103            "BubnovGalerkinConsistency",
1104            bubnov_galerkin_consistency_ty(),
1105        ),
1106        ("GalerkinOrthogonality", galerkin_orthogonality_ty()),
1107        ("HRefinement", h_refinement_ty()),
1108        ("PRefinement", p_refinement_ty()),
1109        ("HPAdaptivity", hp_adaptivity_ty()),
1110        ("ExponentialConvergence", exponential_convergence_ty()),
1111        ("DorflerMarking", dorfler_marking_ty()),
1112        ("NedelecSpace", nedelec_space_ty()),
1113        ("RaviartThomasSpace", raviart_thomas_space_ty()),
1114        ("BDMSpace", bdm_space_ty()),
1115        ("DeRhamComplex", de_rham_complex_ty()),
1116        ("HCurlConforming", hcurl_conforming_ty()),
1117        ("HDivConforming", hdiv_conforming_ty()),
1118        ("ZZEstimator", zz_estimator_ty()),
1119        ("GoalOrientedEstimator", goal_oriented_estimator_ty()),
1120        ("AdaptiveConvergence", adaptive_convergence_ty()),
1121        ("MultigridPreconditioner", multigrid_preconditioner_ty()),
1122        ("DomainDecomposition", domain_decomposition_ty()),
1123        ("FETIPreconditioner", feti_preconditioner_ty()),
1124        ("BDDCPreconditioner", bddc_preconditioner_ty()),
1125        ("MultigridOptimality", multigrid_optimality_ty()),
1126        ("BSplineBasis", bspline_basis_ty()),
1127        ("NURBSSurface", nurbs_surface_ty()),
1128        ("IsogeometricSpace", isogeometric_space_ty()),
1129        ("IsogeometricConvergence", isogeometric_convergence_ty()),
1130        ("TSplineSpace", tspline_space_ty()),
1131        ("SpaceTimeFESpace", space_time_fe_space_ty()),
1132        ("ParabolicStability", parabolic_stability_ty()),
1133        ("WaveEnergyConservation", wave_energy_conservation_ty()),
1134        ("SpaceTimeErrorEstimate", space_time_error_estimate_ty()),
1135        ("NewtonRaphsonFEM", newton_raphson_fem_ty()),
1136        ("ArcLengthMethod", arc_length_method_ty()),
1137        ("ConsistentLinearization", consistent_linearization_ty()),
1138        (
1139            "QuadraticConvergenceNewton",
1140            quadratic_convergence_newton_ty(),
1141        ),
1142        ("ReissnerMindlinPlate", reissner_mindlin_plate_ty()),
1143        ("KirchhoffLovePlate", kirchhoff_love_plate_ty()),
1144        ("ShearLockingFreedom", shear_locking_freedom_ty()),
1145        ("ReducedIntegration", reduced_integration_ty()),
1146        (
1147            "FluidStructureInteraction",
1148            fluid_structure_interaction_ty(),
1149        ),
1150        ("ThermoMechanicalCoupling", thermo_mechanical_coupling_ty()),
1151        ("PoroElasticityBiot", poro_elasticity_biot_ty()),
1152        ("OperatorSplitting", operator_splitting_ty()),
1153    ];
1154    for (name, ty) in axioms {
1155        env.add(Declaration::Axiom {
1156            name: Name::str(*name),
1157            univ_params: vec![],
1158            ty: ty.clone(),
1159        })
1160        .ok();
1161    }
1162}
1163#[cfg(test)]
1164mod tests {
1165    use super::*;
1166    #[test]
1167    fn test_mesh_area() {
1168        let mesh = TriangularMesh2D::unit_square(4);
1169        let area = mesh.total_area();
1170        assert!((area - 1.0).abs() < 1e-12, "Total area = {}", area);
1171    }
1172    #[test]
1173    fn test_mesh_size() {
1174        let mesh = TriangularMesh2D::unit_square(4);
1175        let h = mesh.mesh_size();
1176        let h_expected = std::f64::consts::SQRT_2 / 4.0;
1177        assert!(
1178            (h - h_expected).abs() < 1e-12,
1179            "Mesh size = {}, expected {}",
1180            h,
1181            h_expected
1182        );
1183    }
1184    #[test]
1185    fn test_p1_shape_partition_of_unity() {
1186        let points = [(0.2, 0.3), (0.5, 0.3), (0.1, 0.8), (1.0 / 3.0, 1.0 / 3.0)];
1187        for (xi, eta) in points {
1188            let sum: f64 = (0..3).map(|k| p1_shape(k, xi, eta)).sum();
1189            assert!(
1190                (sum - 1.0).abs() < 1e-12,
1191                "POU failed at ({}, {}): sum = {}",
1192                xi,
1193                eta,
1194                sum
1195            );
1196        }
1197    }
1198    #[test]
1199    fn test_p2_shape_partition_of_unity() {
1200        let points = [(0.2, 0.2), (0.5, 0.1), (0.0, 0.5)];
1201        for (xi, eta) in points {
1202            let sum: f64 = (0..6).map(|k| p2_shape(k, xi, eta)).sum();
1203            assert!(
1204                (sum - 1.0).abs() < 1e-12,
1205                "P2 POU failed at ({}, {}): sum = {}",
1206                xi,
1207                eta,
1208                sum
1209            );
1210        }
1211    }
1212    #[test]
1213    fn test_affine_map_and_jacobian() {
1214        let fmap = AffineTriangleMap::new([0.0, 0.0], [1.0, 0.0], [0.0, 1.0]);
1215        assert!((fmap.det_j() - 1.0).abs() < 1e-12);
1216        let centroid = fmap.apply(1.0 / 3.0, 1.0 / 3.0);
1217        assert!((centroid[0] - 1.0 / 3.0).abs() < 1e-12);
1218        assert!((centroid[1] - 1.0 / 3.0).abs() < 1e-12);
1219    }
1220    #[test]
1221    fn test_stiffness_assembly_diagonal() {
1222        let mesh = TriangularMesh2D::unit_square(2);
1223        let (k, _f) = assemble_p1_stiffness(&mesh);
1224        for i in 0..k.n {
1225            assert!(
1226                k.get(i, i) >= 0.0,
1227                "Diagonal entry [{}] = {} < 0",
1228                i,
1229                k.get(i, i)
1230            );
1231        }
1232    }
1233    #[test]
1234    fn test_dg_advection_conservation() {
1235        let mesh = DGMesh1D::uniform(0.0, 1.0, 10);
1236        let u0: Vec<f64> = mesh
1237            .centers
1238            .iter()
1239            .map(|&x| (2.0 * std::f64::consts::PI * x).sin())
1240            .collect();
1241        let mass0 = mesh.l1_norm(&u0);
1242        let dt = 0.5 * mesh.h;
1243        let u1 = mesh.dg_step(&u0, 1.0, dt);
1244        let mass1 = mesh.l1_norm(&u1);
1245        assert!(
1246            mass1 <= mass0 + 1e-10,
1247            "DG increased mass: {} > {}",
1248            mass1,
1249            mass0
1250        );
1251    }
1252    #[test]
1253    fn test_nitsche_coercivity_threshold() {
1254        let n_data = NitscheData1D::new(10.0, 0.1, 0.0);
1255        assert!(n_data.is_coercive());
1256        let n_data_small = NitscheData1D::new(0.5, 0.1, 0.0);
1257        assert!(!n_data_small.is_coercive());
1258    }
1259    #[test]
1260    fn test_axiom_types_well_formed() {
1261        assert!(matches!(cea_lemma_ty(), Expr::Pi(_, _, _, _)));
1262        assert!(matches!(lax_milgram_discrete_ty(), Expr::Pi(_, _, _, _)));
1263        assert!(matches!(triangulation_ty(), Expr::Pi(_, _, _, _)));
1264        assert!(matches!(
1265            mixed_fem_error_estimate_ty(),
1266            Expr::Pi(_, _, _, _)
1267        ));
1268    }
1269    #[test]
1270    fn test_build_fem_env_extended() {
1271        let mut env = Environment::new();
1272        build_fem_env(&mut env);
1273        build_fem_env_extended(&mut env);
1274        assert!(env
1275            .get(&oxilean_kernel::Name::str("NedelecSpace"))
1276            .is_some());
1277        assert!(env
1278            .get(&oxilean_kernel::Name::str("BDDCPreconditioner"))
1279            .is_some());
1280        assert!(env
1281            .get(&oxilean_kernel::Name::str("IsogeometricSpace"))
1282            .is_some());
1283        assert!(env
1284            .get(&oxilean_kernel::Name::str("ReissnerMindlinPlate"))
1285            .is_some());
1286        assert!(env
1287            .get(&oxilean_kernel::Name::str("FluidStructureInteraction"))
1288            .is_some());
1289    }
1290    #[test]
1291    fn test_galerkin_stiffness_constant() {
1292        let mesh = TriangularMesh2D::unit_square(2);
1293        let gs = GalerkinStiffnessMatrix::constant(1.0, mesh.clone());
1294        let k_gs = gs.assemble();
1295        let (k_p1, _) = assemble_p1_stiffness(&mesh);
1296        assert_eq!(k_gs.n, k_p1.n);
1297        for i in 0..k_gs.n {
1298            assert!(
1299                (k_gs.get(i, i) - k_p1.get(i, i)).abs() < 1e-12,
1300                "Diagonal [{i}] differs: {} vs {}",
1301                k_gs.get(i, i),
1302                k_p1.get(i, i)
1303            );
1304        }
1305    }
1306    #[test]
1307    fn test_galerkin_stiffness_variable() {
1308        let mesh = TriangularMesh2D::unit_square(2);
1309        let gs1 = GalerkinStiffnessMatrix::constant(1.0, mesh.clone());
1310        let gs2 = GalerkinStiffnessMatrix::constant(2.0, mesh.clone());
1311        let k1 = gs1.assemble();
1312        let k2 = gs2.assemble();
1313        for i in 0..k1.n {
1314            assert!(
1315                (k2.get(i, i) - 2.0 * k1.get(i, i)).abs() < 1e-12,
1316                "k=2 diagonal [{i}] should be 2× k=1"
1317            );
1318        }
1319    }
1320    #[test]
1321    fn test_adaptive_refinement_reduces_mesh_size() {
1322        let mesh = TriangularMesh2D::unit_square(2);
1323        let n_before = mesh.num_triangles();
1324        let mut amr = AdaptiveMeshRefinement::new(mesh, 1.0);
1325        let refined = amr.step();
1326        assert!(refined > 0, "should have refined at least one element");
1327        assert!(
1328            amr.mesh.num_triangles() > n_before,
1329            "mesh should grow after refinement"
1330        );
1331    }
1332    #[test]
1333    fn test_adaptive_refinement_area_preserved() {
1334        let mesh = TriangularMesh2D::unit_square(2);
1335        let area_before = mesh.total_area();
1336        let mut amr = AdaptiveMeshRefinement::new(mesh, 1.0);
1337        amr.step();
1338        let area_after = amr.mesh.total_area();
1339        assert!(
1340            (area_after - area_before).abs() < 1e-10,
1341            "area before = {area_before}, after = {area_after}"
1342        );
1343    }
1344    #[test]
1345    fn test_dg_stiffness_coercive() {
1346        let mesh = TriangularMesh2D::unit_square(2);
1347        let dg = DGMethodStiffness::new(mesh, 100.0);
1348        assert!(
1349            dg.is_coercive(),
1350            "DG stiffness with σ=100 should be coercive"
1351        );
1352    }
1353    #[test]
1354    fn test_dg_stiffness_dimension() {
1355        let mesh = TriangularMesh2D::unit_square(2);
1356        let n_tri = mesh.num_triangles();
1357        let dg = DGMethodStiffness::new(mesh, 10.0);
1358        let mat = dg.assemble();
1359        assert_eq!(mat.n, 3 * n_tri, "DG matrix size should be 3 × n_triangles");
1360    }
1361    #[test]
1362    fn test_new_axiom_types_are_pi_or_sort() {
1363        for ty in [
1364            petrov_galerkin_problem_ty(),
1365            nedelec_space_ty(),
1366            multigrid_preconditioner_ty(),
1367            isogeometric_convergence_ty(),
1368            newton_raphson_fem_ty(),
1369            kirchhoff_love_plate_ty(),
1370            fluid_structure_interaction_ty(),
1371        ] {
1372            assert!(
1373                matches!(ty, Expr::Pi(_, _, _, _) | Expr::Sort(_)),
1374                "Expected Pi or Sort, got {:?}",
1375                ty
1376            );
1377        }
1378    }
1379}
1380#[allow(dead_code)]
1381pub fn dist(a: (f64, f64), b: (f64, f64)) -> f64 {
1382    ((b.0 - a.0).powi(2) + (b.1 - a.1).powi(2)).sqrt()
1383}
1384#[cfg(test)]
1385mod tests_fem_extra {
1386    use super::*;
1387    fn unit_triangle_mesh() -> FEMesh2D {
1388        let nodes = vec![(0.0, 0.0), (1.0, 0.0), (0.0, 1.0)];
1389        let elems = vec![(0, 1, 2)];
1390        FEMesh2D::new(nodes, elems)
1391    }
1392    #[test]
1393    fn test_fem_mesh() {
1394        let mesh = unit_triangle_mesh();
1395        assert_eq!(mesh.n_nodes(), 3);
1396        assert_eq!(mesh.n_elements(), 1);
1397        let area = mesh.element_area(0);
1398        assert!((area - 0.5).abs() < 1e-9, "Area should be 0.5, got {area}");
1399        assert!((mesh.total_area() - 0.5).abs() < 1e-9);
1400    }
1401    #[test]
1402    fn test_gauss_quadrature() {
1403        let gl2 = GaussQuadrature::gauss_legendre_2();
1404        assert_eq!(gl2.n_points(), 2);
1405        let integral = gl2.integrate(|x| x * x);
1406        assert!((integral - 2.0 / 3.0).abs() < 1e-6);
1407    }
1408    #[test]
1409    fn test_stiffness_matrix() {
1410        let mut k = StiffnessMatrix::new(3);
1411        k.add_entry(0, 0, 2.0);
1412        k.add_entry(0, 1, -1.0);
1413        k.add_entry(1, 0, -1.0);
1414        k.add_entry(1, 1, 2.0);
1415        k.add_entry(1, 2, -1.0);
1416        k.add_entry(2, 1, -1.0);
1417        k.add_entry(2, 2, 1.0);
1418        assert_eq!(k.n_nonzeros(), 7);
1419        let r = k.apply(&[1.0, 1.0, 1.0]);
1420        assert!((r[0] - 1.0).abs() < 1e-9);
1421    }
1422    #[test]
1423    fn test_poisson_solver() {
1424        let mesh = unit_triangle_mesh();
1425        let mut solver = PoissonFESolver::new(mesh);
1426        solver.add_dirichlet_bc(0, 0.0);
1427        solver.add_dirichlet_bc(1, 0.0);
1428        assert_eq!(solver.n_free_dofs(), 1);
1429        assert!(solver.estimated_condition_number() > 0.0);
1430    }
1431}