use super::*;
pub(crate) struct LocationScaleJointPsiDirection {
pub(crate) block_idx: usize,
pub(crate) local_idx: usize,
pub(crate) x_primary_psi: PsiDesignMap,
pub(crate) x_ls_psi: PsiDesignMap,
pub(crate) z_primary_psi: Array1<f64>,
pub(crate) z_ls_psi: Array1<f64>,
}
pub(crate) struct LocationScaleJointPsiSecondDrifts {
pub(crate) x_primary_ab_action: Option<CustomFamilyPsiSecondDesignAction>,
pub(crate) x_ls_ab_action: Option<CustomFamilyPsiSecondDesignAction>,
pub(crate) x_primary_ab: Option<Array2<f64>>,
pub(crate) x_ls_ab: Option<Array2<f64>>,
pub(crate) z_primary_ab: Array1<f64>,
pub(crate) z_ls_ab: Array1<f64>,
}
pub(crate) trait LocationScaleJointPsiFamily: Clone + Send + Sync + 'static {
type Direction: Send + Sync + 'static;
const LABEL: &'static str;
fn ws_policy(&self) -> &gam_runtime::resource::ResourcePolicy;
fn ws_exact_joint_dense_block_designs<'a>(
&'a self,
specs: Option<&'a [ParameterBlockSpec]>,
) -> Result<Option<(Cow<'a, Array2<f64>>, Cow<'a, Array2<f64>>)>, String>;
fn ws_psi_direction(
&self,
block_states: &[ParameterBlockState],
derivative_blocks: &[Vec<crate::custom_family::CustomFamilyBlockPsiDerivative>],
psi_index: usize,
design_loc: &Array2<f64>,
design_scale: &Array2<f64>,
policy: &gam_runtime::resource::ResourcePolicy,
) -> Result<Option<Self::Direction>, String>;
fn ws_psi_second_order_terms_from_parts(
&self,
block_states: &[ParameterBlockState],
derivative_blocks: &[Vec<crate::custom_family::CustomFamilyBlockPsiDerivative>],
psi_a: &Self::Direction,
psi_b: &Self::Direction,
design_loc: &Array2<f64>,
design_scale: &Array2<f64>,
subsample: Option<&[crate::outer_subsample::WeightedOuterRow]>,
) -> Result<ExactNewtonJointPsiSecondOrderTerms, String>;
fn ws_psi_hessian_directional_from_parts(
&self,
block_states: &[ParameterBlockState],
psi_dir: &Self::Direction,
d_beta_flat: &Array1<f64>,
design_loc: &Array2<f64>,
design_scale: &Array2<f64>,
subsample: Option<&[crate::outer_subsample::WeightedOuterRow]>,
) -> Result<Array2<f64>, String>;
}
impl LocationScaleJointPsiFamily for GaussianLocationScaleFamily {
type Direction = LocationScaleJointPsiDirection;
const LABEL: &'static str = "GaussianLocationScaleFamily";
fn ws_policy(&self) -> &gam_runtime::resource::ResourcePolicy {
&self.policy
}
fn ws_exact_joint_dense_block_designs<'a>(
&'a self,
specs: Option<&'a [ParameterBlockSpec]>,
) -> Result<Option<(Cow<'a, Array2<f64>>, Cow<'a, Array2<f64>>)>, String> {
self.exact_joint_dense_block_designs(specs)
}
fn ws_psi_direction(
&self,
block_states: &[ParameterBlockState],
derivative_blocks: &[Vec<crate::custom_family::CustomFamilyBlockPsiDerivative>],
psi_index: usize,
design_loc: &Array2<f64>,
design_scale: &Array2<f64>,
policy: &gam_runtime::resource::ResourcePolicy,
) -> Result<Option<LocationScaleJointPsiDirection>, String> {
self.exact_newton_joint_psi_direction(
block_states,
derivative_blocks,
psi_index,
design_loc,
design_scale,
policy,
)
}
fn ws_psi_second_order_terms_from_parts(
&self,
block_states: &[ParameterBlockState],
derivative_blocks: &[Vec<crate::custom_family::CustomFamilyBlockPsiDerivative>],
psi_a: &LocationScaleJointPsiDirection,
psi_b: &LocationScaleJointPsiDirection,
design_loc: &Array2<f64>,
design_scale: &Array2<f64>,
subsample: Option<&[crate::outer_subsample::WeightedOuterRow]>,
) -> Result<ExactNewtonJointPsiSecondOrderTerms, String> {
self.exact_newton_joint_psisecond_order_terms_from_parts(
block_states,
derivative_blocks,
psi_a,
psi_b,
design_loc,
design_scale,
subsample,
)
}
fn ws_psi_hessian_directional_from_parts(
&self,
block_states: &[ParameterBlockState],
psi_dir: &LocationScaleJointPsiDirection,
d_beta_flat: &Array1<f64>,
design_loc: &Array2<f64>,
design_scale: &Array2<f64>,
subsample: Option<&[crate::outer_subsample::WeightedOuterRow]>,
) -> Result<Array2<f64>, String> {
self.exact_newton_joint_psihessian_directional_derivative_from_parts(
block_states,
psi_dir,
d_beta_flat,
design_loc,
design_scale,
subsample,
)
}
}
impl LocationScaleJointPsiFamily for GaussianLocationScaleWiggleFamily {
type Direction = LocationScaleJointPsiDirection;
const LABEL: &'static str = "GaussianLocationScaleWiggleFamily";
fn ws_policy(&self) -> &gam_runtime::resource::ResourcePolicy {
&self.policy
}
fn ws_exact_joint_dense_block_designs<'a>(
&'a self,
specs: Option<&'a [ParameterBlockSpec]>,
) -> Result<Option<(Cow<'a, Array2<f64>>, Cow<'a, Array2<f64>>)>, String> {
self.exact_joint_dense_block_designs(specs)
}
fn ws_psi_direction(
&self,
block_states: &[ParameterBlockState],
derivative_blocks: &[Vec<crate::custom_family::CustomFamilyBlockPsiDerivative>],
psi_index: usize,
design_loc: &Array2<f64>,
design_scale: &Array2<f64>,
policy: &gam_runtime::resource::ResourcePolicy,
) -> Result<Option<LocationScaleJointPsiDirection>, String> {
self.exact_newton_joint_psi_direction(
block_states,
derivative_blocks,
psi_index,
design_loc,
design_scale,
policy,
)
}
fn ws_psi_second_order_terms_from_parts(
&self,
block_states: &[ParameterBlockState],
derivative_blocks: &[Vec<crate::custom_family::CustomFamilyBlockPsiDerivative>],
psi_a: &LocationScaleJointPsiDirection,
psi_b: &LocationScaleJointPsiDirection,
design_loc: &Array2<f64>,
design_scale: &Array2<f64>,
_: Option<&[crate::outer_subsample::WeightedOuterRow]>,
) -> Result<ExactNewtonJointPsiSecondOrderTerms, String> {
self.exact_newton_joint_psisecond_order_terms_from_parts(
block_states,
derivative_blocks,
psi_a,
psi_b,
design_loc,
design_scale,
)
}
fn ws_psi_hessian_directional_from_parts(
&self,
block_states: &[ParameterBlockState],
psi_dir: &LocationScaleJointPsiDirection,
d_beta_flat: &Array1<f64>,
design_loc: &Array2<f64>,
design_scale: &Array2<f64>,
_: Option<&[crate::outer_subsample::WeightedOuterRow]>,
) -> Result<Array2<f64>, String> {
self.exact_newton_joint_psihessian_directional_derivative_from_parts(
block_states,
psi_dir,
d_beta_flat,
design_loc,
design_scale,
)
}
}
pub(crate) struct LocationScaleJointPsiWorkspace<F: LocationScaleJointPsiFamily> {
pub(crate) family: F,
pub(crate) block_states: Vec<ParameterBlockState>,
pub(crate) derivative_blocks: Vec<Vec<CustomFamilyBlockPsiDerivative>>,
pub(crate) design_loc: Arc<Array2<f64>>,
pub(crate) design_scale: Arc<Array2<f64>>,
pub(crate) psi_directions: ExactNewtonJointPsiDirectCache<F::Direction>,
pub(crate) outer_score_subsample: Option<Arc<crate::outer_subsample::OuterScoreSubsample>>,
}
impl<F: LocationScaleJointPsiFamily> LocationScaleJointPsiWorkspace<F> {
pub(crate) fn new(
family: F,
block_states: Vec<ParameterBlockState>,
specs: &[ParameterBlockSpec],
derivative_blocks: Vec<Vec<CustomFamilyBlockPsiDerivative>>,
) -> Result<Self, String> {
Self::new_with_subsample(family, block_states, specs, derivative_blocks, None)
}
pub(crate) fn new_with_subsample(
family: F,
block_states: Vec<ParameterBlockState>,
specs: &[ParameterBlockSpec],
derivative_blocks: Vec<Vec<CustomFamilyBlockPsiDerivative>>,
outer_score_subsample: Option<Arc<crate::outer_subsample::OuterScoreSubsample>>,
) -> Result<Self, String> {
let Some((design_loc, design_scale)) =
family.ws_exact_joint_dense_block_designs(Some(specs))?
else {
return Err(GamlssError::UnsupportedConfiguration {
reason: format!(
"{} exact joint psi workspace requires dense block designs",
F::LABEL,
),
}
.into());
};
let design_loc = shared_dense_arc(design_loc.as_ref());
let design_scale = shared_dense_arc(design_scale.as_ref());
let psi_dim = derivative_blocks.iter().map(Vec::len).sum();
Ok(Self {
family,
block_states,
derivative_blocks,
design_loc,
design_scale,
psi_directions: ExactNewtonJointPsiDirectCache::new(psi_dim),
outer_score_subsample,
})
}
pub(crate) fn psi_direction(
&self,
psi_index: usize,
) -> Result<Option<Arc<F::Direction>>, String> {
self.psi_directions.get_or_try_init(psi_index, || {
self.family.ws_psi_direction(
&self.block_states,
&self.derivative_blocks,
psi_index,
self.design_loc.as_ref(),
self.design_scale.as_ref(),
self.family.ws_policy(),
)
})
}
pub(crate) fn subsample_rows(&self) -> Option<&[crate::outer_subsample::WeightedOuterRow]> {
self.outer_score_subsample
.as_ref()
.map(|s| s.rows.as_ref().as_slice())
}
}
impl<F> ExactNewtonJointPsiWorkspace for LocationScaleJointPsiWorkspace<F>
where
F: LocationScaleJointPsiFamily,
{
fn second_order_terms(
&self,
psi_i: usize,
psi_j: usize,
) -> Result<Option<ExactNewtonJointPsiSecondOrderTerms>, String> {
let Some(dir_i) = self.psi_direction(psi_i)? else {
return Ok(None);
};
let Some(dir_j) = self.psi_direction(psi_j)? else {
return Ok(None);
};
Ok(Some(self.family.ws_psi_second_order_terms_from_parts(
&self.block_states,
&self.derivative_blocks,
dir_i.as_ref(),
dir_j.as_ref(),
self.design_loc.as_ref(),
self.design_scale.as_ref(),
self.subsample_rows(),
)?))
}
fn hessian_directional_derivative(
&self,
psi_index: usize,
d_beta_flat: &Array1<f64>,
) -> Result<Option<gam_problem::DriftDerivResult>, String> {
let Some(dir) = self.psi_direction(psi_index)? else {
return Ok(None);
};
Ok(Some(gam_problem::DriftDerivResult::Dense(
self.family.ws_psi_hessian_directional_from_parts(
&self.block_states,
dir.as_ref(),
d_beta_flat,
self.design_loc.as_ref(),
self.design_scale.as_ref(),
self.subsample_rows(),
)?,
)))
}
}
pub(crate) type GaussianLocationScaleExactNewtonJointPsiWorkspace =
LocationScaleJointPsiWorkspace<GaussianLocationScaleFamily>;
pub(crate) type GaussianLocationScaleWiggleExactNewtonJointPsiWorkspace =
LocationScaleJointPsiWorkspace<GaussianLocationScaleWiggleFamily>;
#[derive(Clone)]
pub struct GaussianJointRowScalars {
pub(crate) obs_weight: Array1<f64>,
pub(crate) w: Array1<f64>,
pub(crate) m: Array1<f64>,
pub(crate) n: Array1<f64>,
pub(crate) kappa: Array1<f64>,
pub(crate) kappa_prime: Array1<f64>,
pub(crate) kappa_dprime: Array1<f64>,
}
pub(crate) struct GaussianJointPsiFirstWeights {
pub(crate) objective_psirow: Array1<f64>,
pub(crate) scoremu: Array1<f64>,
pub(crate) score_ls: Array1<f64>,
pub(crate) dscoremu: Array1<f64>,
pub(crate) dscore_ls: Array1<f64>,
pub(crate) hmumu: Array1<f64>,
pub(crate) hmu_ls: Array1<f64>,
pub(crate) h_ls_ls: Array1<f64>,
pub(crate) dhmumu: Array1<f64>,
pub(crate) dhmu_ls: Array1<f64>,
pub(crate) dh_ls_ls: Array1<f64>,
}
pub(crate) struct GaussianJointPsiSecondWeights {
pub(crate) objective_psi_psirow: Array1<f64>,
pub(crate) d2scoremu: Array1<f64>,
pub(crate) d2score_ls: Array1<f64>,
pub(crate) d2hmumu: Array1<f64>,
pub(crate) d2hmu_ls: Array1<f64>,
pub(crate) d2h_ls_ls: Array1<f64>,
}
pub(crate) struct GaussianJointPsiMixedDriftWeights {
pub(crate) dhmumu_u: Array1<f64>,
pub(crate) dhmu_ls_u: Array1<f64>,
pub(crate) dh_ls_ls_u: Array1<f64>,
pub(crate) d2hmumu: Array1<f64>,
pub(crate) d2hmu_ls: Array1<f64>,
pub(crate) d2h_ls_ls: Array1<f64>,
}
pub(crate) fn apply_ht_mask_first(
weights: &mut GaussianJointPsiFirstWeights,
rows: &[crate::outer_subsample::WeightedOuterRow],
) {
let n = weights.objective_psirow.len();
let mut obj = Array1::<f64>::zeros(n);
let mut smu = Array1::<f64>::zeros(n);
let mut sls = Array1::<f64>::zeros(n);
let mut dsmu = Array1::<f64>::zeros(n);
let mut dsls = Array1::<f64>::zeros(n);
let mut hmm = Array1::<f64>::zeros(n);
let mut hml = Array1::<f64>::zeros(n);
let mut hll = Array1::<f64>::zeros(n);
let mut dhmm = Array1::<f64>::zeros(n);
let mut dhml = Array1::<f64>::zeros(n);
let mut dhll = Array1::<f64>::zeros(n);
for r in rows {
let i = r.index;
let w = r.weight;
obj[i] = weights.objective_psirow[i] * w;
smu[i] = weights.scoremu[i] * w;
sls[i] = weights.score_ls[i] * w;
dsmu[i] = weights.dscoremu[i] * w;
dsls[i] = weights.dscore_ls[i] * w;
hmm[i] = weights.hmumu[i] * w;
hml[i] = weights.hmu_ls[i] * w;
hll[i] = weights.h_ls_ls[i] * w;
dhmm[i] = weights.dhmumu[i] * w;
dhml[i] = weights.dhmu_ls[i] * w;
dhll[i] = weights.dh_ls_ls[i] * w;
}
weights.objective_psirow = obj;
weights.scoremu = smu;
weights.score_ls = sls;
weights.dscoremu = dsmu;
weights.dscore_ls = dsls;
weights.hmumu = hmm;
weights.hmu_ls = hml;
weights.h_ls_ls = hll;
weights.dhmumu = dhmm;
weights.dhmu_ls = dhml;
weights.dh_ls_ls = dhll;
}
pub(crate) fn apply_ht_mask_second(
weights: &mut GaussianJointPsiSecondWeights,
rows: &[crate::outer_subsample::WeightedOuterRow],
) {
let n = weights.objective_psi_psirow.len();
let mut obj = Array1::<f64>::zeros(n);
let mut d2smu = Array1::<f64>::zeros(n);
let mut d2sls = Array1::<f64>::zeros(n);
let mut d2hmm = Array1::<f64>::zeros(n);
let mut d2hml = Array1::<f64>::zeros(n);
let mut d2hll = Array1::<f64>::zeros(n);
for r in rows {
let i = r.index;
let w = r.weight;
obj[i] = weights.objective_psi_psirow[i] * w;
d2smu[i] = weights.d2scoremu[i] * w;
d2sls[i] = weights.d2score_ls[i] * w;
d2hmm[i] = weights.d2hmumu[i] * w;
d2hml[i] = weights.d2hmu_ls[i] * w;
d2hll[i] = weights.d2h_ls_ls[i] * w;
}
weights.objective_psi_psirow = obj;
weights.d2scoremu = d2smu;
weights.d2score_ls = d2sls;
weights.d2hmumu = d2hmm;
weights.d2hmu_ls = d2hml;
weights.d2h_ls_ls = d2hll;
}
pub(crate) fn apply_ht_mask_mixed(
weights: &mut GaussianJointPsiMixedDriftWeights,
rows: &[crate::outer_subsample::WeightedOuterRow],
) {
let n = weights.dhmumu_u.len();
let mut dhmm_u = Array1::<f64>::zeros(n);
let mut dhml_u = Array1::<f64>::zeros(n);
let mut dhll_u = Array1::<f64>::zeros(n);
let mut d2hmm = Array1::<f64>::zeros(n);
let mut d2hml = Array1::<f64>::zeros(n);
let mut d2hll = Array1::<f64>::zeros(n);
for r in rows {
let i = r.index;
let w = r.weight;
dhmm_u[i] = weights.dhmumu_u[i] * w;
dhml_u[i] = weights.dhmu_ls_u[i] * w;
dhll_u[i] = weights.dh_ls_ls_u[i] * w;
d2hmm[i] = weights.d2hmumu[i] * w;
d2hml[i] = weights.d2hmu_ls[i] * w;
d2hll[i] = weights.d2h_ls_ls[i] * w;
}
weights.dhmumu_u = dhmm_u;
weights.dhmu_ls_u = dhml_u;
weights.dh_ls_ls_u = dhll_u;
weights.d2hmumu = d2hmm;
weights.d2hmu_ls = d2hml;
weights.d2h_ls_ls = d2hll;
}
pub(crate) fn gaussian_jointrow_scalars(
y: &Array1<f64>,
etamu: &Array1<f64>,
eta_ls: &Array1<f64>,
weights: &Array1<f64>,
) -> Result<GaussianJointRowScalars, String> {
let nobs = y.len();
if etamu.len() != nobs || eta_ls.len() != nobs || weights.len() != nobs {
return Err(GamlssError::DimensionMismatch {
reason: "Gaussian joint row scalar input size mismatch".to_string(),
}
.into());
}
let mut obs_weight = Array1::<f64>::uninit(nobs);
let mut w = Array1::<f64>::uninit(nobs);
let mut m = Array1::<f64>::uninit(nobs);
let mut n = Array1::<f64>::uninit(nobs);
let mut kappa = Array1::<f64>::uninit(nobs);
let mut kappa_prime = Array1::<f64>::uninit(nobs);
let mut kappa_dprime = Array1::<f64>::uninit(nobs);
for i in 0..nobs {
let jet = crate::sigma_link::logb_sigma_jet1_scalar(eta_ls[i]);
let s = jet.sigma;
let ki = logb_dlog_sigma_deta(s, jet.d1);
let kp = ki * (1.0 - ki);
let kdp = kp * (1.0 - 2.0 * ki);
let wi = weights[i] / (s * s);
let ri = y[i] - etamu[i];
obs_weight[i].write(weights[i]);
w[i].write(wi);
m[i].write(ri * wi);
n[i].write(ri * ri * wi);
kappa[i].write(ki);
kappa_prime[i].write(kp);
kappa_dprime[i].write(kdp);
}
let (obs_weight, w, m, n, kappa, kappa_prime, kappa_dprime) = unsafe {
(
obs_weight.assume_init(),
w.assume_init(),
m.assume_init(),
n.assume_init(),
kappa.assume_init(),
kappa_prime.assume_init(),
kappa_dprime.assume_init(),
)
};
Ok(GaussianJointRowScalars {
obs_weight,
w,
m,
n,
kappa,
kappa_prime,
kappa_dprime,
})
}
pub(crate) fn gaussian_joint_first_directionalweights(
scalars: &GaussianJointRowScalars,
dotmu: &Array1<f64>,
dot_eta: &Array1<f64>,
) -> (Array1<f64>, Array1<f64>, Array1<f64>) {
let nobs = scalars.w.len();
let mut w_u = Array1::<f64>::uninit(nobs);
let mut c_u = Array1::<f64>::uninit(nobs);
let mut d_u = Array1::<f64>::uninit(nobs);
for i in 0..nobs {
let wi = scalars.w[i];
let mi = scalars.m[i];
let ki = scalars.kappa[i];
let kpi = scalars.kappa_prime[i];
let ai = scalars.obs_weight[i];
let dm = dotmu[i];
let de = dot_eta[i];
let sde = ki * de;
w_u[i].write(-2.0 * wi * sde);
c_u[i].write(ki * (-2.0 * wi * dm - 4.0 * mi * sde) + 2.0 * mi * kpi * de);
d_u[i].write(4.0 * ki * kpi * ai * de);
}
let (w_u, c_u, d_u) = unsafe { (w_u.assume_init(), c_u.assume_init(), d_u.assume_init()) };
(w_u, c_u, d_u)
}
pub(crate) fn gaussian_jointsecond_directionalweights(
scalars: &GaussianJointRowScalars,
dotmu_u: &Array1<f64>,
dot_eta_u: &Array1<f64>,
dotmuv: &Array1<f64>,
dot_etav: &Array1<f64>,
) -> (Array1<f64>, Array1<f64>, Array1<f64>) {
let nobs = scalars.w.len();
let mut w_uv = Array1::<f64>::uninit(nobs);
let mut c_uv = Array1::<f64>::uninit(nobs);
let mut d_uv = Array1::<f64>::uninit(nobs);
for i in 0..nobs {
let wi = scalars.w[i];
let mi = scalars.m[i];
let ki = scalars.kappa[i];
let kpi = scalars.kappa_prime[i];
let kdpi = scalars.kappa_dprime[i];
let ai = scalars.obs_weight[i];
let dmu = dotmu_u[i];
let dmv = dotmuv[i];
let deu = dot_eta_u[i];
let dev = dot_etav[i];
let sdeu = ki * deu;
let sdev = ki * dev;
let de_sym = dmu * dev + dmv * deu;
let de_eta = deu * dev;
w_uv[i].write(4.0 * wi * sdeu * sdev - 2.0 * wi * kpi * de_eta);
c_uv[i].write(
ki * (4.0 * wi * (dmu * sdev + dmv * sdeu) + 8.0 * mi * sdeu * sdev)
- 2.0 * wi * kpi * de_sym
+ 2.0 * mi * (kdpi - 6.0 * ki * kpi) * de_eta,
);
d_uv[i].write(4.0 * ai * (kpi * kpi + ki * kdpi) * de_eta);
}
let (w_uv, c_uv, d_uv) =
unsafe { (w_uv.assume_init(), c_uv.assume_init(), d_uv.assume_init()) };
(w_uv, c_uv, d_uv)
}
pub(crate) fn gaussian_joint_psi_firstweights(
scalars: &GaussianJointRowScalars,
mu_a: &Array1<f64>,
eta_a: &Array1<f64>,
) -> GaussianJointPsiFirstWeights {
let nobs = scalars.w.len();
let mut objective_psirow = Array1::<f64>::uninit(nobs);
let mut scoremu = Array1::<f64>::uninit(nobs);
let mut score_ls = Array1::<f64>::uninit(nobs);
let mut dscoremu = Array1::<f64>::uninit(nobs);
let mut dscore_ls = Array1::<f64>::uninit(nobs);
let mut hmumu = Array1::<f64>::uninit(nobs);
let mut hmu_ls = Array1::<f64>::uninit(nobs);
let mut h_ls_ls = Array1::<f64>::uninit(nobs);
let mut dhmumu = Array1::<f64>::uninit(nobs);
let mut dhmu_ls = Array1::<f64>::uninit(nobs);
let mut dh_ls_ls = Array1::<f64>::uninit(nobs);
for i in 0..nobs {
let mi = scalars.m[i];
let ni = scalars.n[i];
let ki = scalars.kappa[i];
let kpi = scalars.kappa_prime[i];
let ai = scalars.obs_weight[i];
let ma = mu_a[i];
let ea = eta_a[i];
let sea = ki * ea;
let smu = -mi;
let sls = ki * (ai - ni);
let wi = scalars.w[i];
scoremu[i].write(smu);
score_ls[i].write(sls);
dscoremu[i].write(wi * ma + 2.0 * mi * sea);
dscore_ls[i].write(ki * (2.0 * mi * ma + 2.0 * ni * sea) + kpi * (ai - ni) * ea);
hmumu[i].write(wi);
hmu_ls[i].write(0.0);
h_ls_ls[i].write(2.0 * ki * ki * ai);
dhmumu[i].write(-2.0 * wi * sea);
dhmu_ls[i].write(0.0);
dh_ls_ls[i].write(4.0 * ki * kpi * ai * ea);
objective_psirow[i].write(smu * ma + sls * ea);
}
unsafe {
GaussianJointPsiFirstWeights {
objective_psirow: objective_psirow.assume_init(),
scoremu: scoremu.assume_init(),
score_ls: score_ls.assume_init(),
dscoremu: dscoremu.assume_init(),
dscore_ls: dscore_ls.assume_init(),
hmumu: hmumu.assume_init(),
hmu_ls: hmu_ls.assume_init(),
h_ls_ls: h_ls_ls.assume_init(),
dhmumu: dhmumu.assume_init(),
dhmu_ls: dhmu_ls.assume_init(),
dh_ls_ls: dh_ls_ls.assume_init(),
}
}
}
pub(crate) fn gaussian_joint_psisecondweights(
scalars: &GaussianJointRowScalars,
mu_a: &Array1<f64>,
eta_a: &Array1<f64>,
mu_b: &Array1<f64>,
eta_b: &Array1<f64>,
mu_ab: &Array1<f64>,
eta_ab: &Array1<f64>,
) -> GaussianJointPsiSecondWeights {
let nobs = scalars.w.len();
let mut objective_psi_psirow = Array1::<f64>::uninit(nobs);
let mut d2scoremu = Array1::<f64>::uninit(nobs);
let mut d2score_ls = Array1::<f64>::uninit(nobs);
let mut d2hmumu = Array1::<f64>::uninit(nobs);
let mut d2hmu_ls = Array1::<f64>::uninit(nobs);
let mut d2h_ls_ls = Array1::<f64>::uninit(nobs);
for i in 0..nobs {
let wi = scalars.w[i];
let mi = scalars.m[i];
let ni = scalars.n[i];
let ki = scalars.kappa[i];
let kpi = scalars.kappa_prime[i];
let kdpi = scalars.kappa_dprime[i];
let ai = scalars.obs_weight[i];
let amn = ai - ni;
let ma = mu_a[i];
let mb = mu_b[i];
let mab = mu_ab[i];
let ea = eta_a[i];
let eb = eta_b[i];
let eab = eta_ab[i];
let sea = ki * ea;
let seb = ki * eb;
let seab = ki * eab;
let cross = ma * seb + mb * sea;
let cross_eta = ma * eb + mb * ea;
let sea_seb = sea * seb;
let ea_eb = ea * eb;
let ma_mb = ma * mb;
objective_psi_psirow[i].write(
wi * ma_mb + 2.0 * mi * cross + 2.0 * ni * sea_seb - mi * mab
+ ki * amn * eab
+ kpi * amn * ea_eb,
);
d2scoremu[i].write(
wi * mab - 2.0 * wi * cross - 4.0 * mi * sea_seb
+ 2.0 * mi * seab
+ 2.0 * mi * kpi * ea_eb,
);
d2score_ls[i].write(
ki * (-2.0 * wi * ma_mb - 4.0 * mi * cross - 4.0 * ni * sea_seb
+ 2.0 * mi * mab
+ 2.0 * ni * seab)
+ 2.0 * mi * kpi * cross_eta
+ (kdpi * amn + 6.0 * ki * kpi * ni) * ea_eb
+ kpi * amn * eab,
);
d2hmumu[i].write(4.0 * wi * sea_seb - 2.0 * wi * seab - 2.0 * wi * kpi * ea_eb);
d2hmu_ls[i].write(0.0);
d2h_ls_ls[i].write(4.0 * ai * (kpi * kpi + ki * kdpi) * ea_eb + 4.0 * ai * ki * kpi * eab);
}
unsafe {
GaussianJointPsiSecondWeights {
objective_psi_psirow: objective_psi_psirow.assume_init(),
d2scoremu: d2scoremu.assume_init(),
d2score_ls: d2score_ls.assume_init(),
d2hmumu: d2hmumu.assume_init(),
d2hmu_ls: d2hmu_ls.assume_init(),
d2h_ls_ls: d2h_ls_ls.assume_init(),
}
}
}
pub(crate) fn gaussian_joint_psi_mixed_driftweights(
scalars: &GaussianJointRowScalars,
dot_eta: &Array1<f64>,
eta_a: &Array1<f64>,
dot_eta_a: &Array1<f64>,
) -> GaussianJointPsiMixedDriftWeights {
let nobs = scalars.w.len();
let mut dhmumu_u = Array1::<f64>::uninit(nobs);
let mut dhmu_ls_u = Array1::<f64>::uninit(nobs);
let mut dh_ls_ls_u = Array1::<f64>::uninit(nobs);
let mut d2hmumu = Array1::<f64>::uninit(nobs);
let mut d2hmu_ls = Array1::<f64>::uninit(nobs);
let mut d2h_ls_ls = Array1::<f64>::uninit(nobs);
for i in 0..nobs {
let wi = scalars.w[i];
let ki = scalars.kappa[i];
let kpi = scalars.kappa_prime[i];
let kdpi = scalars.kappa_dprime[i];
let ai = scalars.obs_weight[i];
let de = dot_eta[i];
let ea = eta_a[i];
let dea = dot_eta_a[i];
let sde = ki * de;
let sea = ki * ea;
let sdea = ki * dea;
let de_ea = de * ea;
dhmumu_u[i].write(-2.0 * wi * sde);
dhmu_ls_u[i].write(0.0);
dh_ls_ls_u[i].write(4.0 * ki * kpi * ai * de);
d2hmumu[i].write(4.0 * wi * sde * sea - 2.0 * wi * sdea - 2.0 * wi * kpi * de_ea);
d2hmu_ls[i].write(0.0);
d2h_ls_ls[i].write(4.0 * ai * (kpi * kpi + ki * kdpi) * de_ea + 4.0 * ai * ki * kpi * dea);
}
unsafe {
GaussianJointPsiMixedDriftWeights {
dhmumu_u: dhmumu_u.assume_init(),
dhmu_ls_u: dhmu_ls_u.assume_init(),
dh_ls_ls_u: dh_ls_ls_u.assume_init(),
d2hmumu: d2hmumu.assume_init(),
d2hmu_ls: d2hmu_ls.assume_init(),
d2h_ls_ls: d2h_ls_ls.assume_init(),
}
}
}
pub(crate) fn gaussian_locscale_fisher_joint_row_coeffs(
rows: &GaussianJointRowScalars,
) -> (Array1<f64>, Array1<f64>, Array1<f64>) {
let mm = rows.w.clone();
let ml = Array1::<f64>::zeros(rows.kappa.len());
let ll = 2.0 * &rows.kappa * &rows.kappa * &rows.obs_weight;
(mm, ml, ll)
}
pub(crate) fn gaussian_joint_hessian_from_designs(
xmu: &DenseOrOperator<'_>,
x_ls: &DenseOrOperator<'_>,
hmumu_coeff: &Array1<f64>,
hmu_ls_coeff: &Array1<f64>,
h_ls_ls_coeff: &Array1<f64>,
) -> Result<Array2<f64>, String> {
if xmu.nrows() != hmumu_coeff.len()
|| xmu.nrows() != hmu_ls_coeff.len()
|| xmu.nrows() != h_ls_ls_coeff.len()
|| x_ls.nrows() != xmu.nrows()
{
return Err(GamlssError::DimensionMismatch { reason: format!(
"gaussian_joint_hessian_from_designs dimension mismatch: xmu {}x{}, x_ls {}x{}, coeffs {}/{}/{}",
xmu.nrows(),
xmu.ncols(),
x_ls.nrows(),
x_ls.ncols(),
hmumu_coeff.len(),
hmu_ls_coeff.len(),
h_ls_ls_coeff.len()
) }.into());
}
let n = xmu.nrows();
let pmu = xmu.ncols();
let p_ls = x_ls.ncols();
let total = pmu + p_ls;
let mut out = Array2::<f64>::zeros((total, total));
for rows in exact_design_row_chunks(n, pmu.max(p_ls)) {
let xmu_chunk = xmu.row_chunk(rows.clone())?;
let xls_chunk = x_ls.row_chunk(rows.clone())?;
let hmumu = hmumu_coeff.slice(s![rows.clone()]);
let hmu_ls = hmu_ls_coeff.slice(s![rows.clone()]);
let h_ls_ls = h_ls_ls_coeff.slice(s![rows.clone()]);
let chunk_hessian =
fast_joint_hessian_2x2(&xmu_chunk, &xls_chunk, &hmumu, &hmu_ls, &h_ls_ls);
out += &chunk_hessian;
}
Ok(out)
}
pub(crate) fn gaussian_joint_psihessian_fromweights(
xmu: &Array2<f64>,
x_ls: &Array2<f64>,
xmu_psi: CustomFamilyPsiLinearMapRef<'_>,
x_ls_psi: CustomFamilyPsiLinearMapRef<'_>,
weights: &GaussianJointPsiFirstWeights,
) -> Result<Array2<f64>, String> {
let a_mu = weighted_crossprod_psi_maps(
xmu_psi,
weights.hmumu.view(),
CustomFamilyPsiLinearMapRef::Dense(xmu),
)?;
let hmumu = &a_mu + &a_mu.t() + &xt_diag_x_dense(xmu, &weights.dhmumu)?;
let hmu_ls = weighted_crossprod_psi_maps(
xmu_psi,
weights.hmu_ls.view(),
CustomFamilyPsiLinearMapRef::Dense(x_ls),
)? + &weighted_crossprod_psi_maps(
CustomFamilyPsiLinearMapRef::Dense(xmu),
weights.hmu_ls.view(),
x_ls_psi,
)? + &xt_diag_y_dense(xmu, &weights.dhmu_ls, x_ls)?;
let a_ls = weighted_crossprod_psi_maps(
x_ls_psi,
weights.h_ls_ls.view(),
CustomFamilyPsiLinearMapRef::Dense(x_ls),
)?;
let h_ls_ls = &a_ls + &a_ls.t() + &xt_diag_x_dense(x_ls, &weights.dh_ls_ls)?;
Ok(gaussian_pack_joint_symmetrichessian(
&hmumu, &hmu_ls, &h_ls_ls,
))
}
pub(crate) fn build_two_block_custom_family_joint_psi_operator_from_actions(
left_action: Option<CustomFamilyPsiDesignAction>,
right_action: Option<CustomFamilyPsiDesignAction>,
left_range: std::ops::Range<usize>,
right_range: std::ops::Range<usize>,
left_design: &Array2<f64>,
right_design: &Array2<f64>,
left_weights: &Array1<f64>,
cross_weights: &Array1<f64>,
right_weights: &Array1<f64>,
left_drift_weights: &Array1<f64>,
cross_drift_weights: &Array1<f64>,
right_drift_weights: &Array1<f64>,
) -> Result<Option<std::sync::Arc<dyn gam_problem::HyperOperator>>, String> {
if left_action.is_none() && right_action.is_none() {
return Ok(None);
}
let total = left_design.ncols() + right_design.ncols();
let channels = vec![
CustomFamilyJointDesignChannel::new(left_range, shared_dense_arc(left_design), left_action),
CustomFamilyJointDesignChannel::new(
right_range,
shared_dense_arc(right_design),
right_action,
),
];
let pair_contributions = vec![
CustomFamilyJointDesignPairContribution::new(
0,
0,
left_weights.clone(),
left_drift_weights.clone(),
),
CustomFamilyJointDesignPairContribution::new(
0,
1,
cross_weights.clone(),
cross_drift_weights.clone(),
),
CustomFamilyJointDesignPairContribution::new(
1,
0,
cross_weights.clone(),
cross_drift_weights.clone(),
),
CustomFamilyJointDesignPairContribution::new(
1,
1,
right_weights.clone(),
right_drift_weights.clone(),
),
];
Ok(Some(std::sync::Arc::new(
CustomFamilyJointPsiOperator::new(total, channels, pair_contributions),
)))
}
pub(crate) fn gaussian_joint_psisecondhessian_fromweights(
xmu: &Array2<f64>,
x_ls: &Array2<f64>,
xmu_i: CustomFamilyPsiLinearMapRef<'_>,
x_ls_i: CustomFamilyPsiLinearMapRef<'_>,
xmu_j: CustomFamilyPsiLinearMapRef<'_>,
x_ls_j: CustomFamilyPsiLinearMapRef<'_>,
xmu_ab: CustomFamilyPsiLinearMapRef<'_>,
x_ls_ab: CustomFamilyPsiLinearMapRef<'_>,
weights_i: &GaussianJointPsiFirstWeights,
weights_j: &GaussianJointPsiFirstWeights,
secondweights: &GaussianJointPsiSecondWeights,
) -> Result<Array2<f64>, String> {
let a_ab_mu = weighted_crossprod_psi_maps(
xmu_ab,
weights_i.hmumu.view(),
CustomFamilyPsiLinearMapRef::Dense(xmu),
)?;
let a_ij_mu = weighted_crossprod_psi_maps(xmu_i, weights_i.hmumu.view(), xmu_j)?;
let a_iwj_mu = weighted_crossprod_psi_maps(
xmu_i,
weights_j.dhmumu.view(),
CustomFamilyPsiLinearMapRef::Dense(xmu),
)?;
let a_jwi_mu = weighted_crossprod_psi_maps(
xmu_j,
weights_i.dhmumu.view(),
CustomFamilyPsiLinearMapRef::Dense(xmu),
)?;
let hmumu = &a_ab_mu
+ &a_ab_mu.t()
+ &a_ij_mu
+ a_ij_mu.t()
+ &a_iwj_mu
+ a_iwj_mu.t()
+ &a_jwi_mu
+ a_jwi_mu.t()
+ &xt_diag_x_dense(xmu, &secondweights.d2hmumu)?;
let hmu_ls = weighted_crossprod_psi_maps(
xmu_ab,
weights_i.hmu_ls.view(),
CustomFamilyPsiLinearMapRef::Dense(x_ls),
)? + &weighted_crossprod_psi_maps(xmu_i, weights_i.hmu_ls.view(), x_ls_j)?
+ &weighted_crossprod_psi_maps(xmu_j, weights_i.hmu_ls.view(), x_ls_i)?
+ &weighted_crossprod_psi_maps(
xmu_i,
weights_j.dhmu_ls.view(),
CustomFamilyPsiLinearMapRef::Dense(x_ls),
)?
+ &weighted_crossprod_psi_maps(
xmu_j,
weights_i.dhmu_ls.view(),
CustomFamilyPsiLinearMapRef::Dense(x_ls),
)?
+ &weighted_crossprod_psi_maps(
CustomFamilyPsiLinearMapRef::Dense(xmu),
weights_i.dhmu_ls.view(),
x_ls_j,
)?
+ &weighted_crossprod_psi_maps(
CustomFamilyPsiLinearMapRef::Dense(xmu),
weights_j.dhmu_ls.view(),
x_ls_i,
)?
+ &xt_diag_y_dense(xmu, &secondweights.d2hmu_ls, x_ls)?
+ &weighted_crossprod_psi_maps(
CustomFamilyPsiLinearMapRef::Dense(xmu),
weights_i.hmu_ls.view(),
x_ls_ab,
)?;
let a_ab_ls = weighted_crossprod_psi_maps(
x_ls_ab,
weights_i.h_ls_ls.view(),
CustomFamilyPsiLinearMapRef::Dense(x_ls),
)?;
let a_ij_ls = weighted_crossprod_psi_maps(x_ls_i, weights_i.h_ls_ls.view(), x_ls_j)?;
let a_iwj_ls = weighted_crossprod_psi_maps(
x_ls_i,
weights_j.dh_ls_ls.view(),
CustomFamilyPsiLinearMapRef::Dense(x_ls),
)?;
let a_jwi_ls = weighted_crossprod_psi_maps(
x_ls_j,
weights_i.dh_ls_ls.view(),
CustomFamilyPsiLinearMapRef::Dense(x_ls),
)?;
let h_ls_ls = &a_ab_ls
+ &a_ab_ls.t()
+ &a_ij_ls
+ a_ij_ls.t()
+ &a_iwj_ls
+ a_iwj_ls.t()
+ &a_jwi_ls
+ a_jwi_ls.t()
+ &xt_diag_x_dense(x_ls, &secondweights.d2h_ls_ls)?;
Ok(gaussian_pack_joint_symmetrichessian(
&hmumu, &hmu_ls, &h_ls_ls,
))
}
pub(crate) fn gaussian_joint_psi_mixedhessian_drift_fromweights(
xmu: &Array2<f64>,
x_ls: &Array2<f64>,
xmu_psi: CustomFamilyPsiLinearMapRef<'_>,
x_ls_psi: CustomFamilyPsiLinearMapRef<'_>,
mixedweights: &GaussianJointPsiMixedDriftWeights,
) -> Result<Array2<f64>, String> {
let a_mu = weighted_crossprod_psi_maps(
xmu_psi,
mixedweights.dhmumu_u.view(),
CustomFamilyPsiLinearMapRef::Dense(xmu),
)?;
let hmumu = &a_mu + &a_mu.t() + &xt_diag_x_dense(xmu, &mixedweights.d2hmumu)?;
let hmu_ls = weighted_crossprod_psi_maps(
xmu_psi,
mixedweights.dhmu_ls_u.view(),
CustomFamilyPsiLinearMapRef::Dense(x_ls),
)? + &weighted_crossprod_psi_maps(
CustomFamilyPsiLinearMapRef::Dense(xmu),
mixedweights.dhmu_ls_u.view(),
x_ls_psi,
)? + &xt_diag_y_dense(xmu, &mixedweights.d2hmu_ls, x_ls)?;
let a_ls = weighted_crossprod_psi_maps(
x_ls_psi,
mixedweights.dh_ls_ls_u.view(),
CustomFamilyPsiLinearMapRef::Dense(x_ls),
)?;
let h_ls_ls = &a_ls + &a_ls.t() + &xt_diag_x_dense(x_ls, &mixedweights.d2h_ls_ls)?;
Ok(gaussian_pack_joint_symmetrichessian(
&hmumu, &hmu_ls, &h_ls_ls,
))
}
#[inline]
pub(crate) fn exp_sigma_derivs_up_to_fourth_array(
eta: ArrayView1<'_, f64>,
) -> (
Array1<f64>,
Array1<f64>,
Array1<f64>,
Array1<f64>,
Array1<f64>,
) {
use rayon::iter::{IntoParallelIterator, ParallelIterator};
let n = eta.len();
let tuples: Vec<(f64, f64, f64, f64, f64)> = (0..n)
.into_par_iter()
.map(|i| exp_sigma_derivs_up_to_fourth_scalar(eta[i]))
.collect();
let mut sigma = Array1::<f64>::zeros(n);
let mut d1 = Array1::<f64>::zeros(n);
let mut d2 = Array1::<f64>::zeros(n);
let mut d3 = Array1::<f64>::zeros(n);
let mut d4 = Array1::<f64>::zeros(n);
for (i, (s_i, d1_i, d2_i, d3_i, d4_i)) in tuples.into_iter().enumerate() {
sigma[i] = s_i;
d1[i] = d1_i;
d2[i] = d2_i;
d3[i] = d3_i;
d4[i] = d4_i;
}
(sigma, d1, d2, d3, d4)
}
#[cfg(test)]
mod fisher_single_source_oracle_tests {
use super::*;
use ndarray::array;
const FLOOR: f64 = crate::sigma_link::LOGB_SIGMA_FLOOR;
fn sigma_of(eta_ls: f64) -> f64 {
FLOOR + eta_ls.exp()
}
fn kappa_of(eta_ls: f64) -> f64 {
eta_ls.exp() / sigma_of(eta_ls)
}
fn row_nll(y: f64, mu: f64, eta_ls: f64, a: f64) -> f64 {
let ln2pi = (2.0 * std::f64::consts::PI).ln();
-gaussian_diagonal_row_kernel(y, mu, eta_ls, a, ln2pi).log_likelihood
}
fn observed_hessian(y: f64, mu: f64, eta_ls: f64, a: f64, h: f64) -> (f64, f64, f64) {
let hmm = (row_nll(y, mu + h, eta_ls, a) - 2.0 * row_nll(y, mu, eta_ls, a)
+ row_nll(y, mu - h, eta_ls, a))
/ (h * h);
let hll = (row_nll(y, mu, eta_ls + h, a) - 2.0 * row_nll(y, mu, eta_ls, a)
+ row_nll(y, mu, eta_ls - h, a))
/ (h * h);
let hml = (row_nll(y, mu + h, eta_ls + h, a)
- row_nll(y, mu + h, eta_ls - h, a)
- row_nll(y, mu - h, eta_ls + h, a)
+ row_nll(y, mu - h, eta_ls - h, a))
/ (4.0 * h * h);
(hmm, hml, hll)
}
fn fisher_via_gauss_hermite(mu: f64, eta_ls: f64, a: f64) -> (f64, f64, f64) {
let nodes = [-1.224_744_871_391_589_0_f64, 0.0, 1.224_744_871_391_589_0];
let wts = [
0.295_408_975_150_919_3_f64,
1.181_635_900_603_677_4,
0.295_408_975_150_919_3,
];
let sqpi = std::f64::consts::PI.sqrt();
let s = sigma_of(eta_ls);
let h = 1e-4;
let (mut mm, mut ml, mut ll) = (0.0, 0.0, 0.0);
for k in 0..3 {
let y = mu + s * std::f64::consts::SQRT_2 * nodes[k];
let (hmm, hml, hll) = observed_hessian(y, mu, eta_ls, a, h);
let ww = wts[k] / sqpi;
mm += ww * hmm;
ml += ww * hml;
ll += ww * hll;
}
(mm, ml, ll)
}
fn production_fisher_row(mu: f64, eta_ls: f64, a: f64) -> (f64, f64, f64) {
let rows = gaussian_jointrow_scalars(
&array![mu * 0.0 + 0.3137 + mu], &array![mu],
&array![eta_ls],
&array![a],
)
.expect("row scalars");
let (mm, cross, ll) = gaussian_locscale_fisher_joint_row_coeffs(&rows);
(mm[0], cross[0], ll[0])
}
#[test]
fn fisher_joint_row_coeffs_match_gauss_hermite_single_source() {
let cases = [
(0.3_f64, -0.4_f64, 1.0_f64),
(-1.2, 0.7, 2.5),
(0.0, 1.5, 0.4),
(2.4, -1.1, 0.8),
(-0.6, 0.2, 3.3),
];
for &(mu, eta_ls, a) in &cases {
let (mm_hand, cross_hand, ll_hand) = production_fisher_row(mu, eta_ls, a);
let (mm_mc, ml_mc, ll_mc) = fisher_via_gauss_hermite(mu, eta_ls, a);
assert!(
(mm_hand - mm_mc).abs() <= 1e-6 * mm_hand.abs().max(1.0),
"H_μμ Fisher drift μ={mu} η={eta_ls}: hand={mm_hand} mech={mm_mc}"
);
assert!(
(ll_hand - ll_mc).abs() <= 1e-6 * ll_hand.abs().max(1.0),
"H_lsls Fisher drift μ={mu} η={eta_ls}: hand={ll_hand} mech={ll_mc}"
);
assert_eq!(cross_hand, 0.0, "production cross block must be exactly 0");
assert!(
ml_mc.abs() <= 1e-6,
"Gauss–Hermite Fisher cross block must cancel to ~0 \
(genuine coupling would be O(1)): μ={mu} η={eta_ls} ml={ml_mc:.3e}"
);
}
}
#[test]
fn first_directional_weights_match_fisher_finite_difference() {
let cases = [
(0.3_f64, -0.4_f64, 1.0_f64, 0.5_f64, -0.7_f64),
(-1.2, 0.7, 2.5, 1.1, 0.3),
(0.0, 1.5, 0.4, -0.2, 0.9),
(2.4, -1.1, 0.8, 0.6, -0.4),
];
let t = 1e-6;
for &(mu, eta_ls, a, xi_mu, xi_ls) in &cases {
let rows = gaussian_jointrow_scalars(
&array![mu + 0.137], &array![mu],
&array![eta_ls],
&array![a],
)
.expect("row scalars");
let (w_u, _c_u, d_u) =
gaussian_joint_first_directionalweights(&rows, &array![xi_mu], &array![xi_ls]);
let mm = |_m: f64, e: f64| a / (sigma_of(e) * sigma_of(e));
let ll = |_m: f64, e: f64| {
let k = kappa_of(e);
2.0 * k * k * a
};
let fd_w = (mm(mu + t * xi_mu, eta_ls + t * xi_ls)
- mm(mu - t * xi_mu, eta_ls - t * xi_ls))
/ (2.0 * t);
let fd_d = (ll(mu + t * xi_mu, eta_ls + t * xi_ls)
- ll(mu - t * xi_mu, eta_ls - t * xi_ls))
/ (2.0 * t);
assert!(
(w_u[0] - fd_w).abs() <= 1e-6 * fd_w.abs().max(1.0),
"dH_μμ drift μ={mu} η={eta_ls}: hand={} fd={fd_w}",
w_u[0]
);
assert!(
(d_u[0] - fd_d).abs() <= 1e-6 * fd_d.abs().max(1.0),
"dH_lsls drift μ={mu} η={eta_ls}: hand={} fd={fd_d}",
d_u[0]
);
}
}
#[test]
fn second_directional_weights_match_first_directional_finite_difference() {
let cases = [
(
0.3_f64, -0.4_f64, 1.0_f64, 0.5_f64, -0.7_f64, 0.8_f64, 0.2_f64,
),
(-1.2, 0.7, 2.5, 1.1, 0.3, -0.6, 0.9),
(0.0, 1.5, 0.4, -0.2, 0.9, 0.4, -0.5),
];
let t = 1e-5;
for &(mu, eta_ls, a, xi_mu_u, xi_ls_u, xi_mu_v, xi_ls_v) in &cases {
let rows = gaussian_jointrow_scalars(
&array![mu + 0.137],
&array![mu],
&array![eta_ls],
&array![a],
)
.expect("row scalars");
let (w_uv, _c_uv, d_uv) = gaussian_jointsecond_directionalweights(
&rows,
&array![xi_mu_u],
&array![xi_ls_u],
&array![xi_mu_v],
&array![xi_ls_v],
);
let first_w_and_d = |m: f64, e: f64| -> (f64, f64) {
let r = gaussian_jointrow_scalars(
&array![m + 0.137],
&array![m],
&array![e],
&array![a],
)
.expect("row scalars");
let (w_u, _c, d_u) =
gaussian_joint_first_directionalweights(&r, &array![xi_mu_u], &array![xi_ls_u]);
(w_u[0], d_u[0])
};
let (wp, dp) = first_w_and_d(mu + t * xi_mu_v, eta_ls + t * xi_ls_v);
let (wm, dm) = first_w_and_d(mu - t * xi_mu_v, eta_ls - t * xi_ls_v);
let fd_w = (wp - wm) / (2.0 * t);
let fd_d = (dp - dm) / (2.0 * t);
assert!(
(w_uv[0] - fd_w).abs() <= 1e-5 * fd_w.abs().max(1.0),
"d²H_μμ drift μ={mu} η={eta_ls}: hand={} fd={fd_w}",
w_uv[0]
);
assert!(
(d_uv[0] - fd_d).abs() <= 1e-5 * fd_d.abs().max(1.0),
"d²H_lsls drift μ={mu} η={eta_ls}: hand={} fd={fd_d}",
d_uv[0]
);
}
}
}