pyclsp 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
clsp/__init__.py ADDED
@@ -0,0 +1,8 @@
1
+ __version__ = "1.0.0"
2
+
3
+ from .clsp import CLSP
4
+
5
+ __all__ = [
6
+ "CLSP",
7
+ "__version__"
8
+ ]
clsp/clsp.py ADDED
@@ -0,0 +1,232 @@
1
+ import warnings
2
+ import copy
3
+ import numpy as np
4
+ import scipy.linalg as la
5
+ import scipy.stats as stats
6
+ import cvxpy as cp
7
+ from types import MethodType
8
+ from .errors import CLSPError
9
+ from .utils import CLSPCanonicalForm, CLSPCorrelogram, CLSPTTest
10
+ from .solver import CLSPSolve
11
+
12
+ class CLSP:
13
+ def __init__(self):
14
+ """
15
+ Instantiate a CLSP (Convex Least Squares Programming) object.
16
+
17
+ This class initiates and stores the results of a two-stage estimation
18
+ process for constrained least-squares problems under a canonical matrix
19
+ structure. The approach is designed for underdetermined, ill-posed, or
20
+ structurally constrained systems.
21
+
22
+ Stage 1 estimates the solution z from the system A·z = b using a
23
+ pseudoinverse method. Depending on the projection matrix Z, this may
24
+ be the Moore–Penrose inverse (Z = I) or the Bott–Duffin inverse (Z ≠ I).
25
+ If r > 1, the pseudoinverse solution is iteratively refined using an
26
+ updated slack matrix Q to improve numerical stability and feasibility.
27
+
28
+ Stage 2 optionally refines the estimate using a convex optimization
29
+ step centered around the pseudoinverse estimate ẑ. Regularization is
30
+ governed by the user-specified parameter α:
31
+ • α = 0 → Lasso (ℓ₁ norm),
32
+ • α = 1 → Ridge (ℓ₂ norm),
33
+ • 0 < α < 1 → Elastic Net (ℓ₁ and ℓ₂ combined).
34
+
35
+ The class also supports diagnostic routines such as Monte Carlo
36
+ hypothesis testing and row-wise deletion sensitivity analysis.
37
+
38
+ Attributes
39
+ ----------
40
+ A : np.ndarray or None
41
+ Canonical design matrix with block structure A = [C | S; M | Q].
42
+
43
+ C_idx : tuple
44
+ Pair of integers defining the row and column ranges of the
45
+ C block inside A. Used for matrix slicing and partitioning.
46
+
47
+ b : np.ndarray or None
48
+ Right-hand side vector for the linear system A·z = b.
49
+
50
+ Z : np.ndarray or None
51
+ Projection matrix for Bott–Duffin inversion. Must be symmetric
52
+ and idempotent. Defaults to identity for Moore–Penrose.
53
+
54
+ zhat : np.ndarray or None
55
+ Unregularized pseudoinverse estimate of z from Step 1.
56
+
57
+ final : bool
58
+ Whether to run the second-stage convex refinement. If True,
59
+ the estimate is regularized via convex programming.
60
+
61
+ alpha : float
62
+ Regularization parameter:
63
+ - α = 0: Lasso (L1 penalty),
64
+ - α = 1: Ridge (L2 penalty),
65
+ - 0 < α < 1: Elastic Net combination.
66
+
67
+ z : np.ndarray or None
68
+ Final estimate of z after regularization (if final is True).
69
+ If skipped, z = zhat.
70
+
71
+ x : np.ndarray or None
72
+ Variable component extracted from z, reshaped into m × p.
73
+
74
+ y : np.ndarray or None
75
+ Slack component of z representing inequality residuals.
76
+
77
+ r : int
78
+ Number of refinement iterations performed during Step 1.
79
+ Iteration stops when NRMSE stabilizes or exceeds the limit.
80
+
81
+ kappaC : float or None
82
+ Condition number of the constraint block C (upper-left block
83
+ in A).
84
+
85
+ kappaB : float or None
86
+ Condition number of the projected estimator B^(r) = pinv(C)·A,
87
+ calculated during refinement.
88
+
89
+ kappaA : float or None
90
+ Condition number of the full canonical matrix A^(r) after
91
+ refinement step r.
92
+
93
+ rmsa : float or None
94
+ Total RMSA (Root Mean Square Adjustment) over all rows.
95
+
96
+ rmsa_i : list of float
97
+ Change in RMSA caused by removing each row from [C | S]
98
+ and re-estimating the CLSP solution.
99
+
100
+ rmsa_dkappaC : list of float
101
+ Change in κ(C) caused by removing each row from [C | S]
102
+ and re-estimating the CLSP solution.
103
+
104
+ rmsa_dkappaB : list of float
105
+ Change in κ(B) caused by removing each row from [C | S]
106
+ and re-estimating the CLSP solution.
107
+
108
+ rmsa_dkappaA : list of float
109
+ Change in κ(A) caused by removing each row from [C | S]
110
+ and re-estimating the CLSP solution.
111
+
112
+ rmsa_dnrmse : list of float
113
+ Change in NRMSE caused by removing each row from [C | S]
114
+ and re-estimating the CLSP solution.
115
+
116
+ rmsa_dzhat : list of float
117
+ Change in zhat caused by removing each row from [C | S]
118
+ and re-estimating the CLSP solution.
119
+
120
+ rmsa_dz : list of float
121
+ Change in z caused by removing each row from [C | S]
122
+ and re-estimating the CLSP solution.
123
+
124
+ rmsa_dx : list of float
125
+ Change in x caused by removing each row from [C | S]
126
+ and re-estimating the CLSP solution.
127
+
128
+ r2_partial : float or None
129
+ R² statistic computed over the M block. Reflects partial
130
+ goodness-of-fit in structured systems.
131
+
132
+ nrmse : float or None
133
+ Normalized RMSE between A·z and b, computed over full system.
134
+
135
+ nrmse_partial : float or None
136
+ NRMSE computed only over M block rows.
137
+
138
+ nrmse_ttest : list of float
139
+ NRMSE samples generated via simulation under the null, used
140
+ for empirical t-testing.
141
+
142
+ z_lower : np.ndarray or None
143
+ Lower bound of confidence band on z. Computed from κ(A) and
144
+ residual norm.
145
+
146
+ z_upper : np.ndarray or None
147
+ Upper bound of confidence band on z. Symmetric to z_lower.
148
+
149
+ seed : int
150
+ Random seed used for reproducible Monte Carlo diagnostics.
151
+
152
+ rng : np.random.Generator
153
+ Random number generator initialized using `seed`.
154
+
155
+ distribution : callable
156
+ Function that generates random samples for simulation.
157
+ Must accept a single integer argument `n`.
158
+
159
+ Methods
160
+ -------
161
+ error : method
162
+ Raise a CLSPError with optional diagnostics or contextual details.
163
+
164
+ canonize : method
165
+ Construct matrix A = [C | S; M | Q] and define constraint partitions.
166
+
167
+ solve : method
168
+ Main estimation routine performing pseudoinverse and convex steps.
169
+
170
+ corr : method
171
+ Compute RMSA and condition diagnostics via leave-one-out row deletion
172
+ on [C | S]. Populates all `self.rmsa_*` lists. Output is external.
173
+
174
+ ttest : method
175
+ Monte Carlo t-test for NRMSE using user-defined distribution. Returns
176
+ one-sided and two-sided p-values based on simulated means.
177
+ """
178
+ # Variables
179
+ self.A : np.ndarray | None = None # design matrix, [C|S;M|Q]
180
+ self.C_idx : tuple[int | None, int | None] \
181
+ = (None, None) # indices of the C block
182
+ self.b : np.ndarray | None = None # right-hand side
183
+ self.Z : np.ndarray | None = None # B-D subspace matrix
184
+ self.r : int = 0 # number of iterations
185
+ self.zhat : np.ndarray | None = None # first-step estimate
186
+ self.final : bool = True # inclusion of second step
187
+ self.alpha : float = 1.0 # regularization parameter
188
+ self.z : np.ndarray | None = None # final solution
189
+ self.x : np.ndarray | None = None # variable component of z
190
+ self.y : np.ndarray | None = None # slack component of z
191
+ self.kappaC : float | None = None # spectral κ() for C_canon
192
+ self.kappaB : float | None = None # spectral κ() for B^(r)
193
+ self.kappaA : float | None = None # spectral κ() for A^(r)
194
+ self.rmsa : float | None = None # total RMSA
195
+ self.rmsa_i : list[float] = [] # list of row RMSA
196
+ self.rmsa_dkappaC : list[float] = [] # list of Δκ(C)
197
+ self.rmsa_dkappaB : list[float] = [] # list of Δκ(B)
198
+ self.rmsa_dkappaA : list[float] = [] # list of Δκ(A)
199
+ self.rmsa_dnrmse : list[float] = [] # list of ΔNRMSE
200
+ self.rmsa_dzhat : list[float] = [] # list of Δzhat
201
+ self.rmsa_dz : list[float] = [] # list of Δz
202
+ self.rmsa_dx : list[float] = [] # list of Δx
203
+ self.r2_partial : float | None = None # R^2 for the M block
204
+ self.nrmse : float | None = None # NRMSE for A
205
+ self.nrmse_partial : float | None = None # NRMSE for the M block
206
+ self.nrmse_ttest : list[float] = [] # list of NRMSE
207
+ self.z_lower : np.ndarray | None = None # lower confidence band
208
+ self.z_upper : np.ndarray | None = None # upper confidence band
209
+ self.seed : int = 123456789 # Monte Carlo
210
+ self.rng = np.random.default_rng(self.seed)
211
+ self.distribution = lambda n: self.rng.normal(loc=0, scale=1, size=n)
212
+
213
+ # Methods
214
+ self.error = CLSPError
215
+ self.canonize = MethodType(CLSPCanonicalForm, self)
216
+ self.solve = MethodType(CLSPSolve, self)
217
+ self.corr = MethodType(CLSPCorrelogram, self)
218
+ self.ttest = MethodType(CLSPTTest, self)
219
+
220
+ def __repr__(self):
221
+ """
222
+ Return a formatted string representation of the CLSP object
223
+ """
224
+ if self.z is None:
225
+ return "<CLSP: z=None, x=None, y=None>"
226
+
227
+ z_str = (np.array2string(self.z, max_line_width=80))
228
+ x_str = (np.array2string(self.x, max_line_width=80)
229
+ if self.x is not None else "None")
230
+ y_str = (np.array2string(self.y, max_line_width=80)
231
+ if self.y is not None else "None")
232
+ return f"<CLSP:\n z={z_str},\n x={x_str},\n y={y_str}>"
clsp/errors.py ADDED
@@ -0,0 +1,22 @@
1
+ class CLSPError(Exception):
2
+ """
3
+ Exception class for CLSP-related errors.
4
+
5
+ Represents internal failures in Convex Least Squares Programming
6
+ routines. Supports structured messaging and optional diagnostic
7
+ augmentation.
8
+
9
+ Parameters
10
+ ----------
11
+ message : str, optional
12
+ Description of the error. Defaults to a generic CLSP message.
13
+
14
+ code : int or str, optional
15
+ Optional error code or identifier for downstream handling.
16
+
17
+ Usage
18
+ -----
19
+ raise CLSPError("Matrix A and b are incompatible", code=101)
20
+ """
21
+ def __init__(self, message: str = "An error occurred in CLSP"):
22
+ super().__init__(message)
clsp/solver.py ADDED
@@ -0,0 +1,274 @@
1
+ import warnings
2
+ import numpy as np
3
+ import scipy.linalg as la
4
+ import cvxpy as cp
5
+
6
+ def CLSPSolve(
7
+ self, problem: str = "", C: np.ndarray | None = None,
8
+ S: np.ndarray | None = None, M: np.ndarray | None = None,
9
+ b: np.ndarray | None = None,
10
+ m: int | None = None, p: int | None = None,
11
+ i: int = 1, j: int = 1,
12
+ zero_diagonal: bool = False,
13
+ r: int = 1, Z: np.ndarray | None = None,
14
+ tolerance: float | None = None,
15
+ iteration_limit: int | None = None,
16
+ final: bool | None = None, alpha: float | None = None,
17
+ *args, **kwargs
18
+ ) -> "CLSP":
19
+ """
20
+ Solve the Convex Least Squares Programming (CLSP) problem.
21
+
22
+ This method performs a two-step estimation:
23
+ (1) a pseudoinverse-based solution using either the Moore–Penrose or
24
+ Bott–Duffin inverse, optionally iterated for convergence;
25
+ (2) a convex-programming correction using Lasso, Ridge, or Elastic Net
26
+ regularization (if enabled).
27
+
28
+ Parameters
29
+ ----------
30
+ problem : str, optional
31
+ Structural template for matrix construction. One of:
32
+ - 'ap' or 'tm' : allocation or transaction matrix problem.
33
+ - 'cmls' or 'rp' : constrained modular least squares or RP-type.
34
+ - '' or other: General CLSP problems (user-defined C and/or M).
35
+
36
+ C, S, M : np.ndarray or None
37
+ Blocks of the constraint matrix A = [C | S; M | Q].
38
+ If `C` and/or `M` are provided, the matrix A is constructed
39
+ accordingly. If both are None and A is not yet defined, an error
40
+ is raised.
41
+
42
+ b : np.ndarray or None
43
+ Right-hand side vector. Must have as many rows as A. Required.
44
+
45
+ m, p : int or None
46
+ Dimensions of X ∈ ℝ^{m×p}, relevant for allocation problems ('ap').
47
+
48
+ i, j : int, default = 1
49
+ Grouping sizes for row and column sum constraints in AP problems.
50
+
51
+ zero_diagonal : bool, default = False
52
+ If True, enforces structural zero diagonals via identity truncation.
53
+
54
+ r : int, default = 1
55
+ Number of refinement iterations for the pseudoinverse-based estimator.
56
+ When `r > 1`, the slack block Q is updated iteratively to improve
57
+ feasibility in underdetermined or ill-posed systems.
58
+
59
+ Z : np.ndarray or None
60
+ A symmetric idempotent matrix (projector) defining the subspace for
61
+ Bott–Duffin pseudoinversion. If None, the identity matrix is used,
62
+ reducing to the Moore–Penrose case.
63
+
64
+ tolerance : float, optional
65
+ Convergence tolerance for NRMSE change between iterations. Default is
66
+ √(machine epsilon).
67
+
68
+ iteration_limit : int, default = 50
69
+ Maximum number of iterations allowed in the refinement loop.
70
+
71
+ final : bool, default = True
72
+ If True, a convex programming problem is solved to refine `zhat`.
73
+ The resulting solution `z` minimizes a weighted L1/L2 norm around
74
+ `zhat` subject to Az = b.
75
+
76
+ alpha : float, default = 1.0
77
+ Regularization weight in the final convex program:
78
+ - α = 0: Lasso (L1 norm)
79
+ - α = 1: Ridge (L2 norm)
80
+ - 0 < α < 1: Elastic Net
81
+
82
+ *args, **kwargs : optional
83
+ Additional arguments passed to the CVXPY solver backend.
84
+
85
+ Attributes Set
86
+ ---------------
87
+ self.A : np.ndarray
88
+ Canonical design matrix constructed from (C, S, M, Q).
89
+
90
+ self.b : np.ndarray
91
+ Conformable right-hand side vector.
92
+
93
+ self.Z : np.ndarray
94
+ Projector matrix used for Bott–Duffin inversion.
95
+
96
+ self.zhat : np.ndarray
97
+ First-step solution (unregularized pseudoinverse estimate).
98
+
99
+ self.z : np.ndarray
100
+ Final estimate after optional convex refinement.
101
+
102
+ self.x, self.y : np.ndarray
103
+ Variable and slack components reshaped from `z`.
104
+
105
+ self.nrmse : float
106
+ Normalized root mean squared error over the full system.
107
+
108
+ self.r2_partial : float or np.nan
109
+ R² for the M block (if applicable), computed from partial residuals.
110
+
111
+ self.nrmse_partial : float
112
+ NRMSE over the M block, if defined.
113
+
114
+ self.kappaA, self.kappaB, self.kappaC : float
115
+ Condition numbers for the full, projected, and constrained system.
116
+
117
+ self.z_lower, self.z_upper : np.ndarray
118
+ Condition-weighted confidence band for z.
119
+
120
+ self.r : int
121
+ Number of refinement iterations performed.
122
+
123
+ Raises
124
+ ------
125
+ CLSPError
126
+ If the design matrix A or right-hand side b is malformed, inconsistent,
127
+ or incompatible with the structural assumptions of the problem.
128
+ """
129
+ # (A), (b) Construct a conformable canonical form for the CLSP estimator
130
+ if b is not None:
131
+ self.b = b
132
+ elif self.b is None:
133
+ raise self.error("Right-hand side vector b must be provided.")
134
+ if C is not None or M is not None:
135
+ self.canonize(problem, C, S, M, None, self.b.reshape(-1, 1),
136
+ m, p, i, j, zero_diagonal)
137
+ elif self.A is None:
138
+ raise self.error("At least one of C or M must be provided.")
139
+ if self.A.shape[0] != self.b.shape[0]:
140
+ raise self.error(f"The matrix A and vector b must have the same "
141
+ f"number of rows: A has {self.A.shape[0]}, but "
142
+ f"b has {self.b.shape[0]}")
143
+
144
+ # (zhat) (Iterated if r > 1) first-step estimate
145
+ if r < 1:
146
+ raise self.error("Number of refinement iterations r must be ≥ 1.")
147
+ if Z is not None:
148
+ self.Z = Z
149
+ elif self.Z is None:
150
+ self.Z = np.eye(self.A.shape[1])
151
+ if tolerance is not None:
152
+ self.tolerance = tolerance
153
+ if iteration_limit is not None:
154
+ self.iteration_limit = iteration_limit
155
+ try:
156
+ if (not np.allclose(self.Z, self.Z.T, atol=tolerance) or
157
+ not np.allclose(self.Z @ self.Z, self.Z, atol=tolerance) or
158
+ self.Z.shape[0] != self.A.shape[1]):
159
+ raise ValueError
160
+ except ValueError:
161
+ raise self.error(f"Matrix Z must be symmetric, idempotent and "
162
+ f"match the number of columns in A: expected "
163
+ f"({self.A.shape[1]}, {self.A.shape[1]}), "
164
+ f"got {self.Z.shape}")
165
+ for n_iter in range(1, 1 +
166
+ (r if self.A.shape[0] > self.C_idx[0] else 1)):
167
+ # save A, zhat, and NRMSE from the previous step, construct Q
168
+ if n_iter > 1:
169
+ A_prev = self.A.copy()
170
+ zhat_prev = self.zhat.copy()
171
+ nrmse_prev = (np.linalg.norm(self.b - self.A @ self.zhat) /
172
+ np.sqrt(self.b.shape[0]) / np.std(self.b))
173
+ Q = np.diagflat(-np.sign(self.b -
174
+ self.A @ self.zhat)[self.C_idx[0]:])
175
+ self.canonize(problem, C, S, M, Q, b.reshape(-1, 1),
176
+ m, p, i, j, zero_diagonal)
177
+ # solve via the Bott–Duffin inverse
178
+ self.zhat = (la.pinv(self.Z @ (self.A.T @ self.A) @ self.Z) @
179
+ self.Z @ self.A.T) @ self.b
180
+ self.nrmse = (lambda residuals, sd:
181
+ np.linalg.norm(residuals) / np.sqrt(sd.shape[0]) /
182
+ np.std(sd) if not np.isclose(np.std(sd), 0) else
183
+ np.inf)(self.b - self.A @ self.zhat, self.b)
184
+ # break on convergence
185
+ self.r = n_iter
186
+ if n_iter > 1:
187
+ if (abs(self.nrmse - nrmse_prev) < self.tolerance or
188
+ n_iter > self.iteration_limit):
189
+ del A_prev, zhat_prev, nrmse_prev, Q
190
+ break
191
+ if not np.all(np.isfinite(self.zhat)):
192
+ self.zhat = np.nan
193
+ raise self.error("Pseudoinverse estimate zhat failed")
194
+
195
+ # (z) Final solution (if available), or set self.z = self.zhat
196
+ if final is not None:
197
+ self.final = final
198
+ if alpha is not None:
199
+ self.alpha = alpha
200
+ self.alpha = max(0, min(1, self.alpha))
201
+ if self.final:
202
+ # build a convex problem (p_cvx) and its solver (c_cvx)
203
+ z_cvx = cp.Variable(self.A.shape[1])
204
+ d_cvx = z_cvx - self.zhat.flatten()
205
+ if np.isclose(self.alpha, 0): # Lasso
206
+ f_obj = cp.norm1(d_cvx)
207
+ s_cvx = cp.ECOS
208
+ elif np.isclose(self.alpha, 1): # Ridge
209
+ f_obj = cp.sum_squares(d_cvx)
210
+ s_cvx = cp.OSQP
211
+ else: # Elastic Net
212
+ f_obj = ((1 - self.alpha) * cp.norm1(d_cvx) +
213
+ self.alpha * cp.sum_squares(d_cvx))
214
+ s_cvx = cp.SCS
215
+ c_cvx = [self.A @ z_cvx == self.b.flatten()]
216
+ p_cvx = cp.Problem(cp.Minimize(f_obj), c_cvx)
217
+ # solve
218
+ try:
219
+ p_cvx.solve(solver=s_cvx, verbose=False,
220
+ *args, **kwargs) # pass arguments
221
+ if z_cvx.value is None:
222
+ warnings.warn(
223
+ f"Step 2 infeasible ({p_cvx.status}); falling back to Step 1.",
224
+ category=RuntimeWarning
225
+ )
226
+ self.z = self.zhat
227
+ else:
228
+ self.z = z_cvx.value
229
+ self.nrmse = (lambda residuals, sd:
230
+ np.linalg.norm(residuals) / np.sqrt(sd.shape[0]) /
231
+ np.std(sd) if not np.isclose(np.std(sd), 0) else
232
+ np.inf)(self.b - self.A @ self.z, self.b)
233
+ except (cp.SolverError, ValueError):
234
+ self.z = self.zhat
235
+ else:
236
+ self.z = self.zhat
237
+
238
+ # (x), (y) Variable and slack components of z
239
+ self.x = self.z[:self.C_idx[0]].reshape(m if m is not None else -1,
240
+ p if p is not None else 1)
241
+ self.y = self.z[self.C_idx[0]:]
242
+
243
+ # (kappaC), (kappaB), (kappaA) Condition numbers
244
+ self.kappaC = np.linalg.cond( self.A[:self.C_idx[0], :])
245
+ self.kappaB = np.linalg.cond(la.pinv(self.A[:self.C_idx[0], :]) @
246
+ self.A)
247
+ self.kappaA = np.linalg.cond( self.A)
248
+
249
+ # (r2_partial), (nrmse_partial) M-block-based statistics
250
+ if self.A.shape[0] > self.C_idx[0]:
251
+ M = self.A[self.C_idx[0]:, :self.C_idx[1]]
252
+ b_M = self.b[-M.shape[0]:]
253
+ residuals_M = b_M - M @ self.x.reshape(-1, 1)
254
+ self.r2_partial = (lambda residuals, sd:
255
+ 1 - (np.linalg.norm(residuals) ** 2 /
256
+ np.linalg.norm(sd - np.mean(sd)) ** 2)
257
+ if not np.isclose(np.std(sd), 0) else
258
+ np.nan)(residuals_M, b_M)
259
+ self.nrmse_partial = (lambda residuals, sd:
260
+ np.linalg.norm(residuals) / np.sqrt(sd.shape[0]) /
261
+ np.std(sd) if not np.isclose(np.std(sd), 0) else
262
+ np.inf)(residuals_M, b_M)
263
+ del M, b_M, residuals_M
264
+
265
+ # (z_lower), (z_upper) Condition-weighted confidence band
266
+ dz = (self.kappaA *
267
+ np.linalg.norm(self.b - self.A @ self.z) /
268
+ np.linalg.norm(self.b)
269
+ if not np.isclose(np.linalg.norm(self.b), 0) else
270
+ np.inf)
271
+ self.z_lower = self.z * (1 - dz)
272
+ self.z_upper = self.z * (1 + dz)
273
+
274
+ return self
clsp/utils.py ADDED
@@ -0,0 +1,314 @@
1
+ import copy
2
+ import numpy as np
3
+ import scipy.stats as stats
4
+
5
+ def CLSPCanonicalForm(
6
+ self, problem: str = "", C: np.ndarray | None = None,
7
+ S: np.ndarray | None = None, M: np.ndarray | None = None,
8
+ Q: np.ndarray | None = None, b: np.ndarray | None = None,
9
+ m: int | None = None, p: int | None = None,
10
+ i: int = 1, j: int = 1,
11
+ zero_diagonal: bool = False
12
+ ) -> None:
13
+ """
14
+ Construct the canonical design matrix A = [C | S; M | Q] for CLSP.
15
+
16
+ This method assembles the constraint matrix A from user-supplied or
17
+ internally generated components — C, S, M, and Q — and assigns the
18
+ corresponding right-hand side vector b. It is a required pre-step
19
+ before solving a Convex Least Squares Programming (CLSP) problem.
20
+
21
+ Depending on the specified problem type, it can generate allocation,
22
+ transaction, or modular constraints and enforce optional diagonal
23
+ exclusions. All missing blocks are padded to ensure conformability.
24
+
25
+ Parameters
26
+ ----------
27
+ problem : str, optional
28
+ Structural template for matrix construction. One of:
29
+ - 'ap' or 'tm' : allocation or transaction matrix problem.
30
+ - 'cmls' or 'rp' : constrained modular least squares or RP-type.
31
+ - '' or other: General CLSP problems (user-defined C and/or M).
32
+
33
+ C, S, M : np.ndarray or None
34
+ Blocks of the constraint matrix A = [C | S; M | Q].
35
+ If `C` and/or `M` are provided, the matrix A is constructed
36
+ accordingly. If both are None and A is not yet defined, an error
37
+ is raised.
38
+
39
+ Q : np.ndarray or None
40
+ Externally supplied residual slack matrix used to adjust inequality
41
+ constraints in M. Required only when r > 1. Encodes the sign pattern
42
+ of residuals from the previous iteration and is used to construct the
43
+ [C | S; M | Q] canonical form. Defaults to a conformable zero matrix
44
+ on the first iteration.
45
+
46
+ b : np.ndarray or None
47
+ Right-hand side vector. Must have as many rows as A. Required.
48
+
49
+ m, p : int or None
50
+ Dimensions of X ∈ ℝ^{m×p}, relevant for allocation problems ('ap').
51
+
52
+ i, j : int, default = 1
53
+ Grouping sizes for row and column sum constraints in AP problems.
54
+
55
+ zero_diagonal : bool, default = False
56
+ If True, enforces structural zero diagonals via identity truncation.
57
+
58
+ Attributes Set
59
+ ---------------
60
+ self.A : np.ndarray
61
+ Canonical design matrix constructed from (C, S, M, Q).
62
+
63
+ self.C_idx : tuple
64
+ Tuple (rows, cols) indicating the size of the C block.
65
+
66
+ self.b : np.ndarray
67
+ Conformable right-hand side vector.
68
+
69
+ Raises
70
+ ------
71
+ CLSPError
72
+ If the design matrix A or right-hand side b is malformed, inconsistent,
73
+ or incompatible with the structural assumptions of the problem.
74
+ """
75
+ # (b) Ensure the right-hand side is defined and set `self.b`
76
+ if b is None:
77
+ raise self.error("Right-hand side vector b must be provided.")
78
+ self.b = b
79
+
80
+ # (A) Option 1. AP (TM) problems with an optional zero diagonal
81
+ if 'ap' in problem.lower() or 'tm' in problem.lower():
82
+ if m is None or p is None:
83
+ raise self.error("Both m and p must be specified.")
84
+ if m % i != 0:
85
+ raise self.error(f"m = {m} must be divisible by i = {i}")
86
+ if p % j != 0:
87
+ raise self.error(f"p = {p} must be divisible by j = {j}")
88
+ # construct the C block using Kronecker product
89
+ row_groups = np.kron(np.kron(np.eye(m // i), np.ones((1, i))),
90
+ np.ones((1, p)))
91
+ col_groups = np.kron(np.ones((1, m)), np.kron(np.eye(p // j),
92
+ np.ones((1, j))))
93
+ C = np.vstack([row_groups, col_groups])
94
+ # append an optional identity matrix to M, remove duplicates
95
+ if zero_diagonal:
96
+ M_diag = np.hstack([np.diagflat(np.eye(1, m, k))
97
+ for k in range(p)])
98
+ M = (M_diag.copy() if M is None or M.size == 0 else
99
+ np.unique(np.vstack([M, M_diag]), axis=0))
100
+ b = np.vstack([b, np.zeros((m, 1))])
101
+ del M_diag
102
+
103
+ # (A) Option 2. CMLS and RP problems
104
+ if 'cmls' in problem.lower() or 'rp' in problem.lower():
105
+ if C is None or M is None:
106
+ raise self.error("Both C and M must be provided.")
107
+ if C.shape[0] % M.shape[0] != 0:
108
+ raise self.error(f"Row mismatch: rows(C) = {C.shape[0]} must be "
109
+ f"divisible by rows(M) = {M.shape[0]}")
110
+
111
+ # (A) Option 3. General problems
112
+ if C is None and M is None:
113
+ raise self.error("At least one of C or M must be provided.")
114
+
115
+ # (A) Convert missing blocks to conformable zero matrices
116
+ n_col = C.shape[1] if C is not None else M.shape[1]
117
+ C = C if C is not None else np.zeros((0, n_col))
118
+ M = M if M is not None else np.zeros((0, n_col))
119
+ S = S if S is not None else np.zeros((C.shape[0], 0))
120
+ Q = Q if Q is not None else np.zeros((M.shape[0], 0))
121
+ if C.shape[0] != S.shape[0]:
122
+ raise self.error(f"C and S must have the same number of rows: "
123
+ f"{C.shape[0]} vs {S.shape[0]}")
124
+ if C.shape[1] != M.shape[1]:
125
+ raise self.error(f"C and M must have the same number of columns: "
126
+ f"{C.shape[1]} vs {M.shape[1]}")
127
+
128
+ # (A) Pad C and Q with zeros and set `self.A` and `self.C_idx`
129
+ self.A = np.vstack([
130
+ np.hstack([C, S, np.zeros((C.shape[0], Q.shape[1]))]),
131
+ np.hstack([M, np.zeros((M.shape[0], S.shape[1])), Q])
132
+ ])
133
+ self.C_idx = C.shape
134
+
135
+ def CLSPCorrelogram(
136
+ self, reset: bool = False, threshold: float = 0,
137
+ ) -> dict[str, list[float]]:
138
+ """
139
+ Compute the structural correlogram of the CLSP constraint system.
140
+
141
+ This method performs a row-deletion sensitivity analysis on the canonical
142
+ constraint matrix [C | S], denoted as C_canon, and evaluates the marginal
143
+ effect of each constraint row on numerical stability, angular alignment,
144
+ and estimator sensitivity.
145
+
146
+ For each row i in C_canon, it computes:
147
+ - The Root Mean Square Alignment (RMSA_i) with all other rows j ≠ i.
148
+ - The change in condition numbers κ(C), κ(B), and κ(A) when row i is
149
+ deleted.
150
+ - The effect on estimation quality: changes in NRMSE, zhat, z, and x.
151
+
152
+ Additionally, it computes the total RMSA statistic across all rows,
153
+ summarizing the overall angular alignment of the constraint block.
154
+
155
+ Parameters
156
+ ----------
157
+ reset : bool, default = False
158
+ If True, forces recomputation of all diagnostic values.
159
+
160
+ threshold : float, default = 0
161
+ If positive, limits the output to constraints with RMSA_i ≥ threshold.
162
+
163
+ Returns
164
+ -------
165
+ dict of list
166
+ A dictionary containing per-row diagnostic values:
167
+ {
168
+ "constraint" : [1, 2, ..., k], # 1-based indices
169
+ "rmsa_i" : list of RMSA_i values,
170
+ "rmsa_dkappaC" : list of Δκ(C) after deleting row i,
171
+ "rmsa_dkappaB" : list of Δκ(B) after deleting row i,
172
+ "rmsa_dkappaA" : list of Δκ(A) after deleting row i,
173
+ "rmsa_dnrmse" : list of ΔNRMSE after deleting row i,
174
+ "rmsa_dzhat" : list of Δzhat after deleting row i,
175
+ "rmsa_dz" : list of Δz after deleting row i,
176
+ "rmsa_dx" : list of Δx after deleting row i,
177
+ }
178
+ """
179
+ # (RMSA) Total RMSA
180
+ if self.rmsa == None or reset:
181
+ k = self.C_idx[0]
182
+ p = self.C_idx[1]
183
+ C_canon = self.A[:k]
184
+ norms = np.linalg.norm(C_canon, axis=1)
185
+ self.rmsa = (lambda C: np.sqrt(np.sum([(np.dot(
186
+ C[i] / norms[i], C[j] / norms[j])
187
+ ) ** 2 for i in range(0, k - 1)
188
+ for j in range(i + 1, k )
189
+ ]) * 2 / k / (k - 1)))(C_canon)
190
+
191
+ # (RMSA) Constraint-wise RMSA, changes in condition numbers, and GoF
192
+ if len(self.rmsa_i) != self.C_idx[0] or reset:
193
+ tmp = copy.deepcopy(self)
194
+ k = self.C_idx[0]
195
+ p = self.C_idx[1]
196
+ C_canon = self.A[:k]
197
+ norms = np.linalg.norm(C_canon, axis=1)
198
+ self.rmsa_i = [None] * k
199
+ self.rmsa_dkappaC = [None] * k
200
+ self.rmsa_dkappaB = [None] * k
201
+ self.rmsa_dkappaA = [None] * k
202
+ self.rmsa_dnrmse = [None] * k
203
+ self.rmsa_dzhat = [None] * k
204
+ self.rmsa_dz = [None] * k
205
+ self.rmsa_dx = [None] * k
206
+ for i in range(k):
207
+ tmp.A = np.delete(self.A, i, axis=0)
208
+ tmp.b = np.delete(self.b, i, axis=0)
209
+ tmp.C_idx = (k - 1, p)
210
+ tmp.solve()
211
+ self.rmsa_i[i] = (lambda C, i: np.sqrt(np.sum([(np.dot(
212
+ C[i] / norms[i], C[j] / norms[j])
213
+ ) ** 2 for j in range(k) if j != i
214
+ ]) / (k - 1)))(C_canon, i)
215
+ self.rmsa_dkappaC[i] = tmp.kappaC - self.kappaC
216
+ self.rmsa_dkappaB[i] = tmp.kappaB - self.kappaB
217
+ self.rmsa_dkappaA[i] = tmp.kappaA - self.kappaA
218
+ self.rmsa_dnrmse[i] = tmp.nrmse - self.nrmse
219
+ self.rmsa_dzhat[i] = tmp.zhat - self.zhat
220
+ self.rmsa_dz[i] = tmp.z - self.z
221
+ self.rmsa_dx[i] = tmp.x - self.x
222
+
223
+ # Return the correlogram
224
+ indices = [i for i, r in enumerate(self.rmsa_i) if r >= threshold]
225
+ return {
226
+ "constraint" : [i + 1 for i in indices], # 1-based indexing
227
+ "rmsa_i" : [self.rmsa_i[i] for i in indices],
228
+ "rmsa_dkappaC": [self.rmsa_dkappaC[i] for i in indices],
229
+ "rmsa_dkappaB": [self.rmsa_dkappaB[i] for i in indices],
230
+ "rmsa_dkappaA": [self.rmsa_dkappaA[i] for i in indices],
231
+ "rmsa_dnrmse" : [self.rmsa_dnrmse[i] for i in indices],
232
+ "rmsa_dzhat" : [self.rmsa_dzhat[i] for i in indices],
233
+ "rmsa_dz" : [self.rmsa_dz[i] for i in indices],
234
+ "rmsa_dx" : [self.rmsa_dx[i] for i in indices],
235
+ }
236
+
237
+ def CLSPTTest(
238
+ self, reset: bool = False, sample_size: int = 50,
239
+ seed: int | None = None, distribution: str | None = None
240
+ ) -> dict[str, float]:
241
+ """
242
+ Perform Monte Carlo t-tests on the NRMSE statistic from the CLSP estimator.
243
+
244
+ This function generates synthetic right-hand side vectors `b` using a
245
+ user-defined or default distribution and recomputes the estimator. It
246
+ tests whether the observed NRMSE significantly deviates from the null
247
+ distribution of simulated NRMSE values.
248
+
249
+ Parameters
250
+ ----------
251
+ reset : bool, default = False
252
+ If True, forces recomputation of the NRMSE null distribution.
253
+
254
+ sample_size : int, default = 50
255
+ Size of the Monte Carlo simulated sample under H₀.
256
+
257
+ seed : int or None, optional
258
+ Optional random seed to override the default.
259
+
260
+ distribution : str or None
261
+ Distribution for generating synthetic b vectors. One of:
262
+ 'normal', 'uniform', 'laplace'. Defaults to standard normal.
263
+
264
+ Returns
265
+ -------
266
+ dict
267
+ Dictionary with test results and null distribution statistics:
268
+ {
269
+ 'p_one_left' : P(nrmse ≤ null mean),
270
+ 'p_one_right' : P(nrmse ≥ null mean),
271
+ 'p_two_sided' : 2-sided t-test p-value,
272
+ 'nrmse' : observed value,
273
+ 'mean_null' : mean of null distribution,
274
+ 'std_null' : std of null distribution
275
+ }
276
+ """
277
+ # Set the seed, RNG configuration, and distribution
278
+ if seed is not None:
279
+ self.seed = seed
280
+ self.rng = np.random.default_rng(self.seed)
281
+ if distribution is not None:
282
+ dist_fn = {
283
+ "normal": lambda n: self.rng.normal(loc=0, scale=1, size=(n, 1)),
284
+ "uniform": lambda n: self.rng.uniform(-1, 1, size=(n, 1)),
285
+ "laplace": lambda n: self.rng.laplace(loc=0, scale=1, size=(n, 1))
286
+ }.get(distribution.lower())
287
+ if dist_fn is None:
288
+ raise self.error(f"Unsupported distribution: {distribution}")
289
+
290
+ # (t-test) Simulated NRMSE distribution under H0
291
+ if len(self.nrmse_ttest) != sample_size or reset:
292
+ tmp = copy.deepcopy(self)
293
+ self.nrmse_ttest = [None] * sample_size
294
+ for i in range(sample_size):
295
+ tmp.b = dist_fn(self.b.shape[0])
296
+ tmp.solve()
297
+ self.nrmse_ttest[i] = tmp.nrmse
298
+
299
+ # Return the t-test
300
+ nrmse_null = np.array(self.nrmse_ttest)
301
+ mean_null = np.mean(nrmse_null)
302
+ std_null = np.std(nrmse_null, ddof=1)
303
+ t_stat = (self.nrmse - mean_null) / (std_null / np.sqrt(sample_size))
304
+ p_left = stats.t.cdf(t_stat, df=sample_size - 1)
305
+ p_right = 1 - p_left
306
+ p_two = stats.t.sf(abs(t_stat), df=sample_size - 1) * 2
307
+ return {
308
+ "p_one_left" : p_left,
309
+ "p_one_right": p_right,
310
+ "p_two_sided": p_two,
311
+ "nrmse" : self.nrmse,
312
+ "mean_null" : mean_null,
313
+ "std_null" : std_null
314
+ }
@@ -0,0 +1,259 @@
1
+ Metadata-Version: 2.4
2
+ Name: pyclsp
3
+ Version: 1.0.0
4
+ Summary: Modular Two-Step Convex Optimization Estimator for Ill-Posed Problems
5
+ Author-email: The Economist <29724411+econcz@users.noreply.github.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/econcz/clsp
8
+ Project-URL: Bug Tracker, https://github.com/econcz/clsp/issues
9
+ Keywords: estimators,convex-optimization,least-squares,generalized-inverse,regularization
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.10
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Topic :: Scientific/Engineering :: Mathematics
16
+ Classifier: Topic :: Scientific/Engineering :: Information Analysis
17
+ Requires-Python: >=3.10
18
+ Description-Content-Type: text/markdown
19
+ License-File: LICENSE
20
+ Requires-Dist: numpy>=1.24
21
+ Requires-Dist: scipy>=1.10
22
+ Requires-Dist: cvxpy>=1.3
23
+ Dynamic: license-file
24
+
25
+ # CLSP — Convex Least Squares Programming
26
+
27
+ The **Convex Least Squares Programming (CLSP)** estimator is a two-step method for solving underdetermined, ill-posed, or structurally constrained least-squares problems. It combines pseudoinverse-based estimation with convex-programming correction (e.g., Lasso, Ridge, Elastic Net) to ensure numerical stability, structural coherence, and enhanced interpretability.
28
+
29
+ ## Installation
30
+
31
+ ```bash
32
+ pip install clsp
33
+ ```
34
+
35
+ ## Quick Example
36
+
37
+ ```python
38
+ import numpy as np
39
+ from clsp import CLSP
40
+
41
+ # Example allocation problem
42
+ b = np.array([1, 2, 3, 7, 8, 9], dtype=float)
43
+
44
+ # Initialize estimator
45
+ model = CLSP()
46
+
47
+ # Solve the system
48
+ result = model.solve(problem='ap', b=b, m=3, p=3)
49
+
50
+ # Access diagnostics
51
+ print(model.nrmse)
52
+ print(model.ttest())
53
+
54
+ ```
55
+
56
+ ## User Reference
57
+
58
+ For comprehensive information on the estimator’s capabilities, advanced configuration options, and implementation details, please refer to the docstrings provided in each of the individual .py source files. These docstrings contain complete descriptions of available methods, their parameters, expected input formats, and output structures.
59
+
60
+ ### The `CLSP` Class
61
+
62
+ ```python
63
+ self.__init__()
64
+ ```
65
+
66
+ Stores the solution, goodness-of-fit statistics, and ancillary parameters.
67
+
68
+ The class has three core methods: `solve()`, `corr()`, and `ttest()`.
69
+
70
+ **Selected attributes:**
71
+ `self.A` : *np.ndarray*
72
+ design matrix `A` = [`C` | `S`; `M` | `Q`], where `Q` is either a zero matrix or *S_residual*.
73
+
74
+ `self.b` : *np.ndarray*
75
+ vector of the right-hand side.
76
+
77
+ `self.zhat` : *np.ndarray*
78
+ vector of the first-step estimate.
79
+
80
+ `self.r` : *int*
81
+ number of refinement iterations performed in the first step.
82
+
83
+ `self.z` : *np.ndarray*
84
+ vector of the final solution. If the second step is disabled, it equals `self.zhat`.
85
+
86
+ `self.x` : *np.ndarray*
87
+ `m` x `p` matrix or vector containing the variable component of `z`.
88
+
89
+ `self.y` : *np.ndarray*
90
+ vector containing the slack component of `z`.
91
+
92
+ `self.kappaC` : *float*
93
+ spectral κ() for *C_canon*.
94
+
95
+ `self.kappaB` : *float*
96
+ spectral κ() for *B* = *C_canon^+*`A`.
97
+
98
+ `self.kappaA` : *float*
99
+ spectral κ() for `A`.
100
+
101
+ `self.rmsa` : *float*
102
+ total root mean square alignment (RMSA).
103
+
104
+ `self.r2_partial` : *float*
105
+ R^2 for the `M` block in `A`.
106
+
107
+ `self.nrmse` : *float*
108
+ mean square error calculated from `A` and normalized by standard deviation (NRMSE).
109
+
110
+ `self.nrmse_partial` : *float*
111
+ mean square error calculated from the `M` block in `A` and normalized by standard deviation (NRMSE).
112
+
113
+ `self.z_lower` : *np.ndarray*
114
+ lower bound of the diagnostic interval (confidence band) based on κ(`A`).
115
+
116
+ `self.z_upper` : *np.ndarray*
117
+ upper bound of the diagnostic interval (confidence band) based on κ(`A`).
118
+
119
+ ### Solver Method: `solve()`
120
+
121
+ ```python
122
+ self.solve(problem, C, S, M, b, m, p, i, j, zero_diagonal, r, Z, tolerance, iteration_limit, final, alpha)
123
+ ```
124
+
125
+ Solves the Convex Least Squares Programming (CLSP) problem.
126
+
127
+ This method performs a two-step estimation:
128
+ (1) a pseudoinverse-based solution using either the Moore–Penrose or Bott–Duffin inverse, optionally iterated for refinement;
129
+ (2) a convex-programming correction using Lasso, Ridge, or Elastic Net regularization (if enabled).
130
+
131
+ **Parameters:**
132
+ `problem` : *str*, optional
133
+ Structural template for matrix construction. One of:
134
+ - *'ap'* or *'tm'* : allocation (transaction) matrix problem (AP).
135
+ - *'cmls'* or *'rp'* : constrained-model least squares (regression) problem.
136
+ - anything else: general CLSP problem (user-defined `C` and/or `M`).
137
+
138
+ `C`, `S`, `M` : *np.ndarray* or *None*
139
+ Blocks of the design matrix `A` = [`C` | `S`; `M` | `Q`]. If `C` and/or `M` are provided, the matrix `A` is constructed accordingly (please note that for AP, `C` is constructed automatically and known values are specified in `M`).
140
+
141
+ `b` : *np.ndarray* or *None*
142
+ Right-hand side vector. Must have as many rows as `A` (please note that for AP, it should start with row sums). Required.
143
+
144
+ `m`, `p` : *int* or *None*
145
+ Dimensions of X ∈ ℝ^{m×p}, relevant for AP.
146
+
147
+ `i`, `j` : *int*, default = *1*
148
+ Grouping sizes for row and column sum constraints in AP.
149
+
150
+ `zero_diagonal` : *bool*, default = *False*
151
+ If *True*, enforces structural zero diagonals.
152
+
153
+ `r` : *int*, default = *1*
154
+ Number of refinement iterations for the pseudoinverse-based estimator.
155
+
156
+ `Z` : *np.ndarray* or *None*
157
+ A symmetric idempotent matrix (projector) defining the subspace for Bott–Duffin pseudoinversion. If *None*, the identity matrix is used, reducing the Bott–Duffin inverse to the Moore–Penrose case.
158
+
159
+ `tolerance` : *float*, default = *square root of machine epsilon*
160
+ Convergence tolerance for NRMSE change between refinement iterations.
161
+
162
+ `iteration_limit` : *int*, default = *50*
163
+ Maximum number of iterations allowed in the refinement loop.
164
+
165
+ `final` : *bool*, default = *True*
166
+ If *True*, a convex programming problem is solved to refine `zhat`. The resulting solution `z` minimizes a weighted L1/L2 norm around `zhat` subject to `Az` = `b`.
167
+
168
+ `alpha` : *float*, default = *1.0*
169
+ Regularization parameter (weight) in the final convex program:
170
+ - `α = 0`: Lasso (L1 norm)
171
+ - `α = 1`: Tikhonov Regularization/Ridge (L2 norm)
172
+ - `0 < α < 1`: Elastic Net
173
+
174
+ `*args`, `**kwargs` : optional
175
+ CVXPY arguments passed to the CVXPY solver.
176
+
177
+ **Returns:**
178
+ *self*
179
+
180
+ ### Correlogram Method: `corr()`
181
+
182
+ ```python
183
+ self.corr(reset, threshold)
184
+ ```
185
+
186
+ Computes the structural correlogram of the CLSP constraint part.
187
+
188
+ This method performs a row-deletion sensitivity analysis on the canonical constraint matrix `[C` | `S`], denoted as *C_canon*, and evaluates the marginal effect of each constraint row on numerical stability, angular alignment, and estimator sensitivity.
189
+
190
+ For each row `i` in `C_canon`, it computes:
191
+ - The Root Mean Square Alignment (`RMSA_i`) with all other rows `j` ≠ `i`.
192
+ - The change in condition numbers κ(`C`), κ(`B`), and κ(`A`) when row `i` is deleted.
193
+ - The effect on estimation quality: changes in `nrmse`, `zhat`, `z`, and `x` when row `i` is deleted.
194
+
195
+ Additionally, it computes the total `rmsa` statistic across all rows, summarizing the overall angular alignment of *C_canon*.
196
+
197
+ **Parameters:**
198
+ `reset` : *bool*, default = *False*
199
+ If *True*, forces recomputation of all diagnostic values (the results are preserved for eventual reproduction after the method is called).
200
+
201
+ `threshold` : *float*, default = *0*
202
+ If positive, limits the output to constraints with `RMSA_i` ≥ `threshold`.
203
+
204
+ **Returns:**
205
+ *dict* of *list*
206
+ A dictionary containing per-row diagnostic values:
207
+ {
208
+ `"constraint"` : `[1, 2, ..., k]`, # 1-based indices
209
+ `"rmsa_i"` : list of `RMSA_i` values,
210
+ `"rmsa_dkappaC"` : list of Δκ(`C`) after deleting row `i`,
211
+ `"rmsa_dkappaB"` : list of Δκ(`B`) after deleting row `i`,
212
+ `"rmsa_dkappaA"` : list of Δκ(`A`) after deleting row `i`,
213
+ `"rmsa_dnrmse"` : list of Δ`nrmse` after deleting row `i`,
214
+ `"rmsa_dzhat"` : list of Δ`zhat` after deleting row `i`,
215
+ `"rmsa_dz"` : list of Δ`z` after deleting row `i`,
216
+ `"rmsa_dx"` : list of Δ`x` after deleting row `i`,
217
+ }
218
+
219
+ ### T-Test Method: `ttest`
220
+
221
+ ```python
222
+ self.ttest(reset, sample_size, seed, distribution)
223
+ ```
224
+
225
+ Performs a Monte Carlo-based one- or two-sided t-test on the NRMSE statistic.
226
+
227
+ This function simulates right-hand side vectors `b` using a user-defined or default distribution and recomputes the estimator for every new `b`. It
228
+ tests whether the observed NRMSE significantly deviates from the null distribution (under H₀) of simulated NRMSE values. The quality of the test depends on the size of the simulated sample.
229
+
230
+ **Parameters:**
231
+ `reset` : *bool*, default = *False*
232
+ If *True*, forces recomputation of the NRMSE null distribution (under H₀) (the results are preserved for eventual reproduction after the method is called).
233
+
234
+ `sample_size` : *int*, default = *50*
235
+ Size of the Monte Carlo simulated sample under H₀.
236
+
237
+ `seed` : *int* or *None*, optional
238
+ Optional random seed to override the default.
239
+
240
+ `distribution` : *str* or *None*, default = *’normal’*
241
+ Distribution for generating simulated `b` vectors. One of (standard): *'normal'*, *'uniform'*, or *'laplace'*.
242
+
243
+ **Returns:**
244
+ *dict*
245
+ Dictionary with test results and null distribution statistics:
246
+ {
247
+ `'p_one_left'` : P(nrmse ≤ null mean),
248
+ `'p_one_right'` : P(nrmse ≥ null mean),
249
+ `'p_two_sided'` : 2-sided t-test p-value,
250
+ `'nrmse'` : observed value,
251
+ `'mean_null'` : mean of the null distribution (under H₀),
252
+ `'std_null'` : standard deviation of the null distribution (under H₀)
253
+ }
254
+
255
+ ## Bibliography
256
+ To be added.
257
+
258
+ ## License
259
+ MIT License — see the [LICENSE](LICENSE) file.
@@ -0,0 +1,10 @@
1
+ clsp/__init__.py,sha256=ShvZmk9qOrWOlrm5WKHvEj4ubZGUwU4UBtSQuX7I7FU,91
2
+ clsp/clsp.py,sha256=P5LDe1h9jZ_tm3X8KjCd7zKafyIco1uJEq-6qVGDulc,10119
3
+ clsp/errors.py,sha256=mpFIYB7dumi1xtVBocZcq46W-9tktWSzWjCfuI9x7JU,662
4
+ clsp/solver.py,sha256=YD_ZsrQaOsyB7BVGaAZ9hfAUtxvrUyzGqEMldFZw6e8,11698
5
+ clsp/utils.py,sha256=FdyGlqNwwxZ2nFLXe2tdLb9JfTDJ4ZmRa3lPCBiuXCw,13673
6
+ pyclsp-1.0.0.dist-info/licenses/LICENSE,sha256=GBD85YooWzpjrnQL2N3DQat780nYLRoiJaGcAkv7Kl4,1070
7
+ pyclsp-1.0.0.dist-info/METADATA,sha256=i7wJD_-qnPal6PpdWiZ-czUZBBEe1o62snS1tioSzNY,10064
8
+ pyclsp-1.0.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
9
+ pyclsp-1.0.0.dist-info/top_level.txt,sha256=IalkNGMZVyzpHnuIj0ZFArDGGUrj4HnAnLFiGxKHEEI,5
10
+ pyclsp-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 The Economist
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ clsp