use std::{
collections::HashMap,
ops::{MulAssign, Neg},
sync::Arc,
};
use num::Integer;
use crate::quiver_algebra::{
DegreeLabel, HasHomologicalDegree,
checked_arith::{ChainMultiplyable, CheckedAdd, CheckedAddAssign, CheckedArithError, Ring},
dg_path_algebra::GradedDifferentialQuiver,
quiver_rep::QuiverRep,
};
#[must_use]
pub struct DGModule<VertexLabel, EdgeLabel, MatrixType, const OP_ALG: bool>
where
VertexLabel: std::hash::Hash + Eq + Clone,
EdgeLabel: Eq + Clone + std::hash::Hash,
MatrixType: CheckedAdd + CheckedAddAssign + ChainMultiplyable + Clone,
{
rep: QuiverRep<VertexLabel, DegreeLabel<EdgeLabel>, MatrixType, OP_ALG>,
vertex_differentials: HashMap<VertexLabel, MatrixType>,
}
impl<VertexLabel, EdgeLabel, MatrixType, const OP_ALG: bool>
DGModule<VertexLabel, EdgeLabel, MatrixType, OP_ALG>
where
VertexLabel: std::hash::Hash + Eq + Clone,
EdgeLabel: Eq + Clone + std::hash::Hash,
MatrixType: CheckedAdd + CheckedAddAssign + ChainMultiplyable + Clone,
{
pub fn new(
rep: QuiverRep<VertexLabel, DegreeLabel<EdgeLabel>, MatrixType, OP_ALG>,
vertex_differentials: HashMap<VertexLabel, MatrixType>,
validate: Option<fn(&MatrixType) -> bool>,
) -> Result<Self, Vec<VertexLabel>> {
let to_return = Self {
rep,
vertex_differentials,
};
if let Some(matrix_is_zero) = validate {
let all_errors = to_return.differential_squares_zero(matrix_is_zero);
if !all_errors.is_empty() {
return Err(all_errors);
}
}
Ok(to_return)
}
pub fn rep(&self) -> &QuiverRep<VertexLabel, DegreeLabel<EdgeLabel>, MatrixType, OP_ALG> {
&self.rep
}
pub fn vertex_differential(&self, v: &VertexLabel) -> Option<&MatrixType> {
self.vertex_differentials.get(v)
}
pub fn gauge_transform(
&mut self,
gauge_transformation: &HashMap<VertexLabel, (MatrixType, MatrixType)>,
) -> Result<(), CheckedArithError<MatrixType>> {
self.rep.gauge_transform(gauge_transformation)?;
for (v, delta) in &mut self.vertex_differentials {
if let Some((g_inv, g)) = gauge_transformation.get(v) {
*delta = g_inv
.clone()
.chain_multiply_after([delta.clone(), g.clone()])
.map_err(CheckedArithError::from_mul)?;
}
}
Ok(())
}
#[allow(clippy::missing_panics_doc, clippy::similar_names)]
pub fn leibniz_compatible<Coeffs>(
&self,
dga: &GradedDifferentialQuiver<VertexLabel, EdgeLabel, Coeffs, OP_ALG>,
matrix_close_enough: impl Fn(&MatrixType, &MatrixType) -> bool,
) -> Vec<DegreeLabel<EdgeLabel>>
where
Coeffs: Ring,
MatrixType: MulAssign<Coeffs> + Neg<Output = MatrixType>,
{
let quiver = self.rep.quiver();
assert!(Arc::ptr_eq(quiver, dga.quiver()));
let mut failing = Vec::new();
for a in quiver.edge_labels() {
let (src, tgt) = quiver
.edge_endpoint_labels(a)
.expect("edge is in the quiver");
let rho_a = self
.rep
.get_edge_rep(a)
.expect("rep has a matrix for every arrow")
.clone();
let delta_src = self
.vertex_differentials
.get(&src)
.expect("vertex_differentials has an entry for every vertex")
.clone();
let delta_tgt = self
.vertex_differentials
.get(&tgt)
.expect("vertex_differentials has an entry for every vertex")
.clone();
let lhs = MatrixType::mul_two(rho_a.clone(), delta_tgt)
.unwrap_or_else(|_| panic!("compatible dimensions"));
let mut rhs = MatrixType::mul_two(delta_src, rho_a)
.unwrap_or_else(|_| panic!("compatible dimensions"));
if a.homological_degree()
.expect("has homological degree")
.is_odd()
{
rhs = -rhs;
}
#[allow(clippy::collapsible_if)]
if let Some(da) = dga.apply_differential_letter(a) {
if da.might_be_nonzero() {
let rho_da = self
.rep
.mat_from_path_algebra(da)
.unwrap_or_else(|_| panic!("compatible dimensions"));
rhs = rhs
.checked_add(rho_da)
.unwrap_or_else(|_| panic!("compatible dimensions"));
}
}
if !matrix_close_enough(&lhs, &rhs) {
failing.push(a.clone());
}
}
failing
}
#[allow(clippy::missing_panics_doc)]
pub fn differential_squares_zero(
&self,
matrix_is_zero: impl Fn(&MatrixType) -> bool,
) -> Vec<VertexLabel> {
let mut failing = Vec::new();
for (v, delta) in &self.vertex_differentials {
let delta_sq = MatrixType::mul_two(delta.clone(), delta.clone())
.unwrap_or_else(|_| panic!("compatible dimensions"));
if !matrix_is_zero(&delta_sq) {
failing.push(v.clone());
}
}
failing
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::quiver_algebra::{
BasisElt, DegreeLabel, GradedDifferentialQuiver, PathAlgebra, Quiver, QuiverRep,
};
use nonempty::nonempty;
use proptest::{prelude::Strategy, proptest};
use std::sync::Arc;
proptest! {
#[test]
fn leibniz_detects_wrong_action_on_odd_degree_arrows(
nonzero_c in proptest::num::f64::NORMAL.prop_filter("nonzero",|z| z.abs() > 1e-10),
) {
let x = DegreeLabel::new("x".to_string(), 0i64);
let y1 = DegreeLabel::new("y1".to_string(), -1i64);
let y2 = DegreeLabel::new("y2".to_string(), -1i64);
let mut q: Quiver<&str, DegreeLabel<String>> = Quiver::new();
q.add_edge("v", "v", x.clone());
q.add_edge("v", "v", y1.clone());
q.add_edge("v", "v", y2.clone());
let q_arc = Arc::new(q);
let x_elt =
PathAlgebra::singleton(q_arc.clone(), BasisElt::Path(nonempty![x.clone()]), 1.0f64);
let mut differential: HashMap<String, PathAlgebra<&str, DegreeLabel<String>, f64,true>> =
HashMap::new();
differential.insert("y1".to_string(), x_elt.clone());
differential.insert("y2".to_string(), x_elt.clone());
let dga = GradedDifferentialQuiver::new(q_arc.clone(), differential);
let make_module = |c: f64| {
let edge_reps: HashMap<_, _> =
[(x.clone(), c), (y1.clone(), 0.0f64), (y2.clone(), 0.0f64)]
.into_iter()
.collect();
let vertex_reps: HashMap<_, _> = [("v", 1usize)].into_iter().collect();
let rep = QuiverRep::new(q_arc.clone(), edge_reps, vertex_reps, |_| 1.0).unwrap();
let vertex_diffs: HashMap<_, _> = [("v", 0.0f64)].into_iter().collect();
DGModule::new(rep, vertex_diffs, Some(|z: &f64| *z == 0.0)).unwrap()
};
assert!(
make_module(0.0)
.leibniz_compatible(&dga, |a, b| a == b)
.is_empty()
);
let mut failing = make_module(nonzero_c).leibniz_compatible(&dga, |a, b| a == b);
failing.sort_by_key(|e| e.name().clone());
assert_eq!(failing, vec![y1.clone(), y2.clone()]);
}
#[test]
fn leibniz_detects_wrong_action_on_odd_degree_arrows_reg(
nonzero_c in proptest::num::f64::NORMAL.prop_filter("nonzero",|z| z.abs() > 1e-10),
) {
let x = DegreeLabel::new("x".to_string(), 0i64);
let y1 = DegreeLabel::new("y1".to_string(), -1i64);
let y2 = DegreeLabel::new("y2".to_string(), -1i64);
let mut q: Quiver<&str, DegreeLabel<String>> = Quiver::new();
q.add_edge("v", "v", x.clone());
q.add_edge("v", "v", y1.clone());
q.add_edge("v", "v", y2.clone());
let q_arc = Arc::new(q);
let x_elt =
PathAlgebra::singleton(q_arc.clone(), BasisElt::Path(nonempty![x.clone()]), 1.0f64);
let mut differential: HashMap<String, PathAlgebra<&str, DegreeLabel<String>, f64,false>> =
HashMap::new();
differential.insert("y1".to_string(), x_elt.clone());
differential.insert("y2".to_string(), x_elt.clone());
let dga = GradedDifferentialQuiver::new(q_arc.clone(), differential);
let make_module = |c: f64| {
let edge_reps: HashMap<_, _> =
[(x.clone(), c), (y1.clone(), 0.0f64), (y2.clone(), 0.0f64)]
.into_iter()
.collect();
let vertex_reps: HashMap<_, _> = [("v", 1usize)].into_iter().collect();
let rep = QuiverRep::new(q_arc.clone(), edge_reps, vertex_reps, |_| 1.0).unwrap();
let vertex_diffs: HashMap<_, _> = [("v", 0.0f64)].into_iter().collect();
DGModule::new(rep, vertex_diffs, Some(|z: &f64| *z == 0.0)).unwrap()
};
assert!(
make_module(0.0)
.leibniz_compatible(&dga, |a, b| a == b)
.is_empty()
);
let mut failing = make_module(nonzero_c).leibniz_compatible(&dga, |a, b| a == b);
failing.sort_by_key(|e| e.name().clone());
assert_eq!(failing, vec![y1.clone(), y2.clone()]);
}
}
#[test]
fn gauge_transform_two_vertex_arrow() {
use crate::quiver_algebra::{DegreeLabel, DynMatrix, Quiver, QuiverRep};
use nalgebra::DMatrix;
let a = DegreeLabel::new("a".to_string(), 0i64);
let mut q: Quiver<&str, DegreeLabel<String>> = Quiver::new();
q.add_edge("u", "v", a.clone());
let q_arc = Arc::new(q);
let rho_a = DynMatrix(DMatrix::from_vec(2, 1, vec![1.0_f64, 1.0]));
let edge_reps = [(a.clone(), rho_a)].into_iter().collect();
let vertex_dims = [("u", 1usize), ("v", 2usize)].into_iter().collect();
let rep = QuiverRep::new(q_arc.clone(), edge_reps, vertex_dims, |n| {
DynMatrix::zeros(n, n)
})
.unwrap();
let d_v = DynMatrix(DMatrix::from_vec(2, 2, vec![0.0, 1.0, 0.0, 0.0]));
let d_u = DynMatrix(DMatrix::from_vec(1, 1, vec![0.0]));
let vertex_diffs = [("u", d_u), ("v", d_v)].into_iter().collect();
let is_zero = |m: &DynMatrix<f64>| m.0.iter().all(|&x| x.abs() < 1e-10);
let mut module = DGModule::<_, _, _, true>::new(rep, vertex_diffs, Some(is_zero)).unwrap();
let g_u_inv = DynMatrix(DMatrix::from_vec(1, 1, vec![0.5]));
let g_u = DynMatrix(DMatrix::from_vec(1, 1, vec![2.0]));
let g_v = DynMatrix(DMatrix::from_vec(2, 2, vec![2.0, 0.0, 0.0, 3.0]));
let g_v_inv = DynMatrix(DMatrix::from_vec(2, 2, vec![0.5, 0.0, 0.0, 1.0 / 3.0]));
let gauge = [("u", (g_u_inv, g_u)), ("v", (g_v_inv, g_v))]
.into_iter()
.collect();
module
.gauge_transform(&gauge)
.map_err(|_| ())
.expect("gauge transform succeeds");
let expected_rho = DynMatrix(DMatrix::from_vec(2, 1, vec![1.0, 1.5]));
let actual_rho = module.rep().get_edge_rep(&a).unwrap();
assert!(
(actual_rho.0.clone() - &expected_rho.0).abs().max() < 1e-10,
"ρ(a)' wrong: got {actual_rho:?}"
);
let actual_du = module.vertex_differential(&"u").unwrap();
assert!(is_zero(actual_du), "d_M_u should stay zero");
let expected_dv = DynMatrix(DMatrix::from_vec(2, 2, vec![0.0, 1.5, 0.0, 0.0]));
let actual_dv = module.vertex_differential(&"v").unwrap();
assert!(
(actual_dv.0.clone() - &expected_dv.0).abs().max() < 1e-10,
"d_M_v' wrong: got {actual_dv:?}"
);
}
}