Skip to main content

open_hypergraphs/finite_function/
arrow.rs

1use crate::array::*;
2use crate::category::*;
3
4use core::fmt::Debug;
5use core::ops::{Add, BitOr, Shr};
6use num_traits::{One, Zero};
7
8/// A finite function is an array of indices in a range `{0..N}` for some `N ∈ Nat`
9#[derive(Eq)]
10pub struct FiniteFunction<K: ArrayKind> {
11    pub table: K::Index,
12    pub target: K::I,
13}
14
15// Can't use derived PartialEq because it introduces unwanted bound `K: PartialEq`.
16impl<K: ArrayKind> PartialEq for FiniteFunction<K> {
17    fn eq(&self, other: &Self) -> bool {
18        self.table == other.table && self.target == other.target
19    }
20}
21
22// Ad-hoc methods for finite functions
23impl<K: ArrayKind> FiniteFunction<K> {
24    /// Construct a FiniteFunction from a table of indices
25    pub fn new(table: K::Index, target: K::I) -> Option<FiniteFunction<K>> {
26        // If table was nonempty and had a value larger or equal to codomain, this is invalid.
27        // TODO: should check that table min is greater than zero!
28        if let Some(true) = table.max().map(|m| m >= target) {
29            return None;
30        }
31        Some(FiniteFunction { table, target })
32    }
33
34    /// The length-`a` array of zeroes `!_a : a → 1`.
35    /// TODO: doctest
36    pub fn terminal(a: K::I) -> Self {
37        let table = K::Index::fill(K::I::zero(), a);
38        let target = K::I::one();
39        FiniteFunction { table, target }
40    }
41
42    /// Construct the constant finite function `f : a → x + 1 + b`,
43    /// an array of length `a` mapping all elements to `x`.
44    ///
45    /// Note that the *target* of the finite function must be at least `x+1` to be valid.
46    /// This is equivalent to `!_a ; ι₁ ; ι₀`, where
47    ///
48    /// - `!_a : a → 1` is the terminal map
49    /// - `ι₁ : 1 → x+1` and `ι₀ : x+1 → x+1+b` are injections.
50    ///
51    /// ```rust
52    /// # use open_hypergraphs::category::*;
53    /// # use open_hypergraphs::array::vec::*;
54    /// # use open_hypergraphs::finite_function::*;
55    /// let (x, a, b) = (2, 3, 2);
56    ///
57    /// let actual = FiniteFunction::<VecKind>::constant(a, x, b);
58    /// let expected = FiniteFunction::new(VecArray(vec![2, 2, 2]), x + b + 1).unwrap();
59    /// assert_eq!(actual, expected);
60    ///
61    /// // Check equal to `!_a ; ι₁ ; ι₀`
62    /// let i1 = FiniteFunction::<VecKind>::inj1(x, 1);
63    /// let i0 = FiniteFunction::inj0(x + 1, b);
64    /// let f  = FiniteFunction::terminal(a);
65    /// let h  = f.compose(&i1).expect("b").compose(&i0).expect("c");
66    /// assert_eq!(actual, h);
67    /// ```
68    pub fn constant(a: K::I, x: K::I, b: K::I) -> Self {
69        let table = K::Index::fill(x.clone(), a);
70        let target = x + b + K::I::one(); // We need the +1 to ensure entries in range.
71        FiniteFunction { table, target }
72    }
73
74    /// Directly construct `f ; ι₀` instead of computing by composition.
75    ///
76    /// ```rust
77    /// # use open_hypergraphs::array::vec::*;
78    /// # use open_hypergraphs::category::*;
79    /// # use open_hypergraphs::finite_function::*;
80    /// # let f = FiniteFunction::<VecKind>::identity(5);
81    /// # let b = 3;
82    /// # let i0 = FiniteFunction::<VecKind>::inj0(f.target(), b);
83    /// assert_eq!(Some(f.inject0(b)), &f >> &i0);
84    /// ```
85    pub fn inject0(&self, b: K::I) -> FiniteFunction<K> {
86        FiniteFunction {
87            table: self.table.clone(),
88            target: b + self.target(),
89        }
90    }
91
92    /// Directly construct `f ; ι₁` instead of computing by composition.
93    ///
94    /// ```rust
95    /// # use open_hypergraphs::array::vec::*;
96    /// # use open_hypergraphs::category::*;
97    /// # use open_hypergraphs::finite_function::*;
98    /// # let f = FiniteFunction::<VecKind>::identity(5);
99    /// # let a = 3;
100    /// # let i1 = FiniteFunction::<VecKind>::inj1(a, f.target());
101    /// assert_eq!(Some(f.inject1(a)), &f >> &i1);
102    /// ```
103    pub fn inject1(&self, a: K::I) -> FiniteFunction<K> {
104        FiniteFunction {
105            table: a.clone() + &self.table,
106            target: a + self.target.clone(),
107        }
108    }
109
110    /// Given a finite function `f : A → B`, return the initial map `initial : 0 → B`.
111    pub fn to_initial(&self) -> FiniteFunction<K> {
112        Self::initial(self.target.clone())
113    }
114
115    pub fn coequalizer(&self, other: &Self) -> Option<FiniteFunction<K>> {
116        // if self is parallel to other
117        if self.source() != other.source() || self.target() != other.target() {
118            return None;
119        }
120
121        let (table, target) =
122            K::Index::connected_components(&self.table, &other.table, self.target());
123        Some(FiniteFunction { table, target })
124    }
125
126    pub fn coequalizer_universal(&self, f: &Self) -> Option<Self>
127    where
128        K::Type<K::I>: Array<K, K::I> + PartialEq,
129    {
130        let table = coequalizer_universal(self, f.table.as_ref())?.into();
131        let target = f.target();
132        Some(FiniteFunction { table, target })
133    }
134
135    /// `transpose(a, b)` is the "transposition permutation" for an `a → b` matrix stored in
136    /// row-major order.
137    ///
138    /// Let M be an `a*b`-dimensional input thought of as a matrix in row-major order -- having `b`
139    /// rows and `a` columns.
140    /// Then `transpose(a, b)` computes the "target indices" of the transpose.
141    /// So for matrices `M : a → b` and `N : b → a`, setting the indices `N[transpose(a, b)] = M`
142    /// is the same as writing `N = M.T`.
143    pub fn transpose(a: K::I, b: K::I) -> FiniteFunction<K> {
144        if a.is_zero() {
145            return Self::initial(a);
146        }
147
148        let n = b.clone() * a.clone();
149        let i = K::Index::arange(&K::I::zero(), &n);
150        let (q, r) = i.quot_rem(a);
151        FiniteFunction {
152            target: n,
153            // r * b + q
154            table: r.mul_constant_add(b, &q),
155        }
156    }
157
158    /// Given a finite function `s : N → K`
159    /// representing the objects of the finite coproduct
160    /// `Σ_{n ∈ N} s(n)`
161    /// whose injections have the type
162    /// `ι_x : s(x) → Σ_{n ∈ N} s(n)`,
163    /// and given a finite map
164    /// `a : A → N`,
165    /// compute the coproduct of injections
166    /// ```text
167    /// injections(s, a) : Σ_{x ∈ A} s(x) → Σ_{n ∈ N} s(n)
168    /// injections(s, a) = Σ_{x ∈ A} ι_a(x)
169    /// ```
170    /// So that `injections(s, id) == id`
171    ///
172    /// Note that when a is a permutation,
173    /// injections(s, a) is a "blockwise" version of that permutation with block
174    /// sizes equal to s.
175    /// """
176    pub fn injections(&self, a: &FiniteFunction<K>) -> Option<Self> {
177        let s = self;
178        let p = self.table.cumulative_sum();
179
180        // TODO: better errors!
181        let k = (a >> s)?;
182        let r = k.table.segmented_arange();
183
184        let repeats = k.table;
185        let values = p.gather(a.table.get_range(..));
186        let z = repeats.repeat(values.get_range(..));
187
188        Some(FiniteFunction {
189            table: r + z,
190            target: p.get(p.len() - K::I::one()),
191        })
192    }
193
194    /// Given a finite function `f : A → B`, compute the cumulative sum of `f`, a finite function
195    /// `cumulative_sum(f) : A → sum_i(f())`
196    ///
197    /// ```rust
198    /// # use open_hypergraphs::category::*;
199    /// # use open_hypergraphs::array::vec::*;
200    /// # use open_hypergraphs::finite_function::*;
201    /// let f = FiniteFunction::<VecKind>::new(VecArray(vec![3, 0, 1, 4]), 5).unwrap();
202    /// let c = f.cumulative_sum();
203    /// assert_eq!(c.table, VecArray(vec![0, 3, 3, 4]));
204    /// assert_eq!(c.target(), 8);
205    ///
206    /// let f = FiniteFunction::<VecKind>::new(VecArray(vec![]), 5).unwrap();
207    /// let c = f.cumulative_sum();
208    /// assert_eq!(c.table, VecArray(vec![]));
209    /// assert_eq!(c.target(), 0);
210    /// ```
211    pub fn cumulative_sum(&self) -> Self {
212        let extended_table = self.table.cumulative_sum();
213        let target = extended_table.get(self.source());
214        let table = Array::from_slice(extended_table.get_range(..self.source()));
215        FiniteFunction { table, target }
216    }
217}
218
219impl<K: ArrayKind> FiniteFunction<K>
220where
221    K::Type<K::I>: NaturalArray<K>,
222{
223    /// Check if this finite function is injective.
224    pub fn is_injective(&self) -> bool {
225        if self.source().is_zero() {
226            return true;
227        }
228
229        let counts = self.table.bincount(self.target.clone());
230        counts.max().map_or(true, |m| m <= K::I::one())
231    }
232
233    /// Check whether `self` and `other` have disjoint images in a common codomain.
234    ///
235    /// Domains may differ. Returns `true` exactly when
236    /// `image(self) ∩ image(other) = ∅`.
237    ///
238    /// Returns `false` when codomains differ.
239    pub fn has_disjoint_image(&self, other: &Self) -> bool {
240        if self.target != other.target {
241            return false;
242        }
243
244        let mut seen = K::Index::fill(K::I::zero(), self.target.clone());
245        if !self.table.is_empty() {
246            seen.scatter_assign_constant(&self.table, K::I::one());
247        }
248
249        let other_seen = seen.gather(other.table.get_range(..));
250        other_seen.max().is_none_or(|m| m < K::I::one())
251    }
252
253    /// Build the canonical injection of the complement of `image(self)` in the codomain.
254    ///
255    /// For `self : A -> B`, returns `k : (B \\ image(self)) -> B`.
256    /// The domain may be empty.
257    #[cfg(any(feature = "experimental", test))]
258    pub(crate) fn image_complement_injection(&self) -> Option<Self> {
259        let mut marker = K::Index::fill(K::I::zero(), self.target.clone());
260        if !self.table.is_empty() {
261            marker.scatter_assign_constant(&self.table, K::I::one());
262        }
263        let kept_ix = marker.zero();
264        FiniteFunction::new(kept_ix, self.target.clone())
265    }
266
267    /// Build the canonical injection of `image(self)` into the codomain.
268    ///
269    /// For `self : A -> B`, returns `i : image(self) -> B`.
270    #[cfg(any(feature = "experimental", test))]
271    pub(crate) fn canonical_image_injection(&self) -> Option<Self> {
272        let (unique, _) = self.table.sparse_bincount();
273        FiniteFunction::new(unique, self.target.clone())
274    }
275
276    /// Build the coproduct of parallel maps into a common codomain.
277    ///
278    /// For parallel maps `self, f_i : A_i -> B`, returns
279    /// `[self, f_1, ..., f_n] : A + A_1 + ... + A_n -> B`.
280    #[cfg(any(feature = "experimental", test))]
281    pub(crate) fn coproduct_many(&self, others: &[&Self]) -> Option<Self> {
282        let target = self.target.clone();
283        for m in others {
284            if m.target != target {
285                return None;
286            }
287        }
288
289        let mut tables = Vec::<&K::Index>::with_capacity(others.len() + 1);
290        tables.push(&self.table);
291        for m in others {
292            tables.push(&m.table);
293        }
294
295        Some(FiniteFunction {
296            table: K::Index::concatenate_many(&tables),
297            target,
298        })
299    }
300
301    /// Build a total inverse for an injective map by choosing a fill value outside its image.
302    ///
303    /// For `f : A -> B` injective, returns `f_inv : B -> A` such that:
304    /// - `f_inv ; f = id_A` on `image(f)` (left-inverse property),
305    /// - for `b` not in `image(f)`, `f_inv(b) = fill`.
306    ///
307    /// This is useful when callers only apply `f_inv` to values known to be in `image(f)`,
308    /// while still requiring a total finite function at the type level.
309    ///
310    /// Returns `None` when:
311    /// - `self` is not injective, or
312    /// - `fill` is out of bounds for `A`, or
313    /// - `A` is empty and `B` is non-empty (no total map `B -> A` exists).
314    #[cfg(any(feature = "experimental", test))]
315    pub(crate) fn inverse_with_fill(&self, fill: K::I) -> Option<Self> {
316        if !self.is_injective() {
317            return None;
318        }
319
320        if self.source().is_zero() {
321            if self.target().is_zero() {
322                return Some(FiniteFunction {
323                    table: K::Index::empty(),
324                    target: K::I::zero(),
325                });
326            }
327            return None;
328        }
329
330        if fill >= self.source() {
331            return None;
332        }
333
334        let mut inverse = K::Index::fill(fill, self.target());
335        let values = K::Index::arange(&K::I::zero(), &self.source());
336        inverse.scatter_assign(&self.table, values);
337        Some(FiniteFunction {
338            table: inverse,
339            target: self.source(),
340        })
341    }
342
343    /// Factor `self : A -> C` through an injective map `inj : B -> C`.
344    ///
345    /// Returns the unique `g : A -> B` such that `self = g ; inj`.
346    ///
347    #[cfg(any(feature = "experimental", test))]
348    pub(crate) fn factor_through_injective(&self, inj: &Self) -> Self {
349        assert_eq!(
350            self.target(),
351            inj.target(),
352            "factor_through_injective requires parallel maps"
353        );
354        assert!(
355            inj.is_injective(),
356            "factor_through_injective requires an injective map"
357        );
358
359        // Build a left-inverse table on `inj`'s image and reindex `self`.
360        let values: K::Type<K::I> = K::Index::arange(&K::I::zero(), &inj.source()).into();
361        let inverse: K::Type<K::I> = values.scatter(inj.table.get_range(..), inj.target());
362        let table: K::Index = inverse.gather(self.table.get_range(..)).into();
363        let factored = FiniteFunction {
364            table,
365            target: inj.source(),
366        };
367
368        let recomposed = factored
369            .compose(inj)
370            .expect("factor_through_injective: invalid codomain after factoring");
371        assert!(
372            recomposed == *self,
373            "factor_through_injective requires image(self) subset image(inj)"
374        );
375
376        factored
377    }
378}
379
380/// Compute the universal map for a coequalizer `q : B → Q` and arrow `f : B → T`, generalised to
381/// the case where `T` is an arbitrary set (i.e., `f` is an array of `T`)
382pub fn coequalizer_universal<K: ArrayKind, T>(
383    q: &FiniteFunction<K>,
384    f: &K::Type<T>,
385) -> Option<K::Type<T>>
386where
387    K::Type<T>: Array<K, T> + PartialEq,
388{
389    if q.source() != f.len() {
390        return None;
391    }
392
393    // Compute table by scattering
394    let table = f.scatter(q.table.get_range(..), q.target());
395
396    // TODO: FIXME: we only need SemifiniteFunction to check this is a coequalizer;
397    // we use the >> implementation to check, which is implemented for SemifiniteFunction
398    use crate::semifinite::SemifiniteFunction;
399    let u = SemifiniteFunction(table);
400
401    // NOTE: we expect() here because composition is *defined* for self and u by construction;
402    // if it panics, there is a library bug.
403    let f_prime = (q >> &u).expect("by construction");
404    if f_prime.0 == *f {
405        Some(u.0)
406    } else {
407        None
408    }
409}
410
411impl<K: ArrayKind> Arrow for FiniteFunction<K> {
412    type Object = K::I;
413
414    fn source(&self) -> Self::Object {
415        self.table.len()
416    }
417
418    fn target(&self) -> Self::Object {
419        self.target.clone()
420    }
421
422    fn identity(a: Self::Object) -> Self {
423        let table = K::Index::arange(&K::I::zero(), &a);
424        let target = a.clone();
425        FiniteFunction { table, target }
426    }
427
428    fn compose(&self, other: &Self) -> Option<Self> {
429        if self.target == other.source() {
430            let table = other.table.gather(self.table.get_range(..));
431            let target = other.target.clone();
432            Some(FiniteFunction { table, target })
433        } else {
434            None
435        }
436    }
437}
438
439impl<K: ArrayKind> Coproduct for FiniteFunction<K> {
440    fn initial_object() -> Self::Object {
441        K::I::zero()
442    }
443
444    fn initial(a: Self::Object) -> Self {
445        Self {
446            table: K::Index::empty(),
447            target: a.clone(),
448        }
449    }
450
451    fn coproduct(&self, other: &Self) -> Option<Self> {
452        if self.target != other.target {
453            return None;
454        }
455
456        Some(Self {
457            table: self.table.concatenate(&other.table),
458            target: self.target.clone(),
459        })
460    }
461
462    /// Coproduct injection 0.
463    ///
464    /// As an array, the indices `0..a`
465    fn inj0(a: Self::Object, b: Self::Object) -> Self {
466        let table = K::Index::arange(&K::I::zero(), &a);
467        let target = a.clone() + b.clone();
468        Self { table, target }
469    }
470
471    /// Coproduct injection 1.
472    ///
473    /// As an array, the indices `a..(a+b)`
474    ///
475    /// ```rust
476    /// # use open_hypergraphs::category::*;
477    /// # use open_hypergraphs::finite_function::*;
478    /// # use open_hypergraphs::array::vec::*;
479    /// assert_eq!(
480    ///     FiniteFunction::<VecKind>::inj1(3, 5).table,
481    ///     VecArray(vec![3,4,5,6,7]),
482    ///     )
483    /// ```
484    fn inj1(a: Self::Object, b: Self::Object) -> Self {
485        let target = a.clone() + b.clone();
486        let table = K::Index::arange(&a, &target);
487        Self { table, target }
488    }
489}
490
491impl<K: ArrayKind> Monoidal for FiniteFunction<K> {
492    // the unit object
493    fn unit() -> Self::Object {
494        K::I::zero()
495    }
496
497    fn tensor(&self, other: &Self) -> Self {
498        // NOTE: this uses the `Add<&K::Index>` bound on `K::I` to compute offset piece without
499        // unnecessary cloning.
500        let table = self
501            .table
502            .concatenate(&(self.target.clone() + &other.table));
503        let target = self.target.clone() + other.target.clone();
504        Self { table, target }
505    }
506}
507
508impl<K: ArrayKind> SymmetricMonoidal for FiniteFunction<K> {
509    fn twist(a: K::I, b: K::I) -> Self {
510        // This is more efficiently expressed as arange + b `mod` target,
511        // but this would require adding an operation add_mod(a, b, n) to the array trait.
512        let target = a.clone() + b.clone();
513        let lhs = K::Index::arange(&b, &target);
514        let rhs = K::Index::arange(&K::I::zero(), &b);
515        let table = lhs.concatenate(&rhs);
516        Self { table, target }
517    }
518}
519
520// Syntactic sugar for finite function composition
521impl<K: ArrayKind> Shr<&FiniteFunction<K>> for &FiniteFunction<K> {
522    type Output = Option<FiniteFunction<K>>;
523
524    fn shr(self, rhs: &FiniteFunction<K>) -> Option<FiniteFunction<K>> {
525        self.compose(rhs)
526    }
527}
528
529// Sugar for coproduct
530impl<K: ArrayKind> Add<&FiniteFunction<K>> for &FiniteFunction<K> {
531    type Output = Option<FiniteFunction<K>>;
532
533    fn add(self, rhs: &FiniteFunction<K>) -> Option<FiniteFunction<K>> {
534        self.coproduct(rhs)
535    }
536}
537
538// Tensor product (parallel composition)
539impl<K: ArrayKind> BitOr<&FiniteFunction<K>> for &FiniteFunction<K> {
540    type Output = FiniteFunction<K>;
541    fn bitor(self, rhs: &FiniteFunction<K>) -> FiniteFunction<K> {
542        self.tensor(rhs)
543    }
544}
545
546impl<K: ArrayKind> Clone for FiniteFunction<K> {
547    fn clone(&self) -> Self {
548        Self {
549            table: self.table.clone(),
550            target: self.target.clone(),
551        }
552    }
553}
554
555impl<K: ArrayKind> Debug for FiniteFunction<K>
556where
557    K::Index: Debug,
558{
559    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
560        f.debug_struct("FiniteFunction")
561            .field("table", &self.table)
562            .field("target", &self.target)
563            .finish()
564    }
565}
566
567#[cfg(test)]
568mod tests {
569    use super::FiniteFunction;
570    use crate::array::vec::{VecArray, VecKind};
571    use crate::category::Arrow;
572    use proptest::prelude::{Just, Strategy};
573    use proptest::{prop_assert, prop_assert_eq, proptest};
574
575    fn ff(table: Vec<usize>, target: usize) -> FiniteFunction<VecKind> {
576        FiniteFunction::new(VecArray(table), target).expect("valid finite function")
577    }
578
579    fn finite_function_strategy(
580        max_source: usize,
581        max_target: usize,
582    ) -> impl Strategy<Value = FiniteFunction<VecKind>> {
583        (0..=max_source, 0..=max_target).prop_flat_map(|(source, target)| {
584            let source = if target == 0 { 0 } else { source };
585            proptest::collection::vec(0..target, source).prop_map(move |table| ff(table, target))
586        })
587    }
588
589    fn injective_function_strategy(
590        max_source: usize,
591        max_target: usize,
592    ) -> impl Strategy<Value = FiniteFunction<VecKind>> {
593        (0..=max_target).prop_flat_map(move |target| {
594            (0..=core::cmp::min(max_source, target)).prop_flat_map(move |source| {
595                proptest::sample::subsequence((0..target).collect::<Vec<_>>(), source)
596                    .prop_map(move |table| ff(table, target))
597            })
598        })
599    }
600
601    fn parallel_maps_strategy(
602        max_maps: usize,
603        max_source: usize,
604        max_target: usize,
605    ) -> impl Strategy<Value = (FiniteFunction<VecKind>, Vec<FiniteFunction<VecKind>>)> {
606        (0..=max_target, 1usize..=max_maps).prop_flat_map(move |(target, n_maps)| {
607            if target == 0 {
608                let base = ff(vec![], 0);
609                let others = vec![ff(vec![], 0); n_maps - 1];
610                Just((base, others)).boxed()
611            } else {
612                proptest::collection::vec(
613                    proptest::collection::vec(0..target, 0..=max_source),
614                    n_maps,
615                )
616                .prop_map(move |tables| {
617                    let mut it = tables.into_iter();
618                    let base = ff(it.next().expect("at least one map"), target);
619                    let others = it.map(|t| ff(t, target)).collect();
620                    (base, others)
621                })
622                .boxed()
623            }
624        })
625    }
626
627    fn factorable_through_injective_strategy(
628        max_source: usize,
629        max_target: usize,
630    ) -> impl Strategy<
631        Value = (
632            FiniteFunction<VecKind>,
633            FiniteFunction<VecKind>,
634            FiniteFunction<VecKind>,
635        ),
636    > {
637        injective_function_strategy(max_source, max_target).prop_flat_map(move |inj| {
638            let b = inj.source();
639            if b == 0 {
640                let g = ff(vec![], 0);
641                let self_map = (&g >> &inj).expect("typed composition");
642                Just((self_map, inj, g)).boxed()
643            } else {
644                proptest::collection::vec(0..b, 0..=max_source)
645                    .prop_map(move |table| {
646                        let g = ff(table, b);
647                        let self_map = (&g >> &inj).expect("typed composition");
648                        (self_map, inj.clone(), g)
649                    })
650                    .boxed()
651            }
652        })
653    }
654
655    proptest! {
656        #[test]
657        fn canonical_image_injection_characterizes_image(f in finite_function_strategy(8, 8)) {
658            let i = f.canonical_image_injection().expect("always valid");
659            prop_assert!(i.is_injective());
660            prop_assert_eq!(i.target(), f.target());
661
662            let mut used = vec![false; f.target()];
663            for &x in &f.table.0 {
664                used[x] = true;
665            }
666
667            let mut seen = vec![false; f.target()];
668            for &x in &i.table.0 {
669                prop_assert!(used[x]);
670                prop_assert!(!seen[x]);
671                seen[x] = true;
672            }
673            prop_assert_eq!(used.clone(), seen);
674            prop_assert_eq!(i.source(), used.iter().filter(|&&b| b).count());
675        }
676
677        #[test]
678        fn has_disjoint_image_matches_set_disjointness(
679            f in finite_function_strategy(8, 8),
680            g in finite_function_strategy(8, 8),
681        ) {
682            if f.target() != g.target() {
683                prop_assert!(!f.has_disjoint_image(&g));
684                return Ok(());
685            }
686
687            let mut seen_f = vec![false; f.target()];
688            for &x in &f.table.0 {
689                seen_f[x] = true;
690            }
691            let mut seen_g = vec![false; g.target()];
692            for &x in &g.table.0 {
693                seen_g[x] = true;
694            }
695
696            let expected = (0..f.target()).all(|i| !(seen_f[i] && seen_g[i]));
697            prop_assert_eq!(f.has_disjoint_image(&g), expected);
698        }
699
700        #[test]
701        fn image_complement_injection_is_exact_complement(f in finite_function_strategy(8, 8)) {
702            let k = f.image_complement_injection().expect("always valid");
703            prop_assert!(k.is_injective());
704            prop_assert_eq!(k.target(), f.target());
705
706            let mut used = vec![false; f.target()];
707            for &x in &f.table.0 {
708                used[x] = true;
709            }
710
711            let mut seen = vec![false; f.target()];
712            for &x in &k.table.0 {
713                prop_assert!(!used[x]);
714                prop_assert!(!seen[x]);
715                seen[x] = true;
716            }
717
718            for i in 0..f.target() {
719                prop_assert_eq!(seen[i], !used[i]);
720            }
721
722            // Set-theoretic disjointness of images: no value appears in both images.
723            for i in 0..f.target() {
724                prop_assert!(!(used[i] && seen[i]));
725            }
726        }
727
728        #[test]
729        fn coproduct_many_matches_iterated_coproduct((f0, others) in parallel_maps_strategy(4, 6, 8)) {
730            let refs: Vec<&FiniteFunction<VecKind>> = others.iter().collect();
731            let actual = f0.coproduct_many(&refs).expect("parallel maps");
732            let expected = others.iter().fold(f0.clone(), |acc, m| {
733                (&acc + m).expect("parallel maps")
734            });
735            prop_assert_eq!(actual, expected);
736        }
737
738        #[test]
739        fn inverse_with_fill_left_inverts_injective_maps(
740            f in injective_function_strategy(8, 8),
741            fill in 0usize..8,
742        ) {
743            if f.source() == 0 {
744                prop_assert_eq!(f.inverse_with_fill(fill), if f.target() == 0 { Some(ff(vec![], 0)) } else { None });
745            } else if fill < f.source() {
746                let inv = f.inverse_with_fill(fill).expect("valid inverse");
747                prop_assert_eq!(&f >> &inv, Some(FiniteFunction::<VecKind>::identity(f.source())));
748
749                let mut preimage = vec![None; f.target()];
750                for (i, &y) in f.table.0.iter().enumerate() {
751                    preimage[y] = Some(i);
752                }
753                for (y, &x) in inv.table.0.iter().enumerate() {
754                    match preimage[y] {
755                        Some(i) => prop_assert_eq!(x, i),
756                        None => prop_assert_eq!(x, fill),
757                    }
758                }
759            } else {
760                prop_assert_eq!(f.inverse_with_fill(fill), None);
761            }
762        }
763
764        #[test]
765        fn factor_through_injective_recovers_original_factor(
766            (self_map, inj, g) in factorable_through_injective_strategy(8, 8),
767        ) {
768            let factored = self_map.factor_through_injective(&inj);
769
770            prop_assert_eq!(factored.clone(), g);
771            prop_assert_eq!(&factored >> &inj, Some(self_map));
772        }
773    }
774
775    #[test]
776    fn coproduct_many_returns_none_on_target_mismatch() {
777        let f = ff(vec![0, 1], 3);
778        let g = ff(vec![0], 2);
779        assert!(f.coproduct_many(&[&g]).is_none());
780    }
781
782    #[test]
783    fn inverse_with_fill_rejects_non_injective_map() {
784        let f = ff(vec![0, 0], 2);
785        assert_eq!(f.inverse_with_fill(0), None);
786    }
787}