Skip to main content

omena_cascade/
grn.rs

1//! Boolean GRN-style cascade projection records.
2//!
3//! The API keeps the statistical-mechanics framing explicit while preserving a
4//! conservative V0 contract for checker and observation consumers.
5
6use serde::Serialize;
7
8use omena_syntax::ident::AuthoredPropertyTextV0;
9
10#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
11#[serde(rename_all = "camelCase")]
12pub struct BooleanGRNStateV0 {
13    pub schema_version: &'static str,
14    pub product: &'static str,
15    pub layer_marker: &'static str,
16    pub feature_gate: &'static str,
17    pub top_policy: GrnTopHandlingPolicyV0,
18    pub vertices: Vec<GrnVertexStateV0>,
19}
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
22#[serde(rename_all = "camelCase")]
23pub enum GrnTopHandlingPolicyV0 {
24    ScBoolSeqUnknown,
25}
26
27#[derive(Debug, Clone, Serialize)]
28#[serde(rename_all = "camelCase")]
29pub struct GrnVertexV0 {
30    pub vertex_id: String,
31    pub selector: String,
32    pub property: AuthoredPropertyTextV0,
33}
34
35impl PartialEq for GrnVertexV0 {
36    fn eq(&self, other: &Self) -> bool {
37        self.vertex_id == other.vertex_id
38            && self.selector == other.selector
39            && self
40                .property
41                .to_property_name()
42                .same_as(&other.property.to_property_name())
43    }
44}
45
46impl Eq for GrnVertexV0 {}
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
49#[serde(rename_all = "camelCase")]
50pub enum GrnBooleanState {
51    Applied,
52    LosingButEligible,
53    Inactive,
54    Top,
55}
56
57#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
58#[serde(rename_all = "camelCase")]
59pub struct GrnVertexStateV0 {
60    pub vertex: GrnVertexV0,
61    pub state: GrnBooleanState,
62}
63
64#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
65#[serde(rename_all = "camelCase")]
66pub struct GrnModeDistributionV0 {
67    pub schema_version: &'static str,
68    pub product: &'static str,
69    pub layer_marker: &'static str,
70    pub feature_gate: &'static str,
71    pub applied_count: usize,
72    pub losing_but_eligible_count: usize,
73    pub inactive_count: usize,
74    pub top_count: usize,
75}
76
77#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
78#[serde(rename_all = "camelCase")]
79pub struct CascadeAttractorBasinV0 {
80    pub schema_version: &'static str,
81    pub product: &'static str,
82    pub layer_marker: &'static str,
83    pub feature_gate: &'static str,
84    pub basin_id: String,
85    pub strategy: AttractorEnumerationStrategyV0,
86    pub variable_count: usize,
87    pub state_count: usize,
88    pub transition_count: usize,
89    pub fixed_point_count: usize,
90    pub fixed_point_states: Vec<u64>,
91    pub transition_digest: Option<String>,
92    pub proof: CascadeAttractorBasinProofV0,
93}
94
95#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
96#[serde(rename_all = "camelCase")]
97pub enum AttractorEnumerationStrategyV0 {
98    Explicit,
99    Bdd,
100    Lumped,
101    Deferred,
102    Sampled,
103}
104
105#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
106#[serde(rename_all = "camelCase")]
107pub enum RgFixedPointTagV0 {
108    Exact,
109    Lumped,
110    Deferred,
111    SampledAdvisory,
112}
113
114#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
115#[serde(rename_all = "camelCase")]
116pub struct CascadeAttractorBasinProofV0 {
117    pub schema_version: &'static str,
118    pub product: &'static str,
119    pub layer_marker: &'static str,
120    pub feature_gate: &'static str,
121    pub deterministic: bool,
122    pub fixed_point_tag: RgFixedPointTagV0,
123    pub conservative: bool,
124}
125
126#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
127#[serde(rename_all = "camelCase")]
128pub struct GrnTransitionRecordV0 {
129    pub from_state: u64,
130    pub to_state: u64,
131    pub fixed_point: bool,
132}
133
134#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
135#[serde(rename_all = "camelCase")]
136pub struct GrnExplicitAttractorEnumerationV0 {
137    pub schema_version: &'static str,
138    pub product: &'static str,
139    pub layer_marker: &'static str,
140    pub feature_gate: &'static str,
141    pub variable_count: usize,
142    pub state_count: usize,
143    pub transition_count: usize,
144    pub fixed_point_count: usize,
145    pub fixed_point_states: Vec<u64>,
146    pub transition_digest: String,
147    pub complete: bool,
148    pub transitions: Vec<GrnTransitionRecordV0>,
149}
150
151#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
152#[serde(rename_all = "camelCase")]
153pub struct CascadeOutcomeProjectionRecordV0 {
154    pub schema_version: &'static str,
155    pub product: &'static str,
156    pub layer_marker: &'static str,
157    pub feature_gate: &'static str,
158    pub lint_codes: Vec<&'static str>,
159    pub mode_distribution: GrnModeDistributionV0,
160    pub deep_conflict_report: CascadeDeepConflictReportV0,
161    pub kauffman_regime: KauffmanRegimeV0,
162}
163
164#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
165#[serde(rename_all = "camelCase")]
166pub enum KauffmanRegimeKindV0 {
167    Ordered,
168    Critical,
169    Chaotic,
170    Unknown,
171}
172
173#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
174#[serde(rename_all = "camelCase")]
175pub struct KauffmanRegimeV0 {
176    pub schema_version: &'static str,
177    pub product: &'static str,
178    pub layer_marker: &'static str,
179    pub feature_gate: &'static str,
180    pub variable_count: usize,
181    pub regime: KauffmanRegimeKindV0,
182    pub conservative: bool,
183}
184
185#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
186#[serde(rename_all = "camelCase")]
187pub struct CascadeDeepConflictReportV0 {
188    pub schema_version: &'static str,
189    pub product: &'static str,
190    pub layer_marker: &'static str,
191    pub feature_gate: &'static str,
192    pub lint_code: &'static str,
193    pub conflicting_vertex_count: usize,
194    pub conservative: bool,
195    pub regime: KauffmanRegimeV0,
196}
197
198pub fn summarize_grn_state(vertices: Vec<GrnVertexStateV0>) -> BooleanGRNStateV0 {
199    BooleanGRNStateV0 {
200        schema_version: "0",
201        product: "omena-cascade.boolean-grn-state",
202        layer_marker: "statistical-mechanics",
203        feature_gate: "grn",
204        top_policy: GrnTopHandlingPolicyV0::ScBoolSeqUnknown,
205        vertices,
206    }
207}
208
209pub fn choose_grn_attractor_strategy(variable_count: usize) -> AttractorEnumerationStrategyV0 {
210    match variable_count {
211        0..=16 => AttractorEnumerationStrategyV0::Explicit,
212        17..=64 if cfg!(feature = "bdd-attractor") => AttractorEnumerationStrategyV0::Bdd,
213        17..=256 if cfg!(feature = "naldi-lumping") => AttractorEnumerationStrategyV0::Lumped,
214        _ => AttractorEnumerationStrategyV0::Deferred,
215    }
216}
217
218pub fn transition_cascade_grn_state_v0(variable_count: usize, state: u64) -> u64 {
219    let mask = grn_state_mask(variable_count);
220    let active = state & mask;
221    active & active.wrapping_neg()
222}
223
224pub fn enumerate_explicit_grn_attractor_v0(
225    variable_count: usize,
226) -> GrnExplicitAttractorEnumerationV0 {
227    assert!(
228        variable_count <= 16,
229        "explicit GRN state enumeration is bounded to n <= 16"
230    );
231    let state_count = 1usize << variable_count;
232    let mut fixed_point_states = Vec::new();
233    let mut transitions = Vec::with_capacity(state_count);
234
235    for from_state in 0..state_count {
236        let from_state = from_state as u64;
237        let to_state = transition_cascade_grn_state_v0(variable_count, from_state);
238        let fixed_point = from_state == to_state;
239        if fixed_point {
240            fixed_point_states.push(from_state);
241        }
242        transitions.push(GrnTransitionRecordV0 {
243            from_state,
244            to_state,
245            fixed_point,
246        });
247    }
248
249    let transition_digest = digest_grn_transitions(
250        variable_count,
251        state_count,
252        fixed_point_states.len(),
253        &transitions,
254    );
255
256    GrnExplicitAttractorEnumerationV0 {
257        schema_version: "0",
258        product: "omena-cascade.grn-explicit-attractor-enumeration",
259        layer_marker: "statistical-mechanics",
260        feature_gate: "grn",
261        variable_count,
262        state_count,
263        transition_count: transitions.len(),
264        fixed_point_count: fixed_point_states.len(),
265        fixed_point_states,
266        transition_digest,
267        complete: true,
268        transitions,
269    }
270}
271
272pub fn prove_cascade_attractor_basin(variable_count: usize) -> CascadeAttractorBasinV0 {
273    let strategy = choose_grn_attractor_strategy(variable_count);
274    let fixed_point_tag = match strategy {
275        AttractorEnumerationStrategyV0::Explicit | AttractorEnumerationStrategyV0::Bdd => {
276            RgFixedPointTagV0::Exact
277        }
278        AttractorEnumerationStrategyV0::Lumped => RgFixedPointTagV0::Lumped,
279        AttractorEnumerationStrategyV0::Deferred => RgFixedPointTagV0::Deferred,
280        AttractorEnumerationStrategyV0::Sampled => RgFixedPointTagV0::SampledAdvisory,
281    };
282    let explicit = matches!(strategy, AttractorEnumerationStrategyV0::Explicit)
283        .then(|| enumerate_explicit_grn_attractor_v0(variable_count));
284    let state_count = explicit
285        .as_ref()
286        .map_or(0, |enumeration| enumeration.state_count);
287    let transition_count = explicit
288        .as_ref()
289        .map_or(0, |enumeration| enumeration.transition_count);
290    let fixed_point_states = explicit.as_ref().map_or_else(Vec::new, |enumeration| {
291        enumeration.fixed_point_states.clone()
292    });
293    let fixed_point_count = fixed_point_states.len();
294    let transition_digest = explicit
295        .as_ref()
296        .map(|enumeration| enumeration.transition_digest.clone());
297
298    CascadeAttractorBasinV0 {
299        schema_version: "0",
300        product: "omena-cascade.attractor-basin",
301        layer_marker: "statistical-mechanics",
302        feature_gate: "grn",
303        basin_id: format!("grn-v0-{variable_count}"),
304        strategy,
305        variable_count,
306        state_count,
307        transition_count,
308        fixed_point_count,
309        fixed_point_states,
310        transition_digest,
311        proof: CascadeAttractorBasinProofV0 {
312            schema_version: "0",
313            product: "omena-cascade.attractor-basin-proof",
314            layer_marker: "statistical-mechanics",
315            feature_gate: "grn",
316            deterministic: !matches!(
317                strategy,
318                AttractorEnumerationStrategyV0::Deferred | AttractorEnumerationStrategyV0::Sampled
319            ),
320            fixed_point_tag,
321            conservative: true,
322        },
323    }
324}
325
326fn grn_state_mask(variable_count: usize) -> u64 {
327    assert!(
328        variable_count <= 16,
329        "GRN state bitset helper is bounded to n <= 16"
330    );
331    if variable_count == 0 {
332        0
333    } else {
334        (1u64 << variable_count) - 1
335    }
336}
337
338fn digest_grn_transitions(
339    variable_count: usize,
340    state_count: usize,
341    fixed_point_count: usize,
342    transitions: &[GrnTransitionRecordV0],
343) -> String {
344    let mut digest = 0xcbf29ce484222325u64;
345    for transition in transitions {
346        digest ^= transition.from_state;
347        digest = digest.wrapping_mul(0x100000001b3);
348        digest ^= transition.to_state.rotate_left(17);
349        digest = digest.wrapping_mul(0x100000001b3);
350        digest ^= u64::from(transition.fixed_point);
351        digest = digest.wrapping_mul(0x100000001b3);
352    }
353    format!("grn-v0-{variable_count}-{state_count}-{fixed_point_count}-{digest:016x}")
354}
355
356pub fn project_grn_outcome(vertices: &[GrnVertexStateV0]) -> CascadeOutcomeProjectionRecordV0 {
357    let mut applied_count = 0;
358    let mut losing_but_eligible_count = 0;
359    let mut inactive_count = 0;
360    let mut top_count = 0;
361
362    for vertex in vertices {
363        match vertex.state {
364            GrnBooleanState::Applied => applied_count += 1,
365            GrnBooleanState::LosingButEligible => losing_but_eligible_count += 1,
366            GrnBooleanState::Inactive => inactive_count += 1,
367            GrnBooleanState::Top => top_count += 1,
368        }
369    }
370
371    let kauffman_regime = classify_kauffman_regime(vertices.len());
372
373    CascadeOutcomeProjectionRecordV0 {
374        schema_version: "0",
375        product: "omena-cascade.grn-outcome-projection",
376        layer_marker: "statistical-mechanics",
377        feature_gate: "grn",
378        lint_codes: vec!["cascade.deep-conflict", "cascade.unreachable-rule"],
379        mode_distribution: GrnModeDistributionV0 {
380            schema_version: "0",
381            product: "omena-cascade.grn-mode-distribution",
382            layer_marker: "statistical-mechanics",
383            feature_gate: "grn",
384            applied_count,
385            losing_but_eligible_count,
386            inactive_count,
387            top_count,
388        },
389        deep_conflict_report: CascadeDeepConflictReportV0 {
390            schema_version: "0",
391            product: "omena-cascade.deep-conflict-report",
392            layer_marker: "statistical-mechanics",
393            feature_gate: "grn",
394            lint_code: "cascade.deep-conflict",
395            conflicting_vertex_count: losing_but_eligible_count,
396            conservative: true,
397            regime: kauffman_regime.clone(),
398        },
399        kauffman_regime,
400    }
401}
402
403pub fn classify_kauffman_regime(variable_count: usize) -> KauffmanRegimeV0 {
404    let regime = match variable_count {
405        0..=16 => KauffmanRegimeKindV0::Ordered,
406        17..=64 => KauffmanRegimeKindV0::Critical,
407        65..=256 => KauffmanRegimeKindV0::Chaotic,
408        _ => KauffmanRegimeKindV0::Unknown,
409    };
410
411    KauffmanRegimeV0 {
412        schema_version: "0",
413        product: "omena-cascade.kauffman-regime",
414        layer_marker: "statistical-mechanics",
415        feature_gate: "grn",
416        variable_count,
417        regime,
418        conservative: true,
419    }
420}
421
422pub fn grn_shadow_omena_verbs() -> Vec<&'static str> {
423    vec![
424        "shadow.omena.grnState",
425        "shadow.omena.grnAttractorBasin",
426        "shadow.omena.grnDeepConflict",
427        "shadow.omena.grnUnreachableRule",
428        "shadow.omena.grnModeDistribution",
429    ]
430}
431
432#[cfg(test)]
433mod tests {
434    use super::*;
435
436    #[test]
437    fn grn_strategy_policy_matches_m4_alpha_bounds() {
438        let state = summarize_grn_state(Vec::new());
439
440        assert_eq!(state.layer_marker, "statistical-mechanics");
441        assert_eq!(state.feature_gate, "grn");
442        assert_eq!(
443            choose_grn_attractor_strategy(16),
444            AttractorEnumerationStrategyV0::Explicit
445        );
446        assert_eq!(
447            choose_grn_attractor_strategy(257),
448            AttractorEnumerationStrategyV0::Deferred
449        );
450    }
451
452    #[test]
453    fn grn_explicit_attractor_basin_proof_covers_all_n_le_16() {
454        for variable_count in 0..=16 {
455            let basin = prove_cascade_attractor_basin(variable_count);
456            let expected_state_count = 1usize << variable_count;
457
458            assert_eq!(basin.schema_version, "0");
459            assert_eq!(basin.product, "omena-cascade.attractor-basin");
460            assert_eq!(basin.layer_marker, "statistical-mechanics");
461            assert_eq!(basin.feature_gate, "grn");
462            assert_eq!(basin.strategy, AttractorEnumerationStrategyV0::Explicit);
463            assert_eq!(basin.variable_count, variable_count);
464            assert_eq!(basin.state_count, expected_state_count);
465            assert_eq!(basin.transition_count, expected_state_count);
466            assert_eq!(basin.fixed_point_count, variable_count + 1);
467            assert_eq!(basin.fixed_point_states.len(), variable_count + 1);
468            assert!(basin.fixed_point_states.contains(&0));
469            assert!(basin.transition_digest.as_deref().is_some_and(|digest| {
470                digest.starts_with(&format!(
471                    "grn-v0-{variable_count}-{expected_state_count}-{}-",
472                    variable_count + 1
473                ))
474            }));
475            assert_eq!(basin.proof.schema_version, "0");
476            assert_eq!(basin.proof.feature_gate, "grn");
477            assert!(basin.proof.deterministic);
478            assert_eq!(basin.proof.fixed_point_tag, RgFixedPointTagV0::Exact);
479            assert!(basin.proof.conservative);
480        }
481
482        let expected_strategy = if cfg!(feature = "bdd-attractor") {
483            AttractorEnumerationStrategyV0::Bdd
484        } else if cfg!(feature = "naldi-lumping") {
485            AttractorEnumerationStrategyV0::Lumped
486        } else {
487            AttractorEnumerationStrategyV0::Deferred
488        };
489        assert_eq!(choose_grn_attractor_strategy(17), expected_strategy);
490    }
491
492    #[test]
493    fn grn_explicit_transition_function_enumerates_full_state_space() {
494        let enumeration = enumerate_explicit_grn_attractor_v0(3);
495
496        assert_eq!(
497            enumeration.product,
498            "omena-cascade.grn-explicit-attractor-enumeration"
499        );
500        assert!(enumeration.complete);
501        assert_eq!(enumeration.variable_count, 3);
502        assert_eq!(enumeration.state_count, 8);
503        assert_eq!(enumeration.transition_count, 8);
504        assert_eq!(enumeration.fixed_point_states, vec![0, 1, 2, 4]);
505        assert_eq!(transition_cascade_grn_state_v0(3, 0b000), 0b000);
506        assert_eq!(transition_cascade_grn_state_v0(3, 0b001), 0b001);
507        assert_eq!(transition_cascade_grn_state_v0(3, 0b110), 0b010);
508        assert_eq!(transition_cascade_grn_state_v0(3, 0b111), 0b001);
509        assert_eq!(
510            enumeration
511                .transitions
512                .iter()
513                .filter(|transition| transition.fixed_point)
514                .count(),
515            4
516        );
517    }
518
519    #[test]
520    fn grn_projection_exposes_lint_codes() {
521        let projection = project_grn_outcome(&[]);
522
523        assert_eq!(projection.feature_gate, "grn");
524        assert_eq!(projection.layer_marker, "statistical-mechanics");
525        assert_eq!(projection.mode_distribution.feature_gate, "grn");
526        assert_eq!(projection.deep_conflict_report.schema_version, "0");
527        assert_eq!(
528            projection.deep_conflict_report.lint_code,
529            "cascade.deep-conflict"
530        );
531        assert_eq!(projection.kauffman_regime.schema_version, "0");
532        assert_eq!(projection.kauffman_regime.feature_gate, "grn");
533        assert_eq!(
534            projection.kauffman_regime.regime,
535            KauffmanRegimeKindV0::Ordered
536        );
537        assert_eq!(
538            projection.lint_codes,
539            vec!["cascade.deep-conflict", "cascade.unreachable-rule"]
540        );
541    }
542
543    #[test]
544    fn grn_shadow_omena_verbs_include_required_surface() {
545        let verbs = grn_shadow_omena_verbs();
546
547        assert_eq!(verbs.len(), 5);
548        assert!(verbs.iter().all(|verb| verb.starts_with("shadow.omena.")));
549        assert!(verbs.contains(&"shadow.omena.grnAttractorBasin"));
550        assert!(verbs.contains(&"shadow.omena.grnDeepConflict"));
551    }
552
553    #[cfg(feature = "bdd-attractor")]
554    #[test]
555    fn grn_bdd_strategy_is_feature_gated() {
556        assert_eq!(
557            choose_grn_attractor_strategy(32),
558            AttractorEnumerationStrategyV0::Bdd
559        );
560    }
561
562    #[cfg(feature = "naldi-lumping")]
563    #[test]
564    fn grn_lumping_strategy_is_feature_gated() {
565        assert_eq!(
566            choose_grn_attractor_strategy(65),
567            AttractorEnumerationStrategyV0::Lumped
568        );
569    }
570}