use super::*;
use nalgebra::{DMatrix, Vector3};
use petgraph::visit::EdgeRef;
pub fn etkdg_3d_gradient(
coords: &DMatrix<f32>,
mol: &crate::graph::Molecule,
ff: &Etkdg3DFF,
) -> DMatrix<f32> {
let n = mol.graph.node_count();
let mut grad = DMatrix::zeros(n, 3);
for c in ff
.dist_12
.iter()
.chain(ff.dist_13.iter())
.chain(ff.dist_long.iter())
{
let p1 = Vector3::new(coords[(c.i, 0)], coords[(c.i, 1)], coords[(c.i, 2)]);
let p2 = Vector3::new(coords[(c.j, 0)], coords[(c.j, 1)], coords[(c.j, 2)]);
crate::forcefield::gradients::analytical_grad_distance_constraint(
&p1,
&p2,
c.min_len as f32,
c.max_len as f32,
c.k as f32,
&mut grad,
c.i,
c.j,
);
}
for ac in &ff.angle_constraints {
super::energy::angle_constraint_gradient(coords, ac, &mut grad);
}
if ff.oop_k.abs() > 1e-8 {
for i in 0..n {
let ni = petgraph::graph::NodeIndex::new(i);
if mol.graph[ni].hybridization != crate::graph::Hybridization::SP2 {
continue;
}
let nbs: Vec<_> = mol.graph.neighbors(ni).collect();
if nbs.len() != 3 {
continue;
}
let pc = Vector3::new(coords[(i, 0)], coords[(i, 1)], coords[(i, 2)]);
let p1 = Vector3::new(
coords[(nbs[0].index(), 0)],
coords[(nbs[0].index(), 1)],
coords[(nbs[0].index(), 2)],
);
let p2 = Vector3::new(
coords[(nbs[1].index(), 0)],
coords[(nbs[1].index(), 1)],
coords[(nbs[1].index(), 2)],
);
let p3 = Vector3::new(
coords[(nbs[2].index(), 0)],
coords[(nbs[2].index(), 1)],
coords[(nbs[2].index(), 2)],
);
crate::forcefield::gradients::analytical_grad_oop(
&pc,
&p1,
&p2,
&p3,
ff.oop_k as f32,
&mut grad,
i,
nbs[0].index(),
nbs[1].index(),
nbs[2].index(),
);
}
}
for tc in &ff.torsion_contribs {
let p1 = Vector3::new(coords[(tc.i, 0)], coords[(tc.i, 1)], coords[(tc.i, 2)]);
let p2 = Vector3::new(coords[(tc.j, 0)], coords[(tc.j, 1)], coords[(tc.j, 2)]);
let p3 = Vector3::new(coords[(tc.k, 0)], coords[(tc.k, 1)], coords[(tc.k, 2)]);
let p4 = Vector3::new(coords[(tc.l, 0)], coords[(tc.l, 1)], coords[(tc.l, 2)]);
let m6 = crate::forcefield::etkdg_lite::M6Params {
s: tc.signs.map(|x| x as f32),
v: tc.v.map(|x| x as f32),
};
crate::forcefield::etkdg_lite::calc_torsion_grad_m6(
&p1, &p2, &p3, &p4, &m6, &mut grad, tc.i, tc.j, tc.k, tc.l,
);
}
if ff.torsion_k_omega.abs() > 1e-8 && n >= 4 {
for edge in mol.graph.edge_references() {
let u = edge.source();
let v = edge.target();
let hyb_u = mol.graph[u].hybridization;
let hyb_v = mol.graph[v].hybridization;
if hyb_u == crate::graph::Hybridization::SP || hyb_v == crate::graph::Hybridization::SP
{
continue;
}
let (n_fold, gamma, weight) = crate::forcefield::energy::torsion_params(hyb_u, hyb_v);
let neighbors_u: Vec<_> = mol.graph.neighbors(u).filter(|&x| x != v).collect();
let neighbors_v: Vec<_> = mol.graph.neighbors(v).filter(|&x| x != u).collect();
for &nu in &neighbors_u {
for &nv in &neighbors_v {
let p1 = Vector3::new(
coords[(nu.index(), 0)],
coords[(nu.index(), 1)],
coords[(nu.index(), 2)],
);
let p2 = Vector3::new(
coords[(u.index(), 0)],
coords[(u.index(), 1)],
coords[(u.index(), 2)],
);
let p3 = Vector3::new(
coords[(v.index(), 0)],
coords[(v.index(), 1)],
coords[(v.index(), 2)],
);
let p4 = Vector3::new(
coords[(nv.index(), 0)],
coords[(nv.index(), 1)],
coords[(nv.index(), 2)],
);
crate::forcefield::gradients::analytical_grad_torsion(
&p1,
&p2,
&p3,
&p4,
ff.torsion_k_omega as f32 * weight,
n_fold,
gamma,
&mut grad,
nu.index(),
u.index(),
v.index(),
nv.index(),
);
}
}
}
}
if ff.use_m6_torsions && n >= 4 {
for edge in mol.graph.edge_references() {
let u = edge.source();
let v = edge.target();
if crate::graph::min_path_excluding2(mol, u, v, u, v, 7).is_some() {
continue;
}
let m6 =
crate::forcefield::etkdg_lite::infer_etkdg_parameters(mol, u.index(), v.index());
if m6.v.iter().all(|&x| x.abs() < 1e-6) {
continue;
}
let neighbors_u: Vec<_> = mol.graph.neighbors(u).filter(|&x| x != v).collect();
let neighbors_v: Vec<_> = mol.graph.neighbors(v).filter(|&x| x != u).collect();
if neighbors_u.is_empty() || neighbors_v.is_empty() {
continue;
}
let nu = neighbors_u[0];
let nv = neighbors_v[0];
let p1 = Vector3::new(
coords[(nu.index(), 0)],
coords[(nu.index(), 1)],
coords[(nu.index(), 2)],
);
let p2 = Vector3::new(
coords[(u.index(), 0)],
coords[(u.index(), 1)],
coords[(u.index(), 2)],
);
let p3 = Vector3::new(
coords[(v.index(), 0)],
coords[(v.index(), 1)],
coords[(v.index(), 2)],
);
let p4 = Vector3::new(
coords[(nv.index(), 0)],
coords[(nv.index(), 1)],
coords[(nv.index(), 2)],
);
crate::forcefield::etkdg_lite::calc_torsion_grad_m6(
&p1,
&p2,
&p3,
&p4,
&m6,
&mut grad,
nu.index(),
u.index(),
v.index(),
nv.index(),
);
}
}
grad
}
pub fn etkdg_3d_gradient_f64(
coords: &[f64],
n: usize,
mol: &crate::graph::Molecule,
ff: &Etkdg3DFF,
) -> Vec<f64> {
let c = |atom: usize, d: usize| -> f64 { coords[atom * 3 + d] };
let dim = n * 3;
let mut grad = vec![0.0f64; dim];
for tc in &ff.torsion_contribs {
let r0 = [
c(tc.i, 0) - c(tc.j, 0),
c(tc.i, 1) - c(tc.j, 1),
c(tc.i, 2) - c(tc.j, 2),
];
let r1 = [
c(tc.k, 0) - c(tc.j, 0),
c(tc.k, 1) - c(tc.j, 1),
c(tc.k, 2) - c(tc.j, 2),
];
let r2 = [-r1[0], -r1[1], -r1[2]];
let r3 = [
c(tc.l, 0) - c(tc.k, 0),
c(tc.l, 1) - c(tc.k, 1),
c(tc.l, 2) - c(tc.k, 2),
];
let t0 = [
r0[1] * r1[2] - r0[2] * r1[1],
r0[2] * r1[0] - r0[0] * r1[2],
r0[0] * r1[1] - r0[1] * r1[0],
];
let d0 = (t0[0] * t0[0] + t0[1] * t0[1] + t0[2] * t0[2]).sqrt();
let t1 = [
r2[1] * r3[2] - r2[2] * r3[1],
r2[2] * r3[0] - r2[0] * r3[2],
r2[0] * r3[1] - r2[1] * r3[0],
];
let d1 = (t1[0] * t1[0] + t1[1] * t1[1] + t1[2] * t1[2]).sqrt();
if d0 < 1e-10 || d1 < 1e-10 {
continue;
}
let t0 = [t0[0] / d0, t0[1] / d0, t0[2] / d0];
let t1 = [t1[0] / d1, t1[1] / d1, t1[2] / d1];
let cos_phi = (t0[0] * t1[0] + t0[1] * t1[1] + t0[2] * t1[2]).clamp(-1.0, 1.0);
let sin_phi_sq = 1.0 - cos_phi * cos_phi;
let sin_phi = if sin_phi_sq > 0.0 {
sin_phi_sq.sqrt()
} else {
0.0
};
let cp2 = cos_phi * cos_phi;
let cp3 = cos_phi * cp2;
let cp4 = cos_phi * cp3;
let cp5 = cos_phi * cp4;
let vv = &tc.v;
let ss = &tc.signs;
let de_dphi = -vv[0] * ss[0] * sin_phi
- 2.0 * vv[1] * ss[1] * (2.0 * cos_phi * sin_phi)
- 3.0 * vv[2] * ss[2] * (4.0 * cp2 * sin_phi - sin_phi)
- 4.0 * vv[3] * ss[3] * (8.0 * cp3 * sin_phi - 4.0 * cos_phi * sin_phi)
- 5.0 * vv[4] * ss[4] * (16.0 * cp4 * sin_phi - 12.0 * cp2 * sin_phi + sin_phi)
- 6.0
* vv[4]
* ss[4]
* (32.0 * cp5 * sin_phi - 32.0 * cp3 * sin_phi + 6.0 * cos_phi * sin_phi);
let is_zero_sin = sin_phi < 1e-10;
let sin_term = -de_dphi
* if is_zero_sin {
1.0 / cos_phi
} else {
1.0 / sin_phi
};
let dct = [
(t1[0] - cos_phi * t0[0]) / d0,
(t1[1] - cos_phi * t0[1]) / d0,
(t1[2] - cos_phi * t0[2]) / d0,
(t0[0] - cos_phi * t1[0]) / d1,
(t0[1] - cos_phi * t1[1]) / d1,
(t0[2] - cos_phi * t1[2]) / d1,
];
let g1 = tc.i * 3;
let g2 = tc.j * 3;
let g3 = tc.k * 3;
let g4 = tc.l * 3;
grad[g1] += sin_term * (dct[2] * r1[1] - dct[1] * r1[2]);
grad[g1 + 1] += sin_term * (dct[0] * r1[2] - dct[2] * r1[0]);
grad[g1 + 2] += sin_term * (dct[1] * r1[0] - dct[0] * r1[1]);
grad[g2] += sin_term
* (dct[1] * (r1[2] - r0[2])
+ dct[2] * (r0[1] - r1[1])
+ dct[4] * (-r3[2])
+ dct[5] * r3[1]);
grad[g2 + 1] += sin_term
* (dct[0] * (r0[2] - r1[2])
+ dct[2] * (r1[0] - r0[0])
+ dct[3] * r3[2]
+ dct[5] * (-r3[0]));
grad[g2 + 2] += sin_term
* (dct[0] * (r1[1] - r0[1])
+ dct[1] * (r0[0] - r1[0])
+ dct[3] * (-r3[1])
+ dct[4] * r3[0]);
grad[g3] += sin_term
* (dct[1] * r0[2]
+ dct[2] * (-r0[1])
+ dct[4] * (r3[2] - r2[2])
+ dct[5] * (r2[1] - r3[1]));
grad[g3 + 1] += sin_term
* (dct[0] * (-r0[2])
+ dct[2] * r0[0]
+ dct[3] * (r2[2] - r3[2])
+ dct[5] * (r3[0] - r2[0]));
grad[g3 + 2] += sin_term
* (dct[0] * r0[1]
+ dct[1] * (-r0[0])
+ dct[3] * (r3[1] - r2[1])
+ dct[4] * (r2[0] - r3[0]));
grad[g4] += sin_term * (dct[4] * r2[2] - dct[5] * r2[1]);
grad[g4 + 1] += sin_term * (dct[5] * r2[0] - dct[3] * r2[2]);
grad[g4 + 2] += sin_term * (dct[3] * r2[1] - dct[4] * r2[0]);
}
for ic in &ff.inversion_contribs {
let p1 = [c(ic.at1, 0), c(ic.at1, 1), c(ic.at1, 2)];
let p2 = [c(ic.at2, 0), c(ic.at2, 1), c(ic.at2, 2)];
let p3 = [c(ic.at3, 0), c(ic.at3, 1), c(ic.at3, 2)];
let p4 = [c(ic.at4, 0), c(ic.at4, 1), c(ic.at4, 2)];
let mut rji = [p1[0] - p2[0], p1[1] - p2[1], p1[2] - p2[2]];
let mut rjk = [p3[0] - p2[0], p3[1] - p2[1], p3[2] - p2[2]];
let mut rjl = [p4[0] - p2[0], p4[1] - p2[1], p4[2] - p2[2]];
let dji = (rji[0] * rji[0] + rji[1] * rji[1] + rji[2] * rji[2]).sqrt();
let djk = (rjk[0] * rjk[0] + rjk[1] * rjk[1] + rjk[2] * rjk[2]).sqrt();
let djl = (rjl[0] * rjl[0] + rjl[1] * rjl[1] + rjl[2] * rjl[2]).sqrt();
if dji < 1e-8 || djk < 1e-8 || djl < 1e-8 {
continue;
}
rji[0] /= dji;
rji[1] /= dji;
rji[2] /= dji;
rjk[0] /= djk;
rjk[1] /= djk;
rjk[2] /= djk;
rjl[0] /= djl;
rjl[1] /= djl;
rjl[2] /= djl;
let mut nx = -rji[1] * rjk[2] + rji[2] * rjk[1];
let mut ny = -rji[2] * rjk[0] + rji[0] * rjk[2];
let mut nz = -rji[0] * rjk[1] + rji[1] * rjk[0];
let nl = (nx * nx + ny * ny + nz * nz).sqrt();
if nl < 1e-8 {
continue;
}
nx /= nl;
ny /= nl;
nz /= nl;
let cos_y = (nx * rjl[0] + ny * rjl[1] + nz * rjl[2]).clamp(-1.0, 1.0);
let sin_y_sq = 1.0 - cos_y * cos_y;
let sin_y = sin_y_sq.sqrt().max(1e-8);
let cos_theta = (rji[0] * rjk[0] + rji[1] * rjk[1] + rji[2] * rjk[2]).clamp(-1.0, 1.0);
let sin_theta_sq = 1.0 - cos_theta * cos_theta;
let sin_theta = sin_theta_sq.sqrt().max(1e-8);
let de_dw = -ic.force_constant * (ic.c1 * cos_y - 4.0 * ic.c2 * cos_y * sin_y);
let t1 = [
rjl[1] * rjk[2] - rjl[2] * rjk[1],
rjl[2] * rjk[0] - rjl[0] * rjk[2],
rjl[0] * rjk[1] - rjl[1] * rjk[0],
];
let t2 = [
rji[1] * rjl[2] - rji[2] * rjl[1],
rji[2] * rjl[0] - rji[0] * rjl[2],
rji[0] * rjl[1] - rji[1] * rjl[0],
];
let t3 = [
rjk[1] * rji[2] - rjk[2] * rji[1],
rjk[2] * rji[0] - rjk[0] * rji[2],
rjk[0] * rji[1] - rjk[1] * rji[0],
];
let term1 = sin_y * sin_theta;
let term2 = cos_y / (sin_y * sin_theta_sq);
let tg1 = [
(t1[0] / term1 - (rji[0] - rjk[0] * cos_theta) * term2) / dji,
(t1[1] / term1 - (rji[1] - rjk[1] * cos_theta) * term2) / dji,
(t1[2] / term1 - (rji[2] - rjk[2] * cos_theta) * term2) / dji,
];
let tg3 = [
(t2[0] / term1 - (rjk[0] - rji[0] * cos_theta) * term2) / djk,
(t2[1] / term1 - (rjk[1] - rji[1] * cos_theta) * term2) / djk,
(t2[2] / term1 - (rjk[2] - rji[2] * cos_theta) * term2) / djk,
];
let tg4 = [
(t3[0] / term1 - rjl[0] * cos_y / sin_y) / djl,
(t3[1] / term1 - rjl[1] * cos_y / sin_y) / djl,
(t3[2] / term1 - rjl[2] * cos_y / sin_y) / djl,
];
let g1 = ic.at1 * 3;
let g2 = ic.at2 * 3;
let g3 = ic.at3 * 3;
let g4 = ic.at4 * 3;
for dd in 0..3 {
grad[g1 + dd] += de_dw * tg1[dd];
grad[g2 + dd] += -de_dw * (tg1[dd] + tg3[dd] + tg4[dd]);
grad[g3 + dd] += de_dw * tg3[dd];
grad[g4 + dd] += de_dw * tg4[dd];
}
}
macro_rules! dist_grad {
($constraints:expr) => {
for dc in $constraints {
let dx = c(dc.i, 0) - c(dc.j, 0);
let dy = c(dc.i, 1) - c(dc.j, 1);
let dz = c(dc.i, 2) - c(dc.j, 2);
let d2 = dx * dx + dy * dy + dz * dz;
let pre;
if d2 < dc.min_len * dc.min_len {
let d = d2.sqrt().max(1e-8);
pre = dc.k * (d - dc.min_len) / d;
} else if d2 > dc.max_len * dc.max_len {
let d = d2.sqrt().max(1e-8);
pre = dc.k * (d - dc.max_len) / d;
} else {
continue;
}
let gi = dc.i * 3;
let gj = dc.j * 3;
grad[gi] += pre * dx;
grad[gi + 1] += pre * dy;
grad[gi + 2] += pre * dz;
grad[gj] -= pre * dx;
grad[gj + 1] -= pre * dy;
grad[gj + 2] -= pre * dz;
}
};
}
dist_grad!(&ff.dist_12);
for ac in &ff.angle_constraints {
let r1x = c(ac.i, 0) - c(ac.j, 0);
let r1y = c(ac.i, 1) - c(ac.j, 1);
let r1z = c(ac.i, 2) - c(ac.j, 2);
let r2x = c(ac.k, 0) - c(ac.j, 0);
let r2y = c(ac.k, 1) - c(ac.j, 1);
let r2z = c(ac.k, 2) - c(ac.j, 2);
let l1 = (r1x * r1x + r1y * r1y + r1z * r1z).sqrt();
let l2 = (r2x * r2x + r2y * r2y + r2z * r2z).sqrt();
if l1 < 1e-8 || l2 < 1e-8 {
continue;
}
let cos_theta = ((r1x * r2x + r1y * r2y + r1z * r2z) / (l1 * l2)).clamp(-1.0, 1.0);
let theta_deg = cos_theta.acos() * 180.0 / std::f64::consts::PI;
let angle_term = if theta_deg < ac.min_deg {
theta_deg - ac.min_deg
} else if theta_deg > ac.max_deg {
theta_deg - ac.max_deg
} else {
continue;
};
let de_dtheta = 2.0 * ac.force_k * angle_term * (180.0 / std::f64::consts::PI);
let rpx = r2y * r1z - r2z * r1y;
let rpy = r2z * r1x - r2x * r1z;
let rpz = r2x * r1y - r2y * r1x;
let rp_norm = (rpx * rpx + rpy * rpy + rpz * rpz).sqrt();
if rp_norm < 1e-8 {
continue;
}
let prefactor = de_dtheta / rp_norm;
let l1sq = l1 * l1;
let l2sq = l2 * l2;
let cp1x = r1y * rpz - r1z * rpy;
let cp1y = r1z * rpx - r1x * rpz;
let cp1z = r1x * rpy - r1y * rpx;
let dp1x = -cp1x * prefactor / l1sq;
let dp1y = -cp1y * prefactor / l1sq;
let dp1z = -cp1z * prefactor / l1sq;
let cp3x = r2y * rpz - r2z * rpy;
let cp3y = r2z * rpx - r2x * rpz;
let cp3z = r2x * rpy - r2y * rpx;
let dp3x = cp3x * prefactor / l2sq;
let dp3y = cp3y * prefactor / l2sq;
let dp3z = cp3z * prefactor / l2sq;
let gi = ac.i * 3;
let gj = ac.j * 3;
let gk = ac.k * 3;
grad[gi] += dp1x;
grad[gi + 1] += dp1y;
grad[gi + 2] += dp1z;
grad[gj] += -dp1x - dp3x;
grad[gj + 1] += -dp1y - dp3y;
grad[gj + 2] += -dp1z - dp3z;
grad[gk] += dp3x;
grad[gk + 1] += dp3y;
grad[gk + 2] += dp3z;
}
dist_grad!(&ff.dist_13);
dist_grad!(&ff.dist_long);
if ff.torsion_k_omega.abs() > 1e-8 && n >= 4 {
for edge in mol.graph.edge_references() {
let u = edge.source();
let v = edge.target();
let hyb_u = mol.graph[u].hybridization;
let hyb_v = mol.graph[v].hybridization;
if hyb_u == crate::graph::Hybridization::SP || hyb_v == crate::graph::Hybridization::SP
{
continue;
}
let (n_fold, gamma, weight) = crate::forcefield::energy::torsion_params(hyb_u, hyb_v);
let vv = ff.torsion_k_omega * weight as f64;
let nf = n_fold as f64;
let gm = gamma as f64;
let neighbors_u: Vec<_> = mol.graph.neighbors(u).filter(|&x| x != v).collect();
let neighbors_v: Vec<_> = mol.graph.neighbors(v).filter(|&x| x != u).collect();
for &nu in &neighbors_u {
for &nv in &neighbors_v {
let b1 = [
c(u.index(), 0) - c(nu.index(), 0),
c(u.index(), 1) - c(nu.index(), 1),
c(u.index(), 2) - c(nu.index(), 2),
];
let b2 = [
c(v.index(), 0) - c(u.index(), 0),
c(v.index(), 1) - c(u.index(), 1),
c(v.index(), 2) - c(u.index(), 2),
];
let b3 = [
c(nv.index(), 0) - c(v.index(), 0),
c(nv.index(), 1) - c(v.index(), 1),
c(nv.index(), 2) - c(v.index(), 2),
];
let nn1 = [
b1[1] * b2[2] - b1[2] * b2[1],
b1[2] * b2[0] - b1[0] * b2[2],
b1[0] * b2[1] - b1[1] * b2[0],
];
let nn2 = [
b2[1] * b3[2] - b2[2] * b3[1],
b2[2] * b3[0] - b2[0] * b3[2],
b2[0] * b3[1] - b2[1] * b3[0],
];
let n1sq = nn1[0] * nn1[0] + nn1[1] * nn1[1] + nn1[2] * nn1[2];
let n2sq = nn2[0] * nn2[0] + nn2[1] * nn2[1] + nn2[2] * nn2[2];
if n1sq < 1e-10 || n2sq < 1e-10 {
continue;
}
let b2_len = (b2[0] * b2[0] + b2[1] * b2[1] + b2[2] * b2[2]).sqrt();
if b2_len < 1e-6 {
continue;
}
let n1l = n1sq.sqrt();
let n2l = n2sq.sqrt();
let m1 = [
(nn1[1] * b2[2] - nn1[2] * b2[1]) / (n1l * b2_len),
(nn1[2] * b2[0] - nn1[0] * b2[2]) / (n1l * b2_len),
(nn1[0] * b2[1] - nn1[1] * b2[0]) / (n1l * b2_len),
];
let x = (nn1[0] * nn2[0] + nn1[1] * nn2[1] + nn1[2] * nn2[2]) / (n1l * n2l);
let y = m1[0] * nn2[0] / n2l + m1[1] * nn2[1] / n2l + m1[2] * nn2[2] / n2l;
let phi = y.atan2(x);
let de_dphi = -vv * nf * (nf * phi - gm).sin();
let gg1 = [
b2_len / n1sq * nn1[0],
b2_len / n1sq * nn1[1],
b2_len / n1sq * nn1[2],
];
let gg4 = [
-b2_len / n2sq * nn2[0],
-b2_len / n2sq * nn2[1],
-b2_len / n2sq * nn2[2],
];
let b1_dot_b2 =
(b1[0] * b2[0] + b1[1] * b2[1] + b1[2] * b2[2]) / (b2_len * b2_len);
let b3_dot_b2 =
(b3[0] * b2[0] + b3[1] * b2[1] + b3[2] * b2[2]) / (b2_len * b2_len);
let gg2 = [
(-b1_dot_b2 - 1.0) * gg1[0] + b3_dot_b2 * gg4[0],
(-b1_dot_b2 - 1.0) * gg1[1] + b3_dot_b2 * gg4[1],
(-b1_dot_b2 - 1.0) * gg1[2] + b3_dot_b2 * gg4[2],
];
let gg3 = [
(-b3_dot_b2 - 1.0) * gg4[0] + b1_dot_b2 * gg1[0],
(-b3_dot_b2 - 1.0) * gg4[1] + b1_dot_b2 * gg1[1],
(-b3_dot_b2 - 1.0) * gg4[2] + b1_dot_b2 * gg1[2],
];
let gi1 = nu.index() * 3;
let gi2 = u.index() * 3;
let gi3 = v.index() * 3;
let gi4 = nv.index() * 3;
for dd in 0..3 {
grad[gi1 + dd] += de_dphi * gg1[dd];
grad[gi2 + dd] += de_dphi * gg2[dd];
grad[gi3 + dd] += de_dphi * gg3[dd];
grad[gi4 + dd] += de_dphi * gg4[dd];
}
}
}
}
}
grad
}