forked from elcf/python-openplaning
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathndmath.py
More file actions
256 lines (215 loc) · 7.98 KB
/
Copy pathndmath.py
File metadata and controls
256 lines (215 loc) · 7.98 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
"""
Numerical differentiation and root-finding methods.
This module provides implementations of:
- Finite difference gradient calculation
- Complex step gradient calculation
- N-dimensional Newton-Raphson root finding
These are embedded here to remove external dependencies for Grasshopper compatibility.
"""
import numpy as np
def finiteGrad(func, x0, step):
"""
Calculate the Jacobian of a function using forward finite differences.
Parameters
----------
func : callable
Function to perform numerical differentiation on.
Should accept array-like input and return array-like output.
x0 : array_like
Location to calculate gradient of function.
step : float
Step size for finite difference approximation.
Returns
-------
ndarray
Jacobian matrix of shape (n, m) where n is output dimension
and m is input dimension.
Examples
--------
>>> finiteGrad(lambda x: x**2, [1], 1e-7)
array([[2.]])
"""
x0 = np.array(x0, dtype=float)
xLength = len(x0)
fVal = np.array(func(x0), dtype=float)
h = step
hMatrix = np.eye(xLength) * h
jacobian = np.zeros((len(fVal), xLength))
for n in range(xLength):
x_perturbed = x0 + hMatrix[n, :]
f_perturbed = np.array(func(x_perturbed), dtype=float)
jacobian[:, n] = (f_perturbed - fVal) / h
return jacobian
def complexGrad(func, x0):
"""
Calculate the Jacobian using complex step differentiation.
This method provides higher accuracy than finite differences and
avoids subtractive cancellation errors.
Parameters
----------
func : callable
Function to perform complex differentiation on.
MUST be compatible with complex arithmetic (no operations that
discard imaginary parts).
x0 : array_like
Location to calculate gradient of function.
Returns
-------
ndarray
Jacobian matrix of shape (n, m) where n is output dimension
and m is input dimension.
Examples
--------
>>> complexGrad(lambda x: x**2, [1])
array([[2.]])
Notes
-----
The function MUST properly handle complex numbers. Common issues:
- Using np.real() or .real which discards imaginary parts
- Comparisons with complex numbers
- Some numpy functions that don't support complex dtype
"""
x0 = np.array(x0, dtype=float)
xLength = len(x0)
h = 1e-30 # Arbitrarily small step, no need to modify
# Determine output dimension by evaluating function once
f_val = func(x0)
f_dim = len(np.asarray(f_val))
jacobian = np.zeros((f_dim, xLength))
hMatrix = np.eye(xLength) * h
for n in range(xLength):
x_perturbed = x0 + 1j * hMatrix[n, :]
f_perturbed = np.array(func(x_perturbed), dtype=complex)
jacobian[:, n] = np.imag(f_perturbed) / h
return jacobian
def nDimNewton(func, x0, fprime, tol=1e-6, maxiter=50, xlim=None,
heh=True, hehcon=None):
"""
N-dimensional Newton-Raphson root finding method.
Finds x such that func(x) = 0 using Newton's method with optional
heuristic error handling and bounds checking.
Parameters
----------
func : callable
N-dimensional function to find root for, where n > 1.
Should return array-like output.
x0 : array_like
Initial estimate for the root.
fprime : callable
Function returning the Jacobian matrix of func.
Signature: fprime(x) -> ndarray of shape (n, m).
tol : float, optional
Tolerance for convergence (default: 1e-6).
Convergence achieved when ||func(x)|| < tol.
maxiter : int, optional
Maximum number of iterations (default: 50).
xlim : array_like, optional
Inclusive bounds for variables during iteration.
Shape: ((x0_lower, x0_upper), (x1_lower, x1_upper), ...)
Used to keep iterations in valid domain, not to constrain solution.
heh : bool, optional
Enable heuristic error handling (default: True).
Includes rank checking and bisection backtracking.
hehcon : callable, optional
Constraint function for heuristic error handling.
Should return array-like values where all must be <= 0.
Used to maintain valid domain during iterations.
Returns
-------
ndarray
Approximate root of func(x) = 0.
Raises
------
RuntimeError
If initial estimate doesn't satisfy constraints,
if Jacobian is rank-deficient,
if solution diverges from bounds,
or if maximum iterations exceeded.
Examples
--------
>>> def func(x):
... return [(x[1]-3)**2 + x[0] - 5, 2*x[1] + x[0]**3]
>>> def fprime(x):
... return complexGrad(func, x)
>>> x0 = [0, 0]
>>> root = nDimNewton(func, x0, fprime)
"""
# Initialize values
k = 1 # Iteration count
x = np.array(x0, dtype=float)
xOld = None
f = np.array(func(x), dtype=float)
# Check initial constraint satisfaction
if heh and hehcon is not None:
con_val = np.asarray(hehcon(x))
if any(con_val > 0):
raise RuntimeError(
"Initial estimate x0 does not satisfy hehcon(x0)[:]<=0. "
"Try a different x0."
)
while np.linalg.norm(f) > tol:
Df = fprime(x)
# Check and correct rank deficiency with heuristic handling
if heh:
while np.linalg.matrix_rank(Df) < len(Df):
if xOld is not None:
# Bisection backtracking
xNew = (x + xOld) / 2
x = xNew
Df = fprime(x)
else:
raise RuntimeError(
"Initial estimate x0 does not produce a full rank Jacobian. "
"Try a different x0."
)
xOld = x.copy()
g = np.array(func(x), dtype=float)
# Solve linear system Df @ v = -g
try:
v = np.linalg.solve(Df, -g)
except np.linalg.LinAlgError as e:
raise RuntimeError(f"Linear solve failed: {str(e)}")
# Check bounds violations before updating
if xlim is not None:
xlim = np.asarray(xlim)
runaways_lower = [
f'x[{i}]' for i in range(len(x))
if x[i] <= xlim[i, 0] and v[i] < 0
]
runaways_upper = [
f'x[{i}]' for i in range(len(x))
if x[i] >= xlim[i, 1] and v[i] > 0
]
if runaways_lower or runaways_upper:
message = (
'No solution found inside constraints. Iteration stopped because '
'the following variables were found outside user-defined limits:\n'
)
if runaways_lower:
message += ', '.join(runaways_lower) + ' <= lower bound\n'
if runaways_upper:
message += ', '.join(runaways_upper) + ' >= upper bound\n'
raise RuntimeError(message)
# Update x
x = x + v
# Force constraints if enabled
if heh and xlim is not None:
for i in range(len(x)):
if x[i] < xlim[i, 0]:
x[i] = xlim[i, 0]
elif x[i] > xlim[i, 1]:
x[i] = xlim[i, 1]
# Check iteration limit
if k == maxiter:
raise RuntimeError(
'Solution did not converge after maximum number of iterations.'
)
k += 1
f = np.array(func(x), dtype=float)
# Enforce heuristic constraints with backtracking
if heh and hehcon is not None:
while any(np.asarray(hehcon(x)) > 0):
# Bisection backtracking
x = (x + xOld) / 2
f = np.array(func(x), dtype=float)
return x