bezierv 1.0.0__py3-none-any.whl → 1.1.1__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.
@@ -94,7 +94,7 @@ class DistFit:
94
94
  def fit(self,
95
95
  method: str='projgrad',
96
96
  step_size_PG: float=0.001,
97
- max_iter_PG: float=1000,
97
+ max_iter_PG: int=1000,
98
98
  threshold_PG: float=1e-3,
99
99
  step_size_PS: float=0.001,
100
100
  max_iter_PS: int=1000,
@@ -114,8 +114,14 @@ class DistFit:
114
114
  The maximum number of iterations for the projected gradient descent method (default is 1000).
115
115
  threshold_PG : float, optional
116
116
  The convergence threshold for the projected gradient descent method (default is 1e-3).
117
+ step_size_PS : float, optional
118
+ The step size for the projected subgradient method (default is 0.001).
119
+ max_iter_PS : int, optional
120
+ The maximum number of iterations for the projected subgradient method (default is 1000).
117
121
  solver_NL : str, optional
118
122
  The solver to use for the nonlinear fitting method (default is 'ipopt').
123
+ max_iter_NM : int, optional
124
+ The maximum number of iterations for the Nelder-Mead optimization method (default is 1000).
119
125
 
120
126
  Returns
121
127
  -------
@@ -173,18 +179,6 @@ class DistFit:
173
179
  def get_controls_z(self) -> np.array:
174
180
  """
175
181
  Compute the control points for the z-coordinates of the Bezier curve.
176
-
177
- 'quantile' method is used to determine the control points based on the data quantiles.
178
-
179
- Parameters
180
- ----------
181
- data : np.array
182
- The sorted data points.
183
-
184
- Returns
185
- -------
186
- np.array
187
- The control points for the z-coordinates of the Bezier curve.
188
182
  """
189
183
  controls_z = np.linspace(0, 1, self.n + 1)
190
184
  return controls_z
@@ -197,8 +191,8 @@ class DistFit:
197
191
 
198
192
  Parameters
199
193
  ----------
200
- data : np.array
201
- The sorted data points.
194
+ method : str
195
+ The method to use for initializing the x-coordinates of the control points.
202
196
 
203
197
  Returns
204
198
  -------
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bezierv
3
- Version: 1.0.0
3
+ Version: 1.1.1
4
4
  Summary: This package serves as a computational framework for Bézier distributions.
5
5
  Author-email: Esteban Leiva <e.leivam@uniandes.edu.co>, "Andrés L. Medaglia" <amedagli@uniandes.edu.co>
6
6
  Maintainer-email: Esteban Leiva <e.leivam@uniandes.edu.co>
@@ -26,9 +26,8 @@ License: MIT License
26
26
  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
27
27
  SOFTWARE.
28
28
 
29
- Project-URL: Homepage, https://github.com/you/your-package
30
- Project-URL: Documentation, https://your-package.readthedocs.io/
31
- Project-URL: Source, https://github.com/you/your-package
29
+ Project-URL: Documentation, https://estebanleiva.github.io/bezierv/
30
+ Project-URL: Source, https://github.com/EstebanLeiva/bezierv
32
31
  Keywords: bezier,distribution,optimization
33
32
  Classifier: Programming Language :: Python :: 3 :: Only
34
33
  Classifier: Programming Language :: Python :: 3.10
@@ -43,21 +42,25 @@ Requires-Dist: scipy<1.16,>=1.13; python_version == "3.10"
43
42
  Requires-Dist: matplotlib<3.11,>=3.9; python_version == "3.10"
44
43
  Requires-Dist: statsmodels<0.15,>=0.14.2; python_version == "3.10"
45
44
  Requires-Dist: pyomo<7,>=6.8; python_version == "3.10"
45
+ Requires-Dist: bokeh<4,>=3.4; python_version == "3.10"
46
46
  Requires-Dist: numpy<2.3,>=1.26; python_version == "3.11"
47
47
  Requires-Dist: scipy<1.17,>=1.13; python_version == "3.11"
48
48
  Requires-Dist: matplotlib<3.11,>=3.9; python_version == "3.11"
49
49
  Requires-Dist: statsmodels<0.15,>=0.14.2; python_version == "3.11"
50
50
  Requires-Dist: pyomo<7,>=6.8; python_version == "3.11"
51
+ Requires-Dist: bokeh<4,>=3.4; python_version == "3.11"
51
52
  Requires-Dist: numpy<2.3,>=2.1; python_version == "3.12"
52
53
  Requires-Dist: scipy<1.17,>=1.14.1; python_version == "3.12"
53
54
  Requires-Dist: matplotlib<3.11,>=3.9; python_version == "3.12"
54
55
  Requires-Dist: statsmodels<0.15,>=0.14.2; python_version == "3.12"
55
56
  Requires-Dist: pyomo<7,>=6.8; python_version == "3.12"
57
+ Requires-Dist: bokeh<4,>=3.4; python_version == "3.12"
56
58
  Requires-Dist: numpy<2.4,>=2.2; python_version == "3.13"
57
59
  Requires-Dist: scipy<1.17,>=1.14.1; python_version == "3.13"
58
60
  Requires-Dist: matplotlib<3.11,>=3.9; python_version == "3.13"
59
61
  Requires-Dist: statsmodels<0.15,>=0.14.2; python_version == "3.13"
60
62
  Requires-Dist: pyomo<7,>=6.8; python_version == "3.13"
63
+ Requires-Dist: bokeh<4,>=3.4; python_version == "3.13"
61
64
  Dynamic: license-file
62
65
 
63
66
  <p align="center">
@@ -0,0 +1,16 @@
1
+ bezierv/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ bezierv/algorithms/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
+ bezierv/algorithms/nelder_mead.py,sha256=tMKnCPF36qAAOTRV7MBuHxSuvNBgYOzwgsI2sLtmAQQ,5673
4
+ bezierv/algorithms/non_linear.py,sha256=nNAHbLK1FEdN53nmOg8Np8Fb0PKX7tiHs2JqVPn_VF0,7684
5
+ bezierv/algorithms/proj_grad.py,sha256=6Ve8vVaJSyN3495r3cPsXheL3Uyho9C5qGDr5MXPfbQ,4716
6
+ bezierv/algorithms/proj_subgrad.py,sha256=l9CNZuxJMde44Im3dbbobdKK5LVqD1Ev73Pbgy8LeLM,7158
7
+ bezierv/algorithms/utils.py,sha256=QQecNlGdxMWhDcHPfGEJ_V1nuOIdJdR-2UziKh6N6tw,2396
8
+ bezierv/classes/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
+ bezierv/classes/bezierv.py,sha256=zv-Y-gpo2baA33JqP09Qlbqt0P-HxE_PrfVvc2PKqKA,26323
10
+ bezierv/classes/convolver.py,sha256=h-NewJ__VlL881UQSAUFSamscMEgPBwUxvpb6bHpFD0,2850
11
+ bezierv/classes/distfit.py,sha256=gw7nye1GDkGrvF1aHAoItCCV-45xUKLdm5Dyev0RFRY,8984
12
+ bezierv-1.1.1.dist-info/licenses/LICENSE,sha256=VfWiefIi6eo_kZleNp0Plg8A2eUX4D8fDbKtfj4VCp4,1115
13
+ bezierv-1.1.1.dist-info/METADATA,sha256=FcrW6EzdJvpaJr4k9B-cfCwfdk5f4FTrgSs_939XAOc,4850
14
+ bezierv-1.1.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
15
+ bezierv-1.1.1.dist-info/top_level.txt,sha256=AC8zK0YmUeXyPIHtt0EKuHkLrvFBFeHwzp6bjzYnqJI,8
16
+ bezierv-1.1.1.dist-info/RECORD,,
@@ -1,113 +0,0 @@
1
- import numpy as np
2
- import matplotlib.pyplot as plt
3
- from scipy.integrate import quad
4
-
5
- from bezierv.classes.bezierv import Bezierv
6
- from bezierv.algorithms.proj_grad import ProjGrad
7
-
8
- class ConvBezier:
9
- def __init__(self, bezierv_x: Bezierv, bezierv_y: Bezierv, m: int):
10
- """
11
- Initialize a ConvBezier instance for convolving two Bezier curves.
12
-
13
- This constructor sets up the convolution object by storing the provided Bezierv
14
- random variables, and creates a new Bezierv instance to hold the convolution
15
- result. It also initializes the number of data points to be used in the numerical
16
- convolution process.
17
-
18
- Parameters
19
- ----------
20
- bezierv_x : Bezierv
21
- A Bezierv instance representing the first Bezier random variable.
22
- bezierv_y : Bezierv
23
- A Bezierv instance representing the second Bezier random variable.
24
- m : int
25
- The number of data points to generate for the convolution (more data points
26
- induce a better approximation).
27
-
28
- Attributes
29
- ----------
30
- bezierv_x : Bezierv
31
- The Bezierv instance representing the first random variable.
32
- bezierv_y : Bezierv
33
- The Bezierv instance representing the second random variable.
34
- bezierv_conv : Bezierv
35
- A Bezierv instance that will store the convolution result. Its number of control
36
- points is set to the maximum of control points between bezierv_x and bezierv_y.
37
- m : int
38
- The number of data points used in the convolution.
39
- """
40
- self.bezierv_x = bezierv_x
41
- self.bezierv_y = bezierv_y
42
- self.bezierv_conv = Bezierv(max(bezierv_x.n, bezierv_y.n))
43
- self.m = m
44
-
45
- def cdf_z (self, z: float):
46
- """
47
- Numerically compute the cumulative distribution function (CDF) at a given z-value for the
48
- sum of Bezier random variables.
49
-
50
- This method evaluates the CDF at the specified value z by integrating over the
51
- parameter t of the sum of Bezier random variables.
52
-
53
- Parameters
54
- ----------
55
- z : float
56
- The value at which to evaluate the cumulative distribution function.
57
-
58
- Returns
59
- -------
60
- float
61
- The computed CDF value at z.
62
- """
63
- def integrand(t: float):
64
- y_val = z - self.bezierv_x.poly_x(t)
65
- if y_val < self.bezierv_y.controls_x[0]:
66
- cumul_z = 0
67
- elif y_val > self.bezierv_y.controls_x[-1]:
68
- cumul_z = self.bezierv_x.pdf_numerator(t)
69
- else:
70
- y_inv = self.bezierv_y.root_find(y_val)
71
- cumul_z = self.bezierv_x.pdf_numerator(t) * self.bezierv_y.eval_t(y_inv)[1]
72
- return cumul_z
73
-
74
- result, _ = quad(integrand, 0, 1)
75
- return self.bezierv_x.n * result
76
-
77
- def conv(self, step=0.001):
78
- """
79
- Numerically compute the convolution of two Bezier random variables.
80
-
81
- This method performs the convolution by:
82
- - Defining the lower and upper bounds for the convolution as the sum of the
83
- smallest and largest control points of bezierv_x and bezierv_y, respectively.
84
- - Generating a set of data points over these bounds.
85
- - Evaluating the cumulative distribution function (CDF) at each data point using the
86
- cdf_z method.
87
- - Initializing a ProjGrad instance with the computed data and empirical CDF values.
88
- - Fitting the convolution Bezier random variable via projected gradient descent and updating
89
- bezierv_conv.
90
-
91
- Returns
92
- -------
93
- Bezierv
94
- The updated Bezierv instance representing the convolution of the two input Bezier
95
- random variables.
96
- """
97
- low_bound = self.bezierv_x.controls_x[0] + self.bezierv_y.controls_x[0]
98
- up_bound = self.bezierv_x.controls_x[-1] + self.bezierv_y.controls_x[-1]
99
- data = np.linspace(low_bound, up_bound, self.m)
100
-
101
- emp_cdf_data = np.zeros(self.m)
102
- for i in range(self.m):
103
- emp_cdf_data[i] = self.cdf_z(data[i])
104
-
105
- if self.bezierv_x.n == self.bezierv_y.n:
106
- controls_x = self.bezierv_x.controls_x + self.bezierv_y.controls_x
107
- else:
108
- #TODO: Implement the case where the number of control points is different with quantiles
109
- raise NotImplementedError
110
- proj_grad = ProjGrad(self.bezierv_conv, data, controls_x, emp_cdf_data)
111
- controls_z = np.linspace(0, 1, self.bezierv_conv.n + 1)
112
- self.bezierv_conv = proj_grad.fit(controls_z, step=step)
113
- return self.bezierv_conv
@@ -1,186 +0,0 @@
1
- import pyomo.environ as pyo
2
- import numpy as np
3
-
4
- from statsmodels.distributions.empirical_distribution import ECDF
5
- from pyomo.opt import SolverFactory, SolverStatus, TerminationCondition
6
-
7
- from bezierv.classes.bezierv import Bezierv
8
-
9
- class NonLinearSolver:
10
- def __init__(self, bezierv: Bezierv, data: np.array, emp_cdf_data: np.array=None):
11
- """
12
- Initialize the NonLinearSolver instance with a Bezierv object and data to fit.
13
-
14
- Parameters
15
- ----------
16
- bezierv : Bezierv
17
- An instance of the Bezierv class representing the Bezier random variable.
18
- data : np.array
19
- The data points to be fitted; they will be sorted.
20
- emp_cdf_data : np.array, optional
21
- The empirical CDF values corresponding to the data. If not provided, they
22
- are computed using ECDF from the sorted data.
23
-
24
- Attributes
25
- ----------
26
- bezierv : Bezierv
27
- The Bezierv object.
28
- n : int
29
- The degree of the Bezier random variable (inferred from bezierv.n).
30
- data : np.array
31
- The sorted data points.
32
- m : int
33
- The number of data points.
34
- t_data : np.array
35
- The values of t corresponding to the data points on the Bezier random variable.
36
- fit_error : float
37
- The fitting error, initialized to infinity.
38
- emp_cdf_data : np.array
39
- The empirical CDF values for the data points.
40
- """
41
- self.bezierv = bezierv
42
- self.n = bezierv.n
43
- self.data = np.sort(data)
44
- self.m = len(data)
45
- self.mse = np.inf
46
-
47
- if emp_cdf_data is None:
48
- emp_cdf = ECDF(data)
49
- self.emp_cdf_data = emp_cdf(data)
50
- else:
51
- self.emp_cdf_data = emp_cdf_data
52
-
53
- def fit(self, solver='ipopt'):
54
- """
55
- Fits a Bézier distribution to the given data using n control points.
56
- This method sorts the input data, computes the empirical cumulative
57
- distribution function (CDF), and sets up an optimization model to
58
- fit a Bézier distribution. The control points and the empirical CDF
59
- are automatically saved. The method returns the mean square error (MSE)
60
- of the fit.
61
-
62
- Parameters:
63
- data (list): A list of data points to fit the Bézier distribution.
64
- n (int): The number of control points for the Bézier distribution.
65
- solver (str): The SolverFactory str in pyomo. Values: "ipopt","knitroampl"
66
-
67
- Returns:
68
- float: The mean squared error (MSE) of the fit.
69
-
70
- Raises:
71
- Exception: If the solver fails to find an optimal solution.
72
-
73
- Notes:
74
- - The method uses the IPOPT solver for optimization.
75
- - The control points are constrained to lie within the range of the data.
76
- - The method ensures that the control points and the Bézier 'time' parameters are sorted.
77
- - Convexity constraints are applied to the control points and the Bézier 'time' parameters.
78
- - The first and last control points are fixed to the minimum and maximum of the data, respectively.
79
- - The first and last Bézier 'time' parameters are fixed to 0 and 1, respectively.
80
- """
81
- # Defining the optimization model
82
- model = pyo.ConcreteModel()
83
-
84
- # Sets
85
- model.N = pyo.Set(initialize=list(range(self.n + 1))) # N = 0,...,i,...,n
86
- model.N_n = pyo.Set(initialize=list(range(self.n))) # N = 0,...,i,...,n-1
87
- model.M = pyo.Set(initialize=list(range(1, self.m + 1))) # M = 1,...,j,...,m
88
- model.M_m = pyo.Set(initialize=list(range(1, self.m))) # M = 1,...,j,...,m-1
89
-
90
- # Decision variables
91
- # Control points. Box constraints.
92
- X_min = self.data[0];
93
- X_max = self.data[self.m - 1];
94
- # var x{i in 0..n} >=X[1], <=X[m];
95
- # Initialization: let {i in 1..n-1} x[i] := X[1]+i*(X[m]-X[1])/n;
96
- def init_x_rule(model, i):
97
- return X_min + i*(X_max - X_min) / self.n
98
- model.x = pyo.Var(model.N, within=pyo.Reals, bounds=(X_min, X_max), initialize=init_x_rule)
99
- # var z{i in 0..n} >=0, <=1;
100
- # Initialization: let {i in 1..n-1} z[i] := i*(1/n);
101
- def init_z_rule(model, i):
102
- return i*(1 / self.n)
103
- model.z = pyo.Var(model.N, within=pyo.NonNegativeReals, bounds=(0, 1), initialize=init_z_rule)
104
- # Bezier 'time' parameter t for the j-th sample point.
105
- # var t{j in 1..m} >=0, <= 1;
106
- # Initialization: let {j in 2..m-1} t[j] := j*(1/m);
107
- def init_t_rule(model, j):
108
- return j*(1 / self.m)
109
- model.t = pyo.Var(model.M, within=pyo.NonNegativeReals, bounds=(0,1), initialize=init_t_rule )
110
- # Estimated cdf for the j-th sample point.
111
- # var F_hat{j in 1..m} >=0, <= 1;
112
- model.F_hat = pyo.Var(model.M, within=pyo.NonNegativeReals, bounds=(0,1) )
113
-
114
- # Objective function
115
- # minimize mean_square_error:
116
- # 1/m * sum {j in 1..m} ( ( j/m - F_hat[j] )^2);
117
- def mse_rule(model):
118
- return (1 / self.m) * sum(((j / self.m) - model.F_hat[j])**2 for j in model.M)
119
- model.mse = pyo.Objective(rule=mse_rule, sense=pyo.minimize )
120
-
121
- # Constraints
122
- # subject to F_hat_estimates {j in 1..m}:
123
- # sum{i in 0..n}( comb[i]*t[j]^i*(1-t[j])^(n-i)*z[i] ) = F_hat[j];
124
- def F_hat_rule(model, j):
125
- return sum(self.bezierv.comb[i] * model.t[j]**i * (1 - model.t[j])**(self.n - i) * model.z[i] for i in model.N ) == model.F_hat[j]
126
- model.ctr_F_hat = pyo.Constraint(model.M , rule=F_hat_rule)
127
-
128
- # subject to data_sample {j in 1..m}:
129
- # sum{i in 0..n}( comb[i]*t[j]^i*(1-t[j])^(n-i)*x[i] ) = X[j];
130
- def data_sample_rule(model, j):
131
- return sum(self.bezierv.comb[i] * model.t[j]**i * (1 - model.t[j])**(self.n - i) * model.x[i] for i in model.N ) == self.data[j-1]
132
- model.ctr_sample = pyo.Constraint(model.M , rule=data_sample_rule)
133
-
134
- # subject to sorted_t{j in 1..m-1}:
135
- # t[j] <= t[j+1];
136
- #def sorted_t_rule(model, j):
137
- # return model.t[j] <= model.t[j + 1]
138
- #model.ctr_sorted_t = pyo.Constraint(model.M_m , rule=sorted_t_rule)
139
-
140
- # subject to convexity_x {i in 0..n-1}:
141
- # x[i] <= x[i+1];
142
- def convexity_x_rule(model, i):
143
- return model.x[i] <= model.x[i + 1]
144
- model.ctr_convexity_x = pyo.Constraint(model.N_n , rule=convexity_x_rule)
145
-
146
- # subject to convexity_z {i in 0..n-1}:
147
- # z[i] <= z[i+1];
148
- def convexity_z_rule(model, i):
149
- return model.z[i] <= model.z[i + 1]
150
- model.ctr_convexity_z = pyo.Constraint(model.N_n , rule=convexity_z_rule)
151
-
152
- # subject to first_control_x:
153
- # x[0] = X[1];
154
- model.first_control_x = pyo.Constraint(expr=model.x[0] <= self.data[0]) #==
155
- # subject to first_control_z:
156
- # z[0] = 0;
157
- model.first_control_z = pyo.Constraint(expr=model.z[0] == 0)
158
-
159
- # subject to last_control_x:
160
- # x[n] = X[m];
161
- model.last_control_x = pyo.Constraint(expr=model.x[self.n] >= self.data[self.m - 1]) # ==
162
- # subject to last_control_z:
163
- # z[n] = 1;
164
- model.last_control_z = pyo.Constraint(expr=model.z[self.n] == 1)
165
-
166
- # subject to first_data_t:
167
- # t[1] = 0;
168
- model.first_t = pyo.Constraint(expr=model.t[1] == 0)
169
- # subject to last_data_t:
170
- # t[m] = 1;
171
- model.last_t = pyo.Constraint(expr=model.t[self.m] == 1)
172
-
173
- # Set solver
174
- pyo_solver = SolverFactory(solver)
175
-
176
- try:
177
- results = pyo_solver.solve(model, tee=False, timelimit=60)
178
- if (results.solver.status == SolverStatus.ok) and (results.solver.termination_condition == TerminationCondition.optimal):
179
- controls_x = np.array([model.x[i]() for i in model.N])
180
- controls_z = np.array([model.z[i]() for i in model.N])
181
- self.mse = model.mse()
182
- self.bezierv.update_bezierv(controls_x, controls_z, (self.data[0], self.data[-1]))
183
- except Exception as e:
184
- print("NonLinearSolver [fit]: An exception occurred during model evaluation:", e)
185
-
186
- return self.bezierv
bezierv/tests/__init__.py DELETED
File without changes
File without changes
File without changes
@@ -1,54 +0,0 @@
1
- import numpy as np
2
- from bezierv.algorithms import nelder_mead as nm
3
- from bezierv.classes.bezierv import Bezierv
4
-
5
-
6
- def test_objective_function_zero_for_perfect_fit():
7
- n = 1
8
- m = 3
9
- data = np.array([0.0, 0.5, 1.0])
10
- x = np.array([0.0, 1.0])
11
- z = np.array([0.0, 1.0])
12
- emp_cdf = data.copy()
13
- bez = Bezierv(n)
14
-
15
- concat = np.concatenate((x, z))
16
- val = nm.objective_function(concat, n, m, data, bez, emp_cdf)
17
- assert val < 1e-12, "Perfect fit should give ~0 MSE"
18
-
19
- def test_objective_function_lagrangian_adds_penalty():
20
- n = 1
21
- m = 3
22
- data = np.array([0.0, 0.5, 1.0])
23
- x_bad = np.array([1.0, -0.5])
24
- z_bad = np.array([0.2, 0.2])
25
- emp_cdf = data.copy()
26
- bez = Bezierv(n)
27
- good = np.concatenate((np.array([0.0, 1.0]), np.array([0.0, 1.0])))
28
- bad = np.concatenate((x_bad, z_bad))
29
- mse_good = nm.objective_function_lagrangian(good, n, m, data, bez, emp_cdf)
30
- mse_bad = nm.objective_function_lagrangian(bad, n, m, data, bez, emp_cdf)
31
- assert mse_bad > mse_good + 1.0, "Violating constraints should increase objective markedly"
32
-
33
- def test_fit_returns_low_mse_and_updates_bezier():
34
- n = 1
35
- m = 3
36
- data = np.array([0.0, 0.5, 1.0])
37
- init_x = np.array([-0.05, 1.05])
38
- init_z = np.array([0, 1])
39
- emp_cdf = data.copy()
40
- bez = Bezierv(n)
41
-
42
- fitted_bez, mse = nm.fit(
43
- n=n,
44
- m=m,
45
- data=data,
46
- bezierv=bez,
47
- init_x=init_x,
48
- init_z=init_z,
49
- emp_cdf_data=emp_cdf
50
- )
51
-
52
- assert mse < 1e-5, "Expected near-perfect fit with dummy minimise"
53
- np.testing.assert_allclose(fitted_bez.controls_x, np.array([0.0, 1.0]), atol=1e-2)
54
- np.testing.assert_allclose(fitted_bez.controls_z, np.array([0.0, 1.0]), atol=1e-2)
@@ -1,63 +0,0 @@
1
- import numpy as np
2
- from bezierv.algorithms import proj_grad as pg
3
- from bezierv.algorithms import utils as utils
4
- from bezierv.classes.bezierv import Bezierv
5
-
6
-
7
- def test_grad_zero_error_returns_zero_gradient():
8
- """When the Bézier curve matches the empirical CDF exactly, the gradient is 0."""
9
- n = 1 # linear curve: z(t) = t
10
- t = np.array([0.0, 0.5, 1.0])
11
- m = t.size
12
- emp_cdf = t.copy() # perfect fit
13
- controls_z = np.array([0.0, 1.0])
14
- bez = Bezierv(n)
15
-
16
- g = pg.grad(n, m, t, bez, controls_z, emp_cdf)
17
- np.testing.assert_allclose(g, np.zeros_like(g), atol=1e-12)
18
-
19
- def test_project_z_clips_sorts_and_enforces_bounds():
20
- """
21
- Test the project_z function to ensure it clips, sorts, and enforces bounds correctly.
22
- """
23
- raw = np.array([-0.2, 0.9, 0.7, 1.2])
24
- projected = pg.project_z(raw.copy())
25
- expected = np.array([0.0, 0.7, 0.9, 1.0])
26
- np.testing.assert_allclose(projected, expected)
27
- np.testing.assert_allclose(pg.project_z(projected.copy()), expected)
28
-
29
- def test_fit_converges_and_returns_low_mse():
30
- """
31
- On a toy linear CDF the projected-gradient solver should converge
32
- to z = [0,1] and yield (almost) zero MSE.
33
- """
34
- n = 1
35
- t = np.array([0.0, 0.5, 1.0])
36
- m = t.size
37
- data = t.copy()
38
- emp_cdf = t.copy()
39
- init_x = np.array([0.0, 1.0])
40
- init_z = np.array([0.2, 0.8])
41
- bez = Bezierv(n)
42
-
43
- bezierv , mse = pg.fit(
44
- n=n,
45
- m=m,
46
- data=data,
47
- bezierv=bez,
48
- init_x=init_x,
49
- init_z=init_z,
50
- t=t,
51
- emp_cdf_data=emp_cdf,
52
- step_size=0.2,
53
- max_iter=1_000,
54
- threshold=1e-6,
55
- )
56
-
57
- assert isinstance(bezierv, Bezierv), "Expected Bezierv instance as output"
58
- np.testing.assert_allclose(bezierv.controls_z, np.array([0., 1.])), "Expected z control points to be [0, 1]"
59
- np.testing.assert_allclose(bezierv.controls_x, np.array([0., 1.])), "Expected x control points to be [0, 1]"
60
- assert mse < 1e-6, "Solver failed to reach a near-perfect fit"
61
-
62
-
63
-
@@ -1,65 +0,0 @@
1
- import numpy as np
2
- from bezierv.algorithms import proj_subgrad as ps
3
- from bezierv.algorithms import utils as utils
4
- from bezierv.classes.bezierv import Bezierv
5
-
6
- def test_subgrad_zero_when_perfect_fit():
7
- n = 1
8
- t = np.array([0.0, 0.5, 1.0])
9
- m = t.size
10
- emp_cdf = t.copy()
11
- controls_z = np.array([0.0, 1.0])
12
- bez = Bezierv(n)
13
- g_x, g_z = ps.subgrad(n, m, bez, t, controls_z, emp_cdf)
14
- np.testing.assert_allclose(g_x, 0.0)
15
- np.testing.assert_allclose(g_z, 0.0)
16
-
17
-
18
- def test_project_x_respects_bounds_and_order():
19
- data = np.array([2.0, 5.0, 10.0])
20
- raw_x = np.array([12.0, 3.0, -7.0])
21
- projected = ps.project_x(data, raw_x.copy())
22
- expected = np.array([2.0, 3.0, 10.0])
23
- np.testing.assert_allclose(projected, expected)
24
- np.testing.assert_allclose(ps.project_x(data, projected.copy()), expected)
25
-
26
-
27
- def test_project_z_behaviour():
28
- z_raw = np.array([1.3, -0.1, 0.6])
29
- res = ps.project_z(z_raw.copy())
30
- np.testing.assert_allclose(res, np.array([0.0, 0.6, 1.0]))
31
- np.testing.assert_allclose(ps.project_z(res.copy()), res)
32
-
33
-
34
- def test_fit_converges_to_linear_solution():
35
- """
36
- The full projected-subgradient solver should find the perfect fit on a
37
- toy linear CDF — i.e. MSE near 0 and final z ≈ [0,1].
38
- """
39
- n = 1
40
- t_init = np.array([0.0, 0.5, 1.0])
41
- m = t_init.size
42
- data = t_init.copy()
43
- emp_cdf = t_init.copy()
44
- init_x = np.array([2.0, 8.0])
45
- init_z = np.array([0.4, 0.7])
46
- bez = Bezierv(n)
47
-
48
- _, mse = ps.fit(
49
- n=n,
50
- m=m,
51
- data=data,
52
- bezierv=bez,
53
- init_x=init_x,
54
- init_z=init_z,
55
- init_t=t_init,
56
- emp_cdf_data=emp_cdf,
57
- step_size_x=0.5,
58
- step_size_z=0.5,
59
- max_iter=200,
60
- )
61
-
62
- assert mse < 1e-6, "Expected near-perfect fit"
63
- # check the Bézier object got updated to the best values
64
- np.testing.assert_allclose(bez.controls_z, np.array([0.0, 1.0]), atol=1e-3)
65
- np.testing.assert_allclose(bez.controls_x, data[[0, -1]], atol=1e-3)
@@ -1,42 +0,0 @@
1
- import numpy as np
2
- import pytest
3
- from bezierv.classes.bezierv import Bezierv
4
- from bezierv.algorithms import utils as utils
5
-
6
- def test_root_find_linear_case():
7
- """
8
- For the linear Bézier curve x(t) = t, root_find should return t = data_point.
9
- """
10
- n = 1
11
- bez = Bezierv(n)
12
- controls_x = np.array([0.0, 1.0])
13
- for dp in [0.0, 0.25, 0.9, 1.0]:
14
- t = utils.root_find(n, bez, controls_x, dp)
15
- assert abs(t - dp) < 1e-12, f"Expected {dp}, got {t}"
16
-
17
-
18
- def test_get_t_returns_vector_of_roots():
19
- """
20
- get_t should call root_find for each data point and return the same values
21
- when the mapping is linear.
22
- """
23
- n = 1
24
- m = 5
25
- data = np.linspace(0, 1, m)
26
- controls_x = np.array([0.0, 1.0])
27
- bez = Bezierv(n)
28
-
29
- t_vals = utils.get_t(n, m, data, bez, controls_x)
30
- np.testing.assert_allclose(t_vals, data, atol=1e-12)
31
-
32
-
33
- def test_root_find_errors_outside_interval():
34
- """
35
- Raises ValueError when the data point is outside [0,1].
36
- root_find should propagate that error.
37
- """
38
- n = 1
39
- bez = Bezierv(n)
40
- controls_x = np.array([0.0, 1.0])
41
- with pytest.raises(ValueError):
42
- _ = utils.root_find(n, bez, controls_x, 1.5)