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