Scaffold zigzag-engine library
Initial structure implementing the zigzag construction for associative n-categories (LICS 2022 paper). Includes: - monotone.rs: MonotoneMap with Wraith's R equivalence (complete) - zigzag.rs: Zigzag<T>, ZigzagMap<S> with composition - diagram.rs: Diagram, DiagramN, Cospan, Rewrite, Cone types - signature.rs: Generator, Signature (complete) - degeneracy.rs: Degeneracy detection stubs - normalise.rs: Construction 17 algorithm structure - typecheck.rs: Type checking against signatures - explosion.rs: k-points and Poset for layout - layout.rs: SpringConstraint API surface All 33 unit tests pass. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
commit
cd4b951f78
13 changed files with 3365 additions and 0 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/target
|
||||
642
Cargo.lock
generated
Normal file
642
Cargo.lock
generated
Normal file
|
|
@ -0,0 +1,642 @@
|
|||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.102"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
|
||||
|
||||
[[package]]
|
||||
name = "bit-set"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3"
|
||||
dependencies = [
|
||||
"bit-vec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bit-vec"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af"
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||
|
||||
[[package]]
|
||||
name = "errno"
|
||||
version = "0.3.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fastrand"
|
||||
version = "2.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6"
|
||||
|
||||
[[package]]
|
||||
name = "fnv"
|
||||
version = "1.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
||||
|
||||
[[package]]
|
||||
name = "foldhash"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.3.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"r-efi 5.3.0",
|
||||
"wasip2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"r-efi 6.0.0",
|
||||
"wasip2",
|
||||
"wasip3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.15.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
|
||||
dependencies = [
|
||||
"foldhash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.16.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||
|
||||
[[package]]
|
||||
name = "id-arena"
|
||||
version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954"
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.13.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "45a8a2b9cb3e0b0c1803dbb0758ffac5de2f425b23c28f518faabd9d805342ff"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown 0.16.1",
|
||||
"serde",
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
||||
|
||||
[[package]]
|
||||
name = "leb128fmt"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.184"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "48f5d2a454e16a5ea0f4ced81bd44e4cfc7bd3a507b61887c99fd3538b28e4af"
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||
|
||||
[[package]]
|
||||
name = "ppv-lite86"
|
||||
version = "0.2.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
|
||||
dependencies = [
|
||||
"zerocopy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "prettyplease"
|
||||
version = "0.2.37"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.106"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proptest"
|
||||
version = "1.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4b45fcc2344c680f5025fe57779faef368840d0bd1f42f216291f0dc4ace4744"
|
||||
dependencies = [
|
||||
"bit-set",
|
||||
"bit-vec",
|
||||
"bitflags",
|
||||
"num-traits",
|
||||
"rand",
|
||||
"rand_chacha",
|
||||
"rand_xorshift",
|
||||
"regex-syntax",
|
||||
"rusty-fork",
|
||||
"tempfile",
|
||||
"unarray",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quick-error"
|
||||
version = "1.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0"
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.45"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "r-efi"
|
||||
version = "5.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
|
||||
|
||||
[[package]]
|
||||
name = "r-efi"
|
||||
version = "6.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.9.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
|
||||
dependencies = [
|
||||
"rand_chacha",
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_chacha"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
|
||||
dependencies = [
|
||||
"ppv-lite86",
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.9.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c"
|
||||
dependencies = [
|
||||
"getrandom 0.3.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_xorshift"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a"
|
||||
dependencies = [
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.8.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
|
||||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "1.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rusty-fork"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cc6bf79ff24e648f6da1f8d1f011e9cac26491b619e6b9280f2b47f1774e6ee2"
|
||||
dependencies = [
|
||||
"fnv",
|
||||
"quick-error",
|
||||
"tempfile",
|
||||
"wait-timeout",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "semver"
|
||||
version = "1.0.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_core"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.149"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
"serde",
|
||||
"serde_core",
|
||||
"zmij",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.117"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tempfile"
|
||||
version = "3.27.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
|
||||
dependencies = [
|
||||
"fastrand",
|
||||
"getrandom 0.4.2",
|
||||
"once_cell",
|
||||
"rustix",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.69"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "1.0.69"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unarray"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-xid"
|
||||
version = "0.2.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
|
||||
|
||||
[[package]]
|
||||
name = "wait-timeout"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasip2"
|
||||
version = "1.0.2+wasi-0.2.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5"
|
||||
dependencies = [
|
||||
"wit-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasip3"
|
||||
version = "0.4.0+wasi-0.3.0-rc-2026-01-06"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5"
|
||||
dependencies = [
|
||||
"wit-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-encoder"
|
||||
version = "0.244.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319"
|
||||
dependencies = [
|
||||
"leb128fmt",
|
||||
"wasmparser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-metadata"
|
||||
version = "0.244.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"indexmap",
|
||||
"wasm-encoder",
|
||||
"wasmparser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasmparser"
|
||||
version = "0.244.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"hashbrown 0.15.5",
|
||||
"indexmap",
|
||||
"semver",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-link"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.61.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen"
|
||||
version = "0.51.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
|
||||
dependencies = [
|
||||
"wit-bindgen-rust-macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen-core"
|
||||
version = "0.51.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"heck",
|
||||
"wit-parser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen-rust"
|
||||
version = "0.51.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"heck",
|
||||
"indexmap",
|
||||
"prettyplease",
|
||||
"syn",
|
||||
"wasm-metadata",
|
||||
"wit-bindgen-core",
|
||||
"wit-component",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen-rust-macro"
|
||||
version = "0.51.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"prettyplease",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"wit-bindgen-core",
|
||||
"wit-bindgen-rust",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-component"
|
||||
version = "0.244.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bitflags",
|
||||
"indexmap",
|
||||
"log",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"wasm-encoder",
|
||||
"wasm-metadata",
|
||||
"wasmparser",
|
||||
"wit-parser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-parser"
|
||||
version = "0.244.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"id-arena",
|
||||
"indexmap",
|
||||
"log",
|
||||
"semver",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"unicode-xid",
|
||||
"wasmparser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.8.48"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9"
|
||||
dependencies = [
|
||||
"zerocopy-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy-derive"
|
||||
version = "0.8.48"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zigzag-engine"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"proptest",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zmij"
|
||||
version = "1.0.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
|
||||
12
Cargo.toml
Normal file
12
Cargo.toml
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
[package]
|
||||
name = "zigzag-engine"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
description = "Zigzag construction and normalisation for associative n-categories"
|
||||
license = "BSD-3-Clause"
|
||||
|
||||
[dependencies]
|
||||
thiserror = "1"
|
||||
|
||||
[dev-dependencies]
|
||||
proptest = "1"
|
||||
185
src/degeneracy.rs
Normal file
185
src/degeneracy.rs
Normal file
|
|
@ -0,0 +1,185 @@
|
|||
//! Degeneracy maps
|
||||
//!
|
||||
//! Degeneracy maps are the central concept for normalisation. They "inject identity
|
||||
//! structure" into diagrams. A degeneracy map d: N → T witnesses that N is a
|
||||
//! "sub-diagram" of T obtained by removing redundant identity structure.
|
||||
//!
|
||||
//! Key properties:
|
||||
//! - Lemma 6: Isomorphisms are degeneracy maps
|
||||
//! - Lemma 7: Any degeneracy factors UNIQUELY into simple ∘ parallel
|
||||
//! - Lemma 8: Degeneracy maps are monomorphisms
|
||||
//! - Proposition 13: Pullbacks of degeneracies exist and are degeneracies
|
||||
|
||||
use crate::diagram::{Diagram, DiagramMap, Rewrite};
|
||||
use crate::monotone::MonotoneMap;
|
||||
|
||||
/// Result of factoring a degeneracy map into simple ∘ parallel.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DegeneracyFactorisation {
|
||||
/// The simple degeneracy (π-cocartesian over a monomorphism)
|
||||
pub simple: DiagramMap,
|
||||
/// The parallel degeneracy (π-vertical with degeneracy slices)
|
||||
pub parallel: DiagramMap,
|
||||
}
|
||||
|
||||
/// Check if a zigzag map is a simple degeneracy.
|
||||
///
|
||||
/// A simple degeneracy is π-cocartesian over a monomorphism in Δ₊.
|
||||
/// Geometrically, it inserts identity cospans at certain positions.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `singular_map` - The singular component of the zigzag map
|
||||
/// * `_source` - The source diagram (used for slice verification)
|
||||
/// * `_target` - The target diagram (used for slice verification)
|
||||
pub fn is_simple_degeneracy(
|
||||
singular_map: &MonotoneMap,
|
||||
_source: &Diagram,
|
||||
_target: &Diagram,
|
||||
) -> bool {
|
||||
// A simple degeneracy has an injective singular map (face map composition)
|
||||
// and identity slice maps at all heights
|
||||
singular_map.is_injective()
|
||||
// TODO: Also verify that all slice maps are identities
|
||||
}
|
||||
|
||||
/// Check if a diagram map is a parallel degeneracy.
|
||||
///
|
||||
/// A parallel degeneracy is π-vertical (singular map is identity) with
|
||||
/// all slice maps being degeneracies in the lower dimension.
|
||||
pub fn is_parallel_degeneracy(map: &DiagramMap) -> bool {
|
||||
match &map.rewrite {
|
||||
Rewrite::Identity => true,
|
||||
Rewrite::Rewrite0 { source, target } => source == target,
|
||||
Rewrite::RewriteN(r) => {
|
||||
// π-vertical means no cones (singular map is identity)
|
||||
// and all slices must be degeneracies recursively
|
||||
r.cones.is_empty()
|
||||
// TODO: Verify all implicit slice maps are degeneracies
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if a diagram map is a degeneracy map.
|
||||
///
|
||||
/// Degeneracy maps are generated under composition by simple and parallel
|
||||
/// degeneracies. By Lemma 7, every degeneracy factors uniquely into
|
||||
/// simple ∘ parallel.
|
||||
pub fn is_degeneracy(map: &DiagramMap) -> bool {
|
||||
match &map.rewrite {
|
||||
Rewrite::Identity => true,
|
||||
Rewrite::Rewrite0 { source, target } => {
|
||||
// For dimension 0, degeneracies are isomorphisms
|
||||
source == target
|
||||
}
|
||||
Rewrite::RewriteN(_) => {
|
||||
// TODO: Check if factors into simple ∘ parallel
|
||||
// For now, conservatively return false
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Factor a degeneracy map into simple ∘ parallel (Lemma 7).
|
||||
///
|
||||
/// Every degeneracy map d: N → T factors uniquely (up to isomorphism) as:
|
||||
/// ```text
|
||||
/// N --simple--> P --parallel--> T
|
||||
/// ```
|
||||
/// where:
|
||||
/// - The simple part is π-cocartesian over a monomorphism
|
||||
/// - The parallel part is π-vertical with degeneracy slices
|
||||
///
|
||||
/// # Returns
|
||||
/// Some(factorisation) if the map is a degeneracy, None otherwise.
|
||||
pub fn factor_degeneracy(map: &DiagramMap) -> Option<DegeneracyFactorisation> {
|
||||
if !is_degeneracy(map) {
|
||||
return None;
|
||||
}
|
||||
|
||||
// For identity maps, both parts are identity
|
||||
if map.is_identity() {
|
||||
return Some(DegeneracyFactorisation {
|
||||
simple: map.clone(),
|
||||
parallel: map.clone(),
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: Implement proper factorisation algorithm
|
||||
// This requires:
|
||||
// 1. Extract the simple part (the π-projection to Δ₊)
|
||||
// 2. Extract the parallel part (the fiber data)
|
||||
// 3. Construct the intermediate diagram P
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Result of pulling back two degeneracy maps.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DegeneracyPullback {
|
||||
/// The pullback object
|
||||
pub apex: Diagram,
|
||||
/// Projection to the first diagram
|
||||
pub proj1: DiagramMap,
|
||||
/// Projection to the second diagram
|
||||
pub proj2: DiagramMap,
|
||||
}
|
||||
|
||||
/// Compute the pullback of two degeneracy maps (Proposition 13).
|
||||
///
|
||||
/// Given degeneracy maps f: X → T and g: Y → T, compute their pullback P
|
||||
/// with projections p1: P → X and p2: P → Y that are also degeneracies.
|
||||
///
|
||||
/// This is critical for normalisation: Deg(T) is closed under intersection.
|
||||
///
|
||||
/// # Returns
|
||||
/// Some(pullback) if both maps are degeneracies, None otherwise.
|
||||
pub fn pullback_degeneracies(
|
||||
_f: &DiagramMap,
|
||||
_g: &DiagramMap,
|
||||
_target: &Diagram,
|
||||
) -> Option<DegeneracyPullback> {
|
||||
// TODO: Implement pullback computation
|
||||
// Algorithm sketch:
|
||||
// 1. Factor both f and g into simple ∘ parallel
|
||||
// 2. Compute pullback of simple parts (intersection of image ordinals)
|
||||
// 3. Compute pullback of parallel parts (recursive on slices)
|
||||
// 4. Verify projections are degeneracies
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Create a simple degeneracy that inserts an identity cospan at position i.
|
||||
///
|
||||
/// Given a zigzag of length n, returns the π-cocartesian map over dᵢ: n → n+1
|
||||
/// that inserts an identity cospan at singular height i.
|
||||
pub fn simple_degeneracy_at(_diagram: &Diagram, _i: usize) -> DiagramMap {
|
||||
// TODO: Construct the degeneracy map
|
||||
// The singular map is the face map dᵢ
|
||||
// All slice maps are identities
|
||||
// The inserted cospan has identity forward and backward
|
||||
DiagramMap::new(Rewrite::Identity)
|
||||
}
|
||||
|
||||
/// Check if a cospan is an identity cospan (both legs are isomorphisms).
|
||||
pub fn is_identity_cospan(cospan: &crate::diagram::Cospan) -> bool {
|
||||
cospan.is_identity()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_identity_is_degeneracy() {
|
||||
let id = DiagramMap::new(Rewrite::Identity);
|
||||
assert!(is_degeneracy(&id));
|
||||
assert!(is_parallel_degeneracy(&id));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_factor_identity() {
|
||||
let id = DiagramMap::new(Rewrite::Identity);
|
||||
let factored = factor_degeneracy(&id);
|
||||
assert!(factored.is_some());
|
||||
}
|
||||
}
|
||||
375
src/diagram.rs
Normal file
375
src/diagram.rs
Normal file
|
|
@ -0,0 +1,375 @@
|
|||
//! Diagrams in associative n-categories
|
||||
//!
|
||||
//! An n-diagram is an object of Zⁿ(ℕ), the n-fold iterated zigzag category
|
||||
//! over natural numbers. The natural number at each point encodes the dimension
|
||||
//! of the algebraic generator at that location.
|
||||
//!
|
||||
//! The concrete representation uses:
|
||||
//! - `Diagram`: enum of Diagram0 | DiagramN
|
||||
//! - `DiagramN`: source diagram + list of cospans (the zigzag structure)
|
||||
//! - `Cospan`: forward rewrite + backward rewrite
|
||||
//! - `Rewrite`: zigzag map between diagram slices
|
||||
//! - `Cone`: atomic rewrite data
|
||||
|
||||
use crate::signature::Generator;
|
||||
|
||||
/// An n-diagram in the iterated zigzag category.
|
||||
///
|
||||
/// This is the main data structure representing diagrams in associative n-categories.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum Diagram {
|
||||
/// A 0-diagram: a single generator (point in the signature).
|
||||
Diagram0(Generator),
|
||||
/// An n-diagram for n > 0: source + zigzag of cospans.
|
||||
DiagramN(DiagramN),
|
||||
}
|
||||
|
||||
/// An n-dimensional diagram (n > 0).
|
||||
///
|
||||
/// Represented as:
|
||||
/// - A source (n-1)-diagram (the first regular slice)
|
||||
/// - A sequence of cospans encoding the zigzag structure
|
||||
///
|
||||
/// The regular slices r₀, r₁, ..., rₙ are:
|
||||
/// - r₀ = source
|
||||
/// - rᵢ₊₁ = backward.target of cospan i (equivalently, forward.target of cospan i)
|
||||
///
|
||||
/// The singular slices s₀, s₁, ..., sₙ₋₁ are the apexes of each cospan.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct DiagramN {
|
||||
/// The source diagram (first regular slice, r₀)
|
||||
pub source: Box<Diagram>,
|
||||
/// The cospans forming the zigzag structure
|
||||
pub cospans: Vec<Cospan>,
|
||||
}
|
||||
|
||||
impl DiagramN {
|
||||
/// Create a new n-diagram.
|
||||
pub fn new(source: Diagram, cospans: Vec<Cospan>) -> Self {
|
||||
Self {
|
||||
source: Box::new(source),
|
||||
cospans,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create an identity diagram (zigzag of length 0).
|
||||
pub fn identity(source: Diagram) -> Self {
|
||||
Self {
|
||||
source: Box::new(source),
|
||||
cospans: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
/// The zigzag length (number of cospans / singular heights).
|
||||
pub fn length(&self) -> usize {
|
||||
self.cospans.len()
|
||||
}
|
||||
|
||||
/// Get the source (first regular slice).
|
||||
pub fn source(&self) -> &Diagram {
|
||||
&self.source
|
||||
}
|
||||
|
||||
/// Compute the target (last regular slice).
|
||||
///
|
||||
/// For an identity (length 0), this is the same as source.
|
||||
/// Otherwise, we traverse the rewrites to find the final regular slice.
|
||||
pub fn target(&self) -> Diagram {
|
||||
// TODO: Implement proper slice computation through rewrites
|
||||
// For now, return source for identity diagrams
|
||||
if self.cospans.is_empty() {
|
||||
(*self.source).clone()
|
||||
} else {
|
||||
// Placeholder: proper implementation requires traversing cospan structure
|
||||
(*self.source).clone()
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the regular slice at height h.
|
||||
///
|
||||
/// - h = 0: source
|
||||
/// - h > 0: computed by applying rewrites
|
||||
pub fn regular_slice(&self, h: usize) -> Option<Diagram> {
|
||||
if h == 0 {
|
||||
Some((*self.source).clone())
|
||||
} else if h <= self.cospans.len() {
|
||||
// TODO: Compute via rewrite application
|
||||
None
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the singular slice at height h.
|
||||
pub fn singular_slice(&self, h: usize) -> Option<Diagram> {
|
||||
if h < self.cospans.len() {
|
||||
// TODO: Compute via cospan apex
|
||||
None
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A cospan in a zigzag: rₕ → sₕ ← rₕ₊₁
|
||||
///
|
||||
/// The forward rewrite goes from the left regular to the singular.
|
||||
/// The backward rewrite goes from the right regular to the singular.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Cospan {
|
||||
/// Rewrite from left regular slice to singular apex
|
||||
pub forward: Rewrite,
|
||||
/// Rewrite from right regular slice to singular apex
|
||||
pub backward: Rewrite,
|
||||
}
|
||||
|
||||
impl Cospan {
|
||||
/// Create a new cospan.
|
||||
pub fn new(forward: Rewrite, backward: Rewrite) -> Self {
|
||||
Self { forward, backward }
|
||||
}
|
||||
|
||||
/// Check if this is an identity cospan (both legs are isomorphisms).
|
||||
pub fn is_identity(&self) -> bool {
|
||||
self.forward.is_identity() && self.backward.is_identity()
|
||||
}
|
||||
}
|
||||
|
||||
/// A rewrite (zigzag map) between diagram slices.
|
||||
///
|
||||
/// This encodes how one (n-1)-diagram maps to another within the zigzag structure.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum Rewrite {
|
||||
/// Identity rewrite (does nothing).
|
||||
Identity,
|
||||
/// A 0-dimensional rewrite between generators.
|
||||
Rewrite0 {
|
||||
source: Generator,
|
||||
target: Generator,
|
||||
},
|
||||
/// An n-dimensional rewrite (n > 0), encoded as a sequence of cones.
|
||||
RewriteN(RewriteN),
|
||||
}
|
||||
|
||||
impl Rewrite {
|
||||
/// Check if this is an identity rewrite.
|
||||
pub fn is_identity(&self) -> bool {
|
||||
matches!(self, Rewrite::Identity)
|
||||
}
|
||||
|
||||
/// The dimension of this rewrite.
|
||||
pub fn dimension(&self) -> usize {
|
||||
match self {
|
||||
Rewrite::Identity => 0,
|
||||
Rewrite::Rewrite0 { .. } => 0,
|
||||
Rewrite::RewriteN(r) => r.dimension,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An n-dimensional rewrite (n > 0).
|
||||
///
|
||||
/// Encoded as a sequence of cones, where each cone describes an atomic
|
||||
/// transformation at a specific position.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct RewriteN {
|
||||
/// Dimension of this rewrite (> 0)
|
||||
pub dimension: usize,
|
||||
/// The cones encoding the rewrite structure
|
||||
pub cones: Vec<Cone>,
|
||||
}
|
||||
|
||||
impl RewriteN {
|
||||
/// Create a new n-dimensional rewrite.
|
||||
pub fn new(dimension: usize, cones: Vec<Cone>) -> Self {
|
||||
assert!(dimension > 0, "RewriteN dimension must be > 0");
|
||||
Self { dimension, cones }
|
||||
}
|
||||
|
||||
/// Create an identity rewrite at dimension n.
|
||||
pub fn identity(dimension: usize) -> Self {
|
||||
Self {
|
||||
dimension,
|
||||
cones: vec![],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A cone: atomic rewrite data.
|
||||
///
|
||||
/// A cone describes a single "bubble" in a string diagram — a region where
|
||||
/// a source configuration of cospans is replaced by a target cospan.
|
||||
///
|
||||
/// The singular map structure of the parent rewrite is determined by the
|
||||
/// indices of the cones.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Cone {
|
||||
/// Index in the target zigzag where this cone maps to
|
||||
pub index: usize,
|
||||
/// Source configuration (sequence of cospans being contracted)
|
||||
pub source: Vec<Cospan>,
|
||||
/// Target cospan (what the source contracts to)
|
||||
pub target: Cospan,
|
||||
/// Slice rewrites for each interior boundary
|
||||
pub slices: Vec<Rewrite>,
|
||||
}
|
||||
|
||||
impl Cone {
|
||||
/// Create a new cone.
|
||||
pub fn new(index: usize, source: Vec<Cospan>, target: Cospan, slices: Vec<Rewrite>) -> Self {
|
||||
Self {
|
||||
index,
|
||||
source,
|
||||
target,
|
||||
slices,
|
||||
}
|
||||
}
|
||||
|
||||
/// The number of source cospans this cone contracts.
|
||||
pub fn source_size(&self) -> usize {
|
||||
self.source.len()
|
||||
}
|
||||
}
|
||||
|
||||
// === Diagram methods ===
|
||||
|
||||
impl Diagram {
|
||||
/// The dimension of this diagram.
|
||||
pub fn dimension(&self) -> usize {
|
||||
match self {
|
||||
Diagram::Diagram0(_) => 0,
|
||||
Diagram::DiagramN(d) => 1 + d.source.dimension(),
|
||||
}
|
||||
}
|
||||
|
||||
/// The zigzag length at the top level.
|
||||
///
|
||||
/// For a 0-diagram, this is 0.
|
||||
/// For an n-diagram, this is the number of cospans.
|
||||
pub fn length(&self) -> usize {
|
||||
match self {
|
||||
Diagram::Diagram0(_) => 0,
|
||||
Diagram::DiagramN(d) => d.length(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if this is a 0-diagram.
|
||||
pub fn is_zero(&self) -> bool {
|
||||
matches!(self, Diagram::Diagram0(_))
|
||||
}
|
||||
|
||||
/// Create an identity diagram over this one (adds one dimension).
|
||||
pub fn identity(self) -> DiagramN {
|
||||
DiagramN::identity(self)
|
||||
}
|
||||
|
||||
/// Get the source of this diagram.
|
||||
///
|
||||
/// For a 0-diagram, returns None.
|
||||
/// For an n-diagram, returns the source (n-1)-diagram.
|
||||
pub fn source(&self) -> Option<&Diagram> {
|
||||
match self {
|
||||
Diagram::Diagram0(_) => None,
|
||||
Diagram::DiagramN(d) => Some(&d.source),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the target of this diagram.
|
||||
pub fn target(&self) -> Option<Diagram> {
|
||||
match self {
|
||||
Diagram::Diagram0(_) => None,
|
||||
Diagram::DiagramN(d) => Some(d.target()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if this diagram is globular.
|
||||
///
|
||||
/// A diagram is globular if:
|
||||
/// - It's a 0-diagram, OR
|
||||
/// - Its source and target are equal globular diagrams
|
||||
pub fn is_globular(&self) -> bool {
|
||||
match self {
|
||||
Diagram::Diagram0(_) => true,
|
||||
Diagram::DiagramN(d) => {
|
||||
let src = d.source();
|
||||
let tgt = d.target();
|
||||
src.is_globular() && tgt.is_globular() && src == &tgt
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Generator> for Diagram {
|
||||
fn from(g: Generator) -> Self {
|
||||
Diagram::Diagram0(g)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DiagramN> for Diagram {
|
||||
fn from(d: DiagramN) -> Self {
|
||||
Diagram::DiagramN(d)
|
||||
}
|
||||
}
|
||||
|
||||
/// A map between diagrams (zigzag map in the iterated category).
|
||||
///
|
||||
/// This is the full morphism structure, containing the singular map
|
||||
/// and all slice data recursively.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct DiagramMap {
|
||||
/// The rewrite encoding this map
|
||||
pub rewrite: Rewrite,
|
||||
}
|
||||
|
||||
impl DiagramMap {
|
||||
/// Create a diagram map from a rewrite.
|
||||
pub fn new(rewrite: Rewrite) -> Self {
|
||||
Self { rewrite }
|
||||
}
|
||||
|
||||
/// Create an identity map.
|
||||
pub fn identity(_diagram: &Diagram) -> Self {
|
||||
Self {
|
||||
rewrite: Rewrite::Identity,
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if this is an identity map.
|
||||
pub fn is_identity(&self) -> bool {
|
||||
self.rewrite.is_identity()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn test_generator() -> Generator {
|
||||
Generator::new(0, 0, false)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_diagram_0() {
|
||||
let g = test_generator();
|
||||
let d = Diagram::Diagram0(g.clone());
|
||||
assert_eq!(d.dimension(), 0);
|
||||
assert!(d.is_zero());
|
||||
assert!(d.is_globular());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_diagram_n_identity() {
|
||||
let g = test_generator();
|
||||
let d0 = Diagram::Diagram0(g);
|
||||
let d1 = DiagramN::identity(d0);
|
||||
|
||||
assert_eq!(d1.length(), 0);
|
||||
assert_eq!(Diagram::DiagramN(d1).dimension(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cospan_identity() {
|
||||
let c = Cospan::new(Rewrite::Identity, Rewrite::Identity);
|
||||
assert!(c.is_identity());
|
||||
}
|
||||
}
|
||||
309
src/explosion.rs
Normal file
309
src/explosion.rs
Normal file
|
|
@ -0,0 +1,309 @@
|
|||
//! Explosion: k-points and poset structure for layout
|
||||
//!
|
||||
//! Given an n-diagram X, the k-points Ptₖ(X) form a poset that captures
|
||||
//! the combinatorial structure needed for layout. A full layout is a
|
||||
//! graph embedding Ptₙ(X) → ℝⁿ.
|
||||
//!
|
||||
//! This module provides the interface for the layout algorithm without
|
||||
//! implementing the full spring-constraint solver.
|
||||
|
||||
use std::cmp::Ordering;
|
||||
use crate::diagram::Diagram;
|
||||
|
||||
/// A height label in a diagram: either regular or singular.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum HeightLabel {
|
||||
/// Regular height rⱼ
|
||||
Regular(usize),
|
||||
/// Singular height sᵢ
|
||||
Singular(usize),
|
||||
}
|
||||
|
||||
impl HeightLabel {
|
||||
/// Check if this is a regular height.
|
||||
pub fn is_regular(&self) -> bool {
|
||||
matches!(self, HeightLabel::Regular(_))
|
||||
}
|
||||
|
||||
/// Check if this is a singular height.
|
||||
pub fn is_singular(&self) -> bool {
|
||||
matches!(self, HeightLabel::Singular(_))
|
||||
}
|
||||
|
||||
/// Get the index, regardless of regular/singular.
|
||||
pub fn index(&self) -> usize {
|
||||
match self {
|
||||
HeightLabel::Regular(i) => *i,
|
||||
HeightLabel::Singular(i) => *i,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A point in the explosion of a diagram.
|
||||
///
|
||||
/// Points are sequences of height labels, with the sequence length
|
||||
/// equal to the explosion depth k.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct Point(pub Vec<HeightLabel>);
|
||||
|
||||
impl Point {
|
||||
/// Create a new point from height labels.
|
||||
pub fn new(labels: Vec<HeightLabel>) -> Self {
|
||||
Self(labels)
|
||||
}
|
||||
|
||||
/// The empty point (for 0-explosion).
|
||||
pub fn empty() -> Self {
|
||||
Self(vec![])
|
||||
}
|
||||
|
||||
/// The depth of this point (number of coordinates).
|
||||
pub fn depth(&self) -> usize {
|
||||
self.0.len()
|
||||
}
|
||||
|
||||
/// Get the height label at a given depth.
|
||||
pub fn at(&self, depth: usize) -> Option<&HeightLabel> {
|
||||
self.0.get(depth)
|
||||
}
|
||||
|
||||
/// Extend this point with another height label.
|
||||
pub fn extend(&self, label: HeightLabel) -> Self {
|
||||
let mut labels = self.0.clone();
|
||||
labels.push(label);
|
||||
Self(labels)
|
||||
}
|
||||
}
|
||||
|
||||
/// A poset: a set with a partial order.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Poset<T> {
|
||||
/// Elements of the poset
|
||||
elements: Vec<T>,
|
||||
/// Covering relations: (i, j) means elements[i] < elements[j] and is a cover
|
||||
covers: Vec<(usize, usize)>,
|
||||
}
|
||||
|
||||
impl<T: Clone + Eq> Poset<T> {
|
||||
/// Create an empty poset.
|
||||
pub fn empty() -> Self {
|
||||
Self {
|
||||
elements: vec![],
|
||||
covers: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a poset with a single element.
|
||||
pub fn singleton(element: T) -> Self {
|
||||
Self {
|
||||
elements: vec![element],
|
||||
covers: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
/// Number of elements in the poset.
|
||||
pub fn len(&self) -> usize {
|
||||
self.elements.len()
|
||||
}
|
||||
|
||||
/// Check if the poset is empty.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.elements.is_empty()
|
||||
}
|
||||
|
||||
/// Get the elements of the poset.
|
||||
pub fn elements(&self) -> &[T] {
|
||||
&self.elements
|
||||
}
|
||||
|
||||
/// Get the covering relations.
|
||||
pub fn covers(&self) -> &[(usize, usize)] {
|
||||
&self.covers
|
||||
}
|
||||
|
||||
/// Add an element to the poset.
|
||||
pub fn add_element(&mut self, element: T) -> usize {
|
||||
let idx = self.elements.len();
|
||||
self.elements.push(element);
|
||||
idx
|
||||
}
|
||||
|
||||
/// Add a covering relation (a < b).
|
||||
pub fn add_cover(&mut self, lower: usize, upper: usize) {
|
||||
self.covers.push((lower, upper));
|
||||
}
|
||||
|
||||
/// Find an element by value, returning its index.
|
||||
pub fn find(&self, element: &T) -> Option<usize> {
|
||||
self.elements.iter().position(|e| e == element)
|
||||
}
|
||||
}
|
||||
|
||||
/// Compute the k-points of a diagram.
|
||||
///
|
||||
/// Ptₖ(X) is defined inductively:
|
||||
/// - Pt₀(X) = {()} (single empty point)
|
||||
/// - Ptₖ₊₁(X) = {(sᵢ, x) | i ∈ Xˢ, x ∈ Ptₖ(X(sᵢ))}
|
||||
/// ∪ {(rⱼ, x) | j ∈ Xʳ, x ∈ Ptₖ(X(rⱼ))}
|
||||
pub fn k_points(diagram: &Diagram, k: usize) -> Poset<Point> {
|
||||
if k == 0 {
|
||||
// Base case: single empty point
|
||||
Poset::singleton(Point::empty())
|
||||
} else {
|
||||
match diagram {
|
||||
Diagram::Diagram0(_) => {
|
||||
// A 0-diagram has no structure to explode further
|
||||
// But we still need to return the empty point at any k
|
||||
Poset::singleton(Point::empty())
|
||||
}
|
||||
Diagram::DiagramN(d) => {
|
||||
let mut poset = Poset::empty();
|
||||
|
||||
// Add points from regular heights
|
||||
for j in 0..=d.length() {
|
||||
if let Some(slice) = d.regular_slice(j) {
|
||||
let sub_points = k_points(&slice, k - 1);
|
||||
for sub_point in sub_points.elements() {
|
||||
let point = sub_point.extend(HeightLabel::Regular(j));
|
||||
poset.add_element(point);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add points from singular heights
|
||||
for i in 0..d.length() {
|
||||
if let Some(slice) = d.singular_slice(i) {
|
||||
let sub_points = k_points(&slice, k - 1);
|
||||
for sub_point in sub_points.elements() {
|
||||
let point = sub_point.extend(HeightLabel::Singular(i));
|
||||
poset.add_element(point);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Compute covering relations from cospan structure
|
||||
|
||||
poset
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Compute the full explosion of a diagram.
|
||||
///
|
||||
/// Returns Ptₙ(X) where n = dimension(X).
|
||||
pub fn full_points(diagram: &Diagram) -> Poset<Point> {
|
||||
k_points(diagram, diagram.dimension())
|
||||
}
|
||||
|
||||
impl Diagram {
|
||||
/// Compute k-points of this diagram.
|
||||
pub fn points(&self, k: usize) -> Poset<Point> {
|
||||
k_points(self, k)
|
||||
}
|
||||
|
||||
/// Compute full explosion (n-points where n = dimension).
|
||||
pub fn full_points(&self) -> Poset<Point> {
|
||||
full_points(self)
|
||||
}
|
||||
}
|
||||
|
||||
/// Compare two points in the explosion ordering.
|
||||
///
|
||||
/// Points are ordered by the product ordering on height labels,
|
||||
/// where heights are ordered by their natural ordering within
|
||||
/// each dimension.
|
||||
pub fn compare_points(a: &Point, b: &Point) -> Option<Ordering> {
|
||||
if a.depth() != b.depth() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut result = Ordering::Equal;
|
||||
|
||||
for (label_a, label_b) in a.0.iter().zip(b.0.iter()) {
|
||||
let cmp = compare_labels(label_a, label_b)?;
|
||||
match (result, cmp) {
|
||||
(Ordering::Equal, _) => result = cmp,
|
||||
(_, Ordering::Equal) => {}
|
||||
(Ordering::Less, Ordering::Less) => {}
|
||||
(Ordering::Greater, Ordering::Greater) => {}
|
||||
_ => return None, // Incomparable
|
||||
}
|
||||
}
|
||||
|
||||
Some(result)
|
||||
}
|
||||
|
||||
/// Compare two height labels.
|
||||
fn compare_labels(a: &HeightLabel, b: &HeightLabel) -> Option<Ordering> {
|
||||
match (a, b) {
|
||||
(HeightLabel::Regular(i), HeightLabel::Regular(j)) => Some(i.cmp(j)),
|
||||
(HeightLabel::Singular(i), HeightLabel::Singular(j)) => Some(i.cmp(j)),
|
||||
(HeightLabel::Regular(r), HeightLabel::Singular(s)) => {
|
||||
// Regular r comes before singular s if r ≤ s
|
||||
// and after if r > s
|
||||
if *r <= *s {
|
||||
Some(Ordering::Less)
|
||||
} else {
|
||||
Some(Ordering::Greater)
|
||||
}
|
||||
}
|
||||
(HeightLabel::Singular(s), HeightLabel::Regular(r)) => {
|
||||
// Symmetric to above
|
||||
if *s < *r {
|
||||
Some(Ordering::Less)
|
||||
} else {
|
||||
Some(Ordering::Greater)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::signature::Generator;
|
||||
use crate::diagram::DiagramN;
|
||||
|
||||
#[test]
|
||||
fn test_point_creation() {
|
||||
let p = Point::new(vec![
|
||||
HeightLabel::Regular(0),
|
||||
HeightLabel::Singular(1),
|
||||
]);
|
||||
assert_eq!(p.depth(), 2);
|
||||
assert_eq!(p.at(0), Some(&HeightLabel::Regular(0)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_zero_points() {
|
||||
let g = Generator::point(0);
|
||||
let d = Diagram::Diagram0(g);
|
||||
|
||||
let pts = d.points(0);
|
||||
assert_eq!(pts.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_identity_points() {
|
||||
let g = Generator::point(0);
|
||||
let d0 = Diagram::Diagram0(g);
|
||||
let d1 = Diagram::DiagramN(DiagramN::identity(d0));
|
||||
|
||||
// 1-points of an identity should have one point (the regular height)
|
||||
let pts = d1.points(1);
|
||||
assert!(pts.len() >= 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_compare_labels() {
|
||||
assert_eq!(
|
||||
compare_labels(&HeightLabel::Regular(0), &HeightLabel::Regular(1)),
|
||||
Some(Ordering::Less)
|
||||
);
|
||||
assert_eq!(
|
||||
compare_labels(&HeightLabel::Regular(1), &HeightLabel::Singular(0)),
|
||||
Some(Ordering::Greater)
|
||||
);
|
||||
}
|
||||
}
|
||||
320
src/layout.rs
Normal file
320
src/layout.rs
Normal file
|
|
@ -0,0 +1,320 @@
|
|||
//! Layout constraints for diagram rendering
|
||||
//!
|
||||
//! This module defines the constraint types for the layout algorithm.
|
||||
//! The actual solver is NOT implemented here — this is the API surface
|
||||
//! that a spring-constraint rendering system would use.
|
||||
//!
|
||||
//! The layout problem is:
|
||||
//! - Given: explosion Ptₙ(X) as a poset
|
||||
//! - Find: embedding Ptₙ(X) → ℝⁿ
|
||||
//! - Subject to: ordering constraints (hard), fairness constraints (soft),
|
||||
//! and spring constraints (soft, from learned model)
|
||||
|
||||
use std::collections::HashMap;
|
||||
use crate::explosion::Point;
|
||||
|
||||
/// A computed layout: positions for each point in the explosion.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Layout {
|
||||
/// Positions of each point (keyed by point)
|
||||
pub positions: HashMap<Point, Vec<f64>>,
|
||||
/// Computed width of the diagram in each dimension
|
||||
pub dimensions: Vec<f64>,
|
||||
}
|
||||
|
||||
impl Layout {
|
||||
/// Create a new empty layout.
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
positions: HashMap::new(),
|
||||
dimensions: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the position of a point.
|
||||
pub fn position(&self, point: &Point) -> Option<&Vec<f64>> {
|
||||
self.positions.get(point)
|
||||
}
|
||||
|
||||
/// Set the position of a point.
|
||||
pub fn set_position(&mut self, point: Point, position: Vec<f64>) {
|
||||
let dim = position.len();
|
||||
if self.dimensions.len() < dim {
|
||||
self.dimensions.resize(dim, 0.0);
|
||||
}
|
||||
for (i, &p) in position.iter().enumerate() {
|
||||
if p > self.dimensions[i] {
|
||||
self.dimensions[i] = p;
|
||||
}
|
||||
}
|
||||
self.positions.insert(point, position);
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Layout {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// All constraints for a layout problem.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct LayoutConstraints {
|
||||
/// Hard ordering constraints (must be satisfied)
|
||||
pub ordering: Vec<OrderingConstraint>,
|
||||
/// Soft fairness constraints (centering, symmetry)
|
||||
pub fairness: Vec<FairnessConstraint>,
|
||||
/// Soft spring constraints (from learned model)
|
||||
pub springs: Vec<SpringConstraint>,
|
||||
}
|
||||
|
||||
impl LayoutConstraints {
|
||||
/// Create empty constraints.
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Add an ordering constraint.
|
||||
pub fn add_ordering(&mut self, constraint: OrderingConstraint) {
|
||||
self.ordering.push(constraint);
|
||||
}
|
||||
|
||||
/// Add a fairness constraint.
|
||||
pub fn add_fairness(&mut self, constraint: FairnessConstraint) {
|
||||
self.fairness.push(constraint);
|
||||
}
|
||||
|
||||
/// Add a spring constraint.
|
||||
pub fn add_spring(&mut self, constraint: SpringConstraint) {
|
||||
self.springs.push(constraint);
|
||||
}
|
||||
}
|
||||
|
||||
/// Hard ordering constraint: point a must come before point b in dimension d.
|
||||
///
|
||||
/// This encodes: position(a)[d] < position(b)[d]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct OrderingConstraint {
|
||||
/// Point that must come first
|
||||
pub lower: Point,
|
||||
/// Point that must come second
|
||||
pub upper: Point,
|
||||
/// Dimension in which the ordering applies
|
||||
pub dimension: usize,
|
||||
/// Minimum gap between points (default: some small epsilon)
|
||||
pub min_gap: f64,
|
||||
}
|
||||
|
||||
impl OrderingConstraint {
|
||||
/// Create a new ordering constraint with default gap.
|
||||
pub fn new(lower: Point, upper: Point, dimension: usize) -> Self {
|
||||
Self {
|
||||
lower,
|
||||
upper,
|
||||
dimension,
|
||||
min_gap: 1.0, // Default unit gap
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new ordering constraint with specified gap.
|
||||
pub fn with_gap(lower: Point, upper: Point, dimension: usize, min_gap: f64) -> Self {
|
||||
Self {
|
||||
lower,
|
||||
upper,
|
||||
dimension,
|
||||
min_gap,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Soft fairness constraint: centering and symmetry.
|
||||
///
|
||||
/// Typical fairness constraints:
|
||||
/// - Center a point between its neighbours
|
||||
/// - Make sibling points equidistant from their parent
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct FairnessConstraint {
|
||||
/// The point to position
|
||||
pub point: Point,
|
||||
/// Points to center between (if centering)
|
||||
pub reference_points: Vec<Point>,
|
||||
/// Dimension in which to apply fairness
|
||||
pub dimension: usize,
|
||||
/// Weight of this constraint (higher = more important)
|
||||
pub weight: f64,
|
||||
/// Type of fairness
|
||||
pub kind: FairnessKind,
|
||||
}
|
||||
|
||||
/// Types of fairness constraints.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum FairnessKind {
|
||||
/// Center point between reference points
|
||||
Center,
|
||||
/// Make point equidistant from reference points
|
||||
Equidistant,
|
||||
/// Minimize total edge length
|
||||
MinimizeLength,
|
||||
}
|
||||
|
||||
impl FairnessConstraint {
|
||||
/// Create a centering constraint.
|
||||
pub fn center(point: Point, references: Vec<Point>, dimension: usize, weight: f64) -> Self {
|
||||
Self {
|
||||
point,
|
||||
reference_points: references,
|
||||
dimension,
|
||||
weight,
|
||||
kind: FairnessKind::Center,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Spring constraint: pull a point toward a target position.
|
||||
///
|
||||
/// This is the key extension point for the semiotic layer:
|
||||
/// a learned model predicts target positions τ(p), and spring
|
||||
/// constraints pull the layout toward those targets.
|
||||
///
|
||||
/// Energy contribution: k * ||position(p) - target||²
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SpringConstraint {
|
||||
/// The point to constrain
|
||||
pub point: Point,
|
||||
/// Target position (from learned model)
|
||||
pub target_position: Vec<f64>,
|
||||
/// Spring constant (stiffness)
|
||||
pub stiffness: f64,
|
||||
}
|
||||
|
||||
impl SpringConstraint {
|
||||
/// Create a new spring constraint.
|
||||
pub fn new(point: Point, target_position: Vec<f64>, stiffness: f64) -> Self {
|
||||
Self {
|
||||
point,
|
||||
target_position,
|
||||
stiffness,
|
||||
}
|
||||
}
|
||||
|
||||
/// The energy contribution of this spring given a position.
|
||||
pub fn energy(&self, position: &[f64]) -> f64 {
|
||||
if position.len() != self.target_position.len() {
|
||||
return f64::INFINITY;
|
||||
}
|
||||
|
||||
let dist_sq: f64 = position
|
||||
.iter()
|
||||
.zip(&self.target_position)
|
||||
.map(|(p, t)| (p - t).powi(2))
|
||||
.sum();
|
||||
|
||||
self.stiffness * dist_sq
|
||||
}
|
||||
|
||||
/// The gradient of energy with respect to position.
|
||||
pub fn gradient(&self, position: &[f64]) -> Vec<f64> {
|
||||
position
|
||||
.iter()
|
||||
.zip(&self.target_position)
|
||||
.map(|(p, t)| 2.0 * self.stiffness * (p - t))
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
/// A layout solver interface.
|
||||
///
|
||||
/// This trait defines the interface for layout solvers.
|
||||
/// The actual solver (HiGHS for LP/QP) would implement this.
|
||||
pub trait LayoutSolver {
|
||||
/// Solve the layout problem.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `constraints` - The layout constraints
|
||||
/// * `points` - All points that need positions
|
||||
/// * `dimension` - The dimension of the embedding space
|
||||
///
|
||||
/// # Returns
|
||||
/// A `Layout` with positions for all points, or an error.
|
||||
fn solve(
|
||||
&self,
|
||||
constraints: &LayoutConstraints,
|
||||
points: &[Point],
|
||||
dimension: usize,
|
||||
) -> Result<Layout, LayoutError>;
|
||||
}
|
||||
|
||||
/// Errors that can occur during layout solving.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum LayoutError {
|
||||
/// The constraints are infeasible (no valid layout exists).
|
||||
Infeasible,
|
||||
/// The solver failed for an internal reason.
|
||||
SolverError(String),
|
||||
/// The problem is unbounded.
|
||||
Unbounded,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for LayoutError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
LayoutError::Infeasible => write!(f, "Layout constraints are infeasible"),
|
||||
LayoutError::SolverError(msg) => write!(f, "Layout solver error: {}", msg),
|
||||
LayoutError::Unbounded => write!(f, "Layout problem is unbounded"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for LayoutError {}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::explosion::HeightLabel;
|
||||
|
||||
#[test]
|
||||
fn test_layout_creation() {
|
||||
let mut layout = Layout::new();
|
||||
let p = Point::new(vec![HeightLabel::Regular(0)]);
|
||||
layout.set_position(p.clone(), vec![1.0, 2.0]);
|
||||
|
||||
assert_eq!(layout.position(&p), Some(&vec![1.0, 2.0]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_spring_energy() {
|
||||
let p = Point::new(vec![HeightLabel::Regular(0)]);
|
||||
let spring = SpringConstraint::new(p, vec![0.0, 0.0], 1.0);
|
||||
|
||||
// Position at target: zero energy
|
||||
assert_eq!(spring.energy(&[0.0, 0.0]), 0.0);
|
||||
|
||||
// Position at (1, 0): energy = 1 * 1² = 1
|
||||
assert_eq!(spring.energy(&[1.0, 0.0]), 1.0);
|
||||
|
||||
// Position at (1, 1): energy = 1 * (1² + 1²) = 2
|
||||
assert_eq!(spring.energy(&[1.0, 1.0]), 2.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_spring_gradient() {
|
||||
let p = Point::new(vec![HeightLabel::Regular(0)]);
|
||||
let spring = SpringConstraint::new(p, vec![0.0, 0.0], 1.0);
|
||||
|
||||
// Gradient at (1, 2): [2*1*(1-0), 2*1*(2-0)] = [2, 4]
|
||||
let grad = spring.gradient(&[1.0, 2.0]);
|
||||
assert_eq!(grad, vec![2.0, 4.0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ordering_constraint() {
|
||||
let p1 = Point::new(vec![HeightLabel::Regular(0)]);
|
||||
let p2 = Point::new(vec![HeightLabel::Regular(1)]);
|
||||
|
||||
let constraint = OrderingConstraint::new(p1.clone(), p2.clone(), 0);
|
||||
|
||||
assert_eq!(constraint.lower, p1);
|
||||
assert_eq!(constraint.upper, p2);
|
||||
assert_eq!(constraint.dimension, 0);
|
||||
}
|
||||
}
|
||||
48
src/lib.rs
Normal file
48
src/lib.rs
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
//! Zigzag Engine: Standalone library for associative n-categories
|
||||
//!
|
||||
//! This library implements the zigzag construction and normalisation algorithm
|
||||
//! from "Zigzag normalisation for associative n-categories" (Heidemann, Reutter, Vicary, LICS 2022).
|
||||
//!
|
||||
//! # Overview
|
||||
//!
|
||||
//! The zigzag construction provides a combinatorial model of higher categories where:
|
||||
//! - Objects are n-diagrams (iterated zigzags over natural numbers)
|
||||
//! - Morphisms are zigzag maps (structure-preserving transformations)
|
||||
//! - Normalisation removes redundant identity structure while preserving essential identities
|
||||
//!
|
||||
//! # Core Concepts
|
||||
//!
|
||||
//! - [`MonotoneMap`]: Order-preserving maps between finite ordinals
|
||||
//! - [`Zigzag`]: A sequence of cospans with alternating regular/singular objects
|
||||
//! - [`Diagram`]: An n-diagram in the iterated zigzag category
|
||||
//! - [`Rewrite`]: A zigzag map between diagrams
|
||||
//!
|
||||
//! # Example
|
||||
//!
|
||||
//! ```ignore
|
||||
//! use zigzag_engine::{Diagram, normalise};
|
||||
//!
|
||||
//! let diagram: Diagram = /* construct diagram */;
|
||||
//! let result = diagram.normalise();
|
||||
//! assert!(result.normal_form.is_normalised());
|
||||
//! ```
|
||||
|
||||
pub mod monotone;
|
||||
pub mod zigzag;
|
||||
pub mod diagram;
|
||||
pub mod signature;
|
||||
pub mod degeneracy;
|
||||
pub mod normalise;
|
||||
pub mod typecheck;
|
||||
pub mod explosion;
|
||||
pub mod layout;
|
||||
|
||||
// Re-exports for convenience
|
||||
pub use monotone::MonotoneMap;
|
||||
pub use zigzag::{Zigzag, ZigzagMap};
|
||||
pub use diagram::{Diagram, DiagramN, Cospan, Rewrite, Cone};
|
||||
pub use signature::{Generator, Signature};
|
||||
pub use normalise::{NormalisationResult, normalise, normalise_sink};
|
||||
pub use typecheck::{TypeError, type_check};
|
||||
pub use explosion::{Point, HeightLabel};
|
||||
pub use layout::{Layout, LayoutConstraints, SpringConstraint};
|
||||
325
src/monotone.rs
Normal file
325
src/monotone.rs
Normal file
|
|
@ -0,0 +1,325 @@
|
|||
//! Monotone maps in the simplex category Δ₊
|
||||
//!
|
||||
//! The simplex category Δ₊ has:
|
||||
//! - Objects: finite total orders `n = {0, 1, ..., n-1}` for n ≥ 0
|
||||
//! - Morphisms: order-preserving (monotone) maps
|
||||
//!
|
||||
//! Key constructions:
|
||||
//! - Face maps dᵢ: n → n+1 (the unique injective map omitting i from image)
|
||||
//! - Wraith's equivalence R: Δ₊ → Δ₌ᵒᵖ
|
||||
|
||||
use std::ops::Range;
|
||||
|
||||
/// A monotone (order-preserving) map between finite ordinals.
|
||||
///
|
||||
/// Represents f: source → target where source = self.values.len()
|
||||
/// and target = self.target_size.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct MonotoneMap {
|
||||
/// The function values: values[i] = f(i)
|
||||
values: Vec<usize>,
|
||||
/// Size of the target ordinal
|
||||
target_size: usize,
|
||||
}
|
||||
|
||||
impl MonotoneMap {
|
||||
/// Create a new monotone map with explicit target size.
|
||||
///
|
||||
/// # Panics
|
||||
/// Panics if the map is not monotone or values exceed target_size.
|
||||
pub fn new(values: Vec<usize>, target_size: usize) -> Self {
|
||||
// Validate monotonicity
|
||||
for window in values.windows(2) {
|
||||
assert!(
|
||||
window[0] <= window[1],
|
||||
"Map is not monotone: {} > {}",
|
||||
window[0],
|
||||
window[1]
|
||||
);
|
||||
}
|
||||
// Validate range
|
||||
for &v in &values {
|
||||
assert!(
|
||||
v < target_size || (target_size == 0 && values.is_empty()),
|
||||
"Value {} exceeds target size {}",
|
||||
v,
|
||||
target_size
|
||||
);
|
||||
}
|
||||
Self { values, target_size }
|
||||
}
|
||||
|
||||
/// Create the identity map on n.
|
||||
pub fn identity(n: usize) -> Self {
|
||||
Self {
|
||||
values: (0..n).collect(),
|
||||
target_size: n,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create the unique map from 0 (empty ordinal) to n.
|
||||
pub fn from_empty(target_size: usize) -> Self {
|
||||
Self {
|
||||
values: vec![],
|
||||
target_size,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create the face map dᵢ: n → n+1.
|
||||
///
|
||||
/// The face map is the unique injective monotone map omitting i from its image.
|
||||
/// So dᵢ(j) = j if j < i, and dᵢ(j) = j+1 if j ≥ i.
|
||||
///
|
||||
/// # Panics
|
||||
/// Panics if i > n (i must be in 0..=n).
|
||||
pub fn face_map(n: usize, i: usize) -> Self {
|
||||
assert!(i <= n, "Face map index {} exceeds source size {}", i, n);
|
||||
let values: Vec<usize> = (0..n).map(|j| if j < i { j } else { j + 1 }).collect();
|
||||
Self {
|
||||
values,
|
||||
target_size: n + 1,
|
||||
}
|
||||
}
|
||||
|
||||
/// Size of the source ordinal.
|
||||
pub fn source_size(&self) -> usize {
|
||||
self.values.len()
|
||||
}
|
||||
|
||||
/// Size of the target ordinal.
|
||||
pub fn target_size(&self) -> usize {
|
||||
self.target_size
|
||||
}
|
||||
|
||||
/// Apply the map to a value.
|
||||
///
|
||||
/// # Panics
|
||||
/// Panics if i >= source_size.
|
||||
pub fn apply(&self, i: usize) -> usize {
|
||||
self.values[i]
|
||||
}
|
||||
|
||||
/// Compose two monotone maps: (g ∘ f)(x) = g(f(x)).
|
||||
///
|
||||
/// # Panics
|
||||
/// Panics if f.target_size != g.source_size.
|
||||
pub fn compose(&self, other: &MonotoneMap) -> MonotoneMap {
|
||||
assert_eq!(
|
||||
self.target_size,
|
||||
other.source_size(),
|
||||
"Cannot compose: target size {} != source size {}",
|
||||
self.target_size,
|
||||
other.source_size()
|
||||
);
|
||||
let values = self.values.iter().map(|&i| other.apply(i)).collect();
|
||||
MonotoneMap {
|
||||
values,
|
||||
target_size: other.target_size,
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if this map is injective (a monomorphism in Δ₊).
|
||||
pub fn is_injective(&self) -> bool {
|
||||
for window in self.values.windows(2) {
|
||||
if window[0] == window[1] {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
/// Check if this map is surjective (an epimorphism in Δ₊).
|
||||
pub fn is_surjective(&self) -> bool {
|
||||
if self.target_size == 0 {
|
||||
return true;
|
||||
}
|
||||
if self.values.is_empty() {
|
||||
return false;
|
||||
}
|
||||
// For monotone maps, surjective iff image covers all of target
|
||||
// Since monotone, just need first value = 0 and last value = target_size - 1
|
||||
// and no gaps (which monotone + covering endpoints guarantees)
|
||||
self.values.first() == Some(&0)
|
||||
&& self.values.last() == Some(&(self.target_size - 1))
|
||||
&& self.values.len() >= self.target_size
|
||||
}
|
||||
|
||||
/// Check if this is an identity map.
|
||||
pub fn is_identity(&self) -> bool {
|
||||
self.source_size() == self.target_size
|
||||
&& self.values.iter().enumerate().all(|(i, &v)| i == v)
|
||||
}
|
||||
|
||||
/// Check if this is a face map dᵢ for some i, returning i if so.
|
||||
pub fn is_face_map(&self) -> Option<usize> {
|
||||
if self.target_size != self.source_size() + 1 {
|
||||
return None;
|
||||
}
|
||||
if !self.is_injective() {
|
||||
return None;
|
||||
}
|
||||
// Find which element is missing from image
|
||||
let mut missing = None;
|
||||
let mut expected = 0;
|
||||
for &v in &self.values {
|
||||
if v != expected {
|
||||
if missing.is_some() || v != expected + 1 {
|
||||
return None;
|
||||
}
|
||||
missing = Some(expected);
|
||||
expected = v + 1;
|
||||
} else {
|
||||
expected += 1;
|
||||
}
|
||||
}
|
||||
// If no gap found in iteration, the missing element is at the end
|
||||
Some(missing.unwrap_or(self.target_size - 1))
|
||||
}
|
||||
|
||||
/// Compute the preimage f⁻¹(i) as a range.
|
||||
///
|
||||
/// For monotone maps, preimages are always contiguous intervals.
|
||||
pub fn preimage(&self, i: usize) -> Range<usize> {
|
||||
// Find first j where f(j) >= i
|
||||
let start = self.values.iter().position(|&v| v >= i);
|
||||
// Find first j where f(j) > i
|
||||
let end = self.values.iter().position(|&v| v > i);
|
||||
|
||||
match (start, end) {
|
||||
(Some(s), Some(e)) => {
|
||||
if self.values[s] == i {
|
||||
s..e
|
||||
} else {
|
||||
s..s // empty range
|
||||
}
|
||||
}
|
||||
(Some(s), None) => {
|
||||
if self.values[s] == i {
|
||||
s..self.values.len()
|
||||
} else {
|
||||
s..s
|
||||
}
|
||||
}
|
||||
(None, _) => self.values.len()..self.values.len(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Wraith's equivalence R: Δ₊ → Δ₌ᵒᵖ
|
||||
///
|
||||
/// Given f: n → m, returns (Rf)ᵒᵖ: m+1 → n+1 defined by:
|
||||
/// i ↦ min({j ∈ n | f(j) ≥ i} ∪ {n})
|
||||
///
|
||||
/// Geometric intuition: elements of n sit in gaps between elements of m+1,
|
||||
/// and R computes which gap each element of m+1 falls into.
|
||||
pub fn wraith_r(&self) -> MonotoneMap {
|
||||
let n = self.source_size();
|
||||
let m = self.target_size;
|
||||
|
||||
// (Rf)ᵒᵖ: m+1 → n+1
|
||||
let values: Vec<usize> = (0..=m)
|
||||
.map(|i| {
|
||||
// min({j ∈ n | f(j) ≥ i} ∪ {n})
|
||||
self.values
|
||||
.iter()
|
||||
.position(|&fj| fj >= i)
|
||||
.unwrap_or(n)
|
||||
})
|
||||
.collect();
|
||||
|
||||
MonotoneMap {
|
||||
values,
|
||||
target_size: n + 1,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the raw values of the map.
|
||||
pub fn values(&self) -> &[usize] {
|
||||
&self.values
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_identity() {
|
||||
let id = MonotoneMap::identity(3);
|
||||
assert_eq!(id.source_size(), 3);
|
||||
assert_eq!(id.target_size(), 3);
|
||||
assert!(id.is_identity());
|
||||
assert_eq!(id.apply(0), 0);
|
||||
assert_eq!(id.apply(1), 1);
|
||||
assert_eq!(id.apply(2), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_face_map() {
|
||||
// d₀: 2 → 3, omits 0: [1, 2]
|
||||
let d0 = MonotoneMap::face_map(2, 0);
|
||||
assert_eq!(d0.values(), &[1, 2]);
|
||||
assert_eq!(d0.is_face_map(), Some(0));
|
||||
|
||||
// d₁: 2 → 3, omits 1: [0, 2]
|
||||
let d1 = MonotoneMap::face_map(2, 1);
|
||||
assert_eq!(d1.values(), &[0, 2]);
|
||||
assert_eq!(d1.is_face_map(), Some(1));
|
||||
|
||||
// d₂: 2 → 3, omits 2: [0, 1]
|
||||
let d2 = MonotoneMap::face_map(2, 2);
|
||||
assert_eq!(d2.values(), &[0, 1]);
|
||||
assert_eq!(d2.is_face_map(), Some(2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_compose() {
|
||||
let f = MonotoneMap::new(vec![0, 1], 3); // 2 → 3
|
||||
let g = MonotoneMap::new(vec![1, 1, 2], 3); // 3 → 3
|
||||
let gf = f.compose(&g);
|
||||
assert_eq!(gf.values(), &[1, 1]); // g(f(0)) = g(0) = 1, g(f(1)) = g(1) = 1
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_preimage() {
|
||||
let f = MonotoneMap::new(vec![0, 1, 1, 2], 3);
|
||||
assert_eq!(f.preimage(0), 0..1);
|
||||
assert_eq!(f.preimage(1), 1..3);
|
||||
assert_eq!(f.preimage(2), 3..4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_wraith_r() {
|
||||
// Example: f: 2 → 3 given by [0, 2]
|
||||
// Then Rf: 4 → 3 given by i ↦ min({j | f(j) ≥ i} ∪ {2})
|
||||
// i=0: min({j | f(j) ≥ 0}) = min({0,1}) = 0
|
||||
// i=1: min({j | f(j) ≥ 1}) = min({1}) = 1
|
||||
// i=2: min({j | f(j) ≥ 2}) = min({1}) = 1
|
||||
// i=3: min({j | f(j) ≥ 3} ∪ {2}) = min({2}) = 2
|
||||
let f = MonotoneMap::new(vec![0, 2], 3);
|
||||
let rf = f.wraith_r();
|
||||
assert_eq!(rf.source_size(), 4); // m+1 = 4
|
||||
assert_eq!(rf.target_size(), 3); // n+1 = 3
|
||||
assert_eq!(rf.values(), &[0, 1, 1, 2]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_wraith_r_identity() {
|
||||
// For identity n → n, Rf should be identity (n+1) → (n+1)
|
||||
let id = MonotoneMap::identity(3);
|
||||
let r_id = id.wraith_r();
|
||||
assert_eq!(r_id.source_size(), 4);
|
||||
assert_eq!(r_id.target_size(), 4);
|
||||
assert!(r_id.is_identity());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_injective_surjective() {
|
||||
let f = MonotoneMap::new(vec![0, 1, 1, 2], 3);
|
||||
assert!(!f.is_injective());
|
||||
assert!(f.is_surjective());
|
||||
|
||||
let g = MonotoneMap::new(vec![0, 2], 3);
|
||||
assert!(g.is_injective());
|
||||
assert!(!g.is_surjective());
|
||||
}
|
||||
}
|
||||
428
src/normalise.rs
Normal file
428
src/normalise.rs
Normal file
|
|
@ -0,0 +1,428 @@
|
|||
//! Normalisation algorithm (Construction 17)
|
||||
//!
|
||||
//! The normalisation algorithm computes the smallest element of Deg(T),
|
||||
//! the poset of degeneracy subobjects of a diagram T. This removes all
|
||||
//! redundant identity structure while preserving essential identities.
|
||||
//!
|
||||
//! Key insight: In dimension ≥ 4, some identity cospans are ESSENTIAL —
|
||||
//! removing them would make zigzag maps ill-defined (no monotone function
|
||||
//! of the required type exists). The algorithm detects and preserves these.
|
||||
//!
|
||||
//! # Algorithm Overview (Construction 17)
|
||||
//!
|
||||
//! Input: A sink S = (T, {fᵢ: Aᵢ → T})
|
||||
//! Output: Degeneracy d: N → T and factorisations Aᵢ → N
|
||||
//!
|
||||
//! 1. Base case (dim 0): d = identity
|
||||
//! 2. Recursive case:
|
||||
//! a. Normalise at each regular height (recursive)
|
||||
//! b. Normalise at each singular height (recursive, including cospan legs)
|
||||
//! c. Assemble into zigzag P with parallel degeneracy dP
|
||||
//! d. Remove trivial cospans not in image of any sink map
|
||||
//! e. Compose: d = dP ∘ dS
|
||||
|
||||
use crate::diagram::{Diagram, DiagramN, DiagramMap, Rewrite};
|
||||
|
||||
/// Result of normalising a diagram (or sink).
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct NormalisationResult {
|
||||
/// The normalised diagram N
|
||||
pub normal_form: Diagram,
|
||||
/// The degeneracy map d: N → T
|
||||
pub degeneracy: DiagramMap,
|
||||
/// Factorisations of each sink map through the degeneracy
|
||||
pub factorisations: Vec<DiagramMap>,
|
||||
}
|
||||
|
||||
/// A sink: a target diagram with maps from source diagrams.
|
||||
///
|
||||
/// Used for relative normalisation: find the smallest degeneracy
|
||||
/// through which all sink maps factor.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Sink<'a> {
|
||||
/// The target diagram T
|
||||
pub target: &'a Diagram,
|
||||
/// Maps from source diagrams to T
|
||||
pub maps: Vec<DiagramMap>,
|
||||
}
|
||||
|
||||
impl<'a> Sink<'a> {
|
||||
/// Create a new sink.
|
||||
pub fn new(target: &'a Diagram, maps: Vec<DiagramMap>) -> Self {
|
||||
Self { target, maps }
|
||||
}
|
||||
|
||||
/// Create an empty sink (for absolute normalisation).
|
||||
pub fn empty(target: &'a Diagram) -> Self {
|
||||
Self {
|
||||
target,
|
||||
maps: vec![],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Normalise a sink (Construction 17).
|
||||
///
|
||||
/// This is the core normalisation algorithm from the LICS 2022 paper.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `sink` - The sink to normalise (target diagram + incoming maps)
|
||||
///
|
||||
/// # Returns
|
||||
/// A `NormalisationResult` containing:
|
||||
/// - The normal form N
|
||||
/// - The degeneracy d: N → T
|
||||
/// - Factorisations Aᵢ → N for each sink map
|
||||
pub fn normalise_sink(sink: &Sink) -> NormalisationResult {
|
||||
match sink.target {
|
||||
Diagram::Diagram0(_) => {
|
||||
// Base case: dimension 0
|
||||
// The only degeneracy is the identity
|
||||
NormalisationResult {
|
||||
normal_form: sink.target.clone(),
|
||||
degeneracy: DiagramMap::identity(sink.target),
|
||||
factorisations: sink.maps.clone(),
|
||||
}
|
||||
}
|
||||
Diagram::DiagramN(diagram_n) => {
|
||||
normalise_sink_n(diagram_n, &sink.maps)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Normalise an n-dimensional diagram (n > 0).
|
||||
fn normalise_sink_n(target: &DiagramN, sink_maps: &[DiagramMap]) -> NormalisationResult {
|
||||
// Step 1: Normalise at each regular height
|
||||
let regular_normalisations = normalise_regular_heights(target, sink_maps);
|
||||
|
||||
// Step 2: Normalise at each singular height
|
||||
// CRITICAL: Include P(rₕ) → T(rₕ) → T(sₕ) composites in each sink
|
||||
let singular_normalisations = normalise_singular_heights(
|
||||
target,
|
||||
sink_maps,
|
||||
®ular_normalisations,
|
||||
);
|
||||
|
||||
// Step 3: Assemble into zigzag P with parallel degeneracy dP
|
||||
let (p, d_parallel, assembled_factorisations) = assemble(
|
||||
target,
|
||||
®ular_normalisations,
|
||||
&singular_normalisations,
|
||||
);
|
||||
|
||||
// Step 4: Remove trivial cospans not in image of any sink map
|
||||
// A cospan is removable iff:
|
||||
// - Both legs are isomorphisms (identity cospan)
|
||||
// - AND the singular height is not in the image of any sink map
|
||||
let (n, d_simple, final_factorisations) = remove_trivial_cospans(
|
||||
&p,
|
||||
&assembled_factorisations,
|
||||
);
|
||||
|
||||
// Step 5: Compose degeneracies d = dP ∘ dS
|
||||
let degeneracy = compose_degeneracies(&d_parallel, &d_simple);
|
||||
|
||||
NormalisationResult {
|
||||
normal_form: n,
|
||||
degeneracy,
|
||||
factorisations: final_factorisations,
|
||||
}
|
||||
}
|
||||
|
||||
/// Intermediate result for regular height normalisation.
|
||||
struct RegularNormalisation {
|
||||
/// Normalised diagram at this regular height
|
||||
normal_form: Diagram,
|
||||
/// Degeneracy from normal form to original
|
||||
degeneracy: DiagramMap,
|
||||
/// Factorisations for each sink map at this height
|
||||
factorisations: Vec<DiagramMap>,
|
||||
}
|
||||
|
||||
/// Normalise at each regular height of the diagram.
|
||||
fn normalise_regular_heights(
|
||||
target: &DiagramN,
|
||||
sink_maps: &[DiagramMap],
|
||||
) -> Vec<RegularNormalisation> {
|
||||
let num_regular = target.length() + 1;
|
||||
let mut results = Vec::with_capacity(num_regular);
|
||||
|
||||
for h in 0..num_regular {
|
||||
// Get the regular slice T(rₕ)
|
||||
let t_r_h = target.regular_slice(h).unwrap_or_else(|| {
|
||||
// Fallback to source if slice computation not implemented
|
||||
(*target.source).clone()
|
||||
});
|
||||
|
||||
// Collect sink maps restricted to this regular height
|
||||
// Each fᵢ(rₕ): Aᵢ(r_{fᵢʳ(h)}) → T(rₕ)
|
||||
let restricted_maps: Vec<DiagramMap> = sink_maps
|
||||
.iter()
|
||||
.map(|_| DiagramMap::identity(&t_r_h))
|
||||
.collect();
|
||||
|
||||
// Recursively normalise
|
||||
let sub_sink = Sink::new(&t_r_h, restricted_maps);
|
||||
let sub_result = normalise_sink(&sub_sink);
|
||||
|
||||
results.push(RegularNormalisation {
|
||||
normal_form: sub_result.normal_form,
|
||||
degeneracy: sub_result.degeneracy,
|
||||
factorisations: sub_result.factorisations,
|
||||
});
|
||||
}
|
||||
|
||||
results
|
||||
}
|
||||
|
||||
/// Intermediate result for singular height normalisation.
|
||||
struct SingularNormalisation {
|
||||
/// Normalised diagram at this singular height
|
||||
normal_form: Diagram,
|
||||
/// Degeneracy from normal form to original
|
||||
degeneracy: DiagramMap,
|
||||
/// Forward cospan leg from left regular
|
||||
forward_leg: DiagramMap,
|
||||
/// Backward cospan leg from right regular
|
||||
backward_leg: DiagramMap,
|
||||
/// Factorisations for each sink map at this height
|
||||
factorisations: Vec<DiagramMap>,
|
||||
}
|
||||
|
||||
/// Normalise at each singular height of the diagram.
|
||||
///
|
||||
/// CRITICAL: The sink at each singular height includes:
|
||||
/// - Direct singular maps from sink: fᵢ(sₜ) for t ∈ (fᵢˢ)⁻¹(h)
|
||||
/// - Cospan legs: P(rₕ) → T(rₕ) → T(sₕ) and P(rₕ₊₁) → T(rₕ₊₁) → T(sₕ)
|
||||
fn normalise_singular_heights(
|
||||
target: &DiagramN,
|
||||
sink_maps: &[DiagramMap],
|
||||
regular_results: &[RegularNormalisation],
|
||||
) -> Vec<SingularNormalisation> {
|
||||
let num_singular = target.length();
|
||||
let mut results = Vec::with_capacity(num_singular);
|
||||
|
||||
for h in 0..num_singular {
|
||||
// Get the singular slice T(sₕ)
|
||||
let t_s_h = target.singular_slice(h).unwrap_or_else(|| {
|
||||
// Fallback if slice computation not implemented
|
||||
(*target.source).clone()
|
||||
});
|
||||
|
||||
// Build the sink for this singular height:
|
||||
// 1. Direct maps from sink_maps
|
||||
// 2. Composites P(rₕ) → T(rₕ) → T(sₕ)
|
||||
// 3. Composites P(rₕ₊₁) → T(rₕ₊₁) → T(sₕ)
|
||||
let mut combined_maps: Vec<DiagramMap> = Vec::new();
|
||||
|
||||
// Add direct singular maps from sink
|
||||
for _sink_map in sink_maps {
|
||||
// TODO: Extract and add fᵢ(sₜ) for t in preimage of h
|
||||
combined_maps.push(DiagramMap::identity(&t_s_h));
|
||||
}
|
||||
|
||||
// Add cospan leg composites
|
||||
// TODO: Compose regular normalisations with cospan structure
|
||||
combined_maps.push(regular_results[h].degeneracy.clone());
|
||||
combined_maps.push(regular_results[h + 1].degeneracy.clone());
|
||||
|
||||
// Recursively normalise
|
||||
let sub_sink = Sink::new(&t_s_h, combined_maps);
|
||||
let sub_result = normalise_sink(&sub_sink);
|
||||
|
||||
let forward_leg = DiagramMap::identity(&sub_result.normal_form);
|
||||
let backward_leg = DiagramMap::identity(&sub_result.normal_form);
|
||||
|
||||
results.push(SingularNormalisation {
|
||||
normal_form: sub_result.normal_form,
|
||||
degeneracy: sub_result.degeneracy,
|
||||
forward_leg,
|
||||
backward_leg,
|
||||
factorisations: sub_result.factorisations,
|
||||
});
|
||||
}
|
||||
|
||||
results
|
||||
}
|
||||
|
||||
/// Assemble regular and singular normalisations into a zigzag P.
|
||||
///
|
||||
/// Returns:
|
||||
/// - P: the assembled diagram
|
||||
/// - dP: the parallel degeneracy P → T
|
||||
/// - Assembled factorisations
|
||||
fn assemble(
|
||||
target: &DiagramN,
|
||||
regular_results: &[RegularNormalisation],
|
||||
singular_results: &[SingularNormalisation],
|
||||
) -> (Diagram, DiagramMap, Vec<DiagramMap>) {
|
||||
// Build cospans from the normalisation results
|
||||
let cospans: Vec<crate::diagram::Cospan> = singular_results
|
||||
.iter()
|
||||
.map(|sr| {
|
||||
crate::diagram::Cospan::new(
|
||||
sr.forward_leg.rewrite.clone(),
|
||||
sr.backward_leg.rewrite.clone(),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
// The source of P is the first regular normalisation
|
||||
let source = regular_results
|
||||
.first()
|
||||
.map(|r| r.normal_form.clone())
|
||||
.unwrap_or_else(|| (*target.source).clone());
|
||||
|
||||
let p = Diagram::DiagramN(DiagramN::new(source, cospans));
|
||||
|
||||
// The parallel degeneracy is assembled from slice degeneracies
|
||||
// Since all slice maps are degeneracies, the assembled map is parallel
|
||||
let d_parallel = DiagramMap::new(Rewrite::Identity); // TODO: Proper assembly
|
||||
|
||||
// Assemble factorisations
|
||||
let factorisations = regular_results
|
||||
.first()
|
||||
.map(|r| r.factorisations.clone())
|
||||
.unwrap_or_default();
|
||||
|
||||
(p, d_parallel, factorisations)
|
||||
}
|
||||
|
||||
/// Remove trivial cospans from the assembled diagram P.
|
||||
///
|
||||
/// A cospan at singular height h is removable iff:
|
||||
/// 1. Both legs are isomorphisms (identity cospan)
|
||||
/// 2. h is NOT in the image of any sink map's singular map
|
||||
///
|
||||
/// This is where ESSENTIAL IDENTITIES are detected. In dimension ≥ 4,
|
||||
/// some identity cospans must be preserved because removing them would
|
||||
/// make the zigzag maps ill-defined.
|
||||
///
|
||||
/// Returns:
|
||||
/// - N: the diagram with trivial cospans removed
|
||||
/// - dS: the simple degeneracy N → P that re-inserts them
|
||||
/// - Updated factorisations
|
||||
fn remove_trivial_cospans(
|
||||
p: &Diagram,
|
||||
factorisations: &[DiagramMap],
|
||||
) -> (Diagram, DiagramMap, Vec<DiagramMap>) {
|
||||
match p {
|
||||
Diagram::Diagram0(_) => {
|
||||
// No cospans to remove
|
||||
(p.clone(), DiagramMap::identity(p), factorisations.to_vec())
|
||||
}
|
||||
Diagram::DiagramN(diagram_n) => {
|
||||
// Identify which cospans are trivial (identity) and not in sink image
|
||||
let mut kept_cospans = Vec::new();
|
||||
let _removed_indices = Vec::<usize>::new();
|
||||
|
||||
for (h, cospan) in diagram_n.cospans.iter().enumerate() {
|
||||
let is_identity = cospan.is_identity();
|
||||
let in_sink_image = is_in_sink_image(h, factorisations);
|
||||
|
||||
if !is_identity || in_sink_image {
|
||||
// Keep this cospan (either non-trivial or essential)
|
||||
kept_cospans.push(cospan.clone());
|
||||
}
|
||||
// If trivial AND not in sink image, it's removed
|
||||
}
|
||||
|
||||
// Build N with kept cospans
|
||||
let n = Diagram::DiagramN(DiagramN::new(
|
||||
(*diagram_n.source).clone(),
|
||||
kept_cospans,
|
||||
));
|
||||
|
||||
// Build simple degeneracy dS that re-inserts removed cospans
|
||||
let d_simple = DiagramMap::identity(&n); // TODO: Proper construction
|
||||
|
||||
// Update factorisations to go through dS
|
||||
let updated_factorisations = factorisations.to_vec();
|
||||
|
||||
(n, d_simple, updated_factorisations)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if singular height h is in the image of any sink map.
|
||||
fn is_in_sink_image(_h: usize, _factorisations: &[DiagramMap]) -> bool {
|
||||
// TODO: Extract singular maps from factorisations and check if h is in image
|
||||
// For now, conservatively return true (don't remove anything)
|
||||
true
|
||||
}
|
||||
|
||||
/// Compose two degeneracy maps.
|
||||
fn compose_degeneracies(d_parallel: &DiagramMap, d_simple: &DiagramMap) -> DiagramMap {
|
||||
// TODO: Proper composition
|
||||
if d_parallel.is_identity() {
|
||||
d_simple.clone()
|
||||
} else if d_simple.is_identity() {
|
||||
d_parallel.clone()
|
||||
} else {
|
||||
// Full composition needed
|
||||
d_parallel.clone()
|
||||
}
|
||||
}
|
||||
|
||||
/// Absolute normalisation: normalise with empty sink.
|
||||
///
|
||||
/// This computes the smallest degeneracy subobject of the diagram,
|
||||
/// removing all redundant identity structure.
|
||||
pub fn normalise(diagram: &Diagram) -> NormalisationResult {
|
||||
let sink = Sink::empty(diagram);
|
||||
normalise_sink(&sink)
|
||||
}
|
||||
|
||||
impl Diagram {
|
||||
/// Normalise this diagram (absolute normalisation).
|
||||
pub fn normalise(&self) -> NormalisationResult {
|
||||
normalise(self)
|
||||
}
|
||||
|
||||
/// Check if this diagram is normalised (is its own normal form).
|
||||
pub fn is_normalised(&self) -> bool {
|
||||
let result = self.normalise();
|
||||
result.degeneracy.is_identity()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::signature::Generator;
|
||||
|
||||
#[test]
|
||||
fn test_normalise_zero_diagram() {
|
||||
let g = Generator::point(0);
|
||||
let d = Diagram::Diagram0(g);
|
||||
|
||||
let result = d.normalise();
|
||||
|
||||
assert!(result.degeneracy.is_identity());
|
||||
assert_eq!(result.normal_form, d);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_normalise_identity_diagram() {
|
||||
let g = Generator::point(0);
|
||||
let d0 = Diagram::Diagram0(g);
|
||||
let d1 = Diagram::DiagramN(DiagramN::identity(d0.clone()));
|
||||
|
||||
let result = d1.normalise();
|
||||
|
||||
// Identity diagram should normalise to itself
|
||||
// (an identity zigzag of length 0 has no cospans to remove)
|
||||
assert_eq!(result.normal_form.length(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_normalisation_idempotent() {
|
||||
let g = Generator::point(0);
|
||||
let d = Diagram::Diagram0(g);
|
||||
|
||||
let once = d.normalise();
|
||||
let twice = once.normal_form.normalise();
|
||||
|
||||
assert_eq!(once.normal_form, twice.normal_form);
|
||||
}
|
||||
}
|
||||
200
src/signature.rs
Normal file
200
src/signature.rs
Normal file
|
|
@ -0,0 +1,200 @@
|
|||
//! Signatures for typed diagrams
|
||||
//!
|
||||
//! A signature Σ is a set of generators, each with:
|
||||
//! - A unique identifier
|
||||
//! - A dimension
|
||||
//! - Source and target (n-1)-diagrams (for n > 0)
|
||||
//! - An invertibility flag
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// A generator in a signature.
|
||||
///
|
||||
/// Generators are the atomic building blocks of diagrams.
|
||||
/// A generator of dimension n has source and target (n-1)-diagrams.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct Generator {
|
||||
/// Unique identifier
|
||||
pub id: usize,
|
||||
/// Dimension of this generator
|
||||
pub dimension: usize,
|
||||
/// Whether this generator is invertible
|
||||
pub invertible: bool,
|
||||
}
|
||||
|
||||
impl Generator {
|
||||
/// Create a new generator.
|
||||
pub fn new(id: usize, dimension: usize, invertible: bool) -> Self {
|
||||
Self {
|
||||
id,
|
||||
dimension,
|
||||
invertible,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a 0-dimensional generator (a point).
|
||||
pub fn point(id: usize) -> Self {
|
||||
Self {
|
||||
id,
|
||||
dimension: 0,
|
||||
invertible: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Full generator data including source and target.
|
||||
///
|
||||
/// For dimension 0, source and target are None.
|
||||
/// For dimension n > 0, source and target are (n-1)-diagrams.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct GeneratorData {
|
||||
/// The generator itself
|
||||
pub generator: Generator,
|
||||
/// Source diagram (None for dimension 0)
|
||||
pub source: Option<crate::diagram::Diagram>,
|
||||
/// Target diagram (None for dimension 0)
|
||||
pub target: Option<crate::diagram::Diagram>,
|
||||
/// Optional human-readable label
|
||||
pub label: Option<String>,
|
||||
}
|
||||
|
||||
impl GeneratorData {
|
||||
/// Create generator data for a 0-cell.
|
||||
pub fn zero_cell(id: usize, label: Option<String>) -> Self {
|
||||
Self {
|
||||
generator: Generator::point(id),
|
||||
source: None,
|
||||
target: None,
|
||||
label,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create generator data for an n-cell (n > 0).
|
||||
pub fn n_cell(
|
||||
id: usize,
|
||||
dimension: usize,
|
||||
source: crate::diagram::Diagram,
|
||||
target: crate::diagram::Diagram,
|
||||
invertible: bool,
|
||||
label: Option<String>,
|
||||
) -> Self {
|
||||
Self {
|
||||
generator: Generator::new(id, dimension, invertible),
|
||||
source: Some(source),
|
||||
target: Some(target),
|
||||
label,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A signature: a collection of typed generators.
|
||||
///
|
||||
/// The signature provides the "alphabet" for building diagrams
|
||||
/// and is used for type-checking.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct Signature {
|
||||
/// All generators indexed by id
|
||||
generators: HashMap<usize, GeneratorData>,
|
||||
/// Next available id for auto-generation
|
||||
next_id: usize,
|
||||
}
|
||||
|
||||
impl Signature {
|
||||
/// Create an empty signature.
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Add a generator to the signature.
|
||||
///
|
||||
/// Returns the generator's id.
|
||||
pub fn add(&mut self, data: GeneratorData) -> usize {
|
||||
let id = data.generator.id;
|
||||
self.next_id = self.next_id.max(id + 1);
|
||||
self.generators.insert(id, data);
|
||||
id
|
||||
}
|
||||
|
||||
/// Add a 0-cell (point) to the signature.
|
||||
pub fn add_zero_cell(&mut self, label: Option<String>) -> usize {
|
||||
let id = self.next_id;
|
||||
self.add(GeneratorData::zero_cell(id, label))
|
||||
}
|
||||
|
||||
/// Get a generator by id.
|
||||
pub fn get(&self, id: usize) -> Option<&GeneratorData> {
|
||||
self.generators.get(&id)
|
||||
}
|
||||
|
||||
/// Get the generator (without full data) by id.
|
||||
pub fn generator(&self, id: usize) -> Option<&Generator> {
|
||||
self.generators.get(&id).map(|d| &d.generator)
|
||||
}
|
||||
|
||||
/// Check if a generator exists in the signature.
|
||||
pub fn contains(&self, id: usize) -> bool {
|
||||
self.generators.contains_key(&id)
|
||||
}
|
||||
|
||||
/// Iterate over all generators.
|
||||
pub fn generators(&self) -> impl Iterator<Item = &GeneratorData> {
|
||||
self.generators.values()
|
||||
}
|
||||
|
||||
/// Number of generators in the signature.
|
||||
pub fn len(&self) -> usize {
|
||||
self.generators.len()
|
||||
}
|
||||
|
||||
/// Check if the signature is empty.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.generators.is_empty()
|
||||
}
|
||||
|
||||
/// Get all generators of a specific dimension.
|
||||
pub fn generators_of_dimension(&self, dim: usize) -> impl Iterator<Item = &GeneratorData> {
|
||||
self.generators
|
||||
.values()
|
||||
.filter(move |g| g.generator.dimension == dim)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_generator_creation() {
|
||||
let g = Generator::new(0, 2, true);
|
||||
assert_eq!(g.id, 0);
|
||||
assert_eq!(g.dimension, 2);
|
||||
assert!(g.invertible);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_signature_add() {
|
||||
let mut sig = Signature::new();
|
||||
|
||||
let id0 = sig.add_zero_cell(Some("A".to_string()));
|
||||
let id1 = sig.add_zero_cell(Some("B".to_string()));
|
||||
|
||||
assert_eq!(sig.len(), 2);
|
||||
assert!(sig.contains(id0));
|
||||
assert!(sig.contains(id1));
|
||||
|
||||
let g0 = sig.get(id0).unwrap();
|
||||
assert_eq!(g0.generator.dimension, 0);
|
||||
assert_eq!(g0.label, Some("A".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_signature_iteration() {
|
||||
let mut sig = Signature::new();
|
||||
sig.add_zero_cell(None);
|
||||
sig.add_zero_cell(None);
|
||||
sig.add_zero_cell(None);
|
||||
|
||||
let count = sig.generators().count();
|
||||
assert_eq!(count, 3);
|
||||
}
|
||||
}
|
||||
229
src/typecheck.rs
Normal file
229
src/typecheck.rs
Normal file
|
|
@ -0,0 +1,229 @@
|
|||
//! Type checking for diagrams
|
||||
//!
|
||||
//! Type checking verifies that a diagram is well-formed with respect to a signature.
|
||||
//! The procedure:
|
||||
//! 1. Extract singular content from the diagram
|
||||
//! 2. Break the diagram into pieces (one per singular content element)
|
||||
//! 3. Normalise each piece
|
||||
//! 4. Check that each normalised piece matches a signature element
|
||||
|
||||
use crate::diagram::Diagram;
|
||||
use crate::signature::{Signature, Generator};
|
||||
use thiserror::Error;
|
||||
|
||||
/// Errors that can occur during type checking.
|
||||
#[derive(Debug, Clone, Error)]
|
||||
pub enum TypeError {
|
||||
/// A piece of the diagram doesn't match any generator in the signature.
|
||||
#[error("Piece at index {index} does not match any generator in signature")]
|
||||
UnknownGenerator { index: usize },
|
||||
|
||||
/// A generator was found but with wrong dimension.
|
||||
#[error("Generator {id} has dimension {expected}, but piece has dimension {actual}")]
|
||||
DimensionMismatch {
|
||||
id: usize,
|
||||
expected: usize,
|
||||
actual: usize,
|
||||
},
|
||||
|
||||
/// Source/target mismatch for a generator.
|
||||
#[error("Generator {id} source/target does not match diagram boundary")]
|
||||
BoundaryMismatch { id: usize },
|
||||
|
||||
/// The diagram is not globular.
|
||||
#[error("Diagram is not globular")]
|
||||
NotGlobular,
|
||||
|
||||
/// Internal error during normalisation.
|
||||
#[error("Normalisation failed: {message}")]
|
||||
NormalisationError { message: String },
|
||||
}
|
||||
|
||||
/// A piece of singular content from a diagram.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SingularPiece {
|
||||
/// The piece as a sub-diagram
|
||||
pub diagram: Diagram,
|
||||
/// Path to this piece in the original diagram (sequence of singular indices)
|
||||
pub path: Vec<usize>,
|
||||
}
|
||||
|
||||
/// Type check a diagram against a signature.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `diagram` - The diagram to check
|
||||
/// * `signature` - The signature to check against
|
||||
///
|
||||
/// # Returns
|
||||
/// * `Ok(())` if the diagram type-checks
|
||||
/// * `Err(TypeError)` describing the first error found
|
||||
pub fn type_check(diagram: &Diagram, signature: &Signature) -> Result<(), TypeError> {
|
||||
// Check globularity first
|
||||
if !diagram.is_globular() {
|
||||
return Err(TypeError::NotGlobular);
|
||||
}
|
||||
|
||||
// Extract pieces
|
||||
let pieces = extract_pieces(diagram);
|
||||
|
||||
// Check each piece
|
||||
for (index, piece) in pieces.iter().enumerate() {
|
||||
check_piece(piece, signature, index)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Extract the singular content of a diagram.
|
||||
///
|
||||
/// For an n-diagram D:
|
||||
/// - If n = 0: singular content is a 1-element set (the generator itself)
|
||||
/// - If n > 0: singular content is the disjoint union of singular content
|
||||
/// of all singular slices
|
||||
pub fn extract_singular_content(diagram: &Diagram) -> Vec<SingularPiece> {
|
||||
let mut pieces = Vec::new();
|
||||
extract_singular_content_recursive(diagram, &mut vec![], &mut pieces);
|
||||
pieces
|
||||
}
|
||||
|
||||
fn extract_singular_content_recursive(
|
||||
diagram: &Diagram,
|
||||
path: &mut Vec<usize>,
|
||||
pieces: &mut Vec<SingularPiece>,
|
||||
) {
|
||||
match diagram {
|
||||
Diagram::Diagram0(_) => {
|
||||
// Base case: this is a piece of singular content
|
||||
pieces.push(SingularPiece {
|
||||
diagram: diagram.clone(),
|
||||
path: path.clone(),
|
||||
});
|
||||
}
|
||||
Diagram::DiagramN(d) => {
|
||||
// Recurse into each singular slice
|
||||
for i in 0..d.length() {
|
||||
path.push(i);
|
||||
if let Some(slice) = d.singular_slice(i) {
|
||||
extract_singular_content_recursive(&slice, path, pieces);
|
||||
}
|
||||
path.pop();
|
||||
}
|
||||
|
||||
// If no singular slices (identity diagram), the content is the source
|
||||
if d.length() == 0 {
|
||||
extract_singular_content_recursive(&d.source, path, pieces);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Extract pieces from a diagram.
|
||||
///
|
||||
/// Each piece corresponds to one element of singular content,
|
||||
/// extracted as a sub-diagram by taking preimages.
|
||||
pub fn extract_pieces(diagram: &Diagram) -> Vec<SingularPiece> {
|
||||
// For now, this is the same as singular content extraction
|
||||
// A full implementation would construct the actual sub-diagrams
|
||||
extract_singular_content(diagram)
|
||||
}
|
||||
|
||||
/// Check a single piece against the signature.
|
||||
fn check_piece(piece: &SingularPiece, signature: &Signature, index: usize) -> Result<(), TypeError> {
|
||||
// Normalise the piece
|
||||
let normalised = piece.diagram.normalise();
|
||||
|
||||
// The normalised piece should match a generator
|
||||
match &normalised.normal_form {
|
||||
Diagram::Diagram0(g) => {
|
||||
// Check that this generator is in the signature
|
||||
if !signature.contains(g.id) {
|
||||
return Err(TypeError::UnknownGenerator { index });
|
||||
}
|
||||
|
||||
// Check dimension matches
|
||||
if let Some(sig_gen) = signature.generator(g.id) {
|
||||
if sig_gen.dimension != g.dimension {
|
||||
return Err(TypeError::DimensionMismatch {
|
||||
id: g.id,
|
||||
expected: sig_gen.dimension,
|
||||
actual: g.dimension,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
Diagram::DiagramN(d) => {
|
||||
// For n > 0, check if this matches an n-cell in the signature
|
||||
let dim = normalised.normal_form.dimension();
|
||||
|
||||
// Find matching generators of this dimension
|
||||
let matching: Vec<&Generator> = signature
|
||||
.generators_of_dimension(dim)
|
||||
.map(|gd| &gd.generator)
|
||||
.collect();
|
||||
|
||||
if matching.is_empty() {
|
||||
return Err(TypeError::UnknownGenerator { index });
|
||||
}
|
||||
|
||||
// TODO: Check source/target boundaries match
|
||||
// For now, accept if any generator of matching dimension exists
|
||||
let _ = d; // suppress unused warning
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl Diagram {
|
||||
/// Type check this diagram against a signature.
|
||||
pub fn type_check(&self, signature: &Signature) -> Result<(), TypeError> {
|
||||
type_check(self, signature)
|
||||
}
|
||||
|
||||
/// Extract the singular content of this diagram.
|
||||
pub fn singular_content(&self) -> Vec<SingularPiece> {
|
||||
extract_singular_content(self)
|
||||
}
|
||||
|
||||
/// Break this diagram into pieces.
|
||||
pub fn pieces(&self) -> Vec<SingularPiece> {
|
||||
extract_pieces(self)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::signature::GeneratorData;
|
||||
|
||||
#[test]
|
||||
fn test_type_check_zero_cell() {
|
||||
let mut sig = Signature::new();
|
||||
let id = sig.add(GeneratorData::zero_cell(0, Some("A".to_string())));
|
||||
|
||||
let g = Generator::point(id);
|
||||
let d = Diagram::Diagram0(g);
|
||||
|
||||
assert!(d.type_check(&sig).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_type_check_unknown_generator() {
|
||||
let sig = Signature::new(); // Empty signature
|
||||
|
||||
let g = Generator::point(42);
|
||||
let d = Diagram::Diagram0(g);
|
||||
|
||||
let result = d.type_check(&sig);
|
||||
assert!(matches!(result, Err(TypeError::UnknownGenerator { .. })));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_singular_content_zero() {
|
||||
let g = Generator::point(0);
|
||||
let d = Diagram::Diagram0(g);
|
||||
|
||||
let content = d.singular_content();
|
||||
assert_eq!(content.len(), 1);
|
||||
}
|
||||
}
|
||||
291
src/zigzag.rs
Normal file
291
src/zigzag.rs
Normal file
|
|
@ -0,0 +1,291 @@
|
|||
//! Zigzags and zigzag maps
|
||||
//!
|
||||
//! A zigzag of length n is a diagram:
|
||||
//! ```text
|
||||
//! X(r₀) → X(s₀) ← X(r₁) → X(s₁) ← ... → X(sₙ₋₁) ← X(rₙ)
|
||||
//! ```
|
||||
//!
|
||||
//! - Regular objects X(rⱼ) for j ∈ {0,...,n}
|
||||
//! - Singular objects X(sᵢ) for i ∈ {0,...,n-1}
|
||||
//!
|
||||
//! A zigzag map f: X → Y consists of:
|
||||
//! - A singular map fˢ: n → m in Δ₊
|
||||
//! - A derived regular map fʳ = (Rfˢ)ᵒᵖ: m+1 → n+1
|
||||
//! - Slice maps at each height
|
||||
|
||||
use crate::monotone::MonotoneMap;
|
||||
|
||||
/// A zigzag in a category C, parameterized by the object type T.
|
||||
///
|
||||
/// A zigzag of length n has:
|
||||
/// - n+1 regular objects
|
||||
/// - n singular objects
|
||||
/// - n cospans connecting them
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Zigzag<T> {
|
||||
/// Regular objects X(r₀), X(r₁), ..., X(rₙ) — length n+1
|
||||
pub regular: Vec<T>,
|
||||
/// Singular objects X(s₀), X(s₁), ..., X(sₙ₋₁) — length n
|
||||
pub singular: Vec<T>,
|
||||
// Note: The cospan structure maps (forward/backward arrows) are implicit
|
||||
// in the diagram representation; they're encoded in the Rewrite/Cospan types.
|
||||
}
|
||||
|
||||
impl<T> Zigzag<T> {
|
||||
/// Create a new zigzag with the given regular and singular objects.
|
||||
///
|
||||
/// # Panics
|
||||
/// Panics if regular.len() != singular.len() + 1
|
||||
pub fn new(regular: Vec<T>, singular: Vec<T>) -> Self {
|
||||
assert_eq!(
|
||||
regular.len(),
|
||||
singular.len() + 1,
|
||||
"Zigzag requires regular.len() = singular.len() + 1, got {} and {}",
|
||||
regular.len(),
|
||||
singular.len()
|
||||
);
|
||||
Self { regular, singular }
|
||||
}
|
||||
|
||||
/// Create a zigzag of length 0 (single regular object, no singular objects).
|
||||
pub fn point(obj: T) -> Self {
|
||||
Self {
|
||||
regular: vec![obj],
|
||||
singular: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
/// The length of this zigzag (number of singular objects / cospans).
|
||||
pub fn length(&self) -> usize {
|
||||
self.singular.len()
|
||||
}
|
||||
|
||||
/// Number of regular heights (length + 1).
|
||||
pub fn regular_count(&self) -> usize {
|
||||
self.regular.len()
|
||||
}
|
||||
|
||||
/// Number of singular heights (same as length).
|
||||
pub fn singular_count(&self) -> usize {
|
||||
self.singular.len()
|
||||
}
|
||||
|
||||
/// Get regular object at height h.
|
||||
pub fn regular_at(&self, h: usize) -> Option<&T> {
|
||||
self.regular.get(h)
|
||||
}
|
||||
|
||||
/// Get singular object at height h.
|
||||
pub fn singular_at(&self, h: usize) -> Option<&T> {
|
||||
self.singular.get(h)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Clone> Zigzag<T> {
|
||||
/// Map a function over all objects in the zigzag.
|
||||
pub fn map<U, F: Fn(&T) -> U>(&self, f: F) -> Zigzag<U> {
|
||||
Zigzag {
|
||||
regular: self.regular.iter().map(&f).collect(),
|
||||
singular: self.singular.iter().map(&f).collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A map between zigzags.
|
||||
///
|
||||
/// Given zigzags X (length n) and Y (length m), a zigzag map f: X → Y consists of:
|
||||
/// - A singular map fˢ: n → m in Δ₊
|
||||
/// - A derived regular map fʳ = (Rfˢ)ᵒᵖ: m+1 → n+1 in Δ₌
|
||||
/// - Slice data at each height (stored separately in the category-specific implementation)
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct ZigzagMap<S> {
|
||||
/// The singular map fˢ: source_length → target_length
|
||||
pub singular_map: MonotoneMap,
|
||||
/// Slice data at regular heights (one per target regular height)
|
||||
pub regular_slices: Vec<S>,
|
||||
/// Slice data at singular heights (one per source singular height)
|
||||
pub singular_slices: Vec<S>,
|
||||
}
|
||||
|
||||
impl<S> ZigzagMap<S> {
|
||||
/// Create a new zigzag map.
|
||||
///
|
||||
/// # Arguments
|
||||
/// - `singular_map`: The singular map fˢ: n → m
|
||||
/// - `regular_slices`: Slice data at each target regular height (length m+1)
|
||||
/// - `singular_slices`: Slice data at each source singular height (length n)
|
||||
///
|
||||
/// # Panics
|
||||
/// Panics if slice counts don't match the singular map dimensions.
|
||||
pub fn new(
|
||||
singular_map: MonotoneMap,
|
||||
regular_slices: Vec<S>,
|
||||
singular_slices: Vec<S>,
|
||||
) -> Self {
|
||||
let n = singular_map.source_size();
|
||||
let m = singular_map.target_size();
|
||||
|
||||
assert_eq!(
|
||||
regular_slices.len(),
|
||||
m + 1,
|
||||
"Expected {} regular slices, got {}",
|
||||
m + 1,
|
||||
regular_slices.len()
|
||||
);
|
||||
assert_eq!(
|
||||
singular_slices.len(),
|
||||
n,
|
||||
"Expected {} singular slices, got {}",
|
||||
n,
|
||||
singular_slices.len()
|
||||
);
|
||||
|
||||
Self {
|
||||
singular_map,
|
||||
regular_slices,
|
||||
singular_slices,
|
||||
}
|
||||
}
|
||||
|
||||
/// The source zigzag length (n).
|
||||
pub fn source_length(&self) -> usize {
|
||||
self.singular_map.source_size()
|
||||
}
|
||||
|
||||
/// The target zigzag length (m).
|
||||
pub fn target_length(&self) -> usize {
|
||||
self.singular_map.target_size()
|
||||
}
|
||||
|
||||
/// The regular map fʳ = (Rfˢ)ᵒᵖ: m+1 → n+1.
|
||||
///
|
||||
/// This is derived from the singular map via Wraith's R equivalence.
|
||||
pub fn regular_map(&self) -> MonotoneMap {
|
||||
self.singular_map.wraith_r()
|
||||
}
|
||||
|
||||
/// Get the regular slice at target height j.
|
||||
pub fn regular_slice(&self, j: usize) -> Option<&S> {
|
||||
self.regular_slices.get(j)
|
||||
}
|
||||
|
||||
/// Get the singular slice at source height i.
|
||||
pub fn singular_slice(&self, i: usize) -> Option<&S> {
|
||||
self.singular_slices.get(i)
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: Clone> ZigzagMap<S> {
|
||||
/// Compose two zigzag maps: (g ∘ f) where f: X → Y and g: Y → W.
|
||||
///
|
||||
/// Composition rules:
|
||||
/// - (g ∘ f)ˢ = gˢ ∘ fˢ
|
||||
/// - (g ∘ f)(sⱼ) = g(s_{fˢ(j)}) ∘ f(sⱼ)
|
||||
/// - (g ∘ f)(rᵢ) = g(rᵢ) ∘ f(r_{gʳ(i)})
|
||||
///
|
||||
/// Note: This requires a way to compose slice data. The `compose_slices` function
|
||||
/// is provided to combine slice morphisms.
|
||||
pub fn compose<F>(&self, other: &ZigzagMap<S>, compose_slices: F) -> ZigzagMap<S>
|
||||
where
|
||||
F: Fn(&S, &S) -> S,
|
||||
{
|
||||
// Compose singular maps
|
||||
let composed_singular = self.singular_map.compose(&other.singular_map);
|
||||
|
||||
// Get other's regular map for indexing
|
||||
let other_regular = other.regular_map();
|
||||
|
||||
// Compose regular slices: (g ∘ f)(rᵢ) = g(rᵢ) ∘ f(r_{gʳ(i)})
|
||||
let composed_regular: Vec<S> = (0..other.target_length() + 1)
|
||||
.map(|i| {
|
||||
let g_r_i = other_regular.apply(i);
|
||||
let f_slice = &self.regular_slices[g_r_i];
|
||||
let g_slice = &other.regular_slices[i];
|
||||
compose_slices(f_slice, g_slice)
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Compose singular slices: (g ∘ f)(sⱼ) = g(s_{fˢ(j)}) ∘ f(sⱼ)
|
||||
let composed_singular_slices: Vec<S> = (0..self.source_length())
|
||||
.map(|j| {
|
||||
let f_s_j = self.singular_map.apply(j);
|
||||
let f_slice = &self.singular_slices[j];
|
||||
let g_slice = &other.singular_slices[f_s_j];
|
||||
compose_slices(f_slice, g_slice)
|
||||
})
|
||||
.collect();
|
||||
|
||||
ZigzagMap {
|
||||
singular_map: composed_singular,
|
||||
regular_slices: composed_regular,
|
||||
singular_slices: composed_singular_slices,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The π functor: Z(C) → Δ₊, sending zigzags to their lengths.
|
||||
pub fn pi_length<T>(zigzag: &Zigzag<T>) -> usize {
|
||||
zigzag.length()
|
||||
}
|
||||
|
||||
/// The π functor on maps: sends a zigzag map to its singular map.
|
||||
pub fn pi_map<S>(map: &ZigzagMap<S>) -> &MonotoneMap {
|
||||
&map.singular_map
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_zigzag_point() {
|
||||
let z: Zigzag<i32> = Zigzag::point(42);
|
||||
assert_eq!(z.length(), 0);
|
||||
assert_eq!(z.regular_count(), 1);
|
||||
assert_eq!(z.singular_count(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_zigzag_construction() {
|
||||
let z: Zigzag<char> = Zigzag::new(
|
||||
vec!['a', 'b', 'c'],
|
||||
vec!['x', 'y'],
|
||||
);
|
||||
assert_eq!(z.length(), 2);
|
||||
assert_eq!(z.regular_at(0), Some(&'a'));
|
||||
assert_eq!(z.singular_at(1), Some(&'y'));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_zigzag_map_identity() {
|
||||
// Identity map on a length-2 zigzag
|
||||
let id_singular = MonotoneMap::identity(2);
|
||||
let map: ZigzagMap<()> = ZigzagMap::new(
|
||||
id_singular,
|
||||
vec![(), (), ()], // 3 regular slices
|
||||
vec![(), ()], // 2 singular slices
|
||||
);
|
||||
|
||||
assert_eq!(map.source_length(), 2);
|
||||
assert_eq!(map.target_length(), 2);
|
||||
|
||||
let reg_map = map.regular_map();
|
||||
assert!(reg_map.is_identity());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_zigzag_map_regular_derived() {
|
||||
// Singular map: 1 → 2 given by [0] (maps 0 to 0)
|
||||
let singular = MonotoneMap::new(vec![0], 2);
|
||||
let map: ZigzagMap<()> = ZigzagMap::new(
|
||||
singular.clone(),
|
||||
vec![(), (), ()], // 3 regular slices (target has length 2)
|
||||
vec![()], // 1 singular slice (source has length 1)
|
||||
);
|
||||
|
||||
// Regular map should be R([0]): 3 → 2
|
||||
let reg = map.regular_map();
|
||||
assert_eq!(reg.source_size(), 3);
|
||||
assert_eq!(reg.target_size(), 2);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue