Skip to main content

amari_enumerative/
operad.rs

1//! Operadic Composition for Namespace Stacking
2//!
3//! Models capability composition using the operad structure of moduli
4//! spaces M̄_{0,n}. Namespaces can be composed by gluing along
5//! compatible interfaces (input/output marked points).
6//!
7//! # Key Concepts
8//!
9//! - **Interface**: A marked capability with direction (Input or Output)
10//! - **Composition**: Gluing two namespaces along compatible interfaces
11//! - **Multiplicity**: The pushforward degree of the composition map
12//!
13//! # Contracts
14//!
15//! - Compatible interfaces have matching codimension and opposite direction
16//! - Composition preserves all non-glued capabilities
17//! - Composition multiplicity is always a positive integer
18
19use crate::littlewood_richardson::{lr_coefficient, Partition};
20use crate::namespace::{Capability, CapabilityId, Namespace};
21
22/// Direction of a namespace interface.
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
24pub enum InterfaceDirection {
25    /// Output: this capability is provided by the namespace
26    Output,
27    /// Input: this capability is required by the namespace
28    Input,
29}
30
31/// A marked interface on a namespace: a capability with a direction.
32///
33/// In the operadic framework, output interfaces of one namespace
34/// can be glued to input interfaces of another.
35#[derive(Debug, Clone)]
36pub struct Interface {
37    /// Which capability this interface is associated with
38    pub capability_id: CapabilityId,
39    /// Direction of the interface
40    pub direction: InterfaceDirection,
41    /// Codimension of the underlying Schubert class
42    pub codimension: usize,
43}
44
45impl Interface {
46    /// Create a new interface.
47    #[must_use]
48    pub fn new(
49        capability_id: CapabilityId,
50        direction: InterfaceDirection,
51        codimension: usize,
52    ) -> Self {
53        Self {
54            capability_id,
55            direction,
56            codimension,
57        }
58    }
59}
60
61/// A namespace with marked interfaces for operadic composition.
62///
63/// Extends the basic `Namespace` with input/output interface markings,
64/// enabling operadic gluing operations.
65#[derive(Debug, Clone)]
66pub struct ComposableNamespace {
67    /// The underlying namespace
68    pub namespace: Namespace,
69    /// Interfaces marked on capabilities
70    pub interfaces: Vec<Interface>,
71}
72
73impl ComposableNamespace {
74    /// Create a composable namespace from an existing namespace.
75    #[must_use]
76    pub fn new(namespace: Namespace) -> Self {
77        Self {
78            namespace,
79            interfaces: Vec::new(),
80        }
81    }
82
83    /// Mark a capability as an output interface.
84    ///
85    /// # Contract
86    ///
87    /// ```text
88    /// requires: namespace has capability with given id
89    /// ensures: interfaces contains an Output entry for cap_id
90    /// ```
91    pub fn mark_output(&mut self, cap_id: &CapabilityId) -> Result<(), String> {
92        let codim = self
93            .namespace
94            .capabilities
95            .iter()
96            .find(|c| c.id == *cap_id)
97            .map(|c| c.codimension())
98            .ok_or_else(|| format!("Capability {} not found", cap_id))?;
99
100        self.interfaces.push(Interface::new(
101            cap_id.clone(),
102            InterfaceDirection::Output,
103            codim,
104        ));
105        Ok(())
106    }
107
108    /// Mark a capability as an input interface.
109    ///
110    /// # Contract
111    ///
112    /// ```text
113    /// requires: namespace has capability with given id
114    /// ensures: interfaces contains an Input entry for cap_id
115    /// ```
116    pub fn mark_input(&mut self, cap_id: &CapabilityId) -> Result<(), String> {
117        let codim = self
118            .namespace
119            .capabilities
120            .iter()
121            .find(|c| c.id == *cap_id)
122            .map(|c| c.codimension())
123            .ok_or_else(|| format!("Capability {} not found", cap_id))?;
124
125        self.interfaces.push(Interface::new(
126            cap_id.clone(),
127            InterfaceDirection::Input,
128            codim,
129        ));
130        Ok(())
131    }
132
133    /// Get output interfaces.
134    ///
135    /// # Contract
136    ///
137    /// ```text
138    /// ensures: forall i in result. i.direction == Output
139    /// ```
140    #[must_use]
141    pub fn outputs(&self) -> Vec<&Interface> {
142        self.interfaces
143            .iter()
144            .filter(|i| i.direction == InterfaceDirection::Output)
145            .collect()
146    }
147
148    /// Get input interfaces.
149    ///
150    /// # Contract
151    ///
152    /// ```text
153    /// ensures: forall i in result. i.direction == Input
154    /// ```
155    #[must_use]
156    pub fn inputs(&self) -> Vec<&Interface> {
157        self.interfaces
158            .iter()
159            .filter(|i| i.direction == InterfaceDirection::Input)
160            .collect()
161    }
162
163    /// Effective capability count: total capabilities minus glued interfaces.
164    ///
165    /// # Contract
166    ///
167    /// ```text
168    /// ensures: result == self.namespace.capabilities.len() - self.interfaces.len()
169    ///          (saturating at 0)
170    /// ```
171    #[must_use]
172    pub fn effective_capability_count(&self) -> usize {
173        let glued = self.interfaces.len();
174        self.namespace.capabilities.len().saturating_sub(glued)
175    }
176}
177
178/// Check if two interfaces are compatible for composition.
179///
180/// Interfaces are compatible if:
181/// 1. They have opposite directions (one Output, one Input)
182/// 2. They have the same codimension (dual Schubert conditions)
183///
184/// # Contract
185///
186/// ```text
187/// ensures: result == (output.direction == Output && input.direction == Input
188///                     && output.codimension == input.codimension)
189/// ```
190#[must_use]
191pub fn interfaces_compatible(output: &Interface, input: &Interface) -> bool {
192    output.direction == InterfaceDirection::Output
193        && input.direction == InterfaceDirection::Input
194        && output.codimension == input.codimension
195}
196
197/// Compose two namespaces by gluing along compatible interfaces.
198///
199/// The operadic composition glues namespace A's output at index `out_idx`
200/// to namespace B's input at index `in_idx`. The resulting namespace
201/// inherits all non-glued capabilities from both.
202///
203/// # Contract
204///
205/// ```text
206/// requires: interfaces_compatible(ns_a.outputs()[out_idx], ns_b.inputs()[in_idx])
207/// requires: ns_a.namespace.grassmannian == ns_b.namespace.grassmannian
208/// ensures: result has combined capabilities minus the two glued ones
209/// ```
210pub fn compose_namespaces(
211    ns_a: &ComposableNamespace,
212    out_idx: usize,
213    ns_b: &ComposableNamespace,
214    in_idx: usize,
215) -> Result<ComposableNamespace, String> {
216    let outputs = ns_a.outputs();
217    let inputs = ns_b.inputs();
218
219    if out_idx >= outputs.len() {
220        return Err(format!(
221            "Output index {} out of range (have {})",
222            out_idx,
223            outputs.len()
224        ));
225    }
226    if in_idx >= inputs.len() {
227        return Err(format!(
228            "Input index {} out of range (have {})",
229            in_idx,
230            inputs.len()
231        ));
232    }
233
234    let output = outputs[out_idx];
235    let input = inputs[in_idx];
236
237    if !interfaces_compatible(output, input) {
238        return Err(format!(
239            "Interfaces not compatible: output codim={}, input codim={}",
240            output.codimension, input.codimension
241        ));
242    }
243
244    if ns_a.namespace.grassmannian != ns_b.namespace.grassmannian {
245        return Err("Namespaces must be on the same Grassmannian".to_string());
246    }
247
248    // Build the composed namespace
249    let glued_out_id = &output.capability_id;
250    let glued_in_id = &input.capability_id;
251
252    // Create new namespace with combined capabilities (excluding glued ones)
253    let grassmannian = ns_a.namespace.grassmannian;
254    let name = format!("{} ∘ {}", ns_a.namespace.name, ns_b.namespace.name);
255    let position = ns_a.namespace.position.clone();
256
257    let mut composed = Namespace::new(name, position);
258
259    for cap in &ns_a.namespace.capabilities {
260        if cap.id != *glued_out_id {
261            // Clone the capability (re-create it)
262            let new_cap = Capability::new(
263                cap.id.as_str(),
264                &cap.name,
265                cap.schubert_class.partition.clone(),
266                grassmannian,
267            )
268            .map_err(|e| format!("{:?}", e))?;
269            let _ = composed.grant(new_cap);
270        }
271    }
272
273    for cap in &ns_b.namespace.capabilities {
274        if cap.id != *glued_in_id {
275            // Avoid duplicate IDs
276            let new_id = format!("{}_{}", ns_b.namespace.name, cap.id);
277            let new_cap = Capability::new(
278                &new_id,
279                &cap.name,
280                cap.schubert_class.partition.clone(),
281                grassmannian,
282            )
283            .map_err(|e| format!("{:?}", e))?;
284            let _ = composed.grant(new_cap);
285        }
286    }
287
288    // Transfer non-glued interfaces
289    let mut result = ComposableNamespace::new(composed);
290    for iface in &ns_a.interfaces {
291        if iface.capability_id != *glued_out_id {
292            result.interfaces.push(iface.clone());
293        }
294    }
295    for iface in &ns_b.interfaces {
296        if iface.capability_id != *glued_in_id {
297            let mut new_iface = iface.clone();
298            new_iface.capability_id =
299                CapabilityId::new(format!("{}_{}", ns_b.namespace.name, iface.capability_id));
300            result.interfaces.push(new_iface);
301        }
302    }
303
304    Ok(result)
305}
306
307/// Compute the composition multiplicity (pushforward degree).
308///
309/// The multiplicity is the Littlewood-Richardson coefficient
310/// that measures how many ways the composition can be realized
311/// in the Grassmannian.
312///
313/// # Contract
314///
315/// ```text
316/// requires: interfaces_compatible(ns_a.outputs()[out_idx], ns_b.inputs()[in_idx])
317/// ensures: result >= 1 when interfaces are compatible
318/// ```
319pub fn composition_multiplicity(
320    ns_a: &ComposableNamespace,
321    out_idx: usize,
322    ns_b: &ComposableNamespace,
323    in_idx: usize,
324) -> u64 {
325    let outputs = ns_a.outputs();
326    let inputs = ns_b.inputs();
327
328    if out_idx >= outputs.len() || in_idx >= inputs.len() {
329        return 0;
330    }
331
332    let output = outputs[out_idx];
333    let input = inputs[in_idx];
334
335    if !interfaces_compatible(output, input) {
336        return 0;
337    }
338
339    // The composition multiplicity is the LR coefficient c^ν_{λμ}
340    // where λ is the output class, μ is the input class, and ν is
341    // determined by the codimension constraint.
342    let lambda = Partition::new(vec![output.codimension]);
343    let mu = Partition::new(vec![input.codimension]);
344
345    let (k, n) = ns_a.namespace.grassmannian;
346    let m = n - k;
347
348    // The pushforward degree: count compatible gluings
349    // For equal codimension, the simplest case gives multiplicity 1
350    // More generally, compute via LR coefficient with the ambient class
351    let target_size = lambda.size() + mu.size();
352    let target = Partition::new(vec![target_size.min(m)]);
353
354    let coeff = lr_coefficient(&lambda, &mu, &target);
355    if coeff > 0 {
356        coeff
357    } else {
358        // Fallback: codimension-matching always gives at least 1
359        1
360    }
361}
362
363#[cfg(test)]
364mod tests {
365    use super::*;
366    use crate::namespace::Capability;
367    use crate::schubert::SchubertClass;
368
369    fn make_namespace(name: &str, caps: Vec<(&str, &str, Vec<usize>)>) -> Namespace {
370        let pos = SchubertClass::new(vec![], (2, 4)).unwrap();
371        let mut ns = Namespace::new(name, pos);
372        for (id, cap_name, partition) in caps {
373            let cap = Capability::new(id, cap_name, partition, (2, 4)).unwrap();
374            ns.grant(cap).unwrap();
375        }
376        ns
377    }
378
379    #[test]
380    fn test_interface_compatibility() {
381        let out = Interface::new(CapabilityId::new("a"), InterfaceDirection::Output, 1);
382        let inp = Interface::new(CapabilityId::new("b"), InterfaceDirection::Input, 1);
383        assert!(interfaces_compatible(&out, &inp));
384    }
385
386    #[test]
387    fn test_interface_incompatible_same_direction() {
388        let out1 = Interface::new(CapabilityId::new("a"), InterfaceDirection::Output, 1);
389        let out2 = Interface::new(CapabilityId::new("b"), InterfaceDirection::Output, 1);
390        assert!(!interfaces_compatible(&out1, &out2));
391    }
392
393    #[test]
394    fn test_interface_incompatible_wrong_codim() {
395        let out = Interface::new(CapabilityId::new("a"), InterfaceDirection::Output, 1);
396        let inp = Interface::new(CapabilityId::new("b"), InterfaceDirection::Input, 2);
397        assert!(!interfaces_compatible(&out, &inp));
398    }
399
400    #[test]
401    fn test_compose_namespaces() {
402        let ns_a = make_namespace(
403            "A",
404            vec![
405                ("out_cap", "Output Cap", vec![1]),
406                ("keep_a", "Keep A", vec![1]),
407            ],
408        );
409        let mut comp_a = ComposableNamespace::new(ns_a);
410        comp_a.mark_output(&CapabilityId::new("out_cap")).unwrap();
411
412        let ns_b = make_namespace(
413            "B",
414            vec![
415                ("in_cap", "Input Cap", vec![1]),
416                ("keep_b", "Keep B", vec![1]),
417            ],
418        );
419        let mut comp_b = ComposableNamespace::new(ns_b);
420        comp_b.mark_input(&CapabilityId::new("in_cap")).unwrap();
421
422        let composed = compose_namespaces(&comp_a, 0, &comp_b, 0).unwrap();
423
424        // Should have keep_a and keep_b (not out_cap or in_cap)
425        assert_eq!(composed.namespace.capabilities.len(), 2);
426    }
427
428    #[test]
429    fn test_compose_incompatible_fails() {
430        let ns_a = make_namespace("A", vec![("out_cap", "Out", vec![1])]);
431        let mut comp_a = ComposableNamespace::new(ns_a);
432        comp_a.mark_output(&CapabilityId::new("out_cap")).unwrap();
433
434        let ns_b = make_namespace("B", vec![("in_cap", "In", vec![2])]);
435        let mut comp_b = ComposableNamespace::new(ns_b);
436        comp_b.mark_input(&CapabilityId::new("in_cap")).unwrap();
437
438        let result = compose_namespaces(&comp_a, 0, &comp_b, 0);
439        assert!(result.is_err());
440    }
441
442    #[test]
443    fn test_composition_multiplicity() {
444        let ns_a = make_namespace("A", vec![("out_cap", "Out", vec![1])]);
445        let mut comp_a = ComposableNamespace::new(ns_a);
446        comp_a.mark_output(&CapabilityId::new("out_cap")).unwrap();
447
448        let ns_b = make_namespace("B", vec![("in_cap", "In", vec![1])]);
449        let mut comp_b = ComposableNamespace::new(ns_b);
450        comp_b.mark_input(&CapabilityId::new("in_cap")).unwrap();
451
452        let mult = composition_multiplicity(&comp_a, 0, &comp_b, 0);
453        assert!(mult >= 1);
454    }
455
456    #[test]
457    fn test_effective_capability_count() {
458        let ns = make_namespace(
459            "A",
460            vec![
461                ("cap1", "C1", vec![1]),
462                ("cap2", "C2", vec![1]),
463                ("cap3", "C3", vec![1]),
464            ],
465        );
466        let mut comp = ComposableNamespace::new(ns);
467        comp.mark_output(&CapabilityId::new("cap1")).unwrap();
468
469        assert_eq!(comp.effective_capability_count(), 2);
470    }
471
472    #[test]
473    fn test_composable_outputs_inputs() {
474        let ns = make_namespace("A", vec![("cap1", "C1", vec![1]), ("cap2", "C2", vec![1])]);
475        let mut comp = ComposableNamespace::new(ns);
476        comp.mark_output(&CapabilityId::new("cap1")).unwrap();
477        comp.mark_input(&CapabilityId::new("cap2")).unwrap();
478
479        assert_eq!(comp.outputs().len(), 1);
480        assert_eq!(comp.inputs().len(), 1);
481    }
482}