Thu, 26 Feb 2026 09:32:12 -0500
Initial working version for known convectivity and diffusivity
/*! Python and C++ wrapper for Dolfinx Function<f64> */ use super::ffi; use super::ffi::Function_f64; use alg_tools::bounds::{Bounds, GlobalAnalysis, MinMaxMapping}; use alg_tools::euclidean::Euclidean; use alg_tools::fe_model::base::LocalModel; use alg_tools::instance::Instance; use alg_tools::loc::Loc; use alg_tools::mapping::{BasicDecomposition, DifferentiableImpl, Mapping, Space}; use alg_tools::norms::{Dist, HasDual, Norm, Normed, L2}; use itertools::izip; use pyo3::ffi::c_str; use pyo3::prelude::*; use pyo3::types::{PyDict, PyFunction}; use std::ffi::CStr; /// A helper structure of dealing with dolfinx functions. /// `N` is the domain dimension, `O` the order, and `D` is the codomain dimension. #[allow(non_camel_case_types)] pub struct DolfinxPyFunction_f64<'py, const N: u32, const O: u32 = 1, const D: u32 = 1> { #[allow(dead_code)] /// Python object. /// /// This is crucial for maintaining [`cxx`] live, when we obtained the object from Python. /// /// If we needed to copy-on-write, or initialise without obtaining and object from Python, /// this will become `None`. pub u: Option<Bound<'py, PyAny>>, /// We need to maintain a reference to the FunctionSpace Python wrapper, as it's difficult /// to recreate if we loose `py`. Fenics is a one-way hell full of pointless wrappers upon /// wrappers upon wrappers,very difficult to create having with just the unwrapped object. function_space: Bound<'py, PyAny>, /// Do we have the only reference? /// /// This is check on construction only, when we have the GIL. /// If we have the only reference at that time, this will not change during the lifetime. /// We use this to implement copy-on-write optimisations. owned: bool, /// C++ object pointer cxx: *mut Function_f64, } struct Helpers { dot: Py<PyFunction>, norm2_squared: Py<PyFunction>, dist2_squared: Py<PyFunction>, } use std::sync::OnceLock; static HELPERS: OnceLock<Helpers> = OnceLock::new(); fn get_helpers(py: Python<'_>) -> PyResult<&Helpers> { HELPERS.get_or_try_init(|| { let extras = PyModule::import(py, "pointsource_pde.dolfinx_extras")?; let g = |s| -> PyResult<Py<PyFunction>> { Ok(extras.getattr(s)?.cast_into()?.unbind()) }; Ok(Helpers { dot: g("dot")?, norm2_squared: g("norm2_squared")?, dist2_squared: g("dist2_squared")?, }) }) } impl<'py, const N: u32, const O: u32, const D: u32> Drop for DolfinxPyFunction_f64<'py, N, O, D> { fn drop(&mut self) { // If Python is not managing our C++ object, we need to free it if let None = self.u { if self.cxx != std::ptr::null_mut() { unsafe { ffi::drop_Function_f64(self.cxx) }; } } } } impl<'py, const N: u32, const O: u32, const D: u32> Clone for DolfinxPyFunction_f64<'py, N, O, D> { fn clone(&self) -> Self { DolfinxPyFunction_f64 { u: None, // It's a different new function, so no Python instance function_space: self.function_space.clone(), owned: self.owned, cxx: unsafe { ffi::clone_Function_f64(self.cxx) }, } } } impl<'py, const N: u32, const O: u32, const D: u32> Space for DolfinxPyFunction_f64<'py, N, O, D> { type Principal = Self; type Decomp = BasicDecomposition; } impl<'py, const N: u32, const O: u32, const D: u32> DolfinxPyFunction_f64<'py, N, O, D> { /// Create a new instance, assuming `cxx` has been correctly extracted from `u` /// and checked to match `N`, `O`, `D`, and mesh restrictions. pub(crate) fn new_prechecked(u: Bound<'py, PyAny>, cxx: *mut Function_f64) -> PyResult<Self> { let function_space = u.getattr("_V")?; let owned = u.get_refcnt() <= 1; // TODO: check dimensionality Ok(DolfinxPyFunction_f64 { u: Some(u), function_space, owned, cxx }) } /// Ensures that `self.u` exists. /// Panics if there's a failure to create it if needed. pub(crate) fn _make_py(&mut self, py: Python<'py>) { if let None = self.u { // We need to do a lot of work here, due to Fenics one-wayness. // This takes ownership of our pointer, so if we fail to create the Python // object, we have a lot of restoring to do. let cpp_object = unsafe { Py::<PyAny>::from_owned_ptr( py, ffi::wrap_Function_f64(self.cxx) as *mut pyo3::ffi::PyObject, ) }; let locals = PyDict::new(py); match locals.set_item("cpp_object", cpp_object).and_then(|()| { locals.set_item("V", self.function_space.as_borrowed())?; // TODO: precompile py.run(CREATE_FUNCTION_HACK, None, Some(&locals))?; locals.get_item("wrapper") }) { Err(e) => { // We should restore to recover panic!("Failure to pass object to Python: {:?}", e); } Ok(maybe_p) => { self.u = Some(maybe_p.unwrap()); } } } } /// Returns a (possibly new) Python presentation, without consuming current object. /// /// Please use this functionality through [`pyo3::types::IntoPyObject`]. pub(crate) fn _into_py_ref<'a>( &'a mut self, py: Python<'py>, ) -> PyResult<pyo3::Borrowed<'a, 'py, PyAny>> { self._make_py(py); self.owned = false; match self.u { Some(ref p) => Ok(p.as_borrowed()), None => unreachable!("This cannot happen"), } } /// Returns a (possibly new) Python presentation, consuming current object. /// /// Please use this functionality through [`pyo3::types::IntoPyObject`]. pub(crate) fn _into_py(mut self, py: Python<'py>) -> PyResult<pyo3::Bound<'py, PyAny>> { self._make_py(py); self.cxx = std::ptr::null_mut(); self.owned = false; match std::mem::replace(&mut self.u, None) { Some(p) => Ok(p), None => unreachable!("This cannot happen"), } } /// Create a new FEM function on the same function space, with unintialised data. pub fn similar(&self) -> Self { DolfinxPyFunction_f64 { u: None, function_space: self.function_space.clone(), owned: true, cxx: unsafe { ffi::similar_Function_f64(self.cxx) }, } } /// Create a new FEM function on the same function space, with zero data. pub fn similar_zeros(&self) -> Self { let new = self.similar(); for x in unsafe { ffi::data_mut_Function_f64(new.cxx) } { *x = 0.0; } new } } pub(crate) static CREATE_FUNCTION_HACK: &CStr = c_str!( "\ from dolfinx.fem import Function from dolfinx.la import Vector x = Vector(cpp_object.x) # Fenics is very one-way. It does not provide a proper way to create the wrapper, # directly from the unwrapped C++ wrapper = Function.__new__(Function, V, x, \"?\", x.array.dtype) # This all just repeats Function.__init__ without creating a new ._cpp_object. super(Function, wrapper).__init__(V.ufl_function_space()) wrapper._cpp_object = cpp_object; wrapper._V = V wrapper._x = x wrapper.name = \"?\" " ); trait ExpandCoordDolphinx { fn to_dolphinx(self) -> [f64; 3]; #[allow(dead_code)] fn from_dolphinx(val: [f64; 3]) -> Self; } impl ExpandCoordDolphinx for Loc<1, f64> { #[inline] fn to_dolphinx(self) -> [f64; 3] { let Loc([x1]) = self; [x1, 0.0, 0.0] } #[inline] fn from_dolphinx([x1, _, _]: [f64; 3]) -> Self { [x1].into() } } impl ExpandCoordDolphinx for Loc<2, f64> { #[inline] fn to_dolphinx(self) -> [f64; 3] { let Loc([x1, x2]) = self; [x1, x2, 0.0] } #[inline] fn from_dolphinx([x1, x2, _]: [f64; 3]) -> Self { [x1, x2].into() } } impl ExpandCoordDolphinx for Loc<3, f64> { #[inline] fn to_dolphinx(self) -> [f64; 3] { let Loc([x1, x2, x3]) = self; [x1, x2, x3] } #[inline] fn from_dolphinx([x1, x2, x3]: [f64; 3]) -> Self { [x1, x2, x3].into() } } macro_rules! impl_diff { ($n:literal ; $($o:literal)+) => { $( impl<'py> DifferentiableImpl<Loc<$n, f64>> for DolfinxPyFunction_f64<'py, $n, $o, 1> { type Derivative = Loc<$n, f64>; fn differential_impl<I: Instance<Loc<$n, f64>>>(&self, x: I) -> Self::Derivative { let x_own = x.own(); let f = self.cxx; let expand_x = x_own.to_dolphinx(); // Find cell containing `x` let cell = unsafe { ffi::cell_Function_f64(f, &expand_x) }; if cell < 0 { Loc::ORIGIN } else { let (model, _) = unsafe { // Find the coordinates of the corresponding triangle. We guarantee a // triangular mesh in the constructor of `DolfinxPyFunction_f64`. let simplex_coords = ffi::cell_coords_Function_f64_triangle(f, cell); // Construct a new P2 model on that cell, as this is easier than dealing // with the awful documentation of Dolphinx and C++, and probably having // to reimplemen our P2 stuff anyway, to do any optimisation with the basix // basis functions. super::model_dolfinx_p2_cell(f, &simplex_coords, cell, false) }; model.differential(&x_own) } } } )+ }; } macro_rules! impl_mapping { ($($n:literal)+) => { $( impl<'py, const O: u32> Mapping<Loc<$n, f64>> for DolfinxPyFunction_f64<'py, $n, O, 1> { type Codomain = f64; fn apply<I: Instance<Loc<$n, f64>>>(&self, x: I) -> f64 { let expand_x = x.own().to_dolphinx(); let f = self.cxx; unsafe { // Find cell containing `x` return ffi::eval_Function_f64(f, &expand_x); } } } //impl_diff!($n; 2 3 4 5 6 7 8); )+ }; } impl_mapping!(1 2 3); // Extension to 1D should be easy. //impl_diff!(1; 2 3 4 5 6 7 8); impl_diff!(2; 2 3 4 5 6 7 8); impl<'py> GlobalAnalysis<f64, Bounds<f64>> for DolfinxPyFunction_f64<'py, 2, 2, 1> { fn global_analysis(&self) -> Bounds<f64> { let min = unsafe { ffi::min_Function_f64_p2(self.cxx) }.map_or(f64::NEG_INFINITY, |res| res.v); let max = unsafe { ffi::max_Function_f64_p2(self.cxx) }.map_or(f64::INFINITY, |res| res.v); Bounds(min, max) } } /// Only second-order polynomials on a triangular mesh are implemented. /// /// May panic if assumptions of the underlying functionn do not hold despite /// verification in the constructors of [`DolfinxPyFunction_f64`]. impl<'py> MinMaxMapping<Loc<2>, f64> for DolfinxPyFunction_f64<'py, 2, 2, 1> { fn minimise(&mut self, _tolerance: f64, _max_steps: usize) -> (Loc<2>, f64) { let res = unsafe { ffi::min_Function_f64_p2(self.cxx) }.unwrap(); (res.x.into(), res.v) } fn maximise(&mut self, _tolerance: f64, _max_steps: usize) -> (Loc<2>, f64) { let res = unsafe { ffi::max_Function_f64_p2(self.cxx) }.unwrap(); (res.x.into(), res.v) } } /* enum MaybeValid<A> { Valid(A), Invalid(A), } use MaybeValid::*; impl<A> MaybeValid<A> { #[inline] fn uninitialised_ok(self) -> A { match self { Valid(a) => a, Invalid(a) => a, } } } */ impl<'py, const N: u32, const O: u32, const D: u32> DolfinxPyFunction_f64<'py, N, O, D> { /// If owned, call `f_valid` with mutable data. /// If not owned, create a new C++ fem.Function, uninitialised, and call `f_invalid` with its /// mutable data, as well as the original immutable data. #[inline] fn with_mut_data_and_orig<F, G, Z>(&mut self, f_valid: F, f_invalid: G) -> Z where F: Fn(&mut [f64]) -> Z, G: Fn(&mut [f64], &[f64]) -> Z, { if self.owned { return f_valid(unsafe { ffi::data_mut_Function_f64(self.cxx) }); } else { // Create a completely new Function let old_data = unsafe { ffi::data_mut_Function_f64(self.cxx) }; let new = unsafe { ffi::similar_Function_f64(self.cxx) }; let new_data = unsafe { ffi::data_mut_Function_f64(new) }; let res = f_invalid(new_data, old_data); // Invalidate Python reference; we're managing the object now self.cxx = new; self.u = None; self.owned = true; return res; } } /// If owned, call `f` with mutable data. /// If not owned, create a new C++ fem.Function, uninitialised, and call `f` with its /// mutable data. #[inline] fn with_mut_data<F, Z>(&mut self, f: F) -> Z where F: Fn(&mut [f64]) -> Z, { if self.owned { return f(unsafe { ffi::data_mut_Function_f64(self.cxx) }); } else { // Create a completely new Function let new = unsafe { ffi::similar_Function_f64(self.cxx) }; let new_data = unsafe { ffi::data_mut_Function_f64(new) }; let res = f(new_data); // Invalidate Python reference; we're managing the object now self.cxx = new; self.u = None; self.owned = true; return res; } } /// Create a similar new function, and call `f` with both the mutable (unininitialised) /// new data, and original data. /// /// Returns object for the new data. #[inline] fn with_new_data_and_orig<F>(&self, f: F) -> Self where F: Fn(&mut [f64], &[f64]), { // Create a completely new Function let new = self.similar(); let old_data = unsafe { ffi::data_Function_f64(self.cxx) }; let new_data = unsafe { ffi::data_mut_Function_f64(new.cxx) }; f(new_data, old_data); new } /// Similar to [`with_mut_data`], but check compatibility with `other` and also pass its /// immutable data slice as the final argument. #[inline] fn with_mut_data_and_orig_binop<F, G, Z>( &mut self, other: &DolfinxPyFunction_f64<'py, N, O, D>, f_valid: F, f_invalid: G, ) -> Z where F: Fn(&mut [f64], &[f64]) -> Z, G: Fn(&mut [f64], &[f64], &[f64]) -> Z, { let data_other = unsafe { ffi::check_compat_Function_f64(self.cxx, other.cxx).unwrap(); ffi::data_Function_f64(other.cxx) }; self.with_mut_data_and_orig( |data_self_valid| { assert_eq!(data_self_valid.len(), data_other.len()); f_valid(data_self_valid, data_other) }, |data_self_invalid, data_orig| { assert_eq!(data_self_invalid.len(), data_other.len()); assert_eq!(data_orig.len(), data_other.len()); f_invalid(data_self_invalid, data_orig, data_other) }, ) } /// Similar to [`with_new_data_and_orig`], but check compatibility with `other` and also /// pass its immutable data slice as the final argument. #[inline] fn with_new_data_and_orig_binop<F>( &self, other: &DolfinxPyFunction_f64<'py, N, O, D>, f: F, ) -> Self where F: Fn(&mut [f64], &[f64], &[f64]), { let data_other = unsafe { ffi::check_compat_Function_f64(self.cxx, other.cxx).unwrap(); ffi::data_Function_f64(other.cxx) }; self.with_new_data_and_orig(|data, data_orig| { assert_eq!(data.len(), data_other.len()); assert_eq!(data_orig.len(), data_other.len()); f(data, data_orig, data_other) }) } } macro_rules! impl_unop { ($trait:ident, $fn:ident) => { impl<'py, const N: u32, const O: u32, const D: u32> $trait for DolfinxPyFunction_f64<'py, N, O, D> { type Output = Self; fn $fn(mut self) -> Self { self.with_mut_data_and_orig( |a| a.iter_mut().for_each(|x| *x = x.$fn()), |a, a_orig| { izip!(a.iter_mut(), a_orig.iter()).for_each(|(x, x_orig)| { *x = x_orig.$fn(); }) }, ); return self; } } impl<'py, 'a, const N: u32, const O: u32, const D: u32> $trait for &'a DolfinxPyFunction_f64<'py, N, O, D> { type Output = DolfinxPyFunction_f64<'py, N, O, D>; fn $fn(self) -> Self::Output { self.with_new_data_and_orig(|a, a_orig| { izip!(a.iter_mut(), a_orig.iter()).for_each(|(x, x_orig)| { *x = x_orig.$fn(); }) }) } } }; } use std::ops::Neg; impl_unop!(Neg, neg); macro_rules! impl_scalarop { ($trait:ident, $fn:ident, $trait_assign:ident, $fn_assign:ident) => { impl<'py, const N: u32, const O: u32, const D: u32> $trait<f64> for DolfinxPyFunction_f64<'py, N, O, D> { type Output = Self; fn $fn(mut self, α: f64) -> Self { self.with_mut_data_and_orig( |a| { a.iter_mut().for_each(|x| { x.$fn_assign(α); }) }, |a, a_orig| { izip!(a.iter_mut(), a_orig.iter()).for_each(|(x, x_orig)| { *x = x_orig.$fn(α); }) }, ); return self; } } impl<'py, 'a, const N: u32, const O: u32, const D: u32> $trait<f64> for &'a DolfinxPyFunction_f64<'py, N, O, D> { type Output = DolfinxPyFunction_f64<'py, N, O, D>; fn $fn(self, α: f64) -> Self::Output { self.with_new_data_and_orig(|a, a_orig| { izip!(a.iter_mut(), a_orig.iter()).for_each(|(x, x_orig)| { *x = x_orig.$fn(α); }) }) } } impl<'py, const N: u32, const O: u32, const D: u32> $trait_assign<f64> for DolfinxPyFunction_f64<'py, N, O, D> { fn $fn_assign(&mut self, α: f64) { self.with_mut_data_and_orig( |a| { a.iter_mut().for_each(|x| { x.$fn_assign(α); }) }, |a, a_orig| { izip!(a.iter_mut(), a_orig.iter()).for_each(|(x, x_orig)| { *x = x_orig.$fn(α); }) }, ); } } }; } use std::ops::{Div, DivAssign, Mul, MulAssign}; impl_scalarop!(Mul, mul, MulAssign, mul_assign); impl_scalarop!(Div, div, DivAssign, div_assign); macro_rules! impl_binop_consume { ($trait:ident, $fn:ident, $trait_assign:ident, $fn_assign:ident) => { impl<'py, const N: u32, const O: u32, const D: u32> $trait<Self> for DolfinxPyFunction_f64<'py, N, O, D> { type Output = Self; #[inline] fn $fn(self, other: Self) -> Self { self.$fn(&other) } } impl<'py, const N: u32, const O: u32, const D: u32> $trait_assign<Self> for DolfinxPyFunction_f64<'py, N, O, D> { #[inline] fn $fn_assign(&mut self, other: Self) { self.$fn_assign(&other) } } impl<'py, 'a, const N: u32, const O: u32, const D: u32> $trait<&'a Self> for DolfinxPyFunction_f64<'py, N, O, D> { type Output = Self; fn $fn(mut self, other: &'a Self) -> Self { self.with_mut_data_and_orig_binop( other, |data, data_other| { izip!(data.iter_mut(), data_other.iter()).for_each(|(x, y)| { x.$fn_assign(y); }) }, |data, data_orig, data_other| { izip!(data.iter_mut(), data_orig.iter(), data_other.iter()).for_each( |(x, x_orig, y)| { *x = x_orig.$fn(y); }, ) }, ); return self; } } impl<'py, 'a, const N: u32, const O: u32, const D: u32> $trait_assign<&'a Self> for DolfinxPyFunction_f64<'py, N, O, D> { fn $fn_assign(&mut self, other: &'a Self) { self.with_mut_data_and_orig_binop( other, |data, data_other| { izip!(data.iter_mut(), data_other.iter()).for_each(|(x, y)| { x.$fn_assign(y); }) }, |data, data_orig, data_other| { izip!(data.iter_mut(), data_orig.iter(), data_other.iter()).for_each( |(x, x_orig, y)| { *x = x_orig.$fn(y); }, ) }, ); } } }; } macro_rules! impl_binop_ref { ($trait:ident, $fn:ident) => { impl<'py, 'b, const N: u32, const O: u32, const D: u32> $trait<Self> for &'b DolfinxPyFunction_f64<'py, N, O, D> { type Output = DolfinxPyFunction_f64<'py, N, O, D>; fn $fn(self, other: Self) -> Self::Output { self.$fn(&other) } } impl<'py, 'a, 'b, const N: u32, const O: u32, const D: u32> $trait<&'a Self> for &'b DolfinxPyFunction_f64<'py, N, O, D> { type Output = DolfinxPyFunction_f64<'py, N, O, D>; fn $fn(self, other: &'a Self) -> Self::Output { self.with_new_data_and_orig_binop(other, |data_dest, data, data_other| { izip!(data_dest.iter_mut(), data.iter(), data_other.iter()).for_each( |(x, y, z)| { *x = y.$fn(z); }, ) }) } } }; } macro_rules! impl_binop { ($trait:ident, $fn:ident, $trait_assign:ident, $fn_assign:ident) => { impl_binop_consume!($trait, $fn, $trait_assign, $fn_assign); impl_binop_ref!($trait, $fn); }; } use std::ops::{Add, AddAssign, Sub, SubAssign}; impl_binop!(Add, add, AddAssign, add_assign); impl_binop!(Sub, sub, SubAssign, sub_assign); //use alg_tools::euclidean::Euclidean; use alg_tools::linops::{VectorSpace, AXPY}; //use alg_tools::norms::{Dist, HasDual, Norm, Normed, L2}; use alg_tools::self_ownable; self_ownable!(DolfinxPyFunction_f64<'py, N, O, D> where 'py, const N: u32, const O: u32, const D: u32); impl<'py, const N: u32, const O: u32, const D: u32> Dist<L2, f64> for DolfinxPyFunction_f64<'py, N, O, D> { fn dist<I: Instance<Self>>(&self, other: I, p: L2) -> f64 { other.eval_ref(|v| (self - v).norm(p)) } } impl<'py, const N: u32, const O: u32, const D: u32> Norm<L2, f64> for DolfinxPyFunction_f64<'py, N, O, D> { fn norm(&self, _p: L2) -> f64 { self.norm2_squared().sqrt() } } impl<'py, const N: u32, const O: u32, const D: u32> VectorSpace for DolfinxPyFunction_f64<'py, N, O, D> { type Field = f64; type PrincipalV = Self; /// Return a similar zero as `self`. fn similar_origin(&self) -> Self { let mut new = self.similar(); new.set_zero(); new } } impl<'py, const N: u32, const O: u32, const D: u32> AXPY for DolfinxPyFunction_f64<'py, N, O, D> { /// Computes `y = βy + αx`, where `y` is `Self`. fn axpy<I: Instance<Self>>(&mut self, α: Self::Field, x: I, β: Self::Field) { x.eval(|r| { self.with_mut_data_and_orig_binop( r, |data, data_other| { izip!(data.iter_mut(), data_other.iter()).for_each(|(y, x)| { *y = β * (*y) + α * (*x); }) }, |data, data_orig, data_other| { izip!(data.iter_mut(), data_orig.iter(), data_other.iter()).for_each( |(y, y_orig, x)| { *y = β * (*y_orig) + α * (*x); }, ) }, ); }) } /// Set self to zero. fn set_zero(&mut self) { self.with_mut_data(|data| { data.iter_mut().for_each(|y| *y = 0.0); }) } } impl<'py, const N: u32, const O: u32, const D: u32> HasDual for DolfinxPyFunction_f64<'py, N, O, D> { type DualSpace = Self; fn dual_origin(&self) -> <Self::DualSpace as VectorSpace>::PrincipalV { self.similar_origin() } } impl<'py, const N: u32, const O: u32, const D: u32> Normed for DolfinxPyFunction_f64<'py, N, O, D> { type NormExp = L2; fn norm_exponent(&self) -> L2 { // TODO: maybe H1 more approriate for O=2 L2 } } impl<'py, const N: u32, const O: u32, const D: u32> Euclidean for DolfinxPyFunction_f64<'py, N, O, D> { type PrincipalE = Self; fn dot<I: Instance<Self>>(&self, other: I) -> f64 { let py = self.function_space.py(); get_helpers(py) .unwrap() .dot .call1(py, (self.own(), other.own())) .unwrap() .extract(py) .unwrap() /* let data = unsafe { ffi::data_mut_Function_f64(self.cxx) }; other.eval_decompose(|x| { let data_other = unsafe { ffi::data_mut_Function_f64(x.cxx) }; izip!(data.iter(), data_other.iter()) .map(|(y, x)| y * x) .sum() })*/ } fn norm2_squared(&self) -> f64 { let py = self.function_space.py(); get_helpers(py) .unwrap() .norm2_squared .call1(py, (self.own(),)) .unwrap() .extract(py) .unwrap() } fn dist2_squared<I: Instance<Self>>(&self, other: I) -> f64 { //other.eval_ref(|v| self.norm2_squared() + 2.0 * self.dot(v) + v.norm2_squared_div2()) let py = self.function_space.py(); get_helpers(py) .unwrap() .dist2_squared .call1(py, (self.own(), other.own())) .unwrap() .extract(py) .unwrap() } }