pyroboplan 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.
Files changed (47) hide show
  1. pyroboplan/__init__.py +1 -0
  2. pyroboplan/core/__init__.py +5 -0
  3. pyroboplan/core/planning_context.py +18 -0
  4. pyroboplan/core/utils.py +358 -0
  5. pyroboplan/ik/__init__.py +4 -0
  6. pyroboplan/ik/differential_ik.py +253 -0
  7. pyroboplan/ik/nullspace_components.py +76 -0
  8. pyroboplan/models/__init__.py +1 -0
  9. pyroboplan/models/panda.py +143 -0
  10. pyroboplan/models/panda_description/LICENSE +176 -0
  11. pyroboplan/models/panda_description/README.md +4 -0
  12. pyroboplan/models/panda_description/meshes/collision/finger.stl +0 -0
  13. pyroboplan/models/panda_description/meshes/collision/hand.stl +0 -0
  14. pyroboplan/models/panda_description/meshes/collision/link0.stl +0 -0
  15. pyroboplan/models/panda_description/meshes/collision/link1.stl +0 -0
  16. pyroboplan/models/panda_description/meshes/collision/link2.stl +0 -0
  17. pyroboplan/models/panda_description/meshes/collision/link3.stl +0 -0
  18. pyroboplan/models/panda_description/meshes/collision/link4.stl +0 -0
  19. pyroboplan/models/panda_description/meshes/collision/link5.stl +0 -0
  20. pyroboplan/models/panda_description/meshes/collision/link6.stl +0 -0
  21. pyroboplan/models/panda_description/meshes/collision/link7.stl +0 -0
  22. pyroboplan/models/panda_description/meshes/visual/finger.dae +63 -0
  23. pyroboplan/models/panda_description/meshes/visual/hand.dae +63 -0
  24. pyroboplan/models/panda_description/meshes/visual/link0.dae +63 -0
  25. pyroboplan/models/panda_description/meshes/visual/link1.dae +63 -0
  26. pyroboplan/models/panda_description/meshes/visual/link2.dae +63 -0
  27. pyroboplan/models/panda_description/meshes/visual/link3.dae +63 -0
  28. pyroboplan/models/panda_description/meshes/visual/link4.dae +63 -0
  29. pyroboplan/models/panda_description/meshes/visual/link5.dae +63 -0
  30. pyroboplan/models/panda_description/meshes/visual/link6.dae +63 -0
  31. pyroboplan/models/panda_description/meshes/visual/link7.dae +63 -0
  32. pyroboplan/models/panda_description/urdf/panda.urdf +280 -0
  33. pyroboplan/models/utils.py +16 -0
  34. pyroboplan/planning/__init__.py +4 -0
  35. pyroboplan/planning/cartesian_planner.py +231 -0
  36. pyroboplan/planning/graph.py +137 -0
  37. pyroboplan/planning/rrt.py +417 -0
  38. pyroboplan/planning/utils.py +29 -0
  39. pyroboplan/trajectory/__init__.py +5 -0
  40. pyroboplan/trajectory/polynomial.py +308 -0
  41. pyroboplan/trajectory/trapezoidal_velocity.py +353 -0
  42. pyroboplan/visualization/__init__.py +4 -0
  43. pyroboplan/visualization/meshcat_utils.py +124 -0
  44. pyroboplan-1.0.0.dist-info/METADATA +69 -0
  45. pyroboplan-1.0.0.dist-info/RECORD +47 -0
  46. pyroboplan-1.0.0.dist-info/WHEEL +4 -0
  47. pyroboplan-1.0.0.dist-info/licenses/LICENSE +21 -0
pyroboplan/__init__.py ADDED
@@ -0,0 +1 @@
1
+ """ Educational Python library for manipulator motion planning. """
@@ -0,0 +1,5 @@
1
+ """Core pyroboplan module.
2
+
3
+ This module contains the essential utilities to represent a robot model for motion planning.
4
+ It makes extensive use of the Pinocchio library but adds planning-specific abstractions over its implementation.
5
+ """
@@ -0,0 +1,18 @@
1
+ import pinocchio
2
+
3
+
4
+ class PlanningContext:
5
+ """
6
+ Defines a planning context, which contains Pinocchio models and necessary utilities for motion planning.
7
+ """
8
+
9
+ def __init__(self, model, visual_model=None, collision_model=None):
10
+ """
11
+ Creates a planning context instance given a set of Pinocchio models.
12
+ """
13
+ self.model = model
14
+ self.visual_model = visual_model
15
+ self.collision_model = collision_model
16
+
17
+ self.data = model.createData()
18
+ self.collision_data = collision_model.createData()
@@ -0,0 +1,358 @@
1
+ """ Core utilities for robot modeling. """
2
+
3
+ import copy
4
+ import numpy as np
5
+ import pinocchio
6
+
7
+
8
+ def check_collisions_at_state(model, collision_model, q):
9
+ """
10
+ Checks whether a specified joint configuration is collision-free.
11
+
12
+ Parameters
13
+ ----------
14
+ model : `pinocchio.Model`
15
+ The model from which to generate a random state.
16
+ collision_model : `pinocchio.Model`
17
+ The model to use for collision checking.
18
+ q : array-like
19
+ The joint configuration of the model.
20
+
21
+ Returns
22
+ -------
23
+ bool
24
+ True is there are any collisions, otherwise False.
25
+ """
26
+ data = model.createData()
27
+ collision_data = collision_model.createData()
28
+ stop_at_first_collision = True # For faster computation
29
+
30
+ pinocchio.computeCollisions(
31
+ model, data, collision_model, collision_data, q, stop_at_first_collision
32
+ )
33
+ return np.any([cr.isCollision() for cr in collision_data.collisionResults])
34
+
35
+
36
+ def check_collisions_along_path(model, collision_model, q_path):
37
+ """
38
+ Checks whether a path consisting of multiple joint configurations is collision-free.
39
+
40
+ Parameters
41
+ ----------
42
+ model : `pinocchio.Model`
43
+ The model from which to generate a random state.
44
+ collision_model : `pinocchio.Model`
45
+ The model to use for collision checking.
46
+ q_path : list[array-like]
47
+ A list of joint configurations describing the path.
48
+
49
+ Returns
50
+ -------
51
+ bool
52
+ True is there are any collisions, otherwise False.
53
+ """
54
+ data = model.createData()
55
+ collision_data = collision_model.createData()
56
+ stop_at_first_collision = True # For faster computation
57
+
58
+ for q in q_path:
59
+ pinocchio.computeCollisions(
60
+ model, data, collision_model, collision_data, q, stop_at_first_collision
61
+ )
62
+ if np.any([cr.isCollision() for cr in collision_data.collisionResults]):
63
+ return True
64
+
65
+ return False
66
+
67
+
68
+ def configuration_distance(q_start, q_end):
69
+ """
70
+ Returns the distance between two joint configurations.
71
+
72
+ Parameters
73
+ ----------
74
+ q_start : array-like
75
+ The start joint configuration.
76
+ q_end : array-like
77
+ The end joint configuration.
78
+
79
+ Returns
80
+ -------
81
+ float
82
+ The distance between the two joint configurations.
83
+ """
84
+ return np.linalg.norm(q_end - q_start)
85
+
86
+
87
+ def get_path_length(q_path):
88
+ """
89
+ Returns the configuration distance of a path.
90
+
91
+ Parameters
92
+ ----------
93
+ q_path : list[array-like]
94
+ A list of joint configurations describing a path.
95
+
96
+ Returns
97
+ -------
98
+ float
99
+ The total configuration distance of the entire path.
100
+ """
101
+ total_distance = 0.0
102
+ for idx in range(1, len(q_path)):
103
+ total_distance += configuration_distance(q_path[idx - 1], q_path[idx])
104
+ return total_distance
105
+
106
+
107
+ def get_random_state(model, padding=0.0):
108
+ """
109
+ Returns a random state that is within the model's joint limits.
110
+
111
+ Parameters
112
+ ----------
113
+ model : `pinocchio.Model`
114
+ The model from which to generate a random state.
115
+ padding : float or array-like, optional
116
+ The padding to use around the sampled joint limits.
117
+
118
+ Returns
119
+ -------
120
+ array-like
121
+ A set of randomly generated joint variables.
122
+ """
123
+ return np.random.uniform(
124
+ model.lowerPositionLimit + padding, model.upperPositionLimit - padding
125
+ )
126
+
127
+
128
+ def get_random_collision_free_state(model, collision_model, padding=0.0, max_tries=100):
129
+ """
130
+ Returns a random state that is within the model's joint limits and is collision-free according to the collision model.
131
+
132
+ Parameters
133
+ ----------
134
+ model : `pinocchio.Model`
135
+ The model from which to generate a random state.
136
+ collision_model : `pinocchio.Model`
137
+ The model to use for collision checking.
138
+ padding : float or array-like, optional
139
+ The padding to use around the sampled joint limits.
140
+ max_tries : int, optional
141
+ The maximum number of tries for sampling a collision-free state.
142
+
143
+ Returns
144
+ -------
145
+ array-like
146
+ A set of randomly generated collision-free joint variables, or None if one cannot be found.
147
+ """
148
+ num_tries = 0
149
+ while num_tries < max_tries:
150
+ state = get_random_state(model, padding=padding)
151
+ if not check_collisions_at_state(model, collision_model, state):
152
+ return state
153
+ num_tries += 1
154
+
155
+ print(f"Could not generate collision-free state after {max_tries} tries.")
156
+ return None
157
+
158
+
159
+ def get_random_transform(model, target_frame, joint_padding=0.0):
160
+ """
161
+ Returns a random transform for a target frame that is within the model's joint limits.
162
+
163
+ Parameters
164
+ ----------
165
+ model : `pinocchio.Model`
166
+ The model from which to generate a random transform.
167
+ target_frame : str
168
+ The name of the frame for which to generate a random transform.
169
+ joint_padding : float or array-like, optional
170
+ The padding to use around the sampled joint limits.
171
+
172
+ Returns
173
+ -------
174
+ `pinocchio.SE3`
175
+ A randomly generated transform for the specified target frame.
176
+ """
177
+ q_target = get_random_state(model, joint_padding)
178
+ data = model.createData()
179
+ target_frame_id = model.getFrameId(target_frame)
180
+ pinocchio.framesForwardKinematics(model, data, q_target)
181
+ return data.oMf[target_frame_id]
182
+
183
+
184
+ def get_random_collision_free_transform(
185
+ model, collision_model, target_frame, joint_padding=0.0, max_tries=100
186
+ ):
187
+ """
188
+ Returns a random transform for a target frame that is within the model's joint limits and is collision-free.
189
+
190
+ Parameters
191
+ ----------
192
+ model : `pinocchio.Model`
193
+ The model from which to generate a random transform.
194
+ collision_model : `pinocchio.Model`
195
+ The model to use for collision checking.
196
+ target_frame : str
197
+ The name of the frame for which to generate a random transform.
198
+ joint_padding : float or array-like, optional
199
+ The padding to use around the sampled joint limits.
200
+ max_tries : int, optional
201
+ The maximum number of tries for sampling a collision-free state.
202
+
203
+ Returns
204
+ -------
205
+ `pinocchio.SE3`
206
+ A randomly generated transform for the specified target frame.
207
+ """
208
+ q_target = get_random_collision_free_state(
209
+ model, collision_model, joint_padding, max_tries
210
+ )
211
+ data = model.createData()
212
+ target_frame_id = model.getFrameId(target_frame)
213
+ pinocchio.framesForwardKinematics(model, data, q_target)
214
+ return data.oMf[target_frame_id]
215
+
216
+
217
+ def check_within_limits(model, q):
218
+ """
219
+ Checks whether a particular joint configuration is within the model's joint limits.
220
+
221
+ Parameters
222
+ ----------
223
+ model : `pinocchio.Model`
224
+ The model from which to generate a random state.
225
+ q : array-like
226
+ The joint configuration for the model.
227
+
228
+ Returns
229
+ -------
230
+ bool
231
+ True if the configuration is within joint limits, otherwise False.
232
+ """
233
+ return np.all(q >= model.lowerPositionLimit) and np.all(
234
+ q <= model.upperPositionLimit
235
+ )
236
+
237
+
238
+ def extract_cartesian_pose(model, target_frame, q, data=None):
239
+ """
240
+ Extracts the Cartesian pose of a specified model frame given a joint configuration.
241
+
242
+ Parameters
243
+ ----------
244
+ model : `pinocchio.Model`
245
+ The model from which to perform forward kinematics.
246
+ target_frame : str
247
+ The name of the target frame.
248
+ q : array-like
249
+ The joint configuration values describing the robot state.
250
+ data : `pinocchio.Data`, optional
251
+ The model data to use. If not set, one will be created.
252
+
253
+ Returns
254
+ -------
255
+ `pinocchio.SE3`
256
+ The transform describing the Cartesian pose of the specified frame at the provided joint configuration.
257
+ """
258
+ if data is None:
259
+ data = model.createData()
260
+ target_frame_id = model.getFrameId(target_frame)
261
+ pinocchio.framesForwardKinematics(model, data, q)
262
+ return copy.deepcopy(data.oMf[target_frame_id])
263
+
264
+
265
+ def extract_cartesian_poses(model, target_frame, q_vec, data=None):
266
+ """
267
+ Extracts the Cartesian poses of a specified model frame given a list of joint configurations.
268
+
269
+ Parameters
270
+ ----------
271
+ model : `pinocchio.Model`
272
+ The model from which to perform forward kinematics.
273
+ target_frame : str
274
+ The name of the target frame.
275
+ q_vec : array-like
276
+ A list of joint configuration values describing the path.
277
+ data : `pinocchio.Data`, optional
278
+ The model data to use. If not set, one will be created.
279
+
280
+ Returns
281
+ -------
282
+ list[`pinocchio.SE3`]
283
+ A list of transforms describing the Cartesian poses of the specified frame at the provided joint configurations.
284
+ """
285
+ if data is None:
286
+ data = model.createData()
287
+ target_frame_id = model.getFrameId(target_frame)
288
+ tforms = []
289
+ for q in q_vec:
290
+ pinocchio.framesForwardKinematics(model, data, q)
291
+ tforms.append(copy.deepcopy(data.oMf[target_frame_id]))
292
+ return tforms
293
+
294
+
295
+ def get_collision_geometry_ids(model, collision_model, body):
296
+ """
297
+ Gets a list of collision geometry model IDs for a specified body name.
298
+
299
+ Parameters
300
+ ----------
301
+ model : `pinocchio.Model`
302
+ The model to use for getting frame IDs.
303
+ collision_model : `pinocchio.Model`
304
+ The model to use for collision checking.
305
+ body : str
306
+ The name of the body.
307
+ This can be directly the name of a geometry in the collision model,
308
+ or it can be the name of the frame in the main model.
309
+
310
+ Return
311
+ ------
312
+ list[int]
313
+ A list of collision geometry IDs corresponding to the body name.
314
+ """
315
+ body_collision_geom_ids = []
316
+
317
+ # First, check if this is directly a collision geometry.
318
+ body_collision_geom_id = collision_model.getGeometryId(body)
319
+ if body_collision_geom_id < collision_model.ngeoms:
320
+ body_collision_geom_ids.append(body_collision_geom_id)
321
+ else:
322
+ # Otherwise, look for the frame name in the model and return its associated collision objects.
323
+ body_frame_id = model.getFrameId(body)
324
+ if body_frame_id < model.nframes:
325
+ for id, obj in enumerate(collision_model.geometryObjects):
326
+ if obj.parentFrame == body_frame_id:
327
+ body_collision_geom_ids.append(id)
328
+
329
+ return body_collision_geom_ids
330
+
331
+
332
+ def set_collisions(model, collision_model, body1, body2, enable):
333
+ """
334
+ Sets collision checking between two bodies by searching for their corresponding geometry objects in the collision model.
335
+
336
+ Parameters
337
+ ----------
338
+ model : `pinocchio.Model`
339
+ The model to use for getting frame IDs.
340
+ collision_model : `pinocchio.Model`
341
+ The model to use for collision checking.
342
+ body1 : str
343
+ The name of the first body.
344
+ body2 : str
345
+ The name of the second body.
346
+ enable : bool
347
+ If True, enables collisions. If False, disables collisions.
348
+ """
349
+ body1_collision_ids = get_collision_geometry_ids(model, collision_model, body1)
350
+ body2_collision_ids = get_collision_geometry_ids(model, collision_model, body2)
351
+
352
+ for id1 in body1_collision_ids:
353
+ for id2 in body2_collision_ids:
354
+ pair = pinocchio.CollisionPair(id1, id2)
355
+ if enable:
356
+ collision_model.addCollisionPair(pair)
357
+ else:
358
+ collision_model.removeCollisionPair(pair)
@@ -0,0 +1,4 @@
1
+ """ Inverse kinematics module.
2
+
3
+ This module contains solvers and utilities for inverse kinematics (IK).
4
+ """
@@ -0,0 +1,253 @@
1
+ """ Utilities for differential IK. """
2
+
3
+ import numpy as np
4
+ import pinocchio
5
+ import time
6
+
7
+ from ..core.utils import (
8
+ check_collisions_at_state,
9
+ check_within_limits,
10
+ get_random_state,
11
+ )
12
+ from ..visualization.meshcat_utils import visualize_frame
13
+
14
+ VIZ_INITIAL_RENDER_TIME = 0.5
15
+ VIZ_SLEEP_TIME = 0.05
16
+
17
+
18
+ class DifferentialIkOptions:
19
+ """Options for differential IK."""
20
+
21
+ def __init__(
22
+ self,
23
+ max_iters=200,
24
+ max_retries=10,
25
+ max_translation_error=1e-3,
26
+ max_rotation_error=1e-3,
27
+ damping=1e-3,
28
+ min_step_size=0.1,
29
+ max_step_size=0.5,
30
+ ):
31
+ """
32
+ Initializes a set of differential IK options.
33
+
34
+ Parameters
35
+ ----------
36
+ max_iters : int
37
+ Maximum number of iterations per try.
38
+ max_retries : int
39
+ Maximum number of retries with random restarts.
40
+ If set to 0, only the initial state provided will be used.
41
+ max_translation_error : float
42
+ Maximum translation error, in meters, to consider IK solved.
43
+ max_rotation_error : float
44
+ Maximum rotation error, in radians, to consider IK solved.
45
+ damping : float
46
+ Damping value, between 0 and 1, for the Jacobian pseudoinverse.
47
+ Setting this to a nonzero value is using Levenberg-Marquardt.
48
+ min_step_size : float
49
+ Minimum gradient step, between 0 and 1, based on ratio of current distance to target to initial distance to target.
50
+ To use a fixed step size, set both minimum and maximum values to be equal.
51
+ max_step_size : float
52
+ Maximum gradient step, between 0 and 1, based on ratio of current distance to target to initial distance to target.
53
+ To use a fixed step size, set both minimum and maximum values to be equal.
54
+ """
55
+ self.max_iters = max_iters
56
+ self.max_retries = max_retries
57
+ self.max_translation_error = max_translation_error
58
+ self.max_rotation_error = max_rotation_error
59
+ self.damping = damping
60
+ self.min_step_size = min_step_size
61
+ self.max_step_size = max_step_size
62
+
63
+
64
+ class DifferentialIk:
65
+ """Differential IK solver.
66
+
67
+ This is a numerical IK solver that uses the manipulator's Jacobian to take first-order steps towards a solution.
68
+ It contains several of the common options such as damped least squares (Levenberg-Marquardt), random restarts, and nullspace projection.
69
+
70
+ Some good resources:
71
+ * https://motion.cs.illinois.edu/RoboticSystems/InverseKinematics.html
72
+ * https://homes.cs.washington.edu/~todorov/courses/cseP590/06_JacobianMethods.pdf
73
+ * https://www.cs.cmu.edu/~15464-s13/lectures/lecture6/iksurvey.pdf
74
+ """
75
+
76
+ def __init__(
77
+ self,
78
+ model,
79
+ collision_model=None,
80
+ data=None,
81
+ visualizer=None,
82
+ options=DifferentialIkOptions(),
83
+ ):
84
+ """
85
+ Creates an instance of a DifferentialIk solver.
86
+
87
+ Parameters
88
+ ----------
89
+ model : `pinocchio.Model`
90
+ The model to use for this solver.
91
+ collision_model : `pinocchio.Model`, optional
92
+ The model to use for collision checking. If None, no collision checking takes place.
93
+ data : `pinocchio.Data`, optional
94
+ The model data to use for this solver. If None, data is created automatically.
95
+ visualizer : `pinocchio.visualize.meshcat_visualizer.MeshcatVisualizer`, optional
96
+ The visualizer to use for this solver.
97
+ options : `DifferentialIkOptions`, optional
98
+ The options to use for solving IK. If not specified, default options are used.
99
+ """
100
+ self.model = model
101
+ self.collision_model = collision_model
102
+
103
+ if not data:
104
+ data = model.createData()
105
+ self.data = data
106
+
107
+ self.visualizer = visualizer
108
+ self.options = options
109
+
110
+ def solve(
111
+ self,
112
+ target_frame,
113
+ target_tform,
114
+ init_state=None,
115
+ nullspace_components=[],
116
+ verbose=False,
117
+ ):
118
+ """
119
+ Solves an IK query.
120
+
121
+ Parameters
122
+ ----------
123
+ target_frame : str
124
+ The name of the target frame in the model.
125
+ target_tform : `pinocchio.SE3`
126
+ The desired transformation of the target frame in the model.
127
+ init_state : array-like, optional
128
+ The initial state to solve from. If not specified, a random initial state will be selected.
129
+ nullspace_components : list[function], optional
130
+ An optional list of nullspace components to use when solving.
131
+ These components must take the form `lambda model, q: component(model, q, <other_args>)`.
132
+ verbose : bool, optional
133
+ If True, prints additional information to the console.
134
+
135
+ Returns
136
+ -------
137
+ array-like or None
138
+ A list of joint configuration values with the solution, if one was found. Otherwise, returns None.
139
+ """
140
+ target_frame_id = self.model.getFrameId(target_frame)
141
+
142
+ # Create a random initial state, if not specified
143
+ if init_state is None:
144
+ init_state = get_random_state(self.model)
145
+ if self.visualizer:
146
+ self.visualizer.displayFrames(True, frame_ids=[target_frame_id])
147
+ visualize_frame(self.visualizer, "ik_target_pose", target_tform)
148
+
149
+ # Initialize IK
150
+ solved = False
151
+ n_tries = 0
152
+ q_cur = init_state
153
+ initial_error_norm = None
154
+
155
+ if self.visualizer:
156
+ self.visualizer.display(q_cur)
157
+ time.sleep(VIZ_INITIAL_RENDER_TIME) # Needed to render
158
+
159
+ while n_tries <= self.options.max_retries:
160
+ n_iters = 0
161
+ while n_iters < self.options.max_iters:
162
+ # Compute forward kinematics at the current state
163
+ pinocchio.framesForwardKinematics(self.model, self.data, q_cur)
164
+ cur_tform = self.data.oMf[target_frame_id]
165
+
166
+ # Check the error using actInv
167
+ error = target_tform.actInv(cur_tform)
168
+ error = -pinocchio.log(error).vector
169
+ if (
170
+ np.linalg.norm(error[:3]) < self.options.max_translation_error
171
+ and np.linalg.norm(error[3:]) < self.options.max_rotation_error
172
+ ):
173
+ # Wrap to the range -/+ pi, and then check joint limits and collision.
174
+ q_cur = (q_cur + np.pi) % (2 * np.pi) - np.pi
175
+ if check_within_limits(self.model, q_cur):
176
+ if self.collision_model is not None:
177
+ if check_collisions_at_state(
178
+ self.model, self.collision_model, q_cur
179
+ ):
180
+ if verbose:
181
+ print(
182
+ "Solved and within joint limits, but in collision."
183
+ )
184
+ else:
185
+ solved = True
186
+ if verbose:
187
+ print(
188
+ "Solved, within joint limits, and collision-free!"
189
+ )
190
+ else:
191
+ solved = True
192
+ if verbose:
193
+ print("Solved and within joint limits!")
194
+ else:
195
+ if verbose:
196
+ print("Solved, but outside joint limits.")
197
+ break
198
+
199
+ # Calculate the Jacobian
200
+ J = pinocchio.computeFrameJacobian(
201
+ self.model,
202
+ self.data,
203
+ q_cur,
204
+ target_frame_id,
205
+ pinocchio.ReferenceFrame.LOCAL,
206
+ )
207
+
208
+ # Solve for the gradient using damping and nullspace components,
209
+ # as specified
210
+ jjt = J.dot(J.T) + self.options.damping**2 * np.eye(6)
211
+
212
+ # Compute the gradient descent step size
213
+ error_norm = np.linalg.norm(error)
214
+ if not initial_error_norm:
215
+ initial_error_norm = error_norm
216
+ alpha = self.options.min_step_size + (
217
+ 1.0 - error_norm / initial_error_norm
218
+ ) * (self.options.max_step_size - self.options.min_step_size)
219
+
220
+ # Gradient descent step
221
+ if not nullspace_components:
222
+ q_cur += alpha * J.T @ np.linalg.solve(jjt, error)
223
+ else:
224
+ nullspace_term = sum(
225
+ [comp(self.model, q_cur) for comp in nullspace_components]
226
+ )
227
+ q_cur += alpha * (
228
+ J.T @ (np.linalg.solve(jjt, error - J @ (nullspace_term)))
229
+ + nullspace_term
230
+ )
231
+
232
+ n_iters += 1
233
+
234
+ if self.visualizer:
235
+ self.visualizer.display(q_cur)
236
+ time.sleep(VIZ_SLEEP_TIME)
237
+
238
+ # Check results at the end of this try
239
+ if solved:
240
+ if verbose:
241
+ print(f"Solved in {n_tries+1} tries.")
242
+ break
243
+ else:
244
+ q_cur = get_random_state(self.model)
245
+ n_tries += 1
246
+ if verbose:
247
+ print(f"Retry {n_tries}")
248
+
249
+ # Check final results
250
+ if solved:
251
+ return q_cur
252
+ else:
253
+ return None