Skip to content

Commit 301d454

Browse files
committed
(feature) Adds OMPL geometric planner and validity checking in C++
1 parent 3ae16cd commit 301d454

File tree

11 files changed

+319
-264
lines changed

11 files changed

+319
-264
lines changed

src/kompass_core/third_party/ompl/config.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,9 @@ def _convert_log_level(log_level: Union[ompl.util.LogLevel, str]) -> ompl.util.L
106106
raise ValueError(f"Invalid OMPL log level: {log_level}")
107107

108108

109-
def initializePlanners(log_level: Union[ompl.util.LogLevel, str] = ompl.util.LogLevel.LOG_ERROR):
109+
def initializePlanners(
110+
log_level: Union[ompl.util.LogLevel, str] = ompl.util.LogLevel.LOG_ERROR,
111+
):
110112
"""Initialize planner map, similar to ompl python bindings."""
111113
log_level = _convert_log_level(log_level)
112114
ompl.util.setLogLevel(log_level)

src/kompass_core/third_party/ompl/planner.py

Lines changed: 44 additions & 241 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,12 @@
33
import numpy as np
44
from attrs import asdict, define, field
55
from ...utils.common import BaseAttrs, base_validators
6-
from ...utils.geometry import convert_to_plus_minus_pi
76

87
import omplpy as ompl
9-
from ...models import Robot, RobotState
8+
from ...models import Robot, RobotGeometry
109
from omplpy import base, geometric
10+
from kompass_cpp.planning import OMPL2DGeometricPlanner
1111

12-
from ..fcl.collisions import FCL
13-
from ..fcl.config import FCLConfig
1412
from .config import create_config_class, initializePlanners, optimization_objectives
1513

1614

@@ -54,7 +52,6 @@ def __init__(
5452
use_fcl: bool = True,
5553
config: Optional[OMPLGeometricConfig] = None,
5654
config_file: Optional[str] = None,
57-
map_3d: Optional[np.ndarray] = None,
5855
):
5956
"""
6057
Init Open Motion Planning Lib geometric handler
@@ -93,45 +90,29 @@ def __init__(
9390
self.available_planners = ompl.geometric.planners.getPlanners()
9491

9592
# create a simple setup object
96-
self.setup = geometric.SimpleSetup(self.state_space)
97-
98-
# Configure FCL
99-
if use_fcl:
100-
self.__init_fcl(map_3d)
101-
is_state_valid = self._validity_checker_with_fcl
93+
self._ompl_setup = geometric.SimpleSetup(self.state_space)
10294

103-
else:
104-
is_state_valid = self._validity_checker
95+
# Set the selected planner
96+
self._set_planner()
10597

106-
# Add validity method to ompl setup without custom validity checkers
107-
self.setup.setStateValidityChecker(is_state_valid)
98+
# Setup Path Optimization Objective
99+
optimization_objective = getattr(base, self._config.optimization_objective)(
100+
self._ompl_setup.getSpaceInformation()
101+
)
108102

109-
# Set the selected planner
110-
self.set_planner()
103+
self._ompl_setup.setOptimizationObjective(optimization_objective)
111104

112105
if config_file:
113106
self.configure(config_file)
114107

115-
def __init_fcl(self, map_3d: Optional[np.ndarray] = None):
116-
"""
117-
Setup FCL
118-
119-
:param map_3d: Map data
120-
:type map_3d: np.ndarray
121-
"""
122-
fcl_config = FCLConfig(
108+
self._cpp_planner: Optional[OMPL2DGeometricPlanner] = OMPL2DGeometricPlanner(
109+
robot_shape=RobotGeometry.Type.to_kompass_cpp_lib(
110+
self._robot.geometry_type
111+
),
112+
robot_dimensions=self._robot.geometry_params,
113+
ompl_setup=self._ompl_setup,
123114
map_resolution=self._config.map_resolution,
124-
robot_geometry_type=self._robot.geometry_type,
125-
robot_geometry_params=self._robot.geometry_params.tolist(),
126115
)
127-
if not hasattr(self, "fcl"):
128-
# setup collision check
129-
if map_3d is not None:
130-
self.fcl = FCL(fcl_config, map_3d)
131-
else:
132-
self.fcl = FCL(fcl_config)
133-
else:
134-
self.fcl._setup_from_config()
135116

136117
def configure(
137118
self,
@@ -170,59 +151,19 @@ def configure(
170151

171152
planner_params.from_file(config_file, nested_root_name + "." + planner_name)
172153

173-
self.set_planner(planner_params, planner_id)
154+
self._set_planner(planner_params, planner_id)
174155

175156
self.start = False
176157

177-
# configure FCL if it is used
178-
if self._use_fcl:
179-
self.__init_fcl()
180-
181-
def clear(self):
182-
"""
183-
Clears planning setup
184-
"""
185-
self.setup.clear()
186-
187158
@property
188-
def path_cost(self) -> Optional[float]:
159+
def path_cost(self) -> float:
189160
"""
190161
Getter of solution path cost using the configured optimization objective
191162
192163
:return: Path cost
193164
:rtype: float | None
194165
"""
195-
if self.solution:
196-
optimization_objective = getattr(base, self._config.optimization_objective)(
197-
self.setup.getSpaceInformation()
198-
)
199-
cost = self.solution.cost(optimization_objective)
200-
return cost.value()
201-
return None
202-
203-
def get_cost_using_objective(self, objective_key: str) -> Optional[float]:
204-
"""
205-
Get solution cost using a specific objective
206-
This is used to get the cost using an objective other than the configured objective
207-
To get the cost using the default objective use self.path_cost directly
208-
209-
:param objective_key: _description_
210-
:type objective_key: str
211-
:raises KeyError: Unknown objective name
212-
:return: _description_
213-
:rtype: _type_
214-
"""
215-
if not self.solution:
216-
return None
217-
if objective_key in optimization_objectives:
218-
optimization_objective = getattr(base, objective_key)(
219-
self.setup.getSpaceInformation()
220-
)
221-
cost = self.solution.cost(optimization_objective)
222-
return cost.value()
223-
raise KeyError(
224-
f"Unknown optimization objective. Available optimization objectives are: {optimization_objectives.keys()}"
225-
)
166+
return self._cpp_planner.get_cost()
226167

227168
def setup_problem(
228169
self,
@@ -236,7 +177,7 @@ def setup_problem(
236177
map_3d: Optional[np.ndarray] = None,
237178
):
238179
"""
239-
Setup a new planning problem with a map, start and goal infromation
180+
Setup a new planning problem with a map, start and goal information
240181
241182
:param map_meta_data: Global map meta data as a dictionary
242183
:type map_meta_data: Dict
@@ -255,132 +196,16 @@ def setup_problem(
255196
:param map_3d: 3D array for map PointCloud data, defaults to None
256197
:type map_3d: np.ndarray | None, optional
257198
"""
258-
# Clear previous setup
259-
self.setup.clear()
260-
261-
self.set_space_bounds(map_meta_data)
262-
263-
scoped_start_state = base.ScopedState(self.state_space)
264-
start_state = scoped_start_state.get()
265-
start_state.setX(start_x)
266-
start_state.setY(start_y)
267-
268-
# SE2 takes angle in [-pi,+pi]
269-
yaw = convert_to_plus_minus_pi(start_yaw)
270-
start_state.setYaw(yaw)
271-
272-
scoped_goal_state = base.ScopedState(self.state_space)
273-
goal_state = scoped_goal_state.get()
274-
goal_state.setX(goal_x)
275-
goal_state.setY(goal_y)
276-
277-
# SE2 takes angle in [-pi,+pi]
278-
yaw = convert_to_plus_minus_pi(goal_yaw)
279-
goal_state.setYaw(yaw)
280-
281-
self.setup.setStartAndGoalStates(
282-
start=scoped_start_state, goal=scoped_goal_state
283-
)
284-
285-
# Setup Path Optimization Objective
286-
optimization_objective = getattr(base, self._config.optimization_objective)(
287-
self.setup.getSpaceInformation()
288-
)
289-
290-
self.setup.setOptimizationObjective(optimization_objective)
291-
292-
if self._use_fcl and map_3d is None:
293-
raise ValueError(
294-
"OMPL is started with collision check -> Map should be provided"
295-
)
296-
elif self._use_fcl:
297-
# TODO: Add an option to update the map periodically or just at the start, or after a time interval
298-
if not self.start:
299-
self.fcl.set_map(map_3d)
300-
self.start = True
301-
self.fcl.update_state(
302-
robot_state=RobotState(x=start_x, y=start_y, yaw=start_yaw)
303-
)
304-
305-
def add_validity_check(self, name: str, validity_function: Callable) -> None:
306-
"""
307-
Add method for state validity check during planning
308-
309-
:param name: Method key name
310-
:type name: str
311-
:param validity_function: Validity check method
312-
:type validity_function: Callable
313-
314-
:raises TypeError: If validity check is not callable
315-
:raises TypeError: If validity check method does not return a boolean
316-
"""
317-
# Check that validity function is callable
318-
if callable(validity_function) and callable(validity_function):
319-
# Check if the function returns a boolean value
320-
args = (None,) * validity_function.__code__.co_argcount
321-
if isinstance(validity_function(*args), bool):
322-
self._custom_validity_check[name] = validity_function
323-
else:
324-
raise TypeError("Validity check function needs to return a boolean")
325-
else:
326-
raise TypeError("Validity check function must be callable")
327-
328-
def remove_validity_check(self, name: str) -> bool:
329-
"""
330-
Removes an added validity chec!= Nonek
331-
332-
:param name: Validity check name key
333-
:type name: str
334-
335-
:raises ValueError: If given key does not correspond to an added validity check
336-
337-
:return: If validity check is removed
338-
:rtype: bool
339-
"""
340-
deleted_method = self._custom_validity_check.pop(name, None)
341-
if deleted_method is not None:
342-
return True
343-
else:
344-
raise ValueError(
345-
f"Cannot remove validity check titled {name} as it does not exist"
346-
)
347-
348-
def _validity_checker(self, state, **_) -> bool:
349-
"""
350-
State validity checker method
351-
352-
:param state: Robot state
353-
:type state: SE2State
354-
355-
:return: If state is valid
356-
:rtype: bool
357-
"""
358-
# Run bounds and state check
359-
return self.setup.getSpaceInformation().satisfiesBounds(state)
360-
361-
def _validity_checker_with_fcl(self, state, **_) -> bool:
362-
"""
363-
State validity checker method
364-
365-
:param state: Robot state
366-
:type state: SE2State
367-
368-
:return: If state is valid
369-
:rtype: bool
370-
"""
371-
# Run bounds and state check
372-
state_space_valid: bool = self.setup.getSpaceInformation().satisfiesBounds(
373-
state
199+
self._set_space_bounds(map_meta_data)
200+
self._cpp_planner.setup_problem(
201+
start_x=start_x,
202+
start_y=start_y,
203+
start_yaw=start_yaw,
204+
goal_x=goal_x,
205+
goal_y=goal_y,
206+
goal_yaw=goal_yaw,
207+
map_3d=map_3d,
374208
)
375-
if self.fcl.got_map:
376-
self.fcl.update_state(
377-
RobotState(x=state.getX(), y=state.getY(), yaw=state.getYaw())
378-
)
379-
is_collision = self.fcl.check_collision()
380-
else:
381-
is_collision = False
382-
383-
return state_space_valid and not is_collision
384209

385210
@property
386211
def planner_params(self) -> Optional[ompl.base.ParamSet]:
@@ -420,7 +245,7 @@ def planner_id(self) -> str:
420245
"""
421246
return self._config.planner_id
422247

423-
def set_planner(
248+
def _set_planner(
424249
self,
425250
planner_config: Optional[BaseAttrs] = None,
426251
planner_id: Optional[str] = None,
@@ -441,40 +266,32 @@ def set_planner(
441266
else:
442267
self._config.planner_id = planner_id
443268

444-
self.planner = eval("%s(self.setup.getSpaceInformation())" % planner_id)
269+
self.planner = eval(
270+
"%s(self._ompl_setup.getSpaceInformation())" % planner_id
271+
)
445272

446273
if planner_config:
447274
self.planner_params = planner_config
448275

449-
self.setup.setPlanner(self.planner)
276+
self._ompl_setup.setPlanner(self.planner)
450277

451278
except Exception:
452279
raise
453280

454-
def set_space_bounds(self, map_meta_data: Dict):
281+
def _set_space_bounds(self, map_meta_data: Dict):
455282
"""
456283
Set planning space bounds from map data
457284
458285
:param map_meta_data: Map meta data
459286
:type map_meta_data: Dict
460287
"""
461-
# X-axis bounds
462-
x_lower = map_meta_data["origin_x"]
463-
x_upper = x_lower + map_meta_data["resolution"] * map_meta_data["width"]
464-
465-
# Y-axis bounds
466-
y_lower = map_meta_data["origin_y"]
467-
y_upper = y_lower + map_meta_data["resolution"] * map_meta_data["height"]
468-
469-
# set lower and upper bounds
470-
bounds = base.RealVectorBounds(2)
471-
472-
bounds.setLow(index=0, value=x_lower)
473-
bounds.setLow(index=1, value=y_lower)
474-
bounds.setHigh(index=0, value=x_upper)
475-
bounds.setHigh(index=1, value=y_upper)
476-
477-
self.state_space.setBounds(bounds)
288+
self._cpp_planner.set_space_bounds_from_map(
289+
origin_x=map_meta_data["origin_x"],
290+
origin_y=map_meta_data["origin_y"],
291+
width=map_meta_data["width"],
292+
height=map_meta_data["height"],
293+
resolution=map_meta_data["resolution"],
294+
)
478295

479296
def solve(self) -> ompl.geometric.PathGeometric:
480297
"""
@@ -483,23 +300,9 @@ def solve(self) -> ompl.geometric.PathGeometric:
483300
:return: Solution (Path points) or None if no solution is found
484301
:rtype: ompl.geometric.PathGeometric
485302
"""
486-
solved = self.setup.solve(self._config.planning_timeout)
303+
solved = self._cpp_planner.solve(self._config.planning_timeout)
487304

488305
if solved:
489-
self.solution = self.setup.getSolutionPath()
490-
491-
return self.solution
492-
return None
493-
494-
def simplify_solution(self) -> ompl.geometric.PathGeometric:
495-
"""
496-
Simplify the path
497-
498-
:return: Simplified path
499-
:rtype: ompl.geometric.PathGeometric
500-
"""
501-
if self.solution:
502-
self.setup.simplifySolution(self._config.simplification_timeout)
503-
self.solution = self.setup.getSolutionPath()
306+
self.solution = self._cpp_planner.get_solution()
504307
return self.solution
505308
return None

0 commit comments

Comments
 (0)