Skip to content

Commit 074eb85

Browse files
feat!: rake(bounds=) holds g within bounds during the solve instead of checking after (#422)
1 parent 81a953c commit 074eb85

19 files changed

Lines changed: 1270 additions & 397 deletions

‎packages/svy-rs/CHANGELOG.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ All notable changes to **svy_rs**, the internal Rust extension powering `svy`'s
66

77
### Added
88

9+
- **BREAKING: `rake` no longer takes `ll_bound`/`up_bound`**, and the `BOUNDS_EXCEEDED` error is gone; svy holds raking bounds through `calibrate_bounded(distance="raking")` instead of checking them after IPF.
10+
- `calibrate_bounded` and `calibrate_bounded_by_domain` take `distance="raking"`: g = clamp(exp(u)), with the derivative zero exactly where g is clamped (R's `cal.raking` cuts at `lower - 1`, `upper - 1` and stalls feasible problems), a small Levenberg–Marquardt term so rows held at a bound can move back, and a backtracking search on the dual when the full Newton step does not shrink the misfit.
911
- `calibrate_bounded` and `calibrate_bounded_by_domain`: linear calibration with g held in `[lower, upper]`, a port of R survey's `grake` with `cal.linear` (Newton steps on `MASS::ginv`, R's stopping rule and pre-scaling). Each returns the weights and, per column (and domain), the largest `|misfit| / (1 + |t|)` and the Newton steps taken; a column that did not converge holds the last iterate.
1012

1113
## [0.20.0] — 2026-10-28

‎packages/svy-rs/src/weighting/api.rs‎

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -25,15 +25,12 @@ type TrimOutput = (
2525
// ============================================================================
2626

2727
#[pyfunction]
28-
#[pyo3(signature = (wgt, margin_indices, margin_targets, ll_bound=None, up_bound=None, tol=1e-6, max_iter=100))]
29-
#[allow(clippy::too_many_arguments)]
28+
#[pyo3(signature = (wgt, margin_indices, margin_targets, tol=1e-6, max_iter=100))]
3029
pub(crate) fn rake(
3130
py: Python<'_>,
3231
wgt: PyReadonlyArray2<f64>,
3332
margin_indices: Vec<PyReadonlyArray1<i64>>,
3433
margin_targets: Vec<PyReadonlyArray1<f64>>,
35-
ll_bound: Option<f64>,
36-
up_bound: Option<f64>,
3734
tol: f64,
3835
max_iter: usize,
3936
) -> PyResult<Py<PyArray2<f64>>> {
@@ -47,10 +44,8 @@ pub(crate) fn rake(
4744
.map(|x| x.as_array().to_owned())
4845
.collect();
4946

50-
let result = crate::weighting::raking::rake_impl(
51-
wgt_arr, &indices, &targets, ll_bound, up_bound, tol, max_iter,
52-
)
53-
.map_err(|e| to_py_err(&e))?;
47+
let result = crate::weighting::raking::rake_impl(wgt_arr, &indices, &targets, tol, max_iter)
48+
.map_err(|e| to_py_err(&e))?;
5449

5550
Ok(result.into_pyarray(py).to_owned().into())
5651
}
@@ -216,7 +211,7 @@ type BoundedOutput = (
216211
);
217212

218213
#[pyfunction]
219-
#[pyo3(signature = (wgt, x_matrix, totals, lower, upper, tol, max_iter, scale=None))]
214+
#[pyo3(signature = (wgt, x_matrix, totals, lower, upper, tol, max_iter, scale=None, distance="linear"))]
220215
#[allow(clippy::too_many_arguments)]
221216
pub(crate) fn calibrate_bounded(
222217
py: Python<'_>,
@@ -228,9 +223,12 @@ pub(crate) fn calibrate_bounded(
228223
tol: f64,
229224
max_iter: usize,
230225
scale: Option<PyReadonlyArray1<f64>>,
226+
distance: &str,
231227
) -> PyResult<BoundedOutput> {
228+
let distance =
229+
crate::weighting::calibration::Distance::parse(distance).map_err(|e| to_py_err(&e))?;
232230
let scale_owned: Option<Array1<f64>> = scale.map(|s| s.as_array().to_owned());
233-
let (w, fits) = crate::weighting::calibration::calibrate_linear_bounded(
231+
let (w, fits) = crate::weighting::calibration::calibrate_bounded(
234232
wgt.as_array(),
235233
x_matrix.as_array(),
236234
totals.as_array(),
@@ -240,6 +238,7 @@ pub(crate) fn calibrate_bounded(
240238
upper,
241239
tol,
242240
max_iter,
241+
distance,
243242
},
244243
)
245244
.map_err(|e| to_py_err(&e))?;
@@ -262,7 +261,7 @@ type BoundedByDomainOutput = (
262261
);
263262

264263
#[pyfunction]
265-
#[pyo3(signature = (wgt, x_matrix, domain, controls_dict, lower, upper, tol, max_iter, scale=None))]
264+
#[pyo3(signature = (wgt, x_matrix, domain, controls_dict, lower, upper, tol, max_iter, scale=None, distance="linear"))]
266265
#[allow(clippy::too_many_arguments)]
267266
pub(crate) fn calibrate_bounded_by_domain(
268267
py: Python<'_>,
@@ -275,7 +274,10 @@ pub(crate) fn calibrate_bounded_by_domain(
275274
tol: f64,
276275
max_iter: usize,
277276
scale: Option<PyReadonlyArray1<f64>>,
277+
distance: &str,
278278
) -> PyResult<BoundedByDomainOutput> {
279+
let distance =
280+
crate::weighting::calibration::Distance::parse(distance).map_err(|e| to_py_err(&e))?;
279281
let scale_owned: Option<Array1<f64>> = scale.map(|s| s.as_array().to_owned());
280282
let controls: std::collections::HashMap<i64, Array1<f64>> = controls_dict
281283
.into_iter()
@@ -292,6 +294,7 @@ pub(crate) fn calibrate_bounded_by_domain(
292294
upper,
293295
tol,
294296
max_iter,
297+
distance,
295298
},
296299
)
297300
.map_err(|e| to_py_err(&e))?;

0 commit comments

Comments
 (0)