Skip to main content

dml_core/
datasets.rs

1//! Simulated data sets: the data generating processes of DoubleML's
2//! `make_*` functions, for examples, tests and simulation studies.
3//!
4//! Every generator is a struct whose fields are the parameters, with
5//! DoubleML's defaults, and `generate(seed)` draws a data set. The draws come
6//! from a seeded ChaCha generator, so they are reproducible but not identical
7//! to DoubleML's NumPy draws; the distributions are the same.
8//!
9//! ```
10//! use dml_core::datasets::PlrCcddhnr2018;
11//! use dml_core::{learner::Ols, Plr};
12//!
13//! let sim = PlrCcddhnr2018 { n_obs: 300, dim_x: 5, ..Default::default() }.generate(42).unwrap();
14//! let fit = Plr::new(Ols::new(), Ols::new()).fit(&sim.to_data().unwrap()).unwrap();
15//! assert!((fit.coef()[0] - 0.5).abs() < 0.3);
16//! ```
17//!
18//! | Generator | DoubleML | Design |
19//! |---|---|---|
20//! | [`PlrCcddhnr2018`] | `make_plr_CCDDHNR2018` | partially linear regression |
21//! | [`PlrTurrell2018`] | `make_plr_turrell2018` | partially linear regression |
22//! | [`ConfoundedPlr`] | `make_confounded_plr_data` | PLR with an unobserved confounder |
23//! | [`PlivChs2015`] | `make_pliv_CHS2015` | partially linear IV, many instruments |
24//! | [`PlivMultiwayClusterCkms2021`] | `make_pliv_multiway_cluster_CKMS2021` | PLIV with two-way clustering |
25//! | [`LplrLzz2020`] | `make_lplr_LZZ2020` | logistic partially linear regression |
26//! | [`PlprCp2025`] | `make_plpr_CP2025` | partially linear panel regression |
27//! | [`IrmData`] | `make_irm_data` | interactive regression, binary treatment |
28//! | [`ConfoundedIrm`] | `make_confounded_irm_data` | IRM with an unobserved confounder |
29//! | [`IrmDiscreteTreatments`] | `make_irm_data_discrete_treatments` | multi-valued treatment |
30//! | [`IivmData`] | `make_iivm_data` | binary instrument (LATE) |
31//! | [`SsmData`] | `make_ssm_data` | sample selection |
32//! | [`HeterogeneousData`] | `make_heterogeneous_data` | heterogeneous treatment effects |
33//! | [`DidSz2020`] | `make_did_SZ2020` | two-period difference-in-differences |
34//! | [`DidCs2021`] | `make_did_CS2021`, `make_did_cs_CS2021` | staggered adoption |
35//! | [`SimpleRdd`] | `make_simple_rdd_data` | regression discontinuity |
36
37use ndarray::{s, Array1, Array2, Axis};
38use rand::seq::index::sample;
39use rand::{Rng, SeedableRng};
40use rand_chacha::ChaCha8Rng;
41use rand_distr::StandardNormal;
42
43use crate::compat::{brent_minimize, np_quantile};
44use crate::data::DmlData;
45use crate::error::{DmlError, Result};
46use crate::linalg::{cholesky_factor, sym_eigen};
47use crate::model::StaticPanel;
48use crate::staggered::{PanelData, RepeatedCrossSections};
49
50// ---------------------------------------------------------------------------
51// Shared pieces
52// ---------------------------------------------------------------------------
53
54/// A simulated cross-section: covariates `x`, outcome `y`, treatment `d` and,
55/// depending on the design, instruments `z`, a selection indicator `s`, a
56/// time indicator `t` or cluster labels (one column per clustering).
57#[derive(Debug, Clone)]
58pub struct Simulated {
59    pub x: Array2<f64>,
60    pub y: Array1<f64>,
61    pub d: Array1<f64>,
62    pub z: Option<Array2<f64>>,
63    pub s: Option<Array1<f64>>,
64    pub t: Option<Array1<f64>>,
65    pub clusters: Option<Array2<i64>>,
66}
67
68impl Simulated {
69    fn new(x: Array2<f64>, y: Array1<f64>, d: Array1<f64>) -> Self {
70        Self {
71            x,
72            y,
73            d,
74            z: None,
75            s: None,
76            t: None,
77            clusters: None,
78        }
79    }
80
81    /// The data set as [`DmlData`], with the instruments, selection or time
82    /// indicator and clusters attached.
83    pub fn to_data(&self) -> Result<DmlData> {
84        let mut data = DmlData::new(self.y.clone(), self.d.clone(), self.x.clone())?;
85        if let Some(z) = &self.z {
86            data = data.with_instruments(z.clone())?;
87        }
88        if let Some(s) = &self.s {
89            data = data.with_selection_indicator(s.clone())?;
90        }
91        if let Some(t) = &self.t {
92            data = data.with_time_indicator(t.clone())?;
93        }
94        if let Some(c) = &self.clusters {
95            data = match c.ncols() {
96                1 => data.with_clusters(c.column(0).to_owned())?,
97                _ => data.with_two_way_clusters(c.column(0).to_owned(), c.column(1).to_owned())?,
98            };
99        }
100        Ok(data)
101    }
102}
103
104fn rng(seed: u64) -> ChaCha8Rng {
105    ChaCha8Rng::seed_from_u64(seed)
106}
107
108fn normal(rng: &mut ChaCha8Rng) -> f64 {
109    rng.sample(StandardNormal)
110}
111
112fn normals(rng: &mut ChaCha8Rng, n: usize) -> Array1<f64> {
113    (0..n).map(|_| normal(rng)).collect()
114}
115
116fn uniforms(rng: &mut ChaCha8Rng, n: usize, lo: f64, hi: f64) -> Array1<f64> {
117    (0..n).map(|_| rng.gen_range(lo..hi)).collect()
118}
119
120fn bernoulli(rng: &mut ChaCha8Rng, p: f64) -> f64 {
121    f64::from(rng.gen::<f64>() < p)
122}
123
124fn expit(v: f64) -> f64 {
125    1.0 / (1.0 + (-v).exp())
126}
127
128/// `c^|i−j|` (with `0⁰ = 1`).
129fn toeplitz(c: f64, p: usize) -> Array2<f64> {
130    Array2::from_shape_fn((p, p), |(i, j)| c.powi(i.abs_diff(j) as i32))
131}
132
133/// `n` draws from `N(0, cov)` (rows).
134fn mvn(rng: &mut ChaCha8Rng, n: usize, cov: &Array2<f64>) -> Result<Array2<f64>> {
135    let l = cholesky_factor(cov)?;
136    let p = cov.nrows();
137    let e = Array2::from_shape_fn((n, p), |_| normal(rng));
138    Ok(e.dot(&l.t()))
139}
140
141/// Pairs from a bivariate normal with unit variances and correlation `rho`.
142fn correlated_pair(rng: &mut ChaCha8Rng, n: usize, rho: f64) -> (Array1<f64>, Array1<f64>) {
143    let a = normals(rng, n);
144    let b = normals(rng, n);
145    let second = &a * rho + &b * (1.0 - rho * rho).sqrt();
146    (a, second)
147}
148
149/// Columns centred and scaled by their population standard deviation.
150fn standardize(z: &Array2<f64>) -> Array2<f64> {
151    let mean = z.mean_axis(Axis(0)).expect("rows");
152    let sd = z.std_axis(Axis(0), 0.0);
153    (z - &mean) / &sd
154}
155
156/// The transformed covariates of Sant'Anna and Zhao (2020), standardised;
157/// columns of `x` from `extra_from` on are appended unchanged first.
158fn sz_features(x: &Array2<f64>, extra_from: Option<usize>) -> Array2<f64> {
159    let n = x.nrows();
160    let extra = extra_from.map_or(0, |k| x.ncols().saturating_sub(k));
161    let mut z = Array2::zeros((n, 4 + extra));
162    for i in 0..n {
163        let (x0, x1, x2, x3) = (x[[i, 0]], x[[i, 1]], x[[i, 2]], x[[i, 3]]);
164        z[[i, 0]] = (0.5 * x0).exp();
165        z[[i, 1]] = 10.0 + x1 / (1.0 + x0.exp());
166        z[[i, 2]] = (0.6 + x0 * x2 / 25.0).powi(3);
167        z[[i, 3]] = (20.0 + x1 + x3).powi(2);
168        for j in 0..extra {
169            z[[i, 4 + j]] = x[[i, extra_from.expect("extra") + j]];
170        }
171    }
172    standardize(&z)
173}
174
175fn check(cond: bool, msg: &str) -> Result<()> {
176    if cond {
177        Ok(())
178    } else {
179        Err(DmlError::InvalidArgument(msg.into()))
180    }
181}
182
183fn mean(v: &Array1<f64>) -> f64 {
184    v.mean().expect("non-empty")
185}
186
187fn var(v: &Array1<f64>) -> f64 {
188    let m = mean(v);
189    v.mapv(|x| (x - m) * (x - m)).mean().expect("non-empty")
190}
191
192fn corr(a: &Array1<f64>, b: &Array1<f64>) -> f64 {
193    let (ma, mb) = (mean(a), mean(b));
194    let cov = (a - ma).dot(&(b - mb));
195    cov / ((a - ma).dot(&(a - ma)) * (b - mb).dot(&(b - mb))).sqrt()
196}
197
198// ---------------------------------------------------------------------------
199// Partially linear models
200// ---------------------------------------------------------------------------
201
202/// Chernozhukov et al. (2018): `d = a₀x₀ + a₁·expit(x₂) + s₁ε`,
203/// `y = αd + b₀·expit(x₀) + b₁x₂ + s₂ζ` with Toeplitz-correlated
204/// covariates (`0.7^|i−j|`).
205#[derive(Debug, Clone, PartialEq)]
206pub struct PlrCcddhnr2018 {
207    pub n_obs: usize,
208    pub dim_x: usize,
209    pub alpha: f64,
210    pub a_0: f64,
211    pub a_1: f64,
212    pub s_1: f64,
213    pub b_0: f64,
214    pub b_1: f64,
215    pub s_2: f64,
216}
217
218impl Default for PlrCcddhnr2018 {
219    fn default() -> Self {
220        Self {
221            n_obs: 500,
222            dim_x: 20,
223            alpha: 0.5,
224            a_0: 1.0,
225            a_1: 0.25,
226            s_1: 1.0,
227            b_0: 1.0,
228            b_1: 0.25,
229            s_2: 1.0,
230        }
231    }
232}
233
234impl PlrCcddhnr2018 {
235    pub fn generate(&self, seed: u64) -> Result<Simulated> {
236        check(self.dim_x >= 3, "dim_x must be at least 3")?;
237        let mut rng = rng(seed);
238        let n = self.n_obs;
239        let x = mvn(&mut rng, n, &toeplitz(0.7, self.dim_x))?;
240        let d: Array1<f64> = (0..n)
241            .map(|i| {
242                self.a_0 * x[[i, 0]] + self.a_1 * expit(x[[i, 2]]) + self.s_1 * normal(&mut rng)
243            })
244            .collect();
245        let y: Array1<f64> = (0..n)
246            .map(|i| {
247                self.alpha * d[i]
248                    + self.b_0 * expit(x[[i, 0]])
249                    + self.b_1 * x[[i, 2]]
250                    + self.s_2 * normal(&mut rng)
251            })
252            .collect();
253        Ok(Simulated::new(x, y, d))
254    }
255}
256
257/// Turrell (2018): `d = m(x'b) + ε`, `y = θd + g(x'b) + ζ` with
258/// `g(v) = sin²(v)`, `m(v) = sinh(γ) / (2π(cosh(γ) − cos(v − ν)))`,
259/// `b_k = 1/k` and a random positive definite covariance (scikit-learn's
260/// `make_spd_matrix`).
261#[derive(Debug, Clone, PartialEq)]
262pub struct PlrTurrell2018 {
263    pub n_obs: usize,
264    pub dim_x: usize,
265    pub theta: f64,
266    pub nu: f64,
267    pub gamma: f64,
268}
269
270impl Default for PlrTurrell2018 {
271    fn default() -> Self {
272        Self {
273            n_obs: 100,
274            dim_x: 20,
275            theta: 0.5,
276            nu: 0.0,
277            gamma: 1.0,
278        }
279    }
280}
281
282/// scikit-learn's `make_spd_matrix`: `V (J + diag(u)) Vᵀ` with `V` the
283/// eigenvectors of `AᵀA` for uniform `A` and `J` the all-ones matrix.
284fn spd_matrix(rng: &mut ChaCha8Rng, p: usize) -> Array2<f64> {
285    let a = Array2::from_shape_fn((p, p), |_| rng.gen::<f64>());
286    let (_, v) = sym_eigen(&a.t().dot(&a));
287    let mut middle = Array2::<f64>::ones((p, p));
288    for i in 0..p {
289        middle[[i, i]] += rng.gen::<f64>();
290    }
291    let out = v.dot(&middle).dot(&v.t());
292    // Symmetrise rounding.
293    (&out + &out.t()) / 2.0
294}
295
296impl PlrTurrell2018 {
297    pub fn generate(&self, seed: u64) -> Result<Simulated> {
298        check(self.dim_x >= 1, "dim_x must be at least 1")?;
299        let mut rng = rng(seed);
300        let n = self.n_obs;
301        let sigma = spd_matrix(&mut rng, self.dim_x);
302        let x = mvn(&mut rng, n, &sigma)?;
303        let b: Array1<f64> = (1..=self.dim_x).map(|k| 1.0 / k as f64).collect();
304        let xb = x.dot(&b);
305        let m = |v: f64| {
306            0.5 / std::f64::consts::PI * self.gamma.sinh()
307                / (self.gamma.cosh() - (v - self.nu).cos())
308        };
309        let d: Array1<f64> = xb.iter().map(|&v| m(v) + normal(&mut rng)).collect();
310        let y: Array1<f64> = (0..n)
311            .map(|i| self.theta * d[i] + xb[i].sin().powi(2) + normal(&mut rng))
312            .collect();
313        Ok(Simulated::new(x, y, d))
314    }
315}
316
317/// A partially linear model with an unobserved confounder `A` of given
318/// strength (`cf_y`, `cf_d`), as `make_confounded_plr_data`, for sensitivity
319/// analysis. `ConfoundedPlrSimulated::oracle` holds the long and short
320/// regressions and the confounder.
321#[derive(Debug, Clone, PartialEq)]
322pub struct ConfoundedPlr {
323    pub n_obs: usize,
324    pub theta: f64,
325    pub cf_y: f64,
326    pub cf_d: f64,
327    /// Correlation parameter of the covariates (`c^|i−j|`).
328    pub c: f64,
329    pub dim_x: usize,
330}
331
332impl Default for ConfoundedPlr {
333    fn default() -> Self {
334        Self {
335            n_obs: 500,
336            theta: 5.0,
337            cf_y: 0.04,
338            cf_d: 0.04,
339            c: 0.0,
340            dim_x: 4,
341        }
342    }
343}
344
345/// Oracle values of [`ConfoundedPlr`].
346#[derive(Debug, Clone)]
347pub struct ConfoundedPlrOracle {
348    pub g_long: Array1<f64>,
349    pub g_short: Array1<f64>,
350    pub m_long: Array1<f64>,
351    pub m_short: Array1<f64>,
352    pub theta: f64,
353    pub gamma_a: f64,
354    pub beta_a: f64,
355    pub a: Array1<f64>,
356    pub z: Array2<f64>,
357}
358
359/// A data set of [`ConfoundedPlr`].
360#[derive(Debug, Clone)]
361pub struct ConfoundedPlrSimulated {
362    pub data: Simulated,
363    pub oracle: ConfoundedPlrOracle,
364}
365
366impl ConfoundedPlr {
367    pub fn generate(&self, seed: u64) -> Result<ConfoundedPlrSimulated> {
368        check(self.dim_x >= 4, "dim_x must be at least 4")?;
369        let mut rng = rng(seed);
370        let n = self.n_obs;
371        let x = mvn(&mut rng, n, &toeplitz(self.c, self.dim_x))?;
372        let z = sz_features(&x, Some(4));
373        let (var_eps_y, var_eps_d) = (5.0, 1.0);
374        let eps_y = normals(&mut rng, n) * f64::sqrt(var_eps_y);
375        let eps_d = normals(&mut rng, n) * f64::sqrt(var_eps_d);
376        let a = uniforms(&mut rng, n, -1.0, 1.0);
377        let var_a = 4.0 / 12.0;
378        let m_short: Array1<f64> = (0..n)
379            .map(|i| -z[[i, 0]] + 0.5 * z[[i, 1]] - 0.25 * z[[i, 2]] - 0.1 * z[[i, 3]])
380            .collect();
381        let cf_d = self.cf_d;
382        let mut f_m = |gamma_a: f64| {
383            let rr_long = &eps_d / var_eps_d;
384            let rr_short = (&a * gamma_a + &eps_d) / (gamma_a * gamma_a * var_a + var_eps_d);
385            let (l2, s2) = (
386                rr_long.mapv(|v| v * v).mean().unwrap(),
387                rr_short.mapv(|v| v * v).mean().unwrap(),
388            );
389            let c2 = (l2 - s2) / s2;
390            (c2 / (1.0 + c2) - cf_d).powi(2)
391        };
392        let gamma_a = brent_minimize(&mut f_m, 0.0, 1.0);
393        let m_long = &m_short + &(&a * gamma_a);
394        let d = &m_long + &eps_d;
395        let g_partial: Array1<f64> = (0..n)
396            .map(|i| 210.0 + 27.4 * z[[i, 0]] + 13.7 * (z[[i, 1]] + z[[i, 2]] + z[[i, 3]]))
397            .collect();
398        let var_d = var(&d);
399        let cf_y = self.cf_y;
400        let mut f_g = |beta_a: f64| {
401            let g_diff = (&a - &(&d * (gamma_a * var_a / var_d))) * beta_a;
402            let y_diff = &eps_y + &g_diff;
403            let ratio =
404                g_diff.mapv(|v| v * v).mean().unwrap() / y_diff.mapv(|v| v * v).mean().unwrap();
405            (ratio - cf_y).powi(2)
406        };
407        let beta_a = brent_minimize(&mut f_g, 0.0, 1.0);
408        let g_long = &d * self.theta + &g_partial + &(&a * beta_a);
409        let g_short = &d * (self.theta + gamma_a * beta_a * var_a / var_d) + &g_partial;
410        let y = &g_long + &eps_y;
411        Ok(ConfoundedPlrSimulated {
412            data: Simulated::new(x, y, d),
413            oracle: ConfoundedPlrOracle {
414                g_long,
415                g_short,
416                m_long,
417                m_short,
418                theta: self.theta,
419                gamma_a,
420                beta_a,
421                a,
422                z,
423            },
424        })
425    }
426}
427
428/// Chernozhukov, Hansen and Spindler (2015): partially linear IV with
429/// `dim_z` instruments `z = x[:, :dim_z] + ξ`.
430#[derive(Debug, Clone, PartialEq)]
431pub struct PlivChs2015 {
432    pub n_obs: usize,
433    pub alpha: f64,
434    pub dim_x: usize,
435    pub dim_z: usize,
436}
437
438impl Default for PlivChs2015 {
439    fn default() -> Self {
440        Self {
441            n_obs: 500,
442            alpha: 1.0,
443            dim_x: 200,
444            dim_z: 150,
445        }
446    }
447}
448
449impl PlivChs2015 {
450    pub fn generate(&self, seed: u64) -> Result<Simulated> {
451        check(
452            self.dim_z >= 1 && self.dim_x >= self.dim_z,
453            "need 1 <= dim_z <= dim_x",
454        )?;
455        let mut rng = rng(seed);
456        let n = self.n_obs;
457        let (epsilon, u) = correlated_pair(&mut rng, n, 0.6);
458        let x = mvn(&mut rng, n, &toeplitz(0.5, self.dim_x))?;
459        let xi = Array2::from_shape_fn((n, self.dim_z), |_| 0.5 * normal(&mut rng));
460        let beta: Array1<f64> = (1..=self.dim_x).map(|k| 1.0 / (k * k) as f64).collect();
461        let delta: Array1<f64> = (1..=self.dim_z).map(|k| 1.0 / (k * k) as f64).collect();
462        let z = &x.slice(s![.., ..self.dim_z]) + &xi;
463        let xb = x.dot(&beta);
464        let d = &xb + &z.dot(&delta) + &u;
465        let y = &d * self.alpha + &xb + &epsilon;
466        let mut sim = Simulated::new(x, y, d);
467        sim.z = Some(z);
468        Ok(sim)
469    }
470}
471
472/// Chiang, Kato, Ma and Sasaki (2021): partially linear IV on an `n × m`
473/// grid of two-way clusters, with cluster-correlated covariates and errors.
474#[derive(Debug, Clone, PartialEq)]
475pub struct PlivMultiwayClusterCkms2021 {
476    /// Clusters of the first dimension (DoubleML's `N`).
477    pub n: usize,
478    /// Clusters of the second dimension (DoubleML's `M`).
479    pub m: usize,
480    pub dim_x: usize,
481    pub theta: f64,
482    pub pi_10: f64,
483    /// Coefficients (default `0.5^k`, `k = 1..dim_x`); empty for the default.
484    pub zeta_0: Vec<f64>,
485    pub pi_20: Vec<f64>,
486    pub xi_0: Vec<f64>,
487    /// Weights of the two cluster dimensions in each component.
488    pub omega_x: [f64; 2],
489    pub omega_epsilon: [f64; 2],
490    pub omega_v: [f64; 2],
491    pub omega_vv: [f64; 2],
492    pub s_x: f64,
493    pub s_epsilon_v: f64,
494}
495
496impl Default for PlivMultiwayClusterCkms2021 {
497    fn default() -> Self {
498        Self {
499            n: 25,
500            m: 25,
501            dim_x: 100,
502            theta: 1.0,
503            pi_10: 1.0,
504            zeta_0: Vec::new(),
505            pi_20: Vec::new(),
506            xi_0: Vec::new(),
507            omega_x: [0.25, 0.25],
508            omega_epsilon: [0.25, 0.25],
509            omega_v: [0.25, 0.25],
510            omega_vv: [0.25, 0.25],
511            s_x: 0.25,
512            s_epsilon_v: 0.25,
513        }
514    }
515}
516
517impl PlivMultiwayClusterCkms2021 {
518    pub fn generate(&self, seed: u64) -> Result<Simulated> {
519        let (nn, mm, p) = (self.n, self.m, self.dim_x);
520        check(
521            nn >= 1 && mm >= 1 && p >= 1,
522            "n, m and dim_x must be positive",
523        )?;
524        let default = |v: &Vec<f64>| -> Result<Array1<f64>> {
525            if v.is_empty() {
526                Ok((1..=p).map(|k| 0.5_f64.powi(k as i32)).collect())
527            } else if v.len() == p {
528                Ok(Array1::from(v.clone()))
529            } else {
530                Err(DmlError::InvalidArgument(
531                    "coefficient vectors need dim_x entries".into(),
532                ))
533            }
534        };
535        let (zeta_0, pi_20, xi_0) = (
536            default(&self.zeta_0)?,
537            default(&self.pi_20)?,
538            default(&self.xi_0)?,
539        );
540        let mut rng = rng(seed);
541        let rows = nn * mm;
542        // Row r is the pair (i, j) = (r / m, r % m).
543        let ij = |r: usize| (r / mm, r % mm);
544        let alpha_vv = normals(&mut rng, rows);
545        let alpha_vv_i = normals(&mut rng, nn);
546        let alpha_vv_j = normals(&mut rng, mm);
547        let pair = |rng: &mut ChaCha8Rng, k: usize| correlated_pair(rng, k, self.s_epsilon_v);
548        let (alpha_eps, alpha_v) = pair(&mut rng, rows);
549        let (alpha_eps_i, alpha_v_i) = pair(&mut rng, nn);
550        let (alpha_eps_j, alpha_v_j) = pair(&mut rng, mm);
551        let cov = toeplitz(self.s_x, p);
552        let alpha_x = mvn(&mut rng, rows, &cov)?;
553        let alpha_x_i = mvn(&mut rng, nn, &cov)?;
554        let alpha_x_j = mvn(&mut rng, mm, &cov)?;
555        let mix =
556            |w: [f64; 2], own: f64, a: f64, b: f64| (1.0 - w[0] - w[1]) * own + w[0] * a + w[1] * b;
557        let x = Array2::from_shape_fn((rows, p), |(r, k)| {
558            let (i, j) = ij(r);
559            mix(
560                self.omega_x,
561                alpha_x[[r, k]],
562                alpha_x_i[[i, k]],
563                alpha_x_j[[j, k]],
564            )
565        });
566        let comp =
567            |w: [f64; 2], own: &Array1<f64>, a: &Array1<f64>, b: &Array1<f64>| -> Array1<f64> {
568                (0..rows)
569                    .map(|r| {
570                        let (i, j) = ij(r);
571                        mix(w, own[r], a[i], b[j])
572                    })
573                    .collect()
574            };
575        let eps = comp(self.omega_epsilon, &alpha_eps, &alpha_eps_i, &alpha_eps_j);
576        let v = comp(self.omega_v, &alpha_v, &alpha_v_i, &alpha_v_j);
577        let vv = comp(self.omega_vv, &alpha_vv, &alpha_vv_i, &alpha_vv_j);
578        let z = x.dot(&xi_0) + &vv;
579        let d = &z * self.pi_10 + &x.dot(&pi_20) + &v;
580        let y = &d * self.theta + &x.dot(&zeta_0) + &eps;
581        let clusters = Array2::from_shape_fn((rows, 2), |(r, c)| {
582            let (i, j) = ij(r);
583            if c == 0 {
584                i as i64
585            } else {
586                j as i64
587            }
588        });
589        let mut sim = Simulated::new(x, y, d);
590        sim.z = Some(z.insert_axis(Axis(1)));
591        sim.clusters = Some(clusters);
592        Ok(sim)
593    }
594}
595
596/// Treatment of [`LplrLzz2020`].
597#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
598pub enum LplrTreatment {
599    /// `d = a₀(x)`.
600    #[default]
601    Continuous,
602    /// `d ~ Bernoulli(expit(a₀(x) − mean))`.
603    Binary,
604    /// `d ~ Bernoulli(expit(a₀(x)))`.
605    BinaryUnbalanced,
606}
607
608/// Liu, Zhang and Zhou (2020): a binary outcome with
609/// `P(y = 1) = expit(αd + r₀(x))`, equicorrelated covariates (0.2) clipped
610/// to `[−2, 2]`. `generate` also returns the outcome probabilities.
611#[derive(Debug, Clone, PartialEq)]
612pub struct LplrLzz2020 {
613    pub n_obs: usize,
614    pub dim_x: usize,
615    pub alpha: f64,
616    pub balanced_r0: bool,
617    pub treatment: LplrTreatment,
618}
619
620impl Default for LplrLzz2020 {
621    fn default() -> Self {
622        Self {
623            n_obs: 500,
624            dim_x: 20,
625            alpha: 0.5,
626            balanced_r0: true,
627            treatment: LplrTreatment::Continuous,
628        }
629    }
630}
631
632impl LplrLzz2020 {
633    /// The data set and the outcome probabilities `p`.
634    pub fn generate(&self, seed: u64) -> Result<(Simulated, Array1<f64>)> {
635        check(self.dim_x >= 13, "dim_x must be at least 13")?;
636        let mut rng = rng(seed);
637        let (n, p) = (self.n_obs, self.dim_x);
638        let sigma = Array2::from_shape_fn((p, p), |(i, j)| if i == j { 1.0 } else { 0.2 });
639        let x = mvn(&mut rng, n, &sigma)?.mapv(|v| v.clamp(-2.0, 2.0));
640        let ind = |v: f64| f64::from(v > 0.0);
641        let (big, step) = if self.balanced_r0 {
642            (1.0, 0.25)
643        } else {
644            (4.0, 1.5)
645        };
646        let r0 = |i: usize| {
647            let r = x.row(i);
648            0.1 * r[0] * r[1] * r[2] + 0.1 * r[3] * r[4] + 0.1 * r[5].powi(3)
649                - 0.5 * r[6].sin().powi(2)
650                + 0.5 * r[7].cos()
651                + big / (1.0 + r[8] * r[8])
652                - 1.0 / (1.0 + r[9].exp())
653                + step * ind(r[10])
654                - 0.25 * ind(r[12])
655        };
656        let a0: Array1<f64> = (0..n)
657            .map(|i| {
658                let r = x.row(i);
659                2.0 / (1.0 + r[0].exp()) - 2.0 / (1.0 + r[1].exp())
660                    + r[2].sin()
661                    + r[3].cos()
662                    + 0.5 * ind(r[4])
663                    - 0.5 * ind(r[5])
664                    + 0.2 * r[6] * r[7]
665                    - 0.2 * r[8] * r[9]
666            })
667            .collect();
668        let d = match self.treatment {
669            LplrTreatment::Continuous => a0,
670            LplrTreatment::Binary => {
671                let m = mean(&a0);
672                a0.mapv(|v| bernoulli(&mut rng, expit(v - m)))
673            }
674            LplrTreatment::BinaryUnbalanced => a0.mapv(|v| bernoulli(&mut rng, expit(v))),
675        };
676        let prob: Array1<f64> = (0..n).map(|i| expit(self.alpha * d[i] + r0(i))).collect();
677        let y = prob.mapv(|q| bernoulli(&mut rng, q));
678        Ok((Simulated::new(x, y, d), prob))
679    }
680}
681
682/// Functional form of [`PlprCp2025`].
683#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
684pub enum PlprDesign {
685    /// Linear nuisances.
686    #[default]
687    Dgp1,
688    /// Smooth non-linear nuisances.
689    Dgp2,
690    /// Interactions and kinks.
691    Dgp3,
692}
693
694/// Clarke and Polselli (2025): a static panel with correlated unit effects,
695/// `d = m₀(x) + cᵢ + v`, `y = θd + g₀(x) + αᵢ + u`.
696#[derive(Debug, Clone, PartialEq)]
697pub struct PlprCp2025 {
698    pub num_id: usize,
699    pub num_t: usize,
700    pub dim_x: usize,
701    pub theta: f64,
702    pub design: PlprDesign,
703}
704
705impl Default for PlprCp2025 {
706    fn default() -> Self {
707        Self {
708            num_id: 250,
709            num_t: 10,
710            dim_x: 30,
711            theta: 0.5,
712            design: PlprDesign::Dgp1,
713        }
714    }
715}
716
717/// A simulated panel in long format (one row per unit and period).
718#[derive(Debug, Clone)]
719pub struct SimulatedPanel {
720    pub id: Array1<i64>,
721    pub t: Array1<f64>,
722    pub y: Array1<f64>,
723    pub d: Array1<f64>,
724    pub x: Array2<f64>,
725}
726
727impl SimulatedPanel {
728    /// The panel as [`StaticPanel`] for [`Plpr`](crate::Plpr).
729    pub fn to_static_panel(&self) -> Result<StaticPanel> {
730        StaticPanel::new(
731            self.y.clone(),
732            self.d.clone(),
733            self.x.clone(),
734            self.id.clone(),
735            self.t.clone(),
736        )
737    }
738}
739
740impl PlprCp2025 {
741    pub fn generate(&self, seed: u64) -> Result<SimulatedPanel> {
742        check(self.dim_x >= 3, "dim_x must be at least 3")?;
743        check(
744            self.num_id >= 1 && self.num_t >= 1,
745            "num_id and num_t must be positive",
746        )?;
747        let mut rng = rng(seed);
748        let (a, b, sigma2_a, sigma2_x) = (0.25, 0.5, 0.95_f64, 5.0_f64);
749        let (ni, nt) = (self.num_id, self.num_t);
750        let rows = ni * nt;
751        let a_i: Vec<f64> = (0..ni)
752            .map(|_| sigma2_a.sqrt() * normal(&mut rng))
753            .collect();
754        let c_i: Vec<f64> = (0..ni).map(|_| normal(&mut rng)).collect();
755        let x = Array2::from_shape_fn((rows, self.dim_x), |_| sigma2_x.sqrt() * normal(&mut rng));
756        let u = normals(&mut rng, rows);
757        let v = normals(&mut rng, rows);
758        let logistic = |v: f64| v.exp() / (1.0 + v.exp());
759        let pos = |v: f64| f64::from(v > 0.0);
760        let (g0, m0): (Vec<f64>, Vec<f64>) = (0..rows)
761            .map(|r| {
762                let (x0, x2) = (x[[r, 0]], x[[r, 2]]);
763                match self.design {
764                    PlprDesign::Dgp1 => (a * x0 + x2, a * x0 + x2),
765                    PlprDesign::Dgp2 => (logistic(x0) + a * x2.cos(), x0.cos() + a * logistic(x2)),
766                    PlprDesign::Dgp3 => (
767                        b * (x0 * x2) + a * (x2 * pos(x2)),
768                        a * (x0 * pos(x0)) + b * (x0 * x2),
769                    ),
770                }
771            })
772            .unzip();
773        let d: Array1<f64> = (0..rows).map(|r| m0[r] + c_i[r / nt] + v[r]).collect();
774        let d_mean = mean(&d);
775        let unit_mean = |f: &dyn Fn(usize) -> f64, i: usize| {
776            (0..nt).map(|k| f(i * nt + k)).sum::<f64>() / nt as f64
777        };
778        let alpha: Vec<f64> = (0..ni)
779            .map(|i| {
780                let d_term = unit_mean(&|r| d[r], i) - d_mean;
781                let x_term = unit_mean(&|r| x[[r, 0]] + x[[r, 2]], i);
782                0.25 * d_term + 0.25 * x_term + a_i[i]
783            })
784            .collect();
785        let y: Array1<f64> = (0..rows)
786            .map(|r| d[r] * self.theta + g0[r] + alpha[r / nt] + u[r])
787            .collect();
788        Ok(SimulatedPanel {
789            id: (0..rows).map(|r| (r / nt + 1) as i64).collect(),
790            t: (0..rows).map(|r| (r % nt + 1) as f64).collect(),
791            y,
792            d,
793            x,
794        })
795    }
796}
797
798// ---------------------------------------------------------------------------
799// Interactive models
800// ---------------------------------------------------------------------------
801
802/// Belloni, Chernozhukov, Fernández-Val and Hansen (2017): binary treatment
803/// with `P(d = 1 | x) = expit(c_d x'β)` and `y = θd + d·c_y x'β + ζ`, with
804/// `c_d`, `c_y` set by the target `R²` values.
805#[derive(Debug, Clone, PartialEq)]
806pub struct IrmData {
807    pub n_obs: usize,
808    pub dim_x: usize,
809    pub theta: f64,
810    pub r2_d: f64,
811    pub r2_y: f64,
812}
813
814impl Default for IrmData {
815    fn default() -> Self {
816        Self {
817            n_obs: 500,
818            dim_x: 20,
819            theta: 0.0,
820            r2_d: 0.5,
821            r2_y: 0.5,
822        }
823    }
824}
825
826impl IrmData {
827    pub fn generate(&self, seed: u64) -> Result<Simulated> {
828        check(self.dim_x >= 1, "dim_x must be positive")?;
829        check(
830            self.r2_d > 0.0 && self.r2_d < 1.0 && self.r2_y > 0.0 && self.r2_y < 1.0,
831            "R2_d and R2_y must lie in (0, 1)",
832        )?;
833        let mut rng = rng(seed);
834        let n = self.n_obs;
835        let v = uniforms(&mut rng, n, 0.0, 1.0);
836        let zeta = normals(&mut rng, n);
837        let cov = toeplitz(0.5, self.dim_x);
838        let x = mvn(&mut rng, n, &cov)?;
839        let beta: Array1<f64> = (1..=self.dim_x).map(|k| 1.0 / (k * k) as f64).collect();
840        let b_sigma_b = cov.dot(&beta).dot(&beta);
841        let c_y = (self.r2_y / ((1.0 - self.r2_y) * b_sigma_b)).sqrt();
842        let c_d = (std::f64::consts::PI.powi(2) / 3.0 * self.r2_d
843            / ((1.0 - self.r2_d) * b_sigma_b))
844            .sqrt();
845        let xb = x.dot(&beta);
846        let d: Array1<f64> = (0..n)
847            .map(|i| f64::from(expit(c_d * xb[i]) > v[i]))
848            .collect();
849        let y: Array1<f64> = (0..n)
850            .map(|i| d[i] * self.theta + d[i] * c_y * xb[i] + zeta[i])
851            .collect();
852        Ok(Simulated::new(x, y, d))
853    }
854}
855
856/// A binary treatment with an unobserved confounder `A`, as
857/// `make_confounded_irm_data` (Sant'Anna and Zhao covariates), with oracle
858/// confounding strengths for sensitivity analysis.
859#[derive(Debug, Clone, PartialEq)]
860pub struct ConfoundedIrm {
861    pub n_obs: usize,
862    pub theta: f64,
863    pub gamma_a: f64,
864    pub beta_a: f64,
865    pub linear: bool,
866    pub dim_x: usize,
867    pub trimming_threshold: f64,
868    pub var_eps_y: f64,
869}
870
871impl Default for ConfoundedIrm {
872    fn default() -> Self {
873        Self {
874            n_obs: 500,
875            theta: 0.0,
876            gamma_a: 0.127,
877            beta_a: 0.58,
878            linear: false,
879            dim_x: 5,
880            trimming_threshold: 0.01,
881            var_eps_y: 1.0,
882        }
883    }
884}
885
886/// Oracle values of [`ConfoundedIrm`].
887#[derive(Debug, Clone)]
888pub struct ConfoundedIrmOracle {
889    pub g_long: Array1<f64>,
890    pub g_short: Array1<f64>,
891    pub m_long: Array1<f64>,
892    pub m_short: Array1<f64>,
893    pub gamma_a: f64,
894    pub beta_a: f64,
895    pub a: Array1<f64>,
896    pub y_0: Array1<f64>,
897    pub y_1: Array1<f64>,
898    pub z: Array2<f64>,
899    pub cf_y: f64,
900    pub cf_d_ate: f64,
901    pub cf_d_atte: f64,
902    pub rho_ate: f64,
903    pub rho_atte: f64,
904}
905
906/// A data set of [`ConfoundedIrm`].
907#[derive(Debug, Clone)]
908pub struct ConfoundedIrmSimulated {
909    pub data: Simulated,
910    pub oracle: ConfoundedIrmOracle,
911}
912
913impl ConfoundedIrm {
914    pub fn generate(&self, seed: u64) -> Result<ConfoundedIrmSimulated> {
915        check(self.dim_x >= 5, "dim_x must be at least 5")?;
916        let mut rng = rng(seed);
917        let n = self.n_obs;
918        let xi = 0.75;
919        let x = mvn(&mut rng, n, &toeplitz(0.0, self.dim_x))?;
920        // Four transformed covariates and x₅.
921        let mut z = sz_features(&x, None);
922        z = ndarray::concatenate![Axis(1), z, standardize(&x.slice(s![.., 4..5]).to_owned())];
923        let eps_y = normals(&mut rng, n) * self.var_eps_y.sqrt();
924        let a = uniforms(&mut rng, n, -1.0, 1.0);
925        let var_a = 4.0 / 12.0;
926        let features = if self.linear { &x } else { &z };
927        let f_reg = |i: usize| {
928            let w = features.row(i);
929            2.5 + 0.74 * w[0] + 0.25 * w[1] + 0.137 * (w[2] + w[3])
930        };
931        let f_ps = |i: usize| {
932            let w = features.row(i);
933            xi * (-w[0] + 0.1 * w[1] - 0.25 * w[2] - 0.1 * w[3])
934        };
935        let p: Array1<f64> = (0..n).map(|i| expit(f_ps(i))).collect();
936        let mut m_long = &p + &(&a * self.gamma_a);
937        let mut m_short = p;
938        let t = self.trimming_threshold;
939        if m_long.iter().any(|&m| m < t || m > 1.0 - t) {
940            m_long.mapv_inplace(|m| m.clamp(t, 1.0 - t));
941            m_short.mapv_inplace(|m| m.clamp(t, 1.0 - t));
942        }
943        let u = uniforms(&mut rng, n, 0.0, 1.0);
944        let d: Array1<f64> = (0..n).map(|i| f64::from(m_long[i] >= u[i])).collect();
945        let d1x = z.column(4).mapv(|v| v + 1.0);
946        let var_dx = var(&(&d * &d1x));
947        let cov_adx = self.gamma_a * var_a;
948        let g_partial: Array1<f64> = (0..n).map(f_reg).collect();
949        let g_short_d1 = &d1x * (self.theta + self.beta_a * cov_adx / var_dx) + &g_partial;
950        let g_short = &d * &g_short_d1 + &(&d.mapv(|v| 1.0 - v) * &g_partial);
951        let g_long_d0 = &g_partial + &(&a * self.beta_a);
952        let g_long_d1 = &d1x * self.theta + &g_long_d0;
953        let g_long = &d * &g_long_d1 + &(&d.mapv(|v| 1.0 - v) * &g_long_d0);
954        let y_0 = &g_long_d0 + &eps_y;
955        let y_1 = &g_long_d1 + &eps_y;
956        let y = &d * &y_1 + &(&d.mapv(|v| 1.0 - v) * &y_0);
957        let cf_y = (&g_long - &g_short).mapv(|v| v * v).mean().unwrap()
958            / (&y - &g_short).mapv(|v| v * v).mean().unwrap();
959        let mean_d = mean(&d);
960        let rr = |m: &Array1<f64>| -> (Array1<f64>, Array1<f64>) {
961            let ate = (0..n)
962                .map(|i| d[i] / m[i] - (1.0 - d[i]) / (1.0 - m[i]))
963                .collect();
964            let atte = (0..n)
965                .map(|i| d[i] / mean_d - (1.0 - d[i]) / mean_d * (m[i] / (1.0 - m[i])))
966                .collect();
967            (ate, atte)
968        };
969        let ((ate_long, atte_long), (ate_short, atte_short)) = (rr(&m_long), rr(&m_short));
970        let inv_var = |m: &Array1<f64>| m.mapv(|v| 1.0 / (v * (1.0 - v))).mean().unwrap();
971        let odds = |m: &Array1<f64>| m.mapv(|v| v / (1.0 - v)).mean().unwrap();
972        let cf_d_ate = (inv_var(&m_long) - inv_var(&m_short)) / inv_var(&m_long);
973        let cf_d_atte = (odds(&m_long) - odds(&m_short)) / odds(&m_long);
974        let (rho_ate, rho_atte) = if self.beta_a == 0.0 || self.gamma_a == 0.0 {
975            (0.0, 0.0)
976        } else {
977            let g_diff = &g_long - &g_short;
978            (
979                corr(&g_diff, &(&ate_long - &ate_short)),
980                corr(&g_diff, &(&atte_long - &atte_short)),
981            )
982        };
983        Ok(ConfoundedIrmSimulated {
984            data: Simulated::new(x, y, d),
985            oracle: ConfoundedIrmOracle {
986                g_long,
987                g_short,
988                m_long,
989                m_short,
990                gamma_a: self.gamma_a,
991                beta_a: self.beta_a,
992                a,
993                y_0,
994                y_1,
995                z,
996                cf_y,
997                cf_d_ate,
998                cf_d_atte,
999                rho_ate,
1000                rho_atte,
1001            },
1002        })
1003    }
1004}
1005
1006/// A multi-valued treatment `d ∈ {0, 1, …, n_levels}`, as
1007/// `make_irm_data_discrete_treatments`: a latent continuous treatment cut at
1008/// its quantiles, with a share `1 / n_levels` of untreated units.
1009#[derive(Debug, Clone, PartialEq)]
1010pub struct IrmDiscreteTreatments {
1011    pub n_obs: usize,
1012    pub n_levels: usize,
1013    pub linear: bool,
1014    pub xi: f64,
1015    pub c: f64,
1016    pub dim_x: usize,
1017}
1018
1019impl Default for IrmDiscreteTreatments {
1020    fn default() -> Self {
1021        Self {
1022            n_obs: 200,
1023            n_levels: 3,
1024            linear: false,
1025            xi: 0.3,
1026            c: 0.0,
1027            dim_x: 5,
1028        }
1029    }
1030}
1031
1032/// Oracle values of [`IrmDiscreteTreatments`].
1033#[derive(Debug, Clone)]
1034pub struct DiscreteTreatmentOracle {
1035    pub cont_d: Array1<f64>,
1036    pub level_bounds: Array1<f64>,
1037    pub potential_level: Array1<f64>,
1038    pub ite: Array1<f64>,
1039    pub y0: Array1<f64>,
1040}
1041
1042/// A data set of [`IrmDiscreteTreatments`].
1043#[derive(Debug, Clone)]
1044pub struct DiscreteTreatmentSimulated {
1045    pub data: Simulated,
1046    pub oracle: DiscreteTreatmentOracle,
1047}
1048
1049impl IrmDiscreteTreatments {
1050    pub fn generate(&self, seed: u64) -> Result<DiscreteTreatmentSimulated> {
1051        check(self.n_levels >= 2, "n_levels must be at least 2")?;
1052        check(self.dim_x >= 4, "dim_x must be at least 4")?;
1053        let mut rng = rng(seed);
1054        let n = self.n_obs;
1055        let x = mvn(&mut rng, n, &toeplitz(self.c, self.dim_x))?;
1056        let z = sz_features(&x, Some(4));
1057        let eps_y = normals(&mut rng, n) * 5.0_f64.sqrt();
1058        let eps_d = normals(&mut rng, n);
1059        let features = if self.linear { &x } else { &z };
1060        let g: Array1<f64> = (0..n)
1061            .map(|i| {
1062                let w = features.row(i);
1063                210.0 + 27.4 * w[0] + 13.7 * (w[1] + w[2] + w[3])
1064            })
1065            .collect();
1066        let cont_d: Array1<f64> = (0..n)
1067            .map(|i| {
1068                let w = features.row(i);
1069                self.xi * (-w[0] + 0.5 * w[1] - 0.25 * w[2] - 0.1 * w[3]) + eps_d[i]
1070            })
1071            .collect();
1072        let k = self.n_levels;
1073        let values = cont_d.to_vec();
1074        let level_bounds: Array1<f64> = (0..=k)
1075            .map(|j| np_quantile(&values, j as f64 / k as f64))
1076            .collect();
1077        let potential_level: Array1<f64> = cont_d.mapv(|c| {
1078            1.0 + level_bounds
1079                .slice(s![1..k])
1080                .iter()
1081                .filter(|&&b| c >= b)
1082                .count() as f64
1083        });
1084        let eta = uniforms(&mut rng, n, 0.0, 1.0);
1085        let d: Array1<f64> = (0..n)
1086            .map(|i| f64::from(eta[i] >= 1.0 / k as f64) * potential_level[i])
1087            .collect();
1088        let ite = cont_d.mapv(|c| 15.0 * expit(c + 1.2 * c.cos()) - 2.0);
1089        let y0 = &g + &eps_y;
1090        let y: Array1<f64> = (0..n)
1091            .map(|i| ite[i] * f64::from(d[i] > 0.0) + y0[i])
1092            .collect();
1093        Ok(DiscreteTreatmentSimulated {
1094            data: Simulated::new(x, y, d),
1095            oracle: DiscreteTreatmentOracle {
1096                cont_d,
1097                level_bounds,
1098                potential_level,
1099                ite,
1100                y0,
1101            },
1102        })
1103    }
1104}
1105
1106/// A binary instrument `z` shifting a binary treatment, `d = 1{α_x z + v > 0}`,
1107/// `y = θd + x'β + u` with `corr(u, v) = 0.3`.
1108#[derive(Debug, Clone, PartialEq)]
1109pub struct IivmData {
1110    pub n_obs: usize,
1111    pub dim_x: usize,
1112    pub theta: f64,
1113    pub alpha_x: f64,
1114}
1115
1116impl Default for IivmData {
1117    fn default() -> Self {
1118        Self {
1119            n_obs: 500,
1120            dim_x: 20,
1121            theta: 1.0,
1122            alpha_x: 0.2,
1123        }
1124    }
1125}
1126
1127impl IivmData {
1128    pub fn generate(&self, seed: u64) -> Result<Simulated> {
1129        check(self.dim_x >= 1, "dim_x must be positive")?;
1130        let mut rng = rng(seed);
1131        let n = self.n_obs;
1132        let (u, v) = correlated_pair(&mut rng, n, 0.3);
1133        let x = mvn(&mut rng, n, &toeplitz(0.5, self.dim_x))?;
1134        let beta: Array1<f64> = (1..=self.dim_x).map(|k| 1.0 / (k * k) as f64).collect();
1135        let z: Array1<f64> = (0..n).map(|_| bernoulli(&mut rng, 0.5)).collect();
1136        let d: Array1<f64> = (0..n)
1137            .map(|i| f64::from(self.alpha_x * z[i] + v[i] > 0.0))
1138            .collect();
1139        let y = &d * self.theta + &x.dot(&beta) + &u;
1140        let mut sim = Simulated::new(x, y, d);
1141        sim.z = Some(z.insert_axis(Axis(1)));
1142        Ok(sim)
1143    }
1144}
1145
1146/// Sample selection: the outcome is observed when `s = 1`, with
1147/// `s = 1{x'β + d + γz + e₀ > 0}`; missing at random (`mar`, errors
1148/// independent and `γ = 0`) or nonignorable (`corr(e₀, e₁) = 0.8`, `γ = 1`).
1149/// Unobserved outcomes are 0.
1150#[derive(Debug, Clone, PartialEq)]
1151pub struct SsmData {
1152    pub n_obs: usize,
1153    pub dim_x: usize,
1154    pub theta: f64,
1155    pub mar: bool,
1156}
1157
1158impl Default for SsmData {
1159    fn default() -> Self {
1160        Self {
1161            n_obs: 8000,
1162            dim_x: 100,
1163            theta: 1.0,
1164            mar: true,
1165        }
1166    }
1167}
1168
1169impl SsmData {
1170    pub fn generate(&self, seed: u64) -> Result<Simulated> {
1171        check(self.dim_x >= 1, "dim_x must be positive")?;
1172        let mut rng = rng(seed);
1173        let n = self.n_obs;
1174        let (rho, gamma) = if self.mar { (0.0, 0.0) } else { (0.8, 1.0) };
1175        let (e0, e1) = correlated_pair(&mut rng, n, rho);
1176        let x = mvn(&mut rng, n, &toeplitz(0.5, self.dim_x))?;
1177        let beta: Array1<f64> = (1..=self.dim_x).map(|k| 0.4 / (k * k) as f64).collect();
1178        let xb = x.dot(&beta);
1179        let d: Array1<f64> = (0..n)
1180            .map(|i| f64::from(xb[i] + normal(&mut rng) > 0.0))
1181            .collect();
1182        let z = normals(&mut rng, n);
1183        let s: Array1<f64> = (0..n)
1184            .map(|i| f64::from(xb[i] + d[i] + gamma * z[i] + e0[i] > 0.0))
1185            .collect();
1186        let y: Array1<f64> = (0..n)
1187            .map(|i| {
1188                if s[i] == 1.0 {
1189                    xb[i] + self.theta * d[i] + e1[i]
1190                } else {
1191                    0.0
1192                }
1193            })
1194            .collect();
1195        let mut sim = Simulated::new(x, y, d);
1196        sim.z = Some(z.insert_axis(Axis(1)));
1197        sim.s = Some(s);
1198        Ok(sim)
1199    }
1200}
1201
1202/// Heterogeneous treatment effects `τ(x) = exp(2x₀) + 3 sin(4x₀)` (or
1203/// `3 sin(4x₁)` with `n_x = 2`) on uniform covariates, as
1204/// `make_heterogeneous_data`.
1205#[derive(Debug, Clone, PartialEq)]
1206pub struct HeterogeneousData {
1207    pub n_obs: usize,
1208    /// Number of covariates.
1209    pub p: usize,
1210    /// Covariates with non-zero coefficients.
1211    pub support_size: usize,
1212    /// Covariates the effect depends on (1 or 2).
1213    pub n_x: usize,
1214    pub binary_treatment: bool,
1215}
1216
1217impl Default for HeterogeneousData {
1218    fn default() -> Self {
1219        Self {
1220            n_obs: 200,
1221            p: 30,
1222            support_size: 5,
1223            n_x: 1,
1224            binary_treatment: false,
1225        }
1226    }
1227}
1228
1229/// A data set of [`HeterogeneousData`] with the individual effects.
1230#[derive(Debug, Clone)]
1231pub struct HeterogeneousSimulated {
1232    pub data: Simulated,
1233    pub effects: Array1<f64>,
1234}
1235
1236impl HeterogeneousData {
1237    /// The treatment effect at covariates `x`.
1238    pub fn treatment_effect(&self, x: ndarray::ArrayView2<'_, f64>) -> Array1<f64> {
1239        let k = if self.n_x == 2 { 1 } else { 0 };
1240        (0..x.nrows())
1241            .map(|i| (2.0 * x[[i, 0]]).exp() + 3.0 * (4.0 * x[[i, k]]).sin())
1242            .collect()
1243    }
1244
1245    pub fn generate(&self, seed: u64) -> Result<HeterogeneousSimulated> {
1246        check(self.n_x == 1 || self.n_x == 2, "n_x must be 1 or 2")?;
1247        check(
1248            self.support_size <= self.p && self.p >= self.n_x,
1249            "support_size must not exceed p",
1250        )?;
1251        let mut rng = rng(seed);
1252        let n = self.n_obs;
1253        let support: Vec<usize> = sample(&mut rng, self.p, self.support_size).into_vec();
1254        let coefs_y = uniforms(&mut rng, self.support_size, 0.0, 1.0);
1255        let coefs_d = uniforms(&mut rng, self.support_size, 0.0, 0.3);
1256        let epsilon = uniforms(&mut rng, n, -1.0, 1.0);
1257        let eta = uniforms(&mut rng, n, -1.0, 1.0);
1258        let x = Array2::from_shape_fn((n, self.p), |_| rng.gen::<f64>());
1259        let te = self.treatment_effect(x.view());
1260        let xs = x.select(Axis(1), &support);
1261        let index_d = xs.dot(&coefs_d);
1262        let d: Array1<f64> = if self.binary_treatment {
1263            (0..n).map(|i| f64::from(index_d[i] >= eta[i])).collect()
1264        } else {
1265            &index_d + &eta
1266        };
1267        let y = &te * &d + &xs.dot(&coefs_y) + &epsilon;
1268        Ok(HeterogeneousSimulated {
1269            data: Simulated::new(x, y, d),
1270            effects: te,
1271        })
1272    }
1273}
1274
1275// ---------------------------------------------------------------------------
1276// Difference-in-differences
1277// ---------------------------------------------------------------------------
1278
1279/// Which covariates enter the propensity and outcome models of the DiD
1280/// designs (Sant'Anna and Zhao, 2020): `1` both transformed, `2` propensity
1281/// raw, `3` outcome raw, `4` both raw, `5`/`6` experimental (no
1282/// confounding) with transformed / raw outcome covariates.
1283fn did_features<'a>(
1284    dgp_type: u8,
1285    x: &'a Array2<f64>,
1286    z: &'a Array2<f64>,
1287) -> Result<(Option<&'a Array2<f64>>, &'a Array2<f64>)> {
1288    Ok(match dgp_type {
1289        1 => (Some(z), z),
1290        2 => (Some(x), z),
1291        3 => (Some(z), x),
1292        4 => (Some(x), x),
1293        5 => (None, z),
1294        6 => (None, x),
1295        _ => {
1296            return Err(DmlError::InvalidArgument(
1297                "dgp_type must be between 1 and 6".into(),
1298            ))
1299        }
1300    })
1301}
1302
1303/// Sant'Anna and Zhao (2020): two periods, a binary treatment and the
1304/// transformed covariates `Z`. `generate` gives a panel (`y = Y₁ − Y₀`);
1305/// `generate_cross_sections` repeated cross-sections with a time indicator.
1306#[derive(Debug, Clone, PartialEq)]
1307pub struct DidSz2020 {
1308    pub n_obs: usize,
1309    pub dgp_type: u8,
1310    pub xi: f64,
1311    pub c: f64,
1312    /// Share of post-period observations (cross-sections).
1313    pub lambda_t: f64,
1314}
1315
1316impl Default for DidSz2020 {
1317    fn default() -> Self {
1318        Self {
1319            n_obs: 500,
1320            dgp_type: 1,
1321            xi: 0.75,
1322            c: 0.0,
1323            lambda_t: 0.5,
1324        }
1325    }
1326}
1327
1328/// `(z, d, y0, y1)`: covariates, treatment and the outcomes of both periods.
1329type TwoPeriods = (Array2<f64>, Array1<f64>, Array1<f64>, Array1<f64>);
1330
1331impl DidSz2020 {
1332    /// `(z, d, y0, y1)` of the two periods.
1333    fn draw(&self, rng: &mut ChaCha8Rng) -> Result<TwoPeriods> {
1334        let n = self.n_obs;
1335        let x = mvn(rng, n, &toeplitz(self.c, 4))?;
1336        let z = sz_features(&x, None);
1337        let eps_0 = normals(rng, n);
1338        let eps_1a = normals(rng, n);
1339        let eps_1b = normals(rng, n);
1340        let (ps, reg) = did_features(self.dgp_type, &x, &z)?;
1341        let f_reg = |i: usize| {
1342            let w = reg.row(i);
1343            210.0 + 27.4 * w[0] + 13.7 * (w[1] + w[2] + w[3])
1344        };
1345        let p: Array1<f64> = (0..n)
1346            .map(|i| match ps {
1347                None => 0.5,
1348                Some(w) => {
1349                    let w = w.row(i);
1350                    expit(self.xi * (-w[0] + 0.5 * w[1] - 0.25 * w[2] - 0.1 * w[3]))
1351                }
1352            })
1353            .collect();
1354        let u = uniforms(rng, n, 0.0, 1.0);
1355        let d: Array1<f64> = (0..n).map(|i| f64::from(p[i] >= u[i])).collect();
1356        let fr: Array1<f64> = (0..n).map(f_reg).collect();
1357        let nu: Array1<f64> = (0..n).map(|i| d[i] * fr[i] + normal(rng)).collect();
1358        let y0 = &fr + &nu + &eps_0;
1359        let y1: Array1<f64> = (0..n)
1360            .map(|i| 2.0 * fr[i] + nu[i] + if d[i] == 1.0 { eps_1b[i] } else { eps_1a[i] })
1361            .collect();
1362        Ok((z, d, y0, y1))
1363    }
1364
1365    /// A two-period panel: covariates `z`, treatment and the outcome change.
1366    pub fn generate(&self, seed: u64) -> Result<Simulated> {
1367        let (z, d, y0, y1) = self.draw(&mut rng(seed))?;
1368        Ok(Simulated::new(z, &y1 - &y0, d))
1369    }
1370
1371    /// Repeated cross-sections: each unit is observed once, in the post
1372    /// period (`t = 1`) with probability `lambda_t`.
1373    pub fn generate_cross_sections(&self, seed: u64) -> Result<Simulated> {
1374        check(
1375            self.lambda_t > 0.0 && self.lambda_t < 1.0,
1376            "lambda_t must lie in (0, 1)",
1377        )?;
1378        let mut rng = rng(seed);
1379        let (z, d, y0, y1) = self.draw(&mut rng)?;
1380        let t: Array1<f64> = (0..self.n_obs)
1381            .map(|_| f64::from(rng.gen::<f64>() <= self.lambda_t))
1382            .collect();
1383        let y = &t * &y1 + &(&t.mapv(|v| 1.0 - v) * &y0);
1384        let mut sim = Simulated::new(z, y, d);
1385        sim.t = Some(t);
1386        Ok(sim)
1387    }
1388}
1389
1390/// Callaway and Sant'Anna (2021): staggered adoption over `n_periods`
1391/// periods, with cohorts first treated from period `n_pre_treat_periods`
1392/// on, dynamic and heterogeneous effects, unit and time effects.
1393#[derive(Debug, Clone, PartialEq)]
1394pub struct DidCs2021 {
1395    pub n_obs: usize,
1396    pub dgp_type: u8,
1397    pub include_never_treated: bool,
1398    pub c: f64,
1399    pub xi: f64,
1400    pub n_periods: usize,
1401    pub anticipation_periods: usize,
1402    pub n_pre_treat_periods: usize,
1403}
1404
1405impl Default for DidCs2021 {
1406    fn default() -> Self {
1407        Self {
1408            n_obs: 1000,
1409            dgp_type: 1,
1410            include_never_treated: true,
1411            c: 0.0,
1412            xi: 0.75,
1413            n_periods: 5,
1414            anticipation_periods: 0,
1415            n_pre_treat_periods: 2,
1416        }
1417    }
1418}
1419
1420/// A simulated staggered-adoption panel in long format (periods
1421/// `0, 1, …`); `cohort` is the first treated period, `f64::INFINITY` for
1422/// never treated units.
1423#[derive(Debug, Clone)]
1424pub struct SimulatedStaggered {
1425    pub id: Array1<i64>,
1426    pub t: Array1<f64>,
1427    pub y: Array1<f64>,
1428    pub y0: Array1<f64>,
1429    pub y1: Array1<f64>,
1430    pub cohort: Array1<f64>,
1431    pub x: Array2<f64>,
1432}
1433
1434impl SimulatedStaggered {
1435    fn select(&self, keep: &[bool]) -> Self {
1436        let rows: Vec<usize> = (0..keep.len()).filter(|&r| keep[r]).collect();
1437        let pick = |v: &Array1<f64>| rows.iter().map(|&r| v[r]).collect::<Array1<f64>>();
1438        Self {
1439            id: rows.iter().map(|&r| self.id[r]).collect(),
1440            t: pick(&self.t),
1441            y: pick(&self.y),
1442            y0: pick(&self.y0),
1443            y1: pick(&self.y1),
1444            cohort: pick(&self.cohort),
1445            x: self.x.select(Axis(0), &rows),
1446        }
1447    }
1448
1449    /// The data as a [`PanelData`] for [`StaggeredDid`](crate::StaggeredDid).
1450    pub fn to_panel(&self) -> Result<PanelData> {
1451        PanelData::from_long(
1452            self.id.as_slice().expect("contiguous"),
1453            self.t.as_slice().expect("contiguous"),
1454            self.y.as_slice().expect("contiguous"),
1455            self.cohort.as_slice().expect("contiguous"),
1456            &self.x,
1457        )
1458    }
1459
1460    /// The rows as independent observations ([`RepeatedCrossSections`]).
1461    pub fn to_cross_sections(&self) -> Result<RepeatedCrossSections> {
1462        RepeatedCrossSections::new(
1463            self.y.clone(),
1464            self.t.clone(),
1465            self.cohort.clone(),
1466            self.x.clone(),
1467        )
1468    }
1469}
1470
1471impl DidCs2021 {
1472    /// The panel (`make_did_CS2021` with `time_type="float"`).
1473    pub fn generate(&self, seed: u64) -> Result<SimulatedStaggered> {
1474        self.panel(&mut rng(seed))
1475    }
1476
1477    /// Repeated cross-sections (`make_did_cs_CS2021`): a panel of
1478    /// `⌈n_obs / lambda_t⌉` units of which each row is kept with
1479    /// probability `lambda_t`.
1480    pub fn generate_cross_sections(&self, seed: u64, lambda_t: f64) -> Result<SimulatedStaggered> {
1481        check(
1482            lambda_t > 0.0 && lambda_t <= 1.0,
1483            "lambda_t must lie in (0, 1]",
1484        )?;
1485        let mut rng = rng(seed);
1486        let panel = DidCs2021 {
1487            n_obs: (self.n_obs as f64 / lambda_t).ceil() as usize,
1488            ..self.clone()
1489        }
1490        .panel(&mut rng)?;
1491        let keep: Vec<bool> = (0..panel.y.len())
1492            .map(|_| rng.gen::<f64>() < lambda_t)
1493            .collect();
1494        Ok(panel.select(&keep))
1495    }
1496
1497    fn panel(&self, rng: &mut ChaCha8Rng) -> Result<SimulatedStaggered> {
1498        check(
1499            self.n_pre_treat_periods >= 1,
1500            "n_pre_treat_periods must be at least 1",
1501        )?;
1502        let n_periods = self.n_periods + self.anticipation_periods;
1503        check(
1504            n_periods > self.n_pre_treat_periods,
1505            "need more periods than pre-treatment periods",
1506        )?;
1507        let n = self.n_obs;
1508        let x = mvn(rng, n, &toeplitz(self.c, 4))?;
1509        let z = sz_features(&x, None);
1510        let (ps, reg) = did_features(self.dgp_type, &x, &z)?;
1511        let periods: Vec<f64> = (0..n_periods).map(|t| t as f64).collect();
1512        let max_exposure = n_periods - self.n_pre_treat_periods;
1513        let mut cohorts: Vec<f64> = periods[self.n_pre_treat_periods..].to_vec();
1514        if self.include_never_treated {
1515            cohorts.push(f64::INFINITY);
1516        }
1517        let n_groups = cohorts.len();
1518        // Group of each unit.
1519        let group: Vec<usize> = (0..n)
1520            .map(|i| match ps {
1521                None => rng.gen_range(0..n_groups),
1522                Some(w) => {
1523                    let coef = [-1.0, 0.5, -0.25, -0.2];
1524                    let logits: Vec<f64> = (0..n_groups)
1525                        .map(|g| {
1526                            let scale = 1.0 - g as f64 / n_groups as f64;
1527                            let v: f64 = (0..4).map(|k| w[[i, k]] * coef[k] * scale).sum();
1528                            (self.xi * v).clamp(-2.5, 2.5)
1529                        })
1530                        .collect();
1531                    let e: Vec<f64> = logits.iter().map(|l| l.exp()).collect();
1532                    let total: f64 = e.iter().sum();
1533                    let clipped: Vec<f64> =
1534                        e.iter().map(|v| (v / total).clamp(0.05, 0.95)).collect();
1535                    let total: f64 = clipped.iter().sum();
1536                    let u = rng.gen::<f64>() * total;
1537                    let mut acc = 0.0;
1538                    let mut pick = n_groups - 1;
1539                    for (g, p) in clipped.iter().enumerate() {
1540                        acc += p;
1541                        if u < acc {
1542                            pick = g;
1543                            break;
1544                        }
1545                    }
1546                    pick
1547                }
1548            })
1549            .collect();
1550        let eta: Vec<f64> = group.iter().map(|&g| g as f64 + normal(rng)).collect();
1551        let rows = n * n_periods;
1552        let (mut y, mut y0, mut y1) = (
1553            Array1::zeros(rows),
1554            Array1::zeros(rows),
1555            Array1::zeros(rows),
1556        );
1557        let coef_reg = [27.4, 13.7, 13.7, 13.7];
1558        for i in 0..n {
1559            for t in 0..n_periods {
1560                let r = i * n_periods + t;
1561                let scale = (t + 1) as f64 / n_periods as f64;
1562                let f_reg = 210.0
1563                    + (0..4)
1564                        .map(|k| reg[[i, k]] * coef_reg[k] * scale)
1565                        .sum::<f64>();
1566                let exposure = if t < self.n_pre_treat_periods {
1567                    0.0
1568                } else {
1569                    let e = (t - self.n_pre_treat_periods) as f64 - group[i] as f64 + 1.0;
1570                    if group[i] >= max_exposure {
1571                        0.0
1572                    } else {
1573                        e.max(0.0)
1574                    }
1575                };
1576                let delta_e = exposure + 0.1 * x[[i, 0]] * exposure;
1577                let (e0, e1) = (normal(rng), normal(rng));
1578                y0[r] = f_reg + t as f64 + eta[i] + e0;
1579                y1[r] = y0[r] + delta_e + (e1 - e0);
1580                y[r] = if exposure > 0.0 { y1[r] } else { y0[r] };
1581            }
1582        }
1583        let panel = SimulatedStaggered {
1584            id: (0..rows).map(|r| (r / n_periods) as i64).collect(),
1585            t: (0..rows).map(|r| (r % n_periods) as f64).collect(),
1586            y,
1587            y0,
1588            y1,
1589            cohort: (0..rows).map(|r| cohorts[group[r / n_periods]]).collect(),
1590            x: Array2::from_shape_fn((rows, 4), |(r, k)| z[[r / n_periods, k]]),
1591        };
1592        if self.anticipation_periods == 0 {
1593            return Ok(panel);
1594        }
1595        // Drop the first periods and the cohorts treated too late, and shift
1596        // time back (DoubleML's anticipation handling).
1597        let a = self.anticipation_periods;
1598        let last_cohort = periods[n_periods - 1 - a];
1599        let keep: Vec<bool> = (0..rows)
1600            .map(|r| {
1601                panel.t[r] >= periods[a]
1602                    && (panel.cohort[r] <= last_cohort || panel.cohort[r].is_infinite())
1603            })
1604            .collect();
1605        let mut kept = panel.select(&keep);
1606        kept.t.mapv_inplace(|t| t - a as f64);
1607        Ok(kept)
1608    }
1609}
1610
1611// ---------------------------------------------------------------------------
1612// Regression discontinuity
1613// ---------------------------------------------------------------------------
1614
1615/// A simple RD design (`make_simple_rdd_data`): a normal score, uniform
1616/// covariates entering through polynomials of order `p`, effect `tau` at the
1617/// cutoff; sharp or fuzzy, with a continuous or binary outcome.
1618#[derive(Debug, Clone, PartialEq)]
1619pub struct SimpleRdd {
1620    pub n_obs: usize,
1621    pub p: usize,
1622    pub fuzzy: bool,
1623    pub binary_outcome: bool,
1624    pub cutoff: f64,
1625    pub dim_x: usize,
1626    pub a: f64,
1627    pub tau: f64,
1628}
1629
1630impl Default for SimpleRdd {
1631    fn default() -> Self {
1632        Self {
1633            n_obs: 5000,
1634            p: 4,
1635            fuzzy: true,
1636            binary_outcome: false,
1637            cutoff: 0.0,
1638            dim_x: 3,
1639            a: 0.0,
1640            tau: 1.0,
1641        }
1642    }
1643}
1644
1645/// A data set of [`SimpleRdd`]: running variable, covariates, outcome,
1646/// treatment and both potential outcomes.
1647#[derive(Debug, Clone)]
1648pub struct SimulatedRdd {
1649    pub score: Array1<f64>,
1650    pub x: Array2<f64>,
1651    pub y: Array1<f64>,
1652    pub d: Array1<f64>,
1653    pub y0: Array1<f64>,
1654    pub y1: Array1<f64>,
1655}
1656
1657impl SimulatedRdd {
1658    /// Outcome, treatment and covariates as [`DmlData`] (pass `score` to
1659    /// [`RdFlex::fit`](crate::RdFlex::fit)).
1660    pub fn to_data(&self) -> Result<DmlData> {
1661        DmlData::new(self.y.clone(), self.d.clone(), self.x.clone())
1662    }
1663}
1664
1665impl SimpleRdd {
1666    pub fn generate(&self, seed: u64) -> Result<SimulatedRdd> {
1667        check(self.dim_x >= 1, "dim_x must be positive")?;
1668        let mut rng = rng(seed);
1669        let n = self.n_obs;
1670        let score = normals(&mut rng, n);
1671        let x: Array2<f64> = Array2::from_shape_fn((n, self.dim_x), |_| rng.gen_range(-1.0..1.0));
1672        // Σ_j Σ_k k·x_jᵏ (numpy's Polynomial(arange(p + 1))).
1673        let g_cov: Array1<f64> = (0..n)
1674            .map(|i| {
1675                (0..self.dim_x)
1676                    .map(|j| {
1677                        (1..=self.p)
1678                            .map(|k| k as f64 * x[[i, j]].powi(k as i32))
1679                            .sum::<f64>()
1680                    })
1681                    .sum()
1682            })
1683            .collect();
1684        let g0 = score.mapv(|s| 0.1 * s * s);
1685        let g1: Array1<f64> = (0..n)
1686            .map(|i| {
1687                let s = score[i];
1688                self.tau + 0.1 * s * s - 0.5 * s * s + self.a * x.row(i).sum() * s
1689            })
1690            .collect();
1691        let (y0, y1): (Array1<f64>, Array1<f64>) = if self.binary_outcome {
1692            (
1693                (0..n)
1694                    .map(|i| bernoulli(&mut rng, expit(g0[i] + g_cov[i])))
1695                    .collect(),
1696                (0..n)
1697                    .map(|i| bernoulli(&mut rng, expit(g1[i] + g_cov[i])))
1698                    .collect(),
1699            )
1700        } else {
1701            (
1702                (0..n)
1703                    .map(|i| g0[i] + g_cov[i] + 0.2 * normal(&mut rng))
1704                    .collect(),
1705                (0..n)
1706                    .map(|i| g1[i] + g_cov[i] + 0.2 * normal(&mut rng))
1707                    .collect(),
1708            )
1709        };
1710        let intended = score.mapv(|s| f64::from(s >= self.cutoff));
1711        let d: Array1<f64> = if self.fuzzy {
1712            (0..n)
1713                .map(|i| {
1714                    let (s, z) = (score[i], intended[i]);
1715                    let p = (0.3 + 0.4 * z + 0.01 * s * s - 0.02 * s * s * z + 0.2 * g_cov[i])
1716                        .clamp(0.0, 1.0);
1717                    bernoulli(&mut rng, p)
1718                })
1719                .collect()
1720        } else {
1721            intended
1722        };
1723        let y: Array1<f64> = (0..n)
1724            .map(|i| if d[i] == 1.0 { y1[i] } else { y0[i] })
1725            .collect();
1726        Ok(SimulatedRdd {
1727            score,
1728            x,
1729            y,
1730            d,
1731            y0,
1732            y1,
1733        })
1734    }
1735}
1736
1737#[cfg(test)]
1738mod tests {
1739    use super::*;
1740
1741    #[test]
1742    fn mvn_has_the_requested_covariance() {
1743        let cov = toeplitz(0.7, 3);
1744        let x = mvn(&mut rng(1), 40_000, &cov).unwrap();
1745        let emp = x.t().dot(&x) / x.nrows() as f64;
1746        for ((i, j), &c) in cov.indexed_iter() {
1747            assert!((emp[[i, j]] - c).abs() < 0.03, "cov[{i},{j}]");
1748        }
1749    }
1750
1751    #[test]
1752    fn spd_matrix_is_positive_definite() {
1753        let m = spd_matrix(&mut rng(3), 6);
1754        assert!(cholesky_factor(&m).is_ok());
1755        assert!((&m - &m.t()).iter().all(|v| v.abs() < 1e-12));
1756    }
1757
1758    #[test]
1759    fn generators_are_reproducible_and_validated() {
1760        let a = PlrCcddhnr2018::default().generate(7).unwrap();
1761        let b = PlrCcddhnr2018::default().generate(7).unwrap();
1762        assert_eq!(a.y, b.y);
1763        assert!(PlrCcddhnr2018 {
1764            dim_x: 2,
1765            ..Default::default()
1766        }
1767        .generate(0)
1768        .is_err());
1769        assert!(LplrLzz2020 {
1770            dim_x: 10,
1771            ..Default::default()
1772        }
1773        .generate(0)
1774        .is_err());
1775        assert!(DidSz2020 {
1776            dgp_type: 7,
1777            ..Default::default()
1778        }
1779        .generate(0)
1780        .is_err());
1781    }
1782}