1use 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}