Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions catgraph-applied/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ Format based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this c

### Changed

- `MatR::permute_side` takes its two `matmul` results with `.expect`, whose
message names the length guard checked above, instead of silently discarding
an `Err` ([#298](https://github.com/sustia-llc/catgraph/issues/298)).
- Rustdoc reduced to contract statements; this CHANGELOG rewritten to one
bullet per change ([#365](https://github.com/sustia-llc/catgraph/issues/365)).

Expand All @@ -27,6 +30,10 @@ Format based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this c

### Fixed — tests

- `MatR::permute_side` on non-square `MatR<F64Rig>`: entry pins for each value
of `of_codomain` under a 3-cycle, and unchanged-matrix pins where the
permutation's length matches the opposite side's arity
([#298](https://github.com/sustia-llc/catgraph/issues/298)).
- `Presentation` depth-bound contract: `eq_mod` pinned at `Ok(None)` on both
engines (Structural with `A = A;A` at depth 4; CC at depth 0) next to
`Some(true)`/`Some(false)` on pairs that converge; `normalize` pinned at
Expand Down
12 changes: 6 additions & 6 deletions catgraph-applied/src/mat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -425,9 +425,9 @@ impl<R: Rig> SymmetricMonoidalMorphism<()> for MatR<R> {
}
let perm_mat = Self::permutation_matrix(p);
if of_codomain {
if let Ok(result) = self.matmul(&perm_mat) {
*self = result;
}
*self = self.matmul(&perm_mat).expect(
"invariant: p.len() == self.cols checked above, so self.cols == perm_mat.rows",
);
} else {
// P^T has entries[p(i)][i] = 1; equivalently, the transpose of P.
let n = p.len();
Expand All @@ -440,9 +440,9 @@ impl<R: Rig> SymmetricMonoidalMorphism<()> for MatR<R> {
cols: n,
entries,
};
if let Ok(result) = p_transpose.matmul(self) {
*self = result;
}
*self = p_transpose.matmul(self).expect(
"invariant: p.len() == self.rows checked above, so p_transpose.cols == self.rows",
);
}
}
}
99 changes: 97 additions & 2 deletions catgraph-applied/tests/mat.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
//! Integration tests for `MatR<R>` over concrete rigs (`F64Rig`, `BoolRig`,
//! `Tropical`). Exercises identity / matmul / block-diag / permutation
//! correctness and the categorical interchange law.
//! correctness, `permute_side` and its length guard, and the categorical
//! interchange law.

use catgraph::{category::Composable, errors::CatgraphError};
use catgraph::{category::Composable, errors::CatgraphError, monoidal::SymmetricMonoidalMorphism};
use catgraph_applied::{
mat::MatR,
rig::{BoolRig, F64Rig, Tropical},
Expand Down Expand Up @@ -112,6 +113,100 @@ fn permutation_swap_squared_is_identity() {
assert_eq!(mm, MatR::<F64Rig>::identity(3));
}

// ---- permute_side: each side, and the length guard ----

/// `permute_side` for each value of `of_codomain` and each length outcome of
/// the `p.len() != expected` guard, on non-square matrices.
///
/// Both direction pins use the 3-cycle `pc`, whose inverse differs from it, so
/// each separates `β(p)` from `β(p⁻¹)`; the shape is transposed for the domain
/// case to give that side arity 3. A 2-element permutation cannot make that
/// separation — it is its own inverse.
///
/// The two no-op cases feed the permutation that is *valid on the opposite
/// side* of the 2 × 3 — `pc` has length 3 (its codomain arity) and `pd`
/// length 2 (its domain arity) — so a mismatch is rejected by length alone,
/// never because the permutation was an identity.
#[test]
fn permute_side_permutes_the_matching_side_and_no_ops_on_length_mismatch() {
let wide = || {
MatR::<F64Rig>::new(
2,
3,
vec![
vec![F64Rig(1.0), F64Rig(2.0), F64Rig(3.0)],
vec![F64Rig(4.0), F64Rig(5.0), F64Rig(6.0)],
],
)
.unwrap()
};
let tall = || {
MatR::<F64Rig>::new(
3,
2,
vec![
vec![F64Rig(1.0), F64Rig(2.0)],
vec![F64Rig(3.0), F64Rig(4.0)],
vec![F64Rig(5.0), F64Rig(6.0)],
],
)
.unwrap()
};
// apply = [1, 2, 0]
let pc = permutations::Permutation::rotation_left(3, 1);
// apply = [1, 0]
let pd = permutations::Permutation::transposition(2, 0, 1);

let mut codomain = wide();
codomain.permute_side(&pc, true);
assert_eq!(
codomain,
MatR::<F64Rig>::new(
2,
3,
vec![
vec![F64Rig(3.0), F64Rig(1.0), F64Rig(2.0)],
vec![F64Rig(6.0), F64Rig(4.0), F64Rig(5.0)],
],
)
.unwrap(),
"columns permuted by pc: column p.apply(b) receives column b"
);

let mut domain = tall();
domain.permute_side(&pc, false);
assert_eq!(
domain,
MatR::<F64Rig>::new(
3,
2,
vec![
vec![F64Rig(5.0), F64Rig(6.0)],
vec![F64Rig(1.0), F64Rig(2.0)],
vec![F64Rig(3.0), F64Rig(4.0)],
],
)
.unwrap(),
"rows permuted by pc: row p.apply(i) receives row i"
);

let mut wrong_on_codomain = wide();
wrong_on_codomain.permute_side(&pd, true);
assert_eq!(
wrong_on_codomain,
wide(),
"pd has length 2, the codomain arity is 3: guarded no-op"
);

let mut wrong_on_domain = wide();
wrong_on_domain.permute_side(&pc, false);
assert_eq!(
wrong_on_domain,
wide(),
"pc has length 3, the domain arity is 2: guarded no-op"
);
}

// ---- Tropical smoke test: matmul behaves like shortest-path ----

#[test]
Expand Down
Loading