// dgp_pliv.mbt
//
// Pure-MoonBit port of upstream
// `doubleml.plm.datasets.dgp_pliv_CHS2015.make_pliv_CHS2015`.
//
// PLIV (Partially Linear IV) DGP from Chernozhukov, Hansen,
// Spindler (2015) "Frontier Models, Final Draft". The instrument
// z drives the treatment d, but is independent of the outcome
// noise.
//
// z_i ~ Bernoulli(0.5), // instrument
// propensity_d = sigmoid(0.5 * X_i @ e_1 + 0.5 * X_i @ e_2
// + 1.0 * z_i)
// d_i ~ Bernoulli(propensity_d)
// y_i = theta * d_i + X_i @ e_1 + X_i @ e_2 + v_i, v ~ N(0, 1)
// X ~ N(0, Sigma), Sigma_{kj} = 0.7^|j-k|.
///|
struct PlivData {
theta : Double
x : Matrix
y : Array[Double]
d : Array[Double]
z : Array[Double]
}
///|
pub fn make_pliv_CHS2015(
n_obs : Int,
dim_x : Int,
theta : Double,
seed : Int,
) -> PlivData {
let rng = chacha8_rng(seed)
// 1. Pre-draw X-draw normals.
let n_normals_x = n_obs * dim_x
let z_flat : Array[Double] = Array::make(n_normals_x, 0.0)
let half_z = (n_normals_x + 1) / 2
for i = 0; i < half_z; i = i + 1 {
let (z1, z2) = box_muller_pair(rng)
let idx_a = 2 * i
let idx_b = 2 * i + 1
if idx_a < n_normals_x {
z_flat[idx_a] = z1
}
if idx_b < n_normals_x {
z_flat[idx_b] = z2
}
}
// 2. Build X.
let x_flat : Array[Double] = Array::make(n_normals_x, 0.0)
for i = 0; i < n_obs; i = i + 1 {
for k = 0; k < dim_x; k = k + 1 {
let mut s = 0.0
let mut acc = 1.0
let mut j = k
while j >= 0 {
s = s + acc * z_flat[i * dim_x + j]
acc = acc * 0.7
if j == 0 {
break
}
j = j - 1
}
x_flat[i * dim_x + k] = s
}
}
// 3. Pre-draw v ~ N(0, 1).
let v_flat : Array[Double] = Array::make(n_obs, 0.0)
let half_n = (n_obs + 1) / 2
for i = 0; i < half_n; i = i + 1 {
let (z1, z2) = box_muller_pair(rng)
let idx_a = 2 * i
let idx_b = 2 * i + 1
if idx_a < n_obs {
v_flat[idx_a] = z1
}
if idx_b < n_obs {
v_flat[idx_b] = z2
}
}
// 4. Build z, d, y.
let z : Array[Double] = Array::make(n_obs, 0.0)
let d : Array[Double] = Array::make(n_obs, 0.0)
let y : Array[Double] = Array::make(n_obs, 0.0)
for i = 0; i < n_obs; i = i + 1 {
z[i] = if rng.double() < 0.5 { 1.0 } else { 0.0 }
let x1 = x_flat[i * dim_x + 1]
let x2 = x_flat[i * dim_x + 2]
let p_score = 0.5 * x1 + 0.5 * x2 + 1.0 * z[i]
let p = 1.0 / (1.0 + @math.exp(-p_score))
d[i] = if rng.double() < p { 1.0 } else { 0.0 }
y[i] = theta * d[i] + x1 + x2 + v_flat[i]
}
let x_mat = Matrix::from_array(x_flat, n_obs, dim_x)
{ theta, x: x_mat, y, d, z }
}
///|
pub fn PlivData::theta_get(self : PlivData) -> Double { self.theta }
///|
pub fn PlivData::x_get(self : PlivData) -> Matrix { self.x }
///|
pub fn PlivData::y_get(self : PlivData) -> Array[Double] { self.y }
///|
pub fn PlivData::d_get(self : PlivData) -> Array[Double] { self.d }
///|
pub fn PlivData::z_get(self : PlivData) -> Array[Double] { self.z }
///|
/// Convenience factory matching the upstream
/// `doubleml.plm.datasets._make_pliv_data.make_pliv_data`
/// signature: `(n_obs, dim_x, theta, seed)` with no other
/// arguments. Equivalent to `make_pliv_CHS2015(n_obs, dim_x,
/// theta, seed)` but named for parity with the upstream
/// factory helper.
pub fn make_pliv_data(
n_obs : Int,
dim_x : Int,
theta : Double,
seed : Int,
) -> PlivData {
make_pliv_CHS2015(n_obs, dim_x, theta, seed)
}