1#![forbid(unsafe_code)]
49#![deny(missing_docs)]
50#![warn(clippy::missing_errors_doc, clippy::missing_panics_doc)]
51
52pub mod analysis;
53pub mod callback_plan;
54pub mod design;
55pub mod discovery;
56pub mod discovery_defaults;
57pub mod error;
58pub mod gcm;
59pub mod inference;
60pub mod options;
61pub mod planner;
62pub mod prelude;
63pub mod result;
64pub mod review;
65pub mod state;
66pub mod strategy_table;
67
68pub mod estimate;
69pub mod graph;
70pub mod identify;
71pub mod io;
72pub mod query;
73pub mod validate;
74
75pub use analysis::{
77 AnalysisStageEvent, BatchAnalysis, CausalAnalysis, CausalAnalysisBuilder, ComputeBudget,
78 LatencyMode, PreparedAnalysis, RdConfig, RefuteSuite, StageResultSink,
79};
80#[allow(deprecated)]
81pub use error::AnalysisError;
82pub use error::CausalError;
83pub use estimate::{CausalPosterior, EffectEstimate, EstimatorId, IdentifierId};
84pub use graph::{Dag, DenseNodeId, TemporalDag};
85pub use inference::{BayesianConfig, InferenceMode};
86pub use options::{DiscoveryAccept, FdrControl};
87pub use planner::{CompiledAnalysis, GraphInput};
88pub use query::*;
89pub use result::CausalAnalysisResult;
90
91#[cfg(test)]
96#[allow(clippy::cast_precision_loss, clippy::many_single_char_names)]
97mod tests {
98 use std::sync::Arc;
99
100 use antecedent_core::{
101 AverageEffectQuery, CausalQuery, CausalSchemaBuilder, ExecutionContext, Intervention,
102 InterventionalDistributionQuery, MeasurementSpec, PathSpecificEffectQuery, RoleHint,
103 SmallRoleSet, Value, ValueType, VariableId,
104 };
105 use antecedent_data::{
106 Float64Column, OwnedColumn, OwnedColumnarStorage, TabularData, ValidityBitmap,
107 };
108 use antecedent_graph::{Dag, DenseNodeId};
109 use antecedent_kernels::standard_normal;
110
111 use super::*;
112 use crate::validate::PredictiveCheckKind;
113
114 fn scm() -> (TabularData, Dag, AverageEffectQuery) {
115 let n = 200usize;
116 let mut b = CausalSchemaBuilder::new();
117 b.add_variable(
118 "t",
119 ValueType::Continuous,
120 SmallRoleSet::from_hint(RoleHint::TreatmentCandidate),
121 None,
122 None,
123 MeasurementSpec::default(),
124 )
125 .unwrap();
126 b.add_variable(
127 "y",
128 ValueType::Continuous,
129 SmallRoleSet::from_hint(RoleHint::OutcomeCandidate),
130 None,
131 None,
132 MeasurementSpec::default(),
133 )
134 .unwrap();
135 b.add_variable(
136 "z",
137 ValueType::Continuous,
138 SmallRoleSet::from_hint(RoleHint::Context),
139 None,
140 None,
141 MeasurementSpec::default(),
142 )
143 .unwrap();
144 let schema = b.build().unwrap();
145 let z: Vec<f64> = (0..n).map(|i| (i as f64) / n as f64).collect();
146 let t: Vec<f64> = (0..n).map(|i| if z[i] > 0.5 { 1.0 } else { 0.0 }).collect();
147 let y: Vec<f64> = (0..n).map(|i| 1.0 + 2.0 * t[i] + 3.0 * z[i]).collect();
148 let cols = vec![
149 OwnedColumn::Float64(
150 Float64Column::new(
151 VariableId::from_raw(0),
152 Arc::from(t),
153 ValidityBitmap::all_valid(n),
154 )
155 .unwrap(),
156 ),
157 OwnedColumn::Float64(
158 Float64Column::new(
159 VariableId::from_raw(1),
160 Arc::from(y),
161 ValidityBitmap::all_valid(n),
162 )
163 .unwrap(),
164 ),
165 OwnedColumn::Float64(
166 Float64Column::new(
167 VariableId::from_raw(2),
168 Arc::from(z),
169 ValidityBitmap::all_valid(n),
170 )
171 .unwrap(),
172 ),
173 ];
174 let storage = OwnedColumnarStorage::try_new(schema, cols, None, None).unwrap();
175 let mut dag = Dag::with_variables(3);
176 let z_id = DenseNodeId::from_raw(2);
178 let t_id = DenseNodeId::from_raw(0);
179 let y_id = DenseNodeId::from_raw(1);
180 dag.insert_directed(z_id, t_id).unwrap();
181 dag.insert_directed(z_id, y_id).unwrap();
182 dag.insert_directed(t_id, y_id).unwrap();
183 let query =
184 AverageEffectQuery::binary_ate(VariableId::from_raw(0), VariableId::from_raw(1));
185 (TabularData::new(storage), dag, query)
186 }
187
188 #[test]
189 fn end_to_end_ate() {
190 let (data, graph, query) = scm();
191 let analysis = CausalAnalysis::builder()
192 .data(data)
193 .graph(graph)
194 .query(query)
195 .refute(RefuteSuite::PlaceboAndRcc)
196 .bootstrap_replicates(30)
197 .build()
198 .unwrap();
199 let ctx = ExecutionContext::for_tests(3);
200 let result = analysis.run(&ctx).unwrap();
201 assert!((result.estimate.ate - 2.0).abs() < 1e-6);
202 assert!(result.distribution.is_none());
203 assert_eq!(result.refutations.len(), 2);
204 assert!(!result.provenance.is_empty());
205 assert!(!result.identification.derivation.steps.is_empty());
206
207 let trace = result.analysis_trace_wire();
208 assert_eq!(&*trace.method, "backdoor.adjustment");
209 assert!(!trace.assumptions.is_empty());
210 assert!(!trace.derivation.is_empty());
211 let bytes = antecedent_io::to_cbor(&trace).unwrap();
212 let round: antecedent_io::AnalysisTraceWire = antecedent_io::from_cbor(&bytes).unwrap();
213 assert_eq!(round.method, trace.method);
214 }
215
216 #[test]
217 fn bayesian_ate_attaches_prior_and_posterior_predictive() {
218 let (data, graph, query) = scm();
219 let analysis = CausalAnalysis::builder()
220 .data(data)
221 .graph(graph)
222 .query(query)
223 .inference(InferenceMode::Bayesian(BayesianConfig::conjugate().n_draws(64)))
224 .refute(RefuteSuite::PlaceboAndRcc)
225 .build()
226 .unwrap();
227 let ctx = ExecutionContext::for_tests(1);
228 let result = analysis.run(&ctx).unwrap();
229 assert!(result.posterior.is_some());
230 assert!(
231 result.predictive_checks.iter().any(|c| c.kind == PredictiveCheckKind::Prior),
232 "expected prior predictive check on Bayesian facade path"
233 );
234 assert!(
235 result.predictive_checks.iter().any(|c| c.kind == PredictiveCheckKind::Posterior),
236 "expected posterior predictive check on Bayesian facade path"
237 );
238 let prior =
239 result.predictive_checks.iter().find(|c| c.kind == PredictiveCheckKind::Prior).unwrap();
240 assert!(prior.p_value.is_finite());
241 assert!(prior.predictive_sd.is_finite());
242 let post_ppc = result
243 .predictive_checks
244 .iter()
245 .find(|c| c.kind == PredictiveCheckKind::Posterior)
246 .unwrap();
247 assert!(post_ppc.p_value.is_finite());
248 assert!(post_ppc.predictive_sd.is_finite());
249 assert!(result.refutations.iter().any(|r| r.refuter.as_ref() == "prior_predictive"));
250 assert!(result.refutations.iter().any(|r| r.refuter.as_ref() == "posterior_predictive"));
251 }
252
253 #[test]
254 fn bayesian_exact_dag_posterior_effect_envelope() {
255 let (data, _graph, query) = scm();
256 let analysis = CausalAnalysis::builder()
257 .data(data)
258 .discover_exact_dag_posterior()
259 .query(query)
260 .inference(InferenceMode::Bayesian(
261 BayesianConfig::conjugate().n_draws(80).prior_scale(100.0),
262 ))
263 .refute(RefuteSuite::None)
264 .build()
265 .unwrap();
266 let ctx = ExecutionContext::for_tests(1);
267 let result = analysis.run(&ctx).unwrap();
268 let post = result.posterior.expect("mixture posterior");
269 assert!((0.0..=1.0).contains(&post.unidentified_mass));
270 let eq = post.effect_column().unwrap();
271 assert!(post.summaries.mean[eq].is_finite());
272 assert!(post.summaries.sd[eq].is_finite());
273 assert!(post.draws.n_draws > 0);
274 }
275
276 #[test]
277 fn graph_posterior_discovery_rejects_frequentist() {
278 let (data, _graph, query) = scm();
279 let err = CausalAnalysis::builder()
280 .data(data)
281 .discover_exact_dag_posterior()
282 .query(query)
283 .inference(InferenceMode::Frequentist)
284 .refute(RefuteSuite::None)
285 .build()
286 .unwrap()
287 .compile(&ExecutionContext::for_tests(1))
288 .unwrap_err();
289 let msg = err.to_string();
290 assert!(
291 msg.contains("Bayesian") || msg.contains("graph-posterior"),
292 "unexpected error: {msg}"
293 );
294 }
295
296 #[test]
297 fn end_to_end_interventional_distribution() {
298 let mut b = CausalSchemaBuilder::new();
300 for name in ["t", "y", "z"] {
301 b.add_variable(
302 name,
303 ValueType::Continuous,
304 SmallRoleSet::from_hint(RoleHint::Context),
305 None,
306 None,
307 MeasurementSpec::default(),
308 )
309 .unwrap();
310 }
311 let schema = b.build().unwrap();
312 let combos = [
313 (0.0, 0.0, 0.0, 21),
314 (0.0, 0.0, 1.0, 9),
315 (0.0, 1.0, 0.0, 4),
316 (0.0, 1.0, 1.0, 16),
317 (1.0, 0.0, 0.0, 12),
318 (1.0, 0.0, 1.0, 3),
319 (1.0, 1.0, 0.0, 14),
320 (1.0, 1.0, 1.0, 21),
321 ];
322 let mut t_vals = Vec::new();
323 let mut y_vals = Vec::new();
324 let mut z_vals = Vec::new();
325 for (z, t, y, count) in combos {
326 for _ in 0..count {
327 z_vals.push(z);
328 t_vals.push(t);
329 y_vals.push(y);
330 }
331 }
332 let n = t_vals.len();
333 let cols = vec![
334 OwnedColumn::Float64(
335 Float64Column::new(
336 VariableId::from_raw(0),
337 Arc::from(t_vals),
338 ValidityBitmap::all_valid(n),
339 )
340 .unwrap(),
341 ),
342 OwnedColumn::Float64(
343 Float64Column::new(
344 VariableId::from_raw(1),
345 Arc::from(y_vals),
346 ValidityBitmap::all_valid(n),
347 )
348 .unwrap(),
349 ),
350 OwnedColumn::Float64(
351 Float64Column::new(
352 VariableId::from_raw(2),
353 Arc::from(z_vals),
354 ValidityBitmap::all_valid(n),
355 )
356 .unwrap(),
357 ),
358 ];
359 let data =
360 TabularData::new(OwnedColumnarStorage::try_new(schema, cols, None, None).unwrap());
361 let mut dag = Dag::with_variables(3);
362 dag.insert_directed(DenseNodeId::from_raw(2), DenseNodeId::from_raw(0)).unwrap();
363 dag.insert_directed(DenseNodeId::from_raw(2), DenseNodeId::from_raw(1)).unwrap();
364 dag.insert_directed(DenseNodeId::from_raw(0), DenseNodeId::from_raw(1)).unwrap();
365 let query = InterventionalDistributionQuery::new(
366 VariableId::from_raw(1),
367 [Intervention::set(VariableId::from_raw(0), Value::f64(1.0))],
368 );
369 let analysis = CausalAnalysis::builder()
370 .data(data)
371 .graph(dag)
372 .query(CausalQuery::Distribution(query))
373 .identifier(IdentifierId::GeneralId)
374 .estimator(EstimatorId::FunctionalDistribution)
375 .build()
376 .unwrap();
377 let result = analysis.run(&ExecutionContext::for_tests(0)).unwrap();
378 let dist = result.distribution.expect("distribution payload");
379 assert!((dist.mean - 0.7).abs() < 0.05, "mean={}", dist.mean);
380 assert!(result.estimate.ate.is_finite());
381 }
382
383 #[test]
384 fn end_to_end_path_specific_natural_effect() {
385 let mut b = CausalSchemaBuilder::new();
387 for name in ["t", "m", "y"] {
388 b.add_variable(
389 name,
390 ValueType::Continuous,
391 SmallRoleSet::from_hint(RoleHint::Context),
392 None,
393 None,
394 MeasurementSpec::default(),
395 )
396 .unwrap();
397 }
398 let schema = b.build().unwrap();
399 let mut t_vals = Vec::new();
401 let mut m_vals = Vec::new();
402 let mut y_vals = Vec::new();
403 for t in [0.0, 1.0] {
404 for _ in 0..50 {
405 t_vals.push(t);
406 m_vals.push(t);
407 y_vals.push(t);
408 }
409 }
410 let n = t_vals.len();
411 let cols = vec![
412 OwnedColumn::Float64(
413 Float64Column::new(
414 VariableId::from_raw(0),
415 Arc::from(t_vals),
416 ValidityBitmap::all_valid(n),
417 )
418 .unwrap(),
419 ),
420 OwnedColumn::Float64(
421 Float64Column::new(
422 VariableId::from_raw(1),
423 Arc::from(m_vals),
424 ValidityBitmap::all_valid(n),
425 )
426 .unwrap(),
427 ),
428 OwnedColumn::Float64(
429 Float64Column::new(
430 VariableId::from_raw(2),
431 Arc::from(y_vals),
432 ValidityBitmap::all_valid(n),
433 )
434 .unwrap(),
435 ),
436 ];
437 let data =
438 TabularData::new(OwnedColumnarStorage::try_new(schema, cols, None, None).unwrap());
439 let mut dag = Dag::with_variables(3);
440 dag.insert_directed(DenseNodeId::from_raw(0), DenseNodeId::from_raw(1)).unwrap();
441 dag.insert_directed(DenseNodeId::from_raw(1), DenseNodeId::from_raw(2)).unwrap();
442 let query =
443 PathSpecificEffectQuery::binary(VariableId::from_raw(0), VariableId::from_raw(2))
444 .with_path_nodes([VariableId::from_raw(1)]);
445 let analysis = CausalAnalysis::builder()
446 .data(data)
447 .graph(dag)
448 .query(CausalQuery::PathSpecific(query))
449 .identifier(IdentifierId::PathSpecificNatural)
450 .estimator(EstimatorId::FunctionalEffect)
451 .build()
452 .unwrap();
453 let result = analysis.run(&ExecutionContext::for_tests(0)).unwrap();
454 assert!((result.estimate.ate - 1.0).abs() < 0.05, "ate={}", result.estimate.ate);
455 assert_eq!(result.estimand.method.as_ref(), "path_specific.natural");
456 }
457
458 fn confounded_scm(n: usize, seed: u64) -> (TabularData, Dag, AverageEffectQuery) {
462 let mut rng = ExecutionContext::for_tests(seed).rng.stream(0x1234_u64);
463 let mut z = vec![0.0; n];
464 let mut t = vec![0.0; n];
465 let mut y = vec![0.0; n];
466 for i in 0..n {
467 let zi = standard_normal(&mut rng);
468 let logit = -0.5 + zi;
469 let p = 1.0 / (1.0 + (-logit).exp());
470 let ti = if rng.next_f64() < p { 1.0 } else { 0.0 };
471 let noise = standard_normal(&mut rng) * 0.5;
472 z[i] = zi;
473 t[i] = ti;
474 y[i] = 2.0 * ti + zi + noise;
475 }
476 let mut b = CausalSchemaBuilder::new();
477 b.add_variable(
478 "t",
479 ValueType::Continuous,
480 SmallRoleSet::from_hint(RoleHint::TreatmentCandidate),
481 None,
482 None,
483 MeasurementSpec::default(),
484 )
485 .unwrap();
486 b.add_variable(
487 "y",
488 ValueType::Continuous,
489 SmallRoleSet::from_hint(RoleHint::OutcomeCandidate),
490 None,
491 None,
492 MeasurementSpec::default(),
493 )
494 .unwrap();
495 b.add_variable(
496 "z",
497 ValueType::Continuous,
498 SmallRoleSet::from_hint(RoleHint::Context),
499 None,
500 None,
501 MeasurementSpec::default(),
502 )
503 .unwrap();
504 let schema = b.build().unwrap();
505 let cols = vec![
506 OwnedColumn::Float64(
507 Float64Column::new(
508 VariableId::from_raw(0),
509 Arc::from(t),
510 ValidityBitmap::all_valid(n),
511 )
512 .unwrap(),
513 ),
514 OwnedColumn::Float64(
515 Float64Column::new(
516 VariableId::from_raw(1),
517 Arc::from(y),
518 ValidityBitmap::all_valid(n),
519 )
520 .unwrap(),
521 ),
522 OwnedColumn::Float64(
523 Float64Column::new(
524 VariableId::from_raw(2),
525 Arc::from(z),
526 ValidityBitmap::all_valid(n),
527 )
528 .unwrap(),
529 ),
530 ];
531 let storage = OwnedColumnarStorage::try_new(schema, cols, None, None).unwrap();
532 let mut dag = Dag::with_variables(3);
533 let z_id = DenseNodeId::from_raw(2);
534 let t_id = DenseNodeId::from_raw(0);
535 let y_id = DenseNodeId::from_raw(1);
536 dag.insert_directed(z_id, t_id).unwrap();
537 dag.insert_directed(z_id, y_id).unwrap();
538 dag.insert_directed(t_id, y_id).unwrap();
539 let query =
540 AverageEffectQuery::binary_ate(VariableId::from_raw(0), VariableId::from_raw(1));
541 (TabularData::new(storage), dag, query)
542 }
543
544 #[test]
545 fn end_to_end_propensity_weighting_recovers_confounded_effect() {
546 let (data, graph, query) = confounded_scm(800, 1);
550 let analysis = CausalAnalysis::builder()
551 .data(data)
552 .graph(graph)
553 .query(query)
554 .identifier("backdoor.adjustment")
555 .estimator("propensity.weighting")
556 .bootstrap_replicates(30)
557 .build()
558 .unwrap();
559 let ctx = ExecutionContext::for_tests(11);
560 let result = analysis.run(&ctx).unwrap();
561 assert!((result.estimate.ate - 2.0).abs() < 0.3, "ate={}", result.estimate.ate);
562 assert!(result.refutations.is_empty());
565 assert_eq!(result.logical_plan.estimator.as_deref(), Some("propensity.weighting"));
566 }
567
568 fn iv_scm(n: usize, seed: u64) -> (TabularData, Dag, AverageEffectQuery) {
571 let mut rng = ExecutionContext::for_tests(seed).rng.stream(0x1E71_u64);
572 let mut z = vec![0.0; n];
573 let mut t = vec![0.0; n];
574 let mut y = vec![0.0; n];
575 for i in 0..n {
576 let zi = (i % 2) as f64;
577 let u = standard_normal(&mut rng);
578 let ti = 0.5 * zi + u + 0.1 * standard_normal(&mut rng);
579 let yi = 2.0 * ti + u + 0.1 * standard_normal(&mut rng);
580 z[i] = zi;
581 t[i] = ti;
582 y[i] = yi;
583 }
584 let mut b = CausalSchemaBuilder::new();
585 b.add_variable(
586 "t",
587 ValueType::Continuous,
588 SmallRoleSet::from_hint(RoleHint::TreatmentCandidate),
589 None,
590 None,
591 MeasurementSpec::default(),
592 )
593 .unwrap();
594 b.add_variable(
595 "y",
596 ValueType::Continuous,
597 SmallRoleSet::from_hint(RoleHint::OutcomeCandidate),
598 None,
599 None,
600 MeasurementSpec::default(),
601 )
602 .unwrap();
603 b.add_variable(
604 "z",
605 ValueType::Continuous,
606 SmallRoleSet::from_hint(RoleHint::Context),
607 None,
608 None,
609 MeasurementSpec::default(),
610 )
611 .unwrap();
612 let schema = b.build().unwrap();
613 let cols = vec![
614 OwnedColumn::Float64(
615 Float64Column::new(
616 VariableId::from_raw(0),
617 Arc::from(t),
618 ValidityBitmap::all_valid(n),
619 )
620 .unwrap(),
621 ),
622 OwnedColumn::Float64(
623 Float64Column::new(
624 VariableId::from_raw(1),
625 Arc::from(y),
626 ValidityBitmap::all_valid(n),
627 )
628 .unwrap(),
629 ),
630 OwnedColumn::Float64(
631 Float64Column::new(
632 VariableId::from_raw(2),
633 Arc::from(z),
634 ValidityBitmap::all_valid(n),
635 )
636 .unwrap(),
637 ),
638 ];
639 let storage = OwnedColumnarStorage::try_new(schema, cols, None, None).unwrap();
640 let mut dag = Dag::with_variables(3);
641 let z_id = DenseNodeId::from_raw(2);
642 let t_id = DenseNodeId::from_raw(0);
643 let y_id = DenseNodeId::from_raw(1);
644 dag.insert_directed(z_id, t_id).unwrap();
645 dag.insert_directed(t_id, y_id).unwrap();
646 let query = AverageEffectQuery::with_levels(
647 VariableId::from_raw(0),
648 VariableId::from_raw(1),
649 0.0,
650 1.0,
651 );
652 (TabularData::new(storage), dag, query)
653 }
654
655 #[test]
656 fn end_to_end_iv_two_stage_least_squares() {
657 let (data, graph, query) = iv_scm(4000, 5);
658 let analysis = CausalAnalysis::builder()
659 .data(data)
660 .graph(graph)
661 .query(query)
662 .identifier("iv")
663 .estimator("iv.2sls")
664 .bootstrap_replicates(30)
665 .build()
666 .unwrap();
667 let ctx = ExecutionContext::for_tests(21);
668 let result = analysis.run(&ctx).unwrap();
669 assert!((result.estimate.ate - 2.0).abs() < 0.6, "ate={}", result.estimate.ate);
670 assert!(result.refutations.is_empty());
671 }
672
673 #[test]
674 fn end_to_end_temporal_effect() {
675 use antecedent_core::{Lag, TemporalEffectQuery, TemporalPolicy};
676 use antecedent_data::{SamplingRegularity, TimeIndex, TimeSeriesData};
677 use antecedent_graph::{TemporalDag, ensure_lagged};
678
679 let n = 250usize;
680 let mut b = CausalSchemaBuilder::new();
681 b.add_variable(
682 "pressure",
683 ValueType::Continuous,
684 SmallRoleSet::from_hint(RoleHint::TreatmentCandidate),
685 None,
686 None,
687 MeasurementSpec::default(),
688 )
689 .unwrap();
690 b.add_variable(
691 "defect",
692 ValueType::Continuous,
693 SmallRoleSet::from_hint(RoleHint::OutcomeCandidate),
694 None,
695 None,
696 MeasurementSpec::default(),
697 )
698 .unwrap();
699 let schema = b.build().unwrap();
700 let mut x = vec![0.0; n];
701 let mut y = vec![0.0; n];
702 for t in 1..n {
703 x[t] = ((t as f64) * 0.05).sin();
704 y[t] = 0.75 * x[t - 1];
705 }
706 let cols = vec![
707 OwnedColumn::Float64(
708 Float64Column::new(
709 VariableId::from_raw(0),
710 Arc::from(x),
711 ValidityBitmap::all_valid(n),
712 )
713 .unwrap(),
714 ),
715 OwnedColumn::Float64(
716 Float64Column::new(
717 VariableId::from_raw(1),
718 Arc::from(y),
719 ValidityBitmap::all_valid(n),
720 )
721 .unwrap(),
722 ),
723 ];
724 let storage = OwnedColumnarStorage::try_new(schema, cols, None, None).unwrap();
725 let series = TimeSeriesData::try_new(
726 storage,
727 TimeIndex { regularity: SamplingRegularity::Regular { interval_ns: 1 }, length: n },
728 )
729 .unwrap();
730 let mut g = TemporalDag::empty();
731 let x1 = ensure_lagged(&mut g, VariableId::from_raw(0), Lag::from_raw(1)).unwrap();
732 let y0 = ensure_lagged(&mut g, VariableId::from_raw(1), Lag::CONTEMPORANEOUS).unwrap();
733 g.insert_directed(x1, y0).unwrap();
734 let q = TemporalEffectQuery::pulse(VariableId::from_raw(0), VariableId::from_raw(1), 1.0)
735 .with_policy(TemporalPolicy::pulse(-1))
736 .with_horizon_steps(1);
737 let analysis = CausalAnalysis::builder()
738 .series(series)
739 .temporal_graph(g)
740 .temporal_query(q)
741 .bootstrap_replicates(0)
742 .build()
743 .unwrap();
744 let ctx = ExecutionContext::for_tests(7);
745 let result = analysis.run(&ctx).unwrap();
746 assert!((result.estimate.ate - 0.75).abs() < 0.08, "ate={}", result.estimate.ate);
747 assert_eq!(&*result.logical_plan.plan_id, "temporal_effect");
748 assert!(result.physical_plan.estimated_peak_memory_bytes.is_some());
749 assert!(result.physical_plan.estimated_copy_bytes.is_some());
750 assert!(!result.physical_plan.task_schedule.is_empty());
751 assert!(!result.physical_plan.materializations.is_empty());
752
753 let compiled = analysis.compile(&ctx).unwrap();
754 match compiled {
755 CompiledAnalysis::Ready(plan) => {
756 assert!(plan.temporal_graph().is_some());
757 assert_eq!(plan.record.batch_size, Some(250));
758 }
759 CompiledAnalysis::ReviewRequired(_)
760 | CompiledAnalysis::ReviewRequiredCpdag(_)
761 | CompiledAnalysis::ReviewRequiredStaticCpdag(_)
762 | CompiledAnalysis::ReviewRequiredStaticDag(_)
763 | CompiledAnalysis::ReviewRequiredPag(_)
764 | CompiledAnalysis::ReviewRequiredStaticPag(_) => {
765 panic!("expected Ready")
766 }
767 }
768 }
769}