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