bezierv 0.1.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.
- bezierv/__init__.py +0 -0
- bezierv/algorithms/__init__.py +0 -0
- bezierv/algorithms/conv_bezierv.py +113 -0
- bezierv/algorithms/nelder_mead.py +157 -0
- bezierv/algorithms/non_linear.py +167 -0
- bezierv/algorithms/non_linear_solver.py +186 -0
- bezierv/algorithms/proj_grad.py +132 -0
- bezierv/algorithms/proj_subgrad.py +203 -0
- bezierv/algorithms/utils.py +67 -0
- bezierv/classes/__init__.py +0 -0
- bezierv/classes/bezierv.py +579 -0
- bezierv/classes/convolver.py +74 -0
- bezierv/classes/distfit.py +216 -0
- bezierv/tests/__init__.py +0 -0
- bezierv/tests/conf_test.py +0 -0
- bezierv/tests/test_algorithms/test_conv_bezierv.py +0 -0
- bezierv/tests/test_algorithms/test_nelder_mead.py +54 -0
- bezierv/tests/test_algorithms/test_proj_grad.py +63 -0
- bezierv/tests/test_algorithms/test_proj_subgrad.py +65 -0
- bezierv/tests/test_algorithms/test_utils.py +42 -0
- bezierv/tests/test_classes/conftest.py +42 -0
- bezierv/tests/test_classes/test_bezierv.py +0 -0
- bezierv/tests/test_classes/test_convolver.py +36 -0
- bezierv/tests/test_classes/test_distfit.py +34 -0
- bezierv-0.1.0.dist-info/METADATA +32 -0
- bezierv-0.1.0.dist-info/RECORD +29 -0
- bezierv-0.1.0.dist-info/WHEEL +5 -0
- bezierv-0.1.0.dist-info/licenses/LICENSE +21 -0
- bezierv-0.1.0.dist-info/top_level.txt +1 -0
bezierv/__init__.py
ADDED
File without changes
|
File without changes
|
@@ -0,0 +1,113 @@
|
|
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
|
@@ -0,0 +1,157 @@
|
|
1
|
+
import numpy as np
|
2
|
+
from bezierv.classes.bezierv import Bezierv
|
3
|
+
from scipy.optimize import minimize
|
4
|
+
import bezierv.algorithms.utils as utils
|
5
|
+
|
6
|
+
def objective_function(concatenated: np.array,
|
7
|
+
n: int,
|
8
|
+
m: int,
|
9
|
+
data:np.array,
|
10
|
+
bezierv: Bezierv,
|
11
|
+
emp_cdf_data: np.array) -> float:
|
12
|
+
"""
|
13
|
+
Compute the objective function value for the given control points.
|
14
|
+
|
15
|
+
This method calculates the sum of squared errors between the Bezier random variable's CDF
|
16
|
+
and the empirical CDF data.
|
17
|
+
|
18
|
+
Parameters
|
19
|
+
----------
|
20
|
+
concatenated : np.array
|
21
|
+
A concatenated array containing the control points for z and x coordinates.
|
22
|
+
The first n+1 elements are the z control points, and the remaining elements are the x control points.
|
23
|
+
n : int
|
24
|
+
The number of control points minus one for the Bezier curve.
|
25
|
+
m : int
|
26
|
+
The number of empirical CDF data points.
|
27
|
+
data : np.array
|
28
|
+
The sorted data points used to fit the Bezier distribution.
|
29
|
+
bezierv : Bezierv
|
30
|
+
An instance of the Bezierv class representing the Bezier random variable.
|
31
|
+
emp_cdf_data : np.array
|
32
|
+
The empirical CDF data points used for fitting.
|
33
|
+
|
34
|
+
Returns
|
35
|
+
-------
|
36
|
+
float
|
37
|
+
The value of the objective function (MSE).
|
38
|
+
"""
|
39
|
+
x = concatenated[0 : n + 1]
|
40
|
+
z = concatenated[n + 1:]
|
41
|
+
t = utils.get_t(n, m, data, bezierv, x)
|
42
|
+
se = 0
|
43
|
+
for j in range(m):
|
44
|
+
se += (bezierv.poly_z(t[j], z) - emp_cdf_data[j])**2
|
45
|
+
return se / m
|
46
|
+
|
47
|
+
def objective_function_lagrangian(concatenated: np.array,
|
48
|
+
n: int,
|
49
|
+
m: int,
|
50
|
+
data: np.array,
|
51
|
+
bezierv: Bezierv,
|
52
|
+
emp_cdf_data: np.array,
|
53
|
+
penalty_weight: float=1e3) -> float:
|
54
|
+
"""
|
55
|
+
Compute the objective function value for the given control points.
|
56
|
+
|
57
|
+
This method calculates the sum of squared errors between the Bezier random variable's CDF
|
58
|
+
and the empirical CDF data.
|
59
|
+
|
60
|
+
Parameters
|
61
|
+
----------
|
62
|
+
concatenated : np.array
|
63
|
+
A concatenated array containing the control points for z and x coordinates.
|
64
|
+
The first n+1 elements are the z control points, and the remaining elements are the x control points.
|
65
|
+
n : int
|
66
|
+
The number of control points minus one for the Bezier curve.
|
67
|
+
m : int
|
68
|
+
The number of empirical CDF data points.
|
69
|
+
data : np.array
|
70
|
+
The sorted data points used to fit the Bezier distribution.
|
71
|
+
bezierv : Bezierv
|
72
|
+
An instance of the Bezierv class representing the Bezier random variable.
|
73
|
+
emp_cdf_data : np.array
|
74
|
+
The empirical CDF data points used for fitting.
|
75
|
+
penalty_weight : float, optional
|
76
|
+
The weight for the penalty term in the objective function (default is 1e3).
|
77
|
+
|
78
|
+
Returns
|
79
|
+
-------
|
80
|
+
float
|
81
|
+
The value of the objective function + penalty (MSE + penalty).
|
82
|
+
"""
|
83
|
+
|
84
|
+
x = concatenated[0 : n + 1]
|
85
|
+
z = concatenated[n + 1 : ]
|
86
|
+
|
87
|
+
try:
|
88
|
+
t = utils.get_t(n, m, data, bezierv, x)
|
89
|
+
except ValueError as e:
|
90
|
+
return np.inf
|
91
|
+
|
92
|
+
se = 0
|
93
|
+
for j in range(m):
|
94
|
+
se += (bezierv.poly_z(t[j], z) - emp_cdf_data[j])**2
|
95
|
+
mse = se / m
|
96
|
+
|
97
|
+
penalty = 0.0
|
98
|
+
penalty += abs(z[0] - 0.0)
|
99
|
+
penalty += abs(z[-1] - 1.0)
|
100
|
+
delta_zs = np.diff(z)
|
101
|
+
delta_xs = np.diff(x)
|
102
|
+
penalty += np.sum(abs(np.minimum(0, delta_zs)))
|
103
|
+
penalty += np.sum(abs(np.minimum(0, delta_xs)))
|
104
|
+
penalty += abs(x[0] - data[0])
|
105
|
+
penalty += abs(data[-1] - x[-1])
|
106
|
+
|
107
|
+
return mse + penalty_weight * penalty
|
108
|
+
|
109
|
+
def fit(n: int,
|
110
|
+
m: int,
|
111
|
+
data: np.array,
|
112
|
+
bezierv: Bezierv,
|
113
|
+
init_x: np.array,
|
114
|
+
init_z: np.array,
|
115
|
+
emp_cdf_data: np.array,
|
116
|
+
max_iter: int
|
117
|
+
) -> Bezierv:
|
118
|
+
"""
|
119
|
+
Fit the Bezier random variable to the empirical CDF data using the Nelder-Mead optimization algorithm.
|
120
|
+
|
121
|
+
Parameters
|
122
|
+
----------
|
123
|
+
n : int
|
124
|
+
The number of control points minus one for the Bezier curve.
|
125
|
+
m : int
|
126
|
+
The number of empirical CDF data points.
|
127
|
+
data : np.array
|
128
|
+
The sorted data points used to fit the Bezier distribution.
|
129
|
+
bezierv : Bezierv
|
130
|
+
An instance of the Bezierv class representing the Bezier random variable.
|
131
|
+
init_x : np.array
|
132
|
+
Initial guess for the x-coordinates of the control points.
|
133
|
+
init_z : np.array
|
134
|
+
Initial guess for the z-coordinates of the control points.
|
135
|
+
emp_cdf_data : np.array
|
136
|
+
The empirical CDF data points used for fitting.
|
137
|
+
|
138
|
+
Returns
|
139
|
+
-------
|
140
|
+
Bezierv
|
141
|
+
The fitted Bezierv object with updated control points.
|
142
|
+
float
|
143
|
+
The mean squared error (MSE) of the fit.
|
144
|
+
"""
|
145
|
+
start = np.concatenate((init_x, init_z))
|
146
|
+
result = minimize(
|
147
|
+
fun=objective_function_lagrangian,
|
148
|
+
args=(n, m, data, bezierv, emp_cdf_data),
|
149
|
+
x0=start,
|
150
|
+
method='Nelder-Mead',
|
151
|
+
options={'maxiter': max_iter, 'disp': False})
|
152
|
+
sol = result.x
|
153
|
+
controls_x = sol[0 : n + 1]
|
154
|
+
controls_z = sol[n + 1: ]
|
155
|
+
bezierv.update_bezierv(controls_x, controls_z)
|
156
|
+
mse = objective_function(sol, n, m, data, bezierv, emp_cdf_data)
|
157
|
+
return bezierv, mse
|
@@ -0,0 +1,167 @@
|
|
1
|
+
from pyexpat import model
|
2
|
+
import pyomo.environ as pyo
|
3
|
+
import numpy as np
|
4
|
+
from bezierv.classes.bezierv import Bezierv
|
5
|
+
from pyomo.opt import SolverFactory, SolverStatus, TerminationCondition
|
6
|
+
|
7
|
+
def fit(n: int,
|
8
|
+
m: int,
|
9
|
+
data: np.array,
|
10
|
+
bezierv: Bezierv,
|
11
|
+
init_x: np.array,
|
12
|
+
init_z: np.array,
|
13
|
+
init_t: np.array,
|
14
|
+
emp_cdf_data: np.array,
|
15
|
+
solver: str) -> Bezierv:
|
16
|
+
"""
|
17
|
+
Fit a Bézier random variable to the empirical CDF data using a nonlinear optimization solver.
|
18
|
+
|
19
|
+
Parameters
|
20
|
+
----------
|
21
|
+
n : int
|
22
|
+
The number of control points minus one for the Bezier curve.
|
23
|
+
m : int
|
24
|
+
The number of empirical CDF data points.
|
25
|
+
data : np.array
|
26
|
+
The sorted data points used to fit the Bézier distribution.
|
27
|
+
bezierv : Bezierv
|
28
|
+
An instance of the Bezierv class representing the Bézier random variable.
|
29
|
+
init_x : np.array
|
30
|
+
Initial guess for the x-coordinates of the control points.
|
31
|
+
init_z : np.array
|
32
|
+
Initial guess for the z-coordinates of the control points.
|
33
|
+
init_t : np.array
|
34
|
+
Initial guess for the Bézier 'time' parameters corresponding to the data points.
|
35
|
+
emp_cdf_data : np.array
|
36
|
+
The empirical CDF data points used for fitting.
|
37
|
+
solver : str, optional
|
38
|
+
The name of the solver to use for optimization.
|
39
|
+
|
40
|
+
Returns
|
41
|
+
-------
|
42
|
+
Bezierv
|
43
|
+
The fitted Bezierv object with updated control points.
|
44
|
+
float
|
45
|
+
The mean squared error (MSE) of the fit.
|
46
|
+
|
47
|
+
Raises:
|
48
|
+
Exception: If the solver fails to find an optimal solution.
|
49
|
+
|
50
|
+
Notes:
|
51
|
+
- The method uses the IPOPT solver for optimization.
|
52
|
+
- The control points are constrained to lie within the range of the data.
|
53
|
+
- The method ensures that the control points and the Bézier 'time' parameters are sorted.
|
54
|
+
- Convexity constraints are applied to the control points and the Bézier 'time' parameters.
|
55
|
+
- The first and last control points are fixed to the minimum and maximum of the data, respectively.
|
56
|
+
- The first and last Bézier 'time' parameters are fixed to 0 and 1, respectively.
|
57
|
+
"""
|
58
|
+
# Defining the optimization model
|
59
|
+
model = pyo.ConcreteModel()
|
60
|
+
|
61
|
+
# Sets
|
62
|
+
model.N = pyo.Set(initialize=list(range(n + 1))) # N = 0,...,i,...,n
|
63
|
+
model.N_n = pyo.Set(initialize=list(range(n))) # N = 0,...,i,...,n-1
|
64
|
+
model.M = pyo.Set(initialize=list(range(1, m + 1))) # M = 1,...,j,...,m
|
65
|
+
model.M_m = pyo.Set(initialize=list(range(1, m))) # M = 1,...,j,...,m-1
|
66
|
+
|
67
|
+
# Decision variables
|
68
|
+
# Control points. Box constraints.
|
69
|
+
X_min = data[0];
|
70
|
+
X_max = data[-1];
|
71
|
+
# var x{i in 0..n} >=X[1], <=X[m];
|
72
|
+
# Initialization:
|
73
|
+
def init_x_rule(model, i):
|
74
|
+
return float(init_x[i])
|
75
|
+
model.x = pyo.Var(model.N, within=pyo.Reals, bounds=(X_min, X_max), initialize=init_x_rule)
|
76
|
+
# var z{i in 0..n} >=0, <=1;
|
77
|
+
# Initialization:
|
78
|
+
def init_z_rule(model, i):
|
79
|
+
return float(init_z[i])
|
80
|
+
model.z = pyo.Var(model.N, within=pyo.NonNegativeReals, bounds=(0, 1), initialize=init_z_rule)
|
81
|
+
# Bezier 'time' parameter t for the j-th sample point.
|
82
|
+
# var t{j in 1..m} >=0, <= 1;
|
83
|
+
# Initialization:
|
84
|
+
def init_t_rule(model, j):
|
85
|
+
return float(init_t[j - 1]) # j starts from 1, so we access init_t with j-1
|
86
|
+
model.t = pyo.Var(model.M, within=pyo.NonNegativeReals, bounds=(0,1), initialize=init_t_rule )
|
87
|
+
# Estimated cdf for the j-th sample point.
|
88
|
+
# var F_hat{j in 1..m} >=0, <= 1;
|
89
|
+
model.F_hat = pyo.Var(model.M, within=pyo.NonNegativeReals, bounds=(0,1) )
|
90
|
+
|
91
|
+
# Objective function
|
92
|
+
# minimize mean_square_error:
|
93
|
+
# 1/m * sum {j in 1..m} ( ( j/m - F_hat[j] )^2);
|
94
|
+
def mse_rule(model):
|
95
|
+
return (1 / m) * sum((emp_cdf_data[j - 1] - model.F_hat[j])**2 for j in model.M)
|
96
|
+
model.mse = pyo.Objective(rule=mse_rule, sense=pyo.minimize )
|
97
|
+
|
98
|
+
# Constraints
|
99
|
+
# subject to F_hat_estimates {j in 1..m}:
|
100
|
+
# sum{i in 0..n}( comb[i]*t[j]^i*(1-t[j])^(n-i)*z[i] ) = F_hat[j];
|
101
|
+
def F_hat_rule(model, j):
|
102
|
+
return sum(bezierv.comb[i] * model.t[j]**i * (1 - model.t[j])**(n - i) * model.z[i] for i in model.N ) == model.F_hat[j]
|
103
|
+
model.ctr_F_hat = pyo.Constraint(model.M , rule=F_hat_rule)
|
104
|
+
|
105
|
+
# subject to data_sample {j in 1..m}:
|
106
|
+
# sum{i in 0..n}( comb[i]*t[j]^i*(1-t[j])^(n-i)*x[i] ) = X[j];
|
107
|
+
def data_sample_rule(model, j):
|
108
|
+
return sum(bezierv.comb[i] * model.t[j]**i * (1 - model.t[j])**(n - i) * model.x[i] for i in model.N ) == data[j-1]
|
109
|
+
model.ctr_sample = pyo.Constraint(model.M , rule=data_sample_rule)
|
110
|
+
|
111
|
+
# subject to convexity_x {i in 0..n-1}:
|
112
|
+
# x[i] <= x[i+1];
|
113
|
+
def convexity_x_rule(model, i):
|
114
|
+
return model.x[i] <= model.x[i + 1]
|
115
|
+
model.ctr_convexity_x = pyo.Constraint(model.N_n , rule=convexity_x_rule)
|
116
|
+
|
117
|
+
# subject to convexity_z {i in 0..n-1}:
|
118
|
+
# z[i] <= z[i+1];
|
119
|
+
def convexity_z_rule(model, i):
|
120
|
+
return model.z[i] <= model.z[i + 1]
|
121
|
+
model.ctr_convexity_z = pyo.Constraint(model.N_n , rule=convexity_z_rule)
|
122
|
+
|
123
|
+
# subject to first_control_x:
|
124
|
+
# x[0] = X[1];
|
125
|
+
model.first_control_x = pyo.Constraint(expr=model.x[0] <= data[0])
|
126
|
+
# subject to first_control_z:
|
127
|
+
# z[0] = 0;
|
128
|
+
model.first_control_z = pyo.Constraint(expr=model.z[0] == 0)
|
129
|
+
|
130
|
+
# subject to last_control_x:
|
131
|
+
# x[n] = X[m];
|
132
|
+
model.last_control_x = pyo.Constraint(expr=model.x[n] >= data[-1])
|
133
|
+
# subject to last_control_z:
|
134
|
+
# z[n] = 1;
|
135
|
+
model.last_control_z = pyo.Constraint(expr=model.z[n] == 1)
|
136
|
+
|
137
|
+
# subject to first_data_t:
|
138
|
+
# t[1] = 0;
|
139
|
+
model.first_t = pyo.Constraint(expr=model.t[1] == 0)
|
140
|
+
# subject to last_data_t:
|
141
|
+
# t[m] = 1;
|
142
|
+
model.last_t = pyo.Constraint(expr=model.t[m] == 1)
|
143
|
+
|
144
|
+
delta_z = 0.0001
|
145
|
+
delta_x = 0.5
|
146
|
+
# Left end: x1 ~= x0, z1 ~= z0
|
147
|
+
#model.end_close_z_left = pyo.Constraint(expr = model.z[1] - model.z[0] <= delta_z)
|
148
|
+
#model.end_close_x_left = pyo.Constraint(expr = model.x[1] - model.x[0] <= delta_x)
|
149
|
+
|
150
|
+
# Right end: xn ~= xn-1, zn ~= zn-1
|
151
|
+
#model.end_close_x_right = pyo.Constraint(expr = model.x[n] - model.x[n-1] <= delta_x)
|
152
|
+
#model.end_close_z_right = pyo.Constraint(expr = model.z[n] - model.z[n-1] <= delta_z)
|
153
|
+
|
154
|
+
# Set solver
|
155
|
+
pyo_solver = SolverFactory(solver)
|
156
|
+
|
157
|
+
try:
|
158
|
+
results = pyo_solver.solve(model, tee=False, timelimit=60)
|
159
|
+
if (results.solver.status == SolverStatus.ok) and (results.solver.termination_condition == TerminationCondition.optimal):
|
160
|
+
controls_x = np.array([model.x[i]() for i in model.N])
|
161
|
+
controls_z = np.array([model.z[i]() for i in model.N])
|
162
|
+
mse = model.mse()
|
163
|
+
bezierv.update_bezierv(controls_x, controls_z)
|
164
|
+
except Exception as e:
|
165
|
+
print("NonLinearSolver [fit]: An exception occurred during model evaluation:", e)
|
166
|
+
|
167
|
+
return bezierv, mse
|
@@ -0,0 +1,186 @@
|
|
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
|