use crate::numerical::BVP_Damp::BVP_utils::CustomTimer;
use crate::numerical::BVP_sci::BVP_sci_faer::{
BCFunction, BCJacobian, BVPResult, ODEFunction, ODEJacobian, faer_col, faer_dense_mat,
faer_mat, solve_bvp,
};
use faer::sparse::Triplet;
use std::sync::Arc;
type NumericalRhsClosure = dyn Fn(f64, &[f64], &[f64], &mut [f64]) + Send + Sync;
type NumericalBoundaryResidualClosure = dyn Fn(&[f64], &[f64], &[f64], &mut [f64]) + Send + Sync;
type NumericalRhsJacobianClosure = dyn Fn(f64, &[f64], &[f64]) -> Option<faer_mat> + Send + Sync;
type NumericalBoundaryJacobianClosure =
dyn Fn(&[f64], &[f64], &[f64]) -> Option<(faer_mat, faer_mat, Option<faer_mat>)> + Send + Sync;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum NumericalJacobianMode {
FiniteDifference,
AnalyticalPointwise,
}
#[derive(Clone)]
pub struct NumericalBvpClosureProblem {
dimension: usize,
parameter_dimension: usize,
rhs: Arc<NumericalRhsClosure>,
boundary_residual: Arc<NumericalBoundaryResidualClosure>,
rhs_jacobian: Option<Arc<NumericalRhsJacobianClosure>>,
rhs_param_jacobian: Option<Arc<NumericalRhsJacobianClosure>>,
boundary_jacobian: Option<Arc<NumericalBoundaryJacobianClosure>>,
jacobian_mode: NumericalJacobianMode,
}
pub type BvpSciNumericalProblem = NumericalBvpClosureProblem;
impl NumericalBvpClosureProblem {
pub fn new_fd<Rhs, Bc>(
dimension: usize,
parameter_dimension: usize,
rhs: Rhs,
boundary_residual: Bc,
) -> Self
where
Rhs: Fn(f64, &[f64], &[f64], &mut [f64]) + Send + Sync + 'static,
Bc: Fn(&[f64], &[f64], &[f64], &mut [f64]) + Send + Sync + 'static,
{
Self {
dimension,
parameter_dimension,
rhs: Arc::new(rhs),
boundary_residual: Arc::new(boundary_residual),
rhs_jacobian: None,
rhs_param_jacobian: None,
boundary_jacobian: None,
jacobian_mode: NumericalJacobianMode::FiniteDifference,
}
}
pub fn new_with_jacobian<Rhs, Bc, RhsJ, BcJ>(
dimension: usize,
parameter_dimension: usize,
rhs: Rhs,
boundary_residual: Bc,
rhs_jacobian: RhsJ,
boundary_jacobian: BcJ,
) -> Self
where
Rhs: Fn(f64, &[f64], &[f64], &mut [f64]) + Send + Sync + 'static,
Bc: Fn(&[f64], &[f64], &[f64], &mut [f64]) + Send + Sync + 'static,
RhsJ: Fn(f64, &[f64], &[f64]) -> Option<faer_mat> + Send + Sync + 'static,
BcJ: Fn(&[f64], &[f64], &[f64]) -> Option<(faer_mat, faer_mat, Option<faer_mat>)>
+ Send
+ Sync
+ 'static,
{
Self {
dimension,
parameter_dimension,
rhs: Arc::new(rhs),
boundary_residual: Arc::new(boundary_residual),
rhs_jacobian: Some(Arc::new(rhs_jacobian)),
rhs_param_jacobian: None,
boundary_jacobian: Some(Arc::new(boundary_jacobian)),
jacobian_mode: NumericalJacobianMode::AnalyticalPointwise,
}
}
pub fn with_rhs_param_jacobian<RhsP>(mut self, rhs_param_jacobian: RhsP) -> Self
where
RhsP: Fn(f64, &[f64], &[f64]) -> Option<faer_mat> + Send + Sync + 'static,
{
self.rhs_param_jacobian = Some(Arc::new(rhs_param_jacobian));
self
}
}
impl NumericalBvpProblem for NumericalBvpClosureProblem {
fn dimension(&self) -> usize {
self.dimension
}
fn parameter_dimension(&self) -> usize {
self.parameter_dimension
}
fn rhs(&self, x: f64, y: &[f64], p: &[f64], out: &mut [f64]) {
(self.rhs)(x, y, p, out)
}
fn boundary_residual(&self, ya: &[f64], yb: &[f64], p: &[f64], out: &mut [f64]) {
(self.boundary_residual)(ya, yb, p, out)
}
fn jacobian_mode(&self) -> NumericalJacobianMode {
self.jacobian_mode
}
fn rhs_jacobian(&self, x: f64, y: &[f64], p: &[f64]) -> Option<faer_mat> {
self.rhs_jacobian
.as_ref()
.and_then(|callback| callback(x, y, p))
}
fn rhs_param_jacobian(&self, x: f64, y: &[f64], p: &[f64]) -> Option<faer_mat> {
self.rhs_param_jacobian
.as_ref()
.and_then(|callback| callback(x, y, p))
}
fn boundary_jacobian(
&self,
ya: &[f64],
yb: &[f64],
p: &[f64],
) -> Option<(faer_mat, faer_mat, Option<faer_mat>)> {
self.boundary_jacobian
.as_ref()
.and_then(|callback| callback(ya, yb, p))
}
}
pub trait NumericalBvpProblem {
fn dimension(&self) -> usize;
fn parameter_dimension(&self) -> usize {
0
}
fn rhs(&self, x: f64, y: &[f64], p: &[f64], out: &mut [f64]);
fn boundary_residual(&self, ya: &[f64], yb: &[f64], p: &[f64], out: &mut [f64]);
fn jacobian_mode(&self) -> NumericalJacobianMode {
NumericalJacobianMode::FiniteDifference
}
fn rhs_jacobian(&self, _x: f64, _y: &[f64], _p: &[f64]) -> Option<faer_mat> {
None
}
fn rhs_param_jacobian(&self, _x: f64, _y: &[f64], _p: &[f64]) -> Option<faer_mat> {
None
}
fn boundary_jacobian(
&self,
_ya: &[f64],
_yb: &[f64],
_p: &[f64],
) -> Option<(faer_mat, faer_mat, Option<faer_mat>)> {
None
}
}
#[derive(Clone)]
pub struct NumericalBvpSolveOptions {
pub mesh: faer_col,
pub initial_guess: faer_dense_mat,
pub parameters: Option<faer_col>,
pub singular_term: Option<faer_dense_mat>,
pub tolerance: f64,
pub max_nodes: usize,
pub verbose: u8,
pub bc_tol: Option<f64>,
pub custom_timer: Option<CustomTimer>,
}
pub type BvpSciNumericalOptions = NumericalBvpSolveOptions;
impl NumericalBvpSolveOptions {
pub fn new(
mesh: faer_col,
initial_guess: faer_dense_mat,
tolerance: f64,
max_nodes: usize,
) -> Self {
Self {
mesh,
initial_guess,
parameters: None,
singular_term: None,
tolerance,
max_nodes,
verbose: 0,
bc_tol: None,
custom_timer: None,
}
}
pub fn with_parameters(mut self, parameters: Option<faer_col>) -> Self {
self.parameters = parameters;
self
}
pub fn with_singular_term(mut self, singular_term: Option<faer_dense_mat>) -> Self {
self.singular_term = singular_term;
self
}
pub fn with_tolerance(mut self, tolerance: f64) -> Self {
self.tolerance = tolerance;
self
}
pub fn with_max_nodes(mut self, max_nodes: usize) -> Self {
self.max_nodes = max_nodes;
self
}
pub fn with_mesh_refinement(mut self, tolerance: f64, max_nodes: usize) -> Self {
self.tolerance = tolerance;
self.max_nodes = max_nodes;
self
}
pub fn with_verbose(mut self, verbose: u8) -> Self {
self.verbose = verbose;
self
}
pub fn with_bc_tol(mut self, bc_tol: Option<f64>) -> Self {
self.bc_tol = bc_tol;
self
}
pub fn with_custom_timer(mut self, custom_timer: Option<CustomTimer>) -> Self {
self.custom_timer = custom_timer;
self
}
}
pub fn solve_numerical_bvp<P: NumericalBvpProblem + 'static>(
problem: P,
options: NumericalBvpSolveOptions,
) -> Result<BVPResult, String> {
let problem = Arc::new(problem);
let n = problem.dimension();
let k = problem.parameter_dimension();
let jacobian_mode = problem.jacobian_mode();
if options.initial_guess.nrows() != n {
return Err(format!(
"initial guess row count {} does not match problem dimension {}",
options.initial_guess.nrows(),
n
));
}
if options.initial_guess.ncols() != options.mesh.nrows() {
return Err(format!(
"initial guess column count {} does not match mesh size {}",
options.initial_guess.ncols(),
options.mesh.nrows()
));
}
if let Some(parameters) = options.parameters.as_ref() {
if parameters.nrows() != k {
return Err(format!(
"parameter vector size {} does not match problem parameter dimension {}",
parameters.nrows(),
k
));
}
} else if k > 0 {
return Err(format!(
"problem expects {} parameters but no initial parameter vector was provided",
k
));
}
if matches!(jacobian_mode, NumericalJacobianMode::AnalyticalPointwise) {
let probe_y = vec![0.0; n];
let probe_p = vec![0.0; k];
if problem
.rhs_jacobian(options.mesh[0], &probe_y, &probe_p)
.is_none()
{
return Err(
"NumericalJacobianMode::AnalyticalPointwise requires rhs_jacobian()".to_string(),
);
}
if problem
.boundary_jacobian(&probe_y, &probe_y, &probe_p)
.is_none()
{
return Err(
"NumericalJacobianMode::AnalyticalPointwise requires boundary_jacobian()"
.to_string(),
);
}
}
let problem_for_fun = Arc::clone(&problem);
let fun: Box<ODEFunction> = Box::new(
move |x: &faer_col, y: &faer_dense_mat, p: &faer_col| -> faer_dense_mat {
let point_count = x.nrows();
let mut result = faer_dense_mat::zeros(n, point_count);
let p_values = p.iter().copied().collect::<Vec<_>>();
let mut y_point = vec![0.0; n];
let mut rhs_out = vec![0.0; n];
for col in 0..point_count {
for row in 0..n {
y_point[row] = *y.get(row, col);
}
problem_for_fun.rhs(x[col], &y_point, &p_values, &mut rhs_out);
for row in 0..n {
*result.get_mut(row, col) = rhs_out[row];
}
}
result
},
);
let problem_for_bc = Arc::clone(&problem);
let bc: Box<BCFunction> = Box::new(
move |ya: &faer_col, yb: &faer_col, p: &faer_col| -> faer_col {
let p_values = p.iter().copied().collect::<Vec<_>>();
let ya_values = ya.iter().copied().collect::<Vec<_>>();
let yb_values = yb.iter().copied().collect::<Vec<_>>();
let mut residual = vec![0.0; n + k];
problem_for_bc.boundary_residual(&ya_values, &yb_values, &p_values, &mut residual);
faer_col::from_fn(n + k, |i| residual[i])
},
);
let problem_for_fun_jac = Arc::clone(&problem);
let fun_jacobian: Box<ODEJacobian> =
Box::new(
move |x: &faer_col,
y: &faer_dense_mat,
p: &faer_col|
-> (Vec<faer_mat>, Option<Vec<faer_mat>>) {
let point_count = x.nrows();
let p_values = p.iter().copied().collect::<Vec<_>>();
let mut y_point = vec![0.0; n];
let mut jacobians = Vec::with_capacity(point_count);
let mut param_jacobians = if k > 0 {
Some(Vec::with_capacity(point_count))
} else {
None
};
for col in 0..point_count {
for row in 0..n {
y_point[row] = *y.get(row, col);
}
let jacobian = problem_for_fun_jac
.rhs_jacobian(x[col], &y_point, &p_values)
.expect("AnalyticalPointwise mode requires rhs_jacobian() to return Some(matrix)");
jacobians.push(jacobian);
if let Some(param_store) = param_jacobians.as_mut() {
let param_jacobian =
problem_for_fun_jac.rhs_param_jacobian(x[col], &y_point, &p_values);
match param_jacobian {
Some(matrix) => param_store.push(matrix),
None => {
let empty_triplets: Vec<Triplet<usize, usize, f64>> = Vec::new();
param_store.push(
faer_mat::try_new_from_triplets(n, k, &empty_triplets).expect(
"empty sparse parameter Jacobian should be constructible",
),
);
}
}
}
}
(jacobians, param_jacobians)
},
);
let problem_for_bc_jac = Arc::clone(&problem);
let bc_jacobian: Box<BCJacobian> = Box::new(
move |ya: &faer_col,
yb: &faer_col,
p: &faer_col|
-> (faer_mat, faer_mat, Option<faer_mat>) {
let p_values = p.iter().copied().collect::<Vec<_>>();
let ya_values = ya.iter().copied().collect::<Vec<_>>();
let yb_values = yb.iter().copied().collect::<Vec<_>>();
problem_for_bc_jac
.boundary_jacobian(&ya_values, &yb_values, &p_values)
.expect(
"AnalyticalPointwise mode requires boundary_jacobian() to return Some(matrices)",
)
},
);
let fun_jacobian_ref = if matches!(jacobian_mode, NumericalJacobianMode::AnalyticalPointwise) {
Some(fun_jacobian.as_ref())
} else {
None
};
let bc_jacobian_ref = if matches!(jacobian_mode, NumericalJacobianMode::AnalyticalPointwise) {
Some(bc_jacobian.as_ref())
} else {
None
};
solve_bvp(
fun.as_ref(),
bc.as_ref(),
options.mesh,
options.initial_guess,
options.parameters,
options.singular_term,
fun_jacobian_ref,
bc_jacobian_ref,
options.tolerance,
options.max_nodes,
options.verbose,
options.bc_tol,
options.custom_timer,
)
}
#[derive(Clone)]
struct ForcedJacobianModeProblem<P> {
inner: P,
mode: NumericalJacobianMode,
}
impl<P: NumericalBvpProblem> NumericalBvpProblem for ForcedJacobianModeProblem<P> {
fn dimension(&self) -> usize {
self.inner.dimension()
}
fn parameter_dimension(&self) -> usize {
self.inner.parameter_dimension()
}
fn rhs(&self, x: f64, y: &[f64], p: &[f64], out: &mut [f64]) {
self.inner.rhs(x, y, p, out)
}
fn boundary_residual(&self, ya: &[f64], yb: &[f64], p: &[f64], out: &mut [f64]) {
self.inner.boundary_residual(ya, yb, p, out)
}
fn jacobian_mode(&self) -> NumericalJacobianMode {
self.mode
}
fn rhs_jacobian(&self, x: f64, y: &[f64], p: &[f64]) -> Option<faer_mat> {
self.inner.rhs_jacobian(x, y, p)
}
fn rhs_param_jacobian(&self, x: f64, y: &[f64], p: &[f64]) -> Option<faer_mat> {
self.inner.rhs_param_jacobian(x, y, p)
}
fn boundary_jacobian(
&self,
ya: &[f64],
yb: &[f64],
p: &[f64],
) -> Option<(faer_mat, faer_mat, Option<faer_mat>)> {
self.inner.boundary_jacobian(ya, yb, p)
}
}
pub fn solve_numerical_bvp_fd<P: NumericalBvpProblem + 'static>(
problem: P,
options: NumericalBvpSolveOptions,
) -> Result<BVPResult, String> {
solve_numerical_bvp(
ForcedJacobianModeProblem {
inner: problem,
mode: NumericalJacobianMode::FiniteDifference,
},
options,
)
}
pub fn solve_numerical_bvp_with_jacobian<P: NumericalBvpProblem + 'static>(
problem: P,
options: NumericalBvpSolveOptions,
) -> Result<BVPResult, String> {
solve_numerical_bvp(
ForcedJacobianModeProblem {
inner: problem,
mode: NumericalJacobianMode::AnalyticalPointwise,
},
options,
)
}