use std::fmt::Write as _;
use std::path::Path;
use crate::{
CircularCoordinate, CircularCoordinateArtifact, CircularCoordinateParams, Cocycle,
CohomologyLimits, KineticEventKind, KineticFiltration, KineticLimits, KineticZigzagArtifact,
KineticZigzagArtifactLimits, PersistentClass, SparseDistanceMatrix, circular_coordinate,
circular_coordinate_for_class, circular_coordinate_with_integral_lift,
cocycle_from_ripser_terms, cohomology_relation, cohomology_space, continue_circular_coordinate,
};
use super::args::{CircularCli, CohomologyCli, KineticCli};
use super::circular_input::read_integral_lift;
use super::class_record::read_persistent_class;
use super::input::{
invalid_input, read_circular_cocycle_bounded, read_kinetic_edges, read_proof_input,
write_phases, write_via_temporary,
};
pub(super) fn run_cohomology(cli: CohomologyCli) -> crate::Result<()> {
let graph = read_proof_input(&cli.input, cli.format, cli.threads, Some(cli.scale))?;
let limits = CohomologyLimits::default();
let space = cohomology_space(&graph, cli.dimension, cli.scale, cli.modulus, limits)?;
println!(
"H{} at {} over Z/{} has rank {} and space id {}",
cli.dimension,
cli.scale,
cli.modulus,
space.rank(),
space.id()
);
for class in space.basis() {
let mut terms = String::new();
for (position, term) in class.terms.iter().enumerate() {
if position != 0 {
terms.push_str(", ");
}
write!(terms, "{:?}:{}", term.simplex, term.coefficient)
.expect("writing to a string cannot fail");
}
println!("basis {} {} [{}]", class.basis_index, class.id, terms);
}
if let Some(other) = cli.other {
let other_graph = read_proof_input(&other, cli.format, cli.threads, Some(cli.scale))?;
let other_space =
cohomology_space(&other_graph, cli.dimension, cli.scale, cli.modulus, limits)?;
let relation = cohomology_relation(&graph, &space, &other_graph, &other_space, limits)?;
println!(
"relation rank {} from image ranks {} and {}, kernel ranks {} and {}; isomorphism {}",
relation.relation_rank,
relation.old_image_rank,
relation.new_image_rank,
relation.old_kernel_rank,
relation.new_kernel_rank,
relation.is_isomorphism()
);
}
Ok(())
}
pub(super) fn run_circular(cli: CircularCli) -> crate::Result<()> {
let params = CircularCoordinateParams {
tolerance: cli.tolerance,
max_iterations: cli.max_iterations,
cohomology: CohomologyLimits::default(),
};
let selected = selected_class(&cli)?;
let scale = circular_scale(selected.as_ref(), cli.scale)?;
validate_circular_continuation(&cli, selected.as_ref())?;
let (graph, coordinate) = prepare_circular_input(&cli, selected.as_ref(), params, scale)?;
write_circular_outputs(&cli, &graph, &coordinate, params, scale)
}
fn prepare_circular_input(
cli: &CircularCli,
selected: Option<&PersistentClass>,
params: CircularCoordinateParams,
scale: f64,
) -> crate::Result<(SparseDistanceMatrix, CircularCoordinate)> {
let graph = read_proof_input(&cli.input, cli.format, cli.threads, Some(scale))?;
let coordinate = circular_coordinate_input(cli, selected, &graph, params, scale)?;
Ok((graph, coordinate))
}
fn write_circular_outputs(
cli: &CircularCli,
graph: &SparseDistanceMatrix,
coordinate: &CircularCoordinate,
params: CircularCoordinateParams,
scale: f64,
) -> crate::Result<()> {
if let Some(path) = &cli.phases {
write_phases(path, &coordinate.phase)?;
}
let artifact = circular_artifact(cli, graph, coordinate, params, scale)?;
let bytes = artifact.encode().map_err(invalid_input)?;
write_via_temporary(&cli.output, &bytes)?;
println!(
"wrote {} circular states, {} coordinates, and {} active edges to {}",
artifact.summary().states,
artifact.summary().coordinates,
artifact.summary().edges,
cli.output.display()
);
Ok(())
}
fn circular_scale(selected: Option<&PersistentClass>, declared: Option<f64>) -> crate::Result<f64> {
selected
.map(|class| class.cocycle.scale)
.or(declared)
.ok_or_else(|| crate::Error::InvalidInput("raw circular cocycle rows require --at".into()))
}
fn validate_circular_continuation(
cli: &CircularCli,
selected: Option<&PersistentClass>,
) -> crate::Result<()> {
if cli.integral_lift.is_none() || cli.other.is_none() {
return Ok(());
}
let modulus = selected
.map(|class| class.cocycle.modulus)
.or(cli.modulus)
.unwrap_or(47);
if modulus == 2 {
return Err(crate::Error::InvalidInput(
"modulus 2 supplied lifts cannot be used with --continue-to; continuation computes the new coordinate automatically".into(),
));
}
Ok(())
}
fn circular_coordinate_input(
cli: &CircularCli,
selected: Option<&PersistentClass>,
graph: &SparseDistanceMatrix,
params: CircularCoordinateParams,
scale: f64,
) -> crate::Result<CircularCoordinate> {
if let Some(path) = &cli.integral_lift {
let cocycle = circular_cocycle_input(cli, selected, graph, scale)?;
let integral = read_integral_lift(path, cli.max_record_bytes, graph.len())?;
return circular_coordinate_with_integral_lift(graph, &cocycle, &integral, params);
}
if let Some(class) = selected {
validate_selected_class(cli, class, scale)?;
circular_coordinate_for_class(graph, class, params)
} else {
raw_circular_coordinate(cli, graph, params, scale)
}
}
fn circular_cocycle_input(
cli: &CircularCli,
selected: Option<&PersistentClass>,
graph: &SparseDistanceMatrix,
scale: f64,
) -> crate::Result<Cocycle> {
if let Some(class) = selected {
validate_selected_class(cli, class, scale)?;
class.validate_provenance(graph)?;
return Ok(class.cocycle.clone());
}
raw_circular_cocycle(cli, graph, scale)
}
fn validate_selected_class(
cli: &CircularCli,
class: &PersistentClass,
scale: f64,
) -> crate::Result<()> {
if cli
.scale
.is_some_and(|declared| declared.to_bits() != scale.to_bits())
{
return Err(crate::Error::InvalidInput(
"--at differs from the selected persistent class scale".into(),
));
}
if cli
.modulus
.is_some_and(|declared| declared != class.cocycle.modulus)
{
return Err(crate::Error::InvalidInput(
"--modulus differs from the selected persistent class field".into(),
));
}
Ok(())
}
fn raw_circular_cocycle(
cli: &CircularCli,
graph: &SparseDistanceMatrix,
scale: f64,
) -> crate::Result<Cocycle> {
let modulus = cli.modulus.unwrap_or(47);
let rows = read_circular_cocycle_bounded(&cli.cocycle, modulus, cli.max_record_bytes)?;
cocycle_from_ripser_terms(graph, modulus, scale, &rows)
}
fn raw_circular_coordinate(
cli: &CircularCli,
graph: &SparseDistanceMatrix,
params: CircularCoordinateParams,
scale: f64,
) -> crate::Result<CircularCoordinate> {
let cocycle = raw_circular_cocycle(cli, graph, scale)?;
circular_coordinate(graph, &cocycle, params)
}
fn selected_class(cli: &CircularCli) -> crate::Result<Option<PersistentClass>> {
let Some(selection) = &cli.class else {
return Ok(None);
};
let [space, basis] = selection.as_slice() else {
return Err(crate::Error::InvalidInput(
"--class needs one class-space position and one basis position".into(),
));
};
read_persistent_class(&cli.cocycle, *space, *basis, cli.max_record_bytes).map(Some)
}
fn circular_artifact(
cli: &CircularCli,
graph: &SparseDistanceMatrix,
coordinate: &CircularCoordinate,
params: CircularCoordinateParams,
scale: f64,
) -> crate::Result<CircularCoordinateArtifact> {
let Some(other_path) = &cli.other else {
return CircularCoordinateArtifact::from_coordinate(graph, coordinate);
};
let other = read_proof_input(other_path, cli.format, cli.threads, Some(scale))?;
let continuation = continue_circular_coordinate(graph, coordinate, &other, params)?;
write_continued_phases(cli, &continuation)?;
eprintln!(
"circular continuation: {:?}, ambiguity rank {}",
continuation.topology.kind,
continuation.topology.ambiguity.len()
);
CircularCoordinateArtifact::from_continuation(
graph,
coordinate,
&other,
&continuation,
params.cohomology,
)
}
fn write_continued_phases(
cli: &CircularCli,
continuation: &crate::CircularCoordinateContinuation,
) -> crate::Result<()> {
let Some(path) = &cli.continued_phases else {
return Ok(());
};
let continued = continuation.coordinate.as_ref().ok_or_else(|| {
crate::Error::InvalidInput(format!(
"continued phases need a unique continuation, got {:?}",
continuation.topology.kind
))
})?;
write_phases(path, &continued.phase)
}
fn kinetic_kind(kind: &KineticEventKind) -> String {
match kind {
KineticEventKind::ThresholdCrossing { edge } => {
format!("threshold ({}, {})", edge.u, edge.v)
}
KineticEventKind::EdgeOrderSwap { first, second } => format!(
"order ({}, {}) with ({}, {})",
first.u, first.v, second.u, second.v
),
}
}
pub(super) fn run_kinetic(cli: KineticCli) -> crate::Result<()> {
let edges = read_kinetic_edges(&cli.input, cli.max_artifact_bytes)?;
let trajectory = KineticFiltration::new(
cli.vertices,
edges,
cli.start,
cli.end,
KineticLimits::default(),
)?;
let schedule = trajectory.events(cli.scale)?;
report_kinetic_schedule(&schedule);
if let (Some(dimension), Some(scale)) = (cli.dimension, cli.scale) {
run_kinetic_cohomology(&cli, &trajectory, dimension, scale)?;
}
Ok(())
}
pub(super) fn report_kinetic_schedule(schedule: &crate::KineticSchedule) {
println!(
"certified {} isolated events and {} persistent ties on [{}, {}]",
schedule.events.len(),
schedule.persistent_ties,
schedule.start,
schedule.end
);
for event in &schedule.events {
let kinds = event
.kinds
.iter()
.map(kinetic_kind)
.collect::<Vec<_>>()
.join(", ");
println!(
"event {} in [{}, {}]: {}",
event.time, event.lower, event.upper, kinds
);
}
}
pub(super) fn run_kinetic_cohomology(
cli: &KineticCli,
trajectory: &KineticFiltration,
dimension: usize,
scale: f64,
) -> crate::Result<()> {
let relations =
trajectory.cohomology_events(dimension, scale, cli.modulus, CohomologyLimits::default())?;
for event in relations {
println!(
"H{} event {}: rank {} to {}, relation rank {}",
dimension,
event.event.time,
event.before_rank,
event.after_rank,
event.relation.relation_rank
);
}
if let Some(output) = &cli.zigzag {
write_kinetic_zigzag(cli, trajectory, dimension, scale, output)?;
}
Ok(())
}
fn write_kinetic_zigzag(
cli: &KineticCli,
trajectory: &KineticFiltration,
dimension: usize,
scale: f64,
output: &Path,
) -> crate::Result<()> {
let limits = KineticZigzagArtifactLimits {
max_bytes: cli.max_artifact_bytes,
..KineticZigzagArtifactLimits::default()
};
let (artifact, zigzag) =
KineticZigzagArtifact::build(trajectory, dimension, scale, cli.modulus, limits)?;
let bytes = artifact.encode(limits)?;
write_via_temporary(output, &bytes)?;
let summary = artifact.summary();
println!(
"wrote H{} kinetic zigzag with {} nodes, {} arrows, {} interval spaces, {} interval copies, and {} bytes",
dimension,
summary.nodes,
summary.arrows,
summary.intervals,
summary.interval_copies,
bytes.len()
);
for interval in zigzag.barcode.intervals {
println!(
"zigzag [{}..={}] multiplicity {} id {}",
interval.start, interval.end, interval.multiplicity, interval.id
);
}
Ok(())
}