diff --git a/CMakeLists.txt b/CMakeLists.txt index bfe561e1..b6f5930f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -78,6 +78,7 @@ set(${PROJECT_NAME}_HEADERS include/pyhpp/core/fwd.hh include/pyhpp/core/path/fwd.hh include/pyhpp/core/path-planner.hh + include/pyhpp/core/path-validation.hh include/pyhpp/core/pathOptimization/fwd.hh include/pyhpp/core/problem.hh include/pyhpp/core/problemTarget/fwd.hh diff --git a/doc/doxygen_xml_parser.py b/doc/doxygen_xml_parser.py index 26812e70..85148562 100644 --- a/doc/doxygen_xml_parser.py +++ b/doc/doxygen_xml_parser.py @@ -34,7 +34,7 @@ def _getDoc(el): b = el.find("briefdescription") d = el.find("detaileddescription") brief = etree.tostring(b, method="text", encoding="unicode").strip() - detailed = d.text.strip() if d.text else "" + detailed = etree.tostring(d, method="text", encoding="unicode").strip() return brief, detailed def _getMemberFromGroup(self, memberDefKind, name): @@ -124,11 +124,7 @@ def _getMember(self, sectionKind, memberDefKind, name): raise IndexError(msg) def getClassDoc(self): - b_el = self.compound.find("briefdescription") - d_el = self.compound.find("detaileddescription") - brief = etree.tostring(b_el, method="text", encoding="unicode").strip() - detailed = etree.tostring(d_el, method="text", encoding="unicode").strip() - return brief, detailed + return self._getDoc(self.compound) def getClassMemberDoc(self, membername): # member = self.compound.find ("sectiondef[@kind='public-attrib']/memberdef[@kind='variable' and name='" + methodname + "']") diff --git a/doc/gen_api_md.py b/doc/gen_api_md.py index cc1df883..498227ee 100644 --- a/doc/gen_api_md.py +++ b/doc/gen_api_md.py @@ -294,6 +294,11 @@ def _unparse_ann(ann: ast.expr | None) -> str: # --------------------------------------------------------------------------- ClassIndex = dict[str, str] # type name variants → "page.md#anchor" +ClassDefinitionIndex = dict[str, ast.ClassDef] + + +def _stub_module_name(relative_stub: str) -> str: + return str(Path(relative_stub).with_suffix("")).replace("/", ".") def build_class_index(stubs_root: Path) -> ClassIndex: @@ -307,7 +312,7 @@ def build_class_index(stubs_root: Path) -> ClassIndex: if tree is None: continue page = slug(module_name) - stub_dotted = str(Path(rel_stub).with_suffix("")).replace("/", ".") + stub_dotted = _stub_module_name(rel_stub) for item in tree.body: if not isinstance(item, ast.ClassDef): continue @@ -334,6 +339,30 @@ def build_class_index(stubs_root: Path) -> ClassIndex: return index +def build_class_definition_index(stubs_root: Path) -> ClassDefinitionIndex: + index: ClassDefinitionIndex = {} + + def add_class(class_node: ast.ClassDef, parent: str) -> None: + qualified_class = f"{parent}.{class_node.name}" + index[qualified_class] = class_node + for item in class_node.body: + if isinstance(item, ast.ClassDef): + add_class(item, qualified_class) + + for rel_stub, _, _ in MODULES: + stub_path = stubs_root / rel_stub + if not stub_path.exists(): + continue + tree = _parse_stub(stub_path) + if tree is None: + continue + stub_module = _stub_module_name(rel_stub) + for item in tree.body: + if isinstance(item, ast.ClassDef): + add_class(item, stub_module) + return index + + def _type_link(type_str: str, class_index: ClassIndex, current_page: str) -> str: """Markdown link for a type (used outside code blocks, e.g. base-class line).""" if not type_str: @@ -410,11 +439,30 @@ def _fmt_ann(ann: str, class_index: ClassIndex, current_page: str) -> str: def _build_stub_module_index() -> dict[str, str]: index: dict[str, str] = {} for rel_stub, module_name, _ in MODULES: - dotted = str(Path(rel_stub).with_suffix("")).replace("/", ".") - index[dotted] = slug(module_name) + index[_stub_module_name(rel_stub)] = slug(module_name) return index +def _base_name(base: ast.expr) -> str: + if isinstance(base, ast.Attribute): + return ast.unparse(base) + if isinstance(base, ast.Name): + return base.id + return "" + + +def _resolve_base( + base: ast.expr, + current_stub_module: str, + class_definitions: ClassDefinitionIndex, +) -> str | None: + raw = _base_name(base) + if not raw: + return None + qualified = raw if "." in raw else f"{current_stub_module}.{raw}" + return qualified if qualified in class_definitions else None + + def _base_links( class_node: ast.ClassDef, stub_module_index: dict[str, str], @@ -423,11 +471,7 @@ def _base_links( ) -> list[str]: links = [] for b in class_node.bases: - raw = ( - ast.unparse(b) - if isinstance(b, ast.Attribute) - else (b.id if isinstance(b, ast.Name) else "") - ) + raw = _base_name(b) if not raw: continue class_name = raw.split(".")[-1] @@ -449,6 +493,7 @@ def _base_links( # --------------------------------------------------------------------------- type BodyGroup = list[ast.FunctionDef] | ast.ClassDef +type ClassMethods = dict[str, tuple[list[ast.FunctionDef], str]] def _group_body(body: list[ast.stmt]) -> list[BodyGroup]: @@ -473,6 +518,80 @@ def _group_body(body: list[ast.stmt]) -> list[BodyGroup]: return result +def _class_mro( + qualified_class: str, + class_definitions: ClassDefinitionIndex, + cache: dict[str, list[str]], +) -> list[str]: + if qualified_class in cache: + return cache[qualified_class] + + class_node = class_definitions[qualified_class] + current_stub_module = qualified_class.rsplit(".", 1)[0] + bases = [ + resolved + for base in class_node.bases + if (resolved := _resolve_base(base, current_stub_module, class_definitions)) + ] + sequences = [_class_mro(base, class_definitions, cache).copy() for base in bases] + sequences.append(bases.copy()) + + result = [qualified_class] + while sequences: + sequences = [sequence for sequence in sequences if sequence] + if not sequences: + break + candidate = next( + ( + sequence[0] + for sequence in sequences + if not any(sequence[0] in other[1:] for other in sequences) + ), + None, + ) + if candidate is None: + raise ValueError(f"Inconsistent class hierarchy for {qualified_class}") + result.append(candidate) + for sequence in sequences: + if sequence[0] == candidate: + sequence.pop(0) + + cache[qualified_class] = result + return result + + +def _direct_method_groups( + class_node: ast.ClassDef, +) -> dict[str, list[ast.FunctionDef]]: + methods: dict[str, list[ast.FunctionDef]] = {} + for group in _group_body(class_node.body): + if isinstance(group, ast.ClassDef): + continue + node = group[0] + methods.setdefault(node.name, []).extend(group) + return methods + + +def _collect_class_methods( + qualified_class: str, + class_definitions: ClassDefinitionIndex, + cache: dict[str, ClassMethods], + mro_cache: dict[str, list[str]], +) -> ClassMethods: + if qualified_class in cache: + return cache[qualified_class] + + methods: ClassMethods = {} + for owner in _class_mro(qualified_class, class_definitions, mro_cache): + for name, group in _direct_method_groups(class_definitions[owner]).items(): + if name == "__init__" and owner != qualified_class: + continue + methods.setdefault(name, (group, owner)) + + cache[qualified_class] = methods + return methods + + # --------------------------------------------------------------------------- # Signature rendering — HTML
with embedded links
# ---------------------------------------------------------------------------
@@ -664,6 +783,8 @@ def render_class(
class_index: ClassIndex,
current_page: str,
stub_module_index: dict[str, str],
+ qualified_class: str,
+ class_methods: dict[str, ClassMethods],
heading_level: int = 2,
) -> tuple[list[str], bool]:
"""Render a class and return (lines, has_content)."""
@@ -679,22 +800,37 @@ def render_class(
for g in groups:
if isinstance(g, ast.ClassDef):
sub_lines, _ = render_class(
- g, class_index, current_page, stub_module_index, heading_level + 1
+ g,
+ class_index,
+ current_page,
+ stub_module_index,
+ f"{qualified_class}.{g.name}",
+ class_methods,
+ heading_level + 1,
)
nested_class_lines.extend(sub_lines)
- else:
- node = g[0]
- if node.name in SKIP_NAMES or is_setter(node):
+
+ methods = class_methods.get(qualified_class)
+ if methods is None:
+ methods = {
+ name: (group, qualified_class)
+ for name, group in _direct_method_groups(class_node).items()
+ }
+ for group, owner in methods.values():
+ node = group[0]
+ if node.name in SKIP_NAMES or is_setter(node):
+ continue
+ if node.name == "__init__":
+ if any(
+ get_docstring(ov) and NOT_INSTANTIABLE.search(get_docstring(ov))
+ for ov in group
+ ):
continue
- if node.name == "__init__":
- if any(
- get_docstring(ov) and NOT_INSTANTIABLE.search(get_docstring(ov))
- for ov in g
- ):
- continue
- sig = _sig_cell(g, class_index, current_page)
- desc = _desc_cell(g)
- method_rows.append((sig, desc))
+ sig = _sig_cell(group, class_index, current_page)
+ if owner != qualified_class:
+ sig += f" *(inherited from {_type_link(owner, class_index, current_page)})*"
+ desc = _desc_cell(group)
+ method_rows.append((sig, desc))
has_content = bool(raw_doc or bases or method_rows or nested_class_lines)
if not has_content:
@@ -744,8 +880,10 @@ def render_class(
def generate_module_md(
stub_path: Path,
module_name: str,
+ current_stub_module: str,
class_index: ClassIndex,
stub_module_index: dict[str, str],
+ class_methods: dict[str, ClassMethods],
) -> str:
tree = _parse_stub(stub_path)
if tree is None:
@@ -791,7 +929,12 @@ def generate_module_md(
for item in tree.body:
if isinstance(item, ast.ClassDef):
class_lines, ok = render_class(
- item, class_index, current_page, stub_module_index
+ item,
+ class_index,
+ current_page,
+ stub_module_index,
+ f"{current_stub_module}.{item.name}",
+ class_methods,
)
if ok:
lines.extend(class_lines)
@@ -828,7 +971,14 @@ def main() -> None:
args.output.mkdir(parents=True, exist_ok=True)
class_index = build_class_index(args.stubs)
+ class_definitions = build_class_definition_index(args.stubs)
stub_module_index = _build_stub_module_index()
+ class_methods: dict[str, ClassMethods] = {}
+ mro_cache: dict[str, list[str]] = {}
+ for qualified_class in class_definitions:
+ _collect_class_methods(
+ qualified_class, class_definitions, class_methods, mro_cache
+ )
generated: list[tuple[str, str, str]] = []
for rel_stub, module_name, description in MODULES:
@@ -838,7 +988,14 @@ def main() -> None:
continue
out_file = args.output / f"{slug(module_name)}.md"
out_file.write_text(
- generate_module_md(stub_path, module_name, class_index, stub_module_index)
+ generate_module_md(
+ stub_path,
+ module_name,
+ _stub_module_name(rel_stub),
+ class_index,
+ stub_module_index,
+ class_methods,
+ )
)
generated.append((module_name, slug(module_name), description))
print(f" {out_file.name}")
diff --git a/include/pyhpp/core/fwd.hh b/include/pyhpp/core/fwd.hh
index 64cc175c..36bebba0 100644
--- a/include/pyhpp/core/fwd.hh
+++ b/include/pyhpp/core/fwd.hh
@@ -61,7 +61,9 @@ void exposeProblem();
// forward declaration of some classes
class PathPlanner;
+struct PathValidation;
class Problem;
+typedef std::shared_ptr PyWPathValidationPtr_t;
} // namespace core
} // namespace pyhpp
diff --git a/include/pyhpp/core/path-validation.hh b/include/pyhpp/core/path-validation.hh
new file mode 100644
index 00000000..f20f8c3f
--- /dev/null
+++ b/include/pyhpp/core/path-validation.hh
@@ -0,0 +1,64 @@
+//
+// Copyright (c) 2026 CNRS
+// Author: Paul Sardin
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions
+// are met:
+
+// 1. Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+
+// 2. Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following
+// disclaimer in the documentation and/or other materials provided
+// with the distribution.
+
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+// FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+// COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+// INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+// HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
+// OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#ifndef PYHPP_CORE_PATH_VALIDATION_HH
+#define PYHPP_CORE_PATH_VALIDATION_HH
+
+#include
+#include
+#include
+
+namespace pyhpp {
+namespace core {
+
+struct PathValidation {
+ hpp::core::PathValidationPtr_t obj;
+ hpp::core::PathValidationBuilder_t factory;
+ hpp::core::value_type tolerance;
+
+ PathValidation(const hpp::core::PathValidationPtr_t& obj)
+ : obj(obj), factory(), tolerance(0) {}
+
+ PathValidation(const hpp::core::DevicePtr_t& robot,
+ const hpp::core::PathValidationBuilder_t& factory,
+ const hpp::core::value_type& tolerance)
+ : obj(factory(robot, tolerance)),
+ factory(factory),
+ tolerance(tolerance) {}
+
+ PathValidation(const hpp::core::PathValidationPtr_t& obj,
+ const hpp::core::PathValidationBuilder_t& factory,
+ const hpp::core::value_type& tolerance)
+ : obj(obj), factory(factory), tolerance(tolerance) {}
+};
+
+} // namespace core
+} // namespace pyhpp
+
+#endif // PYHPP_CORE_PATH_VALIDATION_HH
diff --git a/include/pyhpp/core/problem.hh b/include/pyhpp/core/problem.hh
index 8d79f967..c582e23f 100644
--- a/include/pyhpp/core/problem.hh
+++ b/include/pyhpp/core/problem.hh
@@ -73,6 +73,7 @@ struct Problem {
PyWSteeringMethodPtr_t steeringMethod() const;
const ConfigValidationsPtr_t& configValidation() const;
PathValidationPtr_t pathValidation() const;
+ PyWPathValidationPtr_t pyPathValidation() const;
PathProjectorPtr_t pathProjector() const;
DistancePtr_t distance() const;
const ProblemTargetPtr_t& target() const;
@@ -81,6 +82,7 @@ struct Problem {
void configValidation(const ConfigValidationsPtr_t& cv);
void clearConfigValidations();
void pathValidation(const PathValidationPtr_t& pv);
+ void pyPathValidation(const PyWPathValidationPtr_t& pv);
void pathProjector(const PathProjectorPtr_t& pp);
void distance(const DistancePtr_t& d);
void target(const ProblemTargetPtr_t& t);
@@ -146,6 +148,7 @@ struct Problem {
boost::python::tuple directPath(ConfigurationIn_t start,
ConfigurationIn_t end, bool validate);
hpp::core::ConstraintSetPtr_t constraints_;
+ PyWPathValidationPtr_t pathValidation_;
value_type errorThreshold_;
size_type maxIterProjection_;
};
diff --git a/src/pyhpp/core/path-planner.cc b/src/pyhpp/core/path-planner.cc
index 32526847..b37f6796 100644
--- a/src/pyhpp/core/path-planner.cc
+++ b/src/pyhpp/core/path-planner.cc
@@ -55,27 +55,70 @@ using namespace boost::python;
namespace pathPlanner {
-#define DEFINE_PLANNER_WRAPPER(WrapperName, PlannerType, PlannerPtr) \
- struct WrapperName : public pyhpp::core::PathPlanner { \
- WrapperName(const pyhpp::core::Problem& problem) { \
- obj = PlannerType::create(problem.obj); \
- } \
- }; \
- void expose##WrapperName() { \
- class_>( \
- #WrapperName, init()); \
+#define DEFINE_PLANNER_WRAPPER(WrapperName, PlannerType) \
+ struct WrapperName : public pyhpp::core::PathPlanner { \
+ WrapperName(const pyhpp::core::Problem& problem) { \
+ obj = PlannerType::create(problem.obj); \
+ } \
}
-DEFINE_PLANNER_WRAPPER(DiffusingPlanner, hpp::core::DiffusingPlanner,
- hpp::core::DiffusingPlannerPtr_t)
-DEFINE_PLANNER_WRAPPER(BiRRTPlanner, hpp::core::BiRRTPlanner,
- hpp::core::BiRRTPlannerPtr_t)
-DEFINE_PLANNER_WRAPPER(VisibilityPrmPlanner, hpp::core::VisibilityPrmPlanner,
- hpp::core::VisibilityPrmPlannerPtr_t)
-DEFINE_PLANNER_WRAPPER(BiRrtStar, hpp::core::pathPlanner::BiRrtStar,
- hpp::core::pathPlanner::BiRrtStarPtr_t)
-DEFINE_PLANNER_WRAPPER(kPrmStar, hpp::core::pathPlanner::kPrmStar,
- hpp::core::pathPlanner::kPrmStarPtr_t)
+DEFINE_PLANNER_WRAPPER(DiffusingPlanner, hpp::core::DiffusingPlanner);
+DEFINE_PLANNER_WRAPPER(BiRRTPlanner, hpp::core::BiRRTPlanner);
+DEFINE_PLANNER_WRAPPER(VisibilityPrmPlanner, hpp::core::VisibilityPrmPlanner);
+DEFINE_PLANNER_WRAPPER(BiRrtStar, hpp::core::pathPlanner::BiRrtStar);
+DEFINE_PLANNER_WRAPPER(kPrmStar, hpp::core::pathPlanner::kPrmStar);
+
+// DocClass(DiffusingPlanner)
+void exposeDiffusingPlanner() {
+ class_>(
+ "DiffusingPlanner", DocClassDoc(), init())
+ .def("startSolve", &pyhpp::core::PathPlanner::startSolve,
+ DocClassMethod(startSolve))
+ .def("oneStep", &pyhpp::core::PathPlanner::oneStep,
+ DocClassMethod(oneStep));
+}
+
+// DocClass(BiRRTPlanner)
+void exposeBiRRTPlanner() {
+ class_>(
+ "BiRRTPlanner", DocClassDoc(), init())
+ .def("startSolve", &pyhpp::core::PathPlanner::startSolve,
+ DocClassMethod(startSolve))
+ .def("oneStep", &pyhpp::core::PathPlanner::oneStep,
+ DocClassMethod(oneStep));
+}
+
+// DocClass(VisibilityPrmPlanner)
+void exposeVisibilityPrmPlanner() {
+ class_>(
+ "VisibilityPrmPlanner", DocClassDoc(),
+ init())
+ .def("oneStep", &pyhpp::core::PathPlanner::oneStep,
+ DocClassMethod(oneStep));
+}
+
+// DocClass(pathPlanner::BiRrtStar)
+void exposeBiRrtStar() {
+ class_>(
+ "BiRrtStar", DocClassDoc(), init())
+ .def("startSolve", &pyhpp::core::PathPlanner::startSolve,
+ DocClassMethod(startSolve))
+ .def("oneStep", &pyhpp::core::PathPlanner::oneStep,
+ DocClassMethod(oneStep));
+}
+
+// DocClass(pathPlanner::kPrmStar)
+void exposekPrmStar() {
+ class_>(
+ "kPrmStar", DocClassDoc(), init())
+ .def("startSolve", &pyhpp::core::PathPlanner::startSolve,
+ DocClassMethod(startSolve))
+ .def("tryConnectInitAndGoals",
+ &pyhpp::core::PathPlanner::tryConnectInitAndGoals,
+ DocClassMethod(tryConnectInitAndGoals))
+ .def("oneStep", &pyhpp::core::PathPlanner::oneStep,
+ DocClassMethod(oneStep));
+}
struct SearchInRoadmap : public pyhpp::core::PathPlanner {
SearchInRoadmap(const pyhpp::core::Problem& problem,
@@ -85,10 +128,16 @@ struct SearchInRoadmap : public pyhpp::core::PathPlanner {
}
};
+// DocClass(pathPlanner::SearchInRoadmap)
void exposeSearchInRoadmap() {
class_>(
- "SearchInRoadmap",
- init());
+ "SearchInRoadmap", DocClassDoc(),
+ init())
+ .def("tryConnectInitAndGoals",
+ &pyhpp::core::PathPlanner::tryConnectInitAndGoals,
+ DocClassMethod(tryConnectInitAndGoals))
+ .def("oneStep", &pyhpp::core::PathPlanner::oneStep,
+ DocClassMethod(oneStep));
}
struct PlanAndOptimize : public pyhpp::core::PathPlanner {
@@ -97,9 +146,16 @@ struct PlanAndOptimize : public pyhpp::core::PathPlanner {
}
};
+// DocClass(PlanAndOptimize)
void exposePlanAndOptimize() {
class_>(
- "PlanAndOptimize", init());
+ "PlanAndOptimize", DocClassDoc(), init())
+ .def("startSolve", &pyhpp::core::PathPlanner::startSolve,
+ DocClassMethod(startSolve))
+ .def("oneStep", &pyhpp::core::PathPlanner::oneStep,
+ DocClassMethod(oneStep))
+ .def("finishSolve", &pyhpp::core::PathPlanner::finishSolve,
+ DocClassMethod(finishSolve));
}
void exposePathPlanners() {
diff --git a/src/pyhpp/core/path-validation.cc b/src/pyhpp/core/path-validation.cc
index ab8f8647..6daf7dd8 100644
--- a/src/pyhpp/core/path-validation.cc
+++ b/src/pyhpp/core/path-validation.cc
@@ -1,6 +1,6 @@
//
-// Copyright (c) 2018 - 2023, CNRS
-// Authors: Joseph Mirabel, Florent Lamiraux
+// Copyright (c) 2018 - 2026, CNRS
+// Authors: Joseph Mirabel, Florent Lamiraux, Paul Sardin
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions
@@ -33,108 +33,163 @@
#include
#include
#include
-#include
#include
#include
#include
-#include
-#include
+#include
+#include
#include
// DocNamespace(hpp::core)
-using namespace boost::python;
-
namespace pyhpp {
namespace core {
-using namespace hpp::core;
+
+using namespace boost::python;
+
+namespace {
+
+hpp::core::PathValidationPtr_t createDiscretizedJointBoundAndCollisionChecking(
+ const hpp::core::DevicePtr_t& robot,
+ const hpp::core::value_type& stepSize) {
+ return hpp::core::pathValidation::Discretized::create(
+ stepSize, {hpp::core::JointBoundValidation::create(robot),
+ hpp::core::CollisionValidation::create(robot)});
+}
+
+const hpp::core::PathValidationBuilder_t& noValidationFactory() {
+ static const hpp::core::PathValidationBuilder_t factory =
+ hpp::core::ProblemSolver::create()->pathValidations.get("NoValidation");
+ return factory;
+}
struct PVWrapper {
- static bool validate(PathValidation* pv, const PathPtr_t path, bool reverse,
- PathPtr_t& validPart,
- PathValidationReportPtr_t& report) {
- return pv->validate(path, reverse, validPart, report);
+ static bool validate(PathValidation* pv, const hpp::core::PathPtr_t path,
+ bool reverse, hpp::core::PathPtr_t& validPart,
+ hpp::core::PathValidationReportPtr_t& report) {
+ return pv->obj->validate(path, reverse, validPart, report);
}
- static tuple py_validate(PathValidation* pv, const PathPtr_t path,
- bool reverse = false) {
- PathPtr_t validPart;
- PathValidationReportPtr_t report;
- bool res = pv->validate(path, reverse, validPart, report);
- return boost::python::make_tuple(res, validPart, report);
- }
- static tuple validateConfiguration(PathValidation* pv, ConfigurationIn_t q) {
- ValidationReportPtr_t report;
- bool res = pv->validate(q, report);
- return boost::python::make_tuple(res, report);
+ static tuple pyValidate(PathValidation* pv, const hpp::core::PathPtr_t path,
+ bool reverse = false) {
+ hpp::core::PathPtr_t validPart;
+ hpp::core::PathValidationReportPtr_t report;
+ bool result = pv->obj->validate(path, reverse, validPart, report);
+ return boost::python::make_tuple(result, validPart, report);
}
- static pathValidation::DiscretizedPtr_t
- createDiscretizedJointBoundAndCollisionChecking(const DevicePtr_t& robot,
- const value_type& stepSize) {
- using namespace pathValidation;
- return Discretized::create(stepSize,
- {
- JointBoundValidation::create(robot),
- CollisionValidation::create(robot),
- });
+ static tuple validateConfiguration(PathValidation* pv,
+ hpp::core::ConfigurationIn_t q) {
+ hpp::core::ValidationReportPtr_t report;
+ bool result = pv->obj->validate(q, report);
+ return boost::python::make_tuple(result, report);
}
};
+namespace pathValidation {
+
+struct NoValidation : PathValidation {
+ NoValidation(const hpp::core::DevicePtr_t& robot,
+ const hpp::core::value_type& tolerance)
+ : PathValidation(robot, noValidationFactory(), tolerance) {}
+};
+
+struct Discretized : PathValidation {
+ Discretized(const hpp::core::DevicePtr_t& robot,
+ const hpp::core::value_type& stepSize)
+ : PathValidation(
+ robot,
+ hpp::core::pathValidation::createDiscretizedCollisionChecking,
+ stepSize) {}
+};
+
+struct DiscretizedCollision : PathValidation {
+ DiscretizedCollision(const hpp::core::DevicePtr_t& robot,
+ const hpp::core::value_type& stepSize)
+ : PathValidation(
+ robot,
+ hpp::core::pathValidation::createDiscretizedCollisionChecking,
+ stepSize) {}
+};
+
+struct DiscretizedJointBound : PathValidation {
+ DiscretizedJointBound(const hpp::core::DevicePtr_t& robot,
+ const hpp::core::value_type& stepSize)
+ : PathValidation(robot,
+ hpp::core::pathValidation::createDiscretizedJointBound,
+ stepSize) {}
+};
+
+struct DiscretizedCollisionAndJointBound : PathValidation {
+ DiscretizedCollisionAndJointBound(const hpp::core::DevicePtr_t& robot,
+ const hpp::core::value_type& stepSize)
+ : PathValidation(robot, createDiscretizedJointBoundAndCollisionChecking,
+ stepSize) {}
+};
+
+struct Progressive : PathValidation {
+ Progressive(const hpp::core::DevicePtr_t& robot,
+ const hpp::core::value_type& tolerance)
+ : PathValidation(robot,
+ hpp::core::continuousValidation::Progressive::create,
+ tolerance) {}
+};
+
+struct Dichotomy : PathValidation {
+ Dichotomy(const hpp::core::DevicePtr_t& robot,
+ const hpp::core::value_type& tolerance)
+ : PathValidation(robot,
+ hpp::core::continuousValidation::Dichotomy::create,
+ tolerance) {}
+};
+
+} // namespace pathValidation
+} // namespace
+
void exposePathValidation() {
+ register_ptr_to_python();
+
// DocClass(PathValidation)
- class_(
+ class_(
"PathValidation", DocClassDoc(), no_init)
.def("validate", &PVWrapper::validate, DocClassMethod(validate))
- .def("validate", &PVWrapper::py_validate,
+ .def("validate", &PVWrapper::pyValidate,
"Validate path; returns (valid, validPart, report).")
.def("validateConfiguration", &PVWrapper::validateConfiguration,
"Validate a configuration; returns (valid, report).");
- class_,
- hpp::core::pathValidation::DiscretizedPtr_t, boost::noncopyable>(
- "Discretized", DocClassDoc(), no_init)
- .def("__init__",
- make_constructor(
- +[](const DevicePtr_t& robot, const value_type& stepSize) {
- return pathValidation::createDiscretizedCollisionChecking(
- robot, stepSize);
- },
- default_call_policies(), (arg("robot"), arg("stepSize"))),
- "Create a discretized collision-checking path validation.");
-
- hpp::core::continuousValidation::ProgressivePtr_t (*ProgressiveConstructor)(
- const DevicePtr_t&, const value_type&) =
- &continuousValidation::Progressive::create;
- class_,
- hpp::core::continuousValidation::ProgressivePtr_t, boost::noncopyable>(
- "Progressive", DocClassDoc(), no_init)
- .def("__init__",
- make_constructor(ProgressiveConstructor, default_call_policies(),
- (arg("robot"), arg("tolerance"))),
- "Create a progressive continuous path validation.");
-
- hpp::core::continuousValidation::DichotomyPtr_t (*DichotomyConstructor)(
- const DevicePtr_t&, const value_type&) =
- &continuousValidation::Dichotomy::create;
- class_,
- hpp::core::continuousValidation::DichotomyPtr_t, boost::noncopyable>(
- "Dichotomy", DocClassDoc(), no_init)
- .def("__init__",
- make_constructor(DichotomyConstructor, default_call_policies(),
- (arg("robot"), arg("tolerance"))),
- "Create a dichotomy-based continuous path validation.");
-
- def("DiscretizedCollision",
- &pathValidation::createDiscretizedCollisionChecking,
- (arg("robot"), arg("stepSize")),
- "Create a discretized collision-checking path validation.");
- def("DiscretizedJointBound", &pathValidation::createDiscretizedJointBound,
- (arg("robot"), arg("stepSize")),
- "Create a discretized joint-bound path validation.");
- def("DiscretizedCollisionAndJointBound",
- &PVWrapper::createDiscretizedJointBoundAndCollisionChecking,
- (arg("robot"), arg("stepSize")),
+ class_>(
+ "NoValidation", "Create a path validation that accepts every path.",
+ init(
+ (arg("robot"), arg("tolerance"))));
+ class_>(
+ "Discretized", "Create a discretized collision-checking path validation.",
+ init(
+ (arg("robot"), arg("stepSize"))));
+ class_>(
+ "DiscretizedCollision",
+ "Create a discretized collision-checking path validation.",
+ init(
+ (arg("robot"), arg("stepSize"))));
+ class_>(
+ "DiscretizedJointBound",
+ "Create a discretized joint-bound path validation.",
+ init(
+ (arg("robot"), arg("stepSize"))));
+ class_>(
+ "DiscretizedCollisionAndJointBound",
"Create a discretized path validation checking both collision and joint "
- "bounds.");
+ "bounds.",
+ init(
+ (arg("robot"), arg("stepSize"))));
+ class_>(
+ "Progressive", "Create a progressive continuous path validation.",
+ init(
+ (arg("robot"), arg("tolerance"))));
+ class_>(
+ "Dichotomy", "Create a dichotomy-based continuous path validation.",
+ init(
+ (arg("robot"), arg("tolerance"))));
}
+
} // namespace core
} // namespace pyhpp
diff --git a/src/pyhpp/core/problem.cc b/src/pyhpp/core/problem.cc
index 474b4aaa..494175b1 100644
--- a/src/pyhpp/core/problem.cc
+++ b/src/pyhpp/core/problem.cc
@@ -51,6 +51,7 @@
#include
#include
#include
+#include
#include
namespace {
@@ -171,6 +172,12 @@ PathValidationPtr_t Problem::pathValidation() const {
return obj->pathValidation();
}
+PyWPathValidationPtr_t Problem::pyPathValidation() const {
+ if (pathValidation_ && pathValidation_->obj == obj->pathValidation())
+ return pathValidation_;
+ return std::make_shared(obj->pathValidation());
+}
+
PathProjectorPtr_t Problem::pathProjector() const {
return obj->pathProjector();
}
@@ -193,6 +200,12 @@ void Problem::configValidation(const ConfigValidationsPtr_t& cv) {
void Problem::pathValidation(const PathValidationPtr_t& pv) {
obj->pathValidation(pv);
+ pathValidation_.reset();
+}
+
+void Problem::pyPathValidation(const PyWPathValidationPtr_t& pv) {
+ obj->pathValidation(pv->obj);
+ pathValidation_ = pv;
}
void Problem::pathProjector(const PathProjectorPtr_t& pp) {
@@ -586,8 +599,8 @@ typedef void (Problem::*SetSteeringMethod)(const PyWSteeringMethodPtr_t&);
typedef const ConfigValidationsPtr_t& (Problem::*GetConfigValidation)() const;
typedef void (Problem::*SetConfigValidation)(const ConfigValidationsPtr_t&);
-typedef PathValidationPtr_t (Problem::*GetPathValidation)() const;
-typedef void (Problem::*SetPathValidation)(const PathValidationPtr_t&);
+typedef PyWPathValidationPtr_t (Problem::*GetPathValidation)() const;
+typedef void (Problem::*SetPathValidation)(const PyWPathValidationPtr_t&);
typedef PathProjectorPtr_t (Problem::*GetPathProjector)() const;
typedef void (Problem::*SetPathProjector)(const PathProjectorPtr_t&);
@@ -692,10 +705,10 @@ void exposeProblem() {
DocClassMethod(clearConfigValidations))
.def("pathValidation",
- static_cast(&Problem::pathValidation),
+ static_cast(&Problem::pyPathValidation),
DOC_P_PV_GET)
.def("pathValidation",
- static_cast(&Problem::pathValidation),
+ static_cast(&Problem::pyPathValidation),
(arg("pathValidation")), DOC_P_PV_SET)
.def("pathProjector",
diff --git a/src/pyhpp/manipulation/graph.cc b/src/pyhpp/manipulation/graph.cc
index 37ad8cfe..a123c68c 100644
--- a/src/pyhpp/manipulation/graph.cc
+++ b/src/pyhpp/manipulation/graph.cc
@@ -44,6 +44,7 @@
#include
#include "hpp/manipulation/constraint-set.hh"
+#include "pyhpp/core/path-validation.hh"
#include "pyhpp/core/problem.hh"
// DocNamespace(hpp::manipulation::graph)
@@ -952,6 +953,10 @@ PathValidationPtr_t PyWEdge::pathValidation() const {
return obj->pathValidation();
}
+pyhpp::core::PyWPathValidationPtr_t PyWEdge::pyPathValidation() const {
+ return std::make_shared(obj->pathValidation());
+}
+
// =============================================================================
// Subgraph management
// =============================================================================
@@ -1305,7 +1310,7 @@ void exposeGraph() {
.def("nbWaypoints", &PyWEdge::nbWaypoints, DocClassMethod(nbWaypoints))
.def("waypoint", &PyWEdge::waypoint, DocClassMethod(waypoint))
.def("nbWaypoints", &PyWEdge::nbWaypoints, DocClassMethod(nbWaypoints))
- .def("pathValidation", &PyWEdge::pathValidation,
+ .def("pathValidation", &PyWEdge::pyPathValidation,
DocClassMethod(pathValidation));
// DocClass(Graph)
diff --git a/src/pyhpp/manipulation/graph.hh b/src/pyhpp/manipulation/graph.hh
index 47779f9c..f7d62219 100644
--- a/src/pyhpp/manipulation/graph.hh
+++ b/src/pyhpp/manipulation/graph.hh
@@ -32,6 +32,7 @@
#include
#include
+#include
#include
namespace pyhpp {
@@ -76,6 +77,7 @@ struct PyWEdge {
std::size_t weight() const;
PyWEdge waypoint(int index) const;
PathValidationPtr_t pathValidation() const;
+ pyhpp::core::PyWPathValidationPtr_t pyPathValidation() const;
};
typedef std::shared_ptr PyWEdgePtr_t;
diff --git a/src/pyhpp/manipulation/path-planner.cc b/src/pyhpp/manipulation/path-planner.cc
index d4c2d522..842b8b30 100644
--- a/src/pyhpp/manipulation/path-planner.cc
+++ b/src/pyhpp/manipulation/path-planner.cc
@@ -56,6 +56,21 @@ const char* DOC_CHECKFEASIBILITYONLY =
"If enabled, only add one solution to the roadmap. "
"Otherwise add all solutions.";
+template
+void plannerStartSolve(Planner& planner) {
+ planner.obj->startSolve();
+}
+
+template
+void plannerTryConnectInitAndGoals(Planner& planner) {
+ planner.obj->tryConnectInitAndGoals();
+}
+
+template
+void plannerOneStep(Planner& planner) {
+ planner.obj->oneStep();
+}
+
} // namespace
namespace pyhpp {
@@ -322,6 +337,10 @@ void exposePathPlanners() {
boost::python::bases>(
"TransitionPlanner", DocClassDoc(),
boost::python::init())
+ .def("startSolve", &plannerStartSolve,
+ DocClassMethod(startSolve))
+ .def("oneStep", &plannerOneStep,
+ DocClassMethod(oneStep))
.def("innerPlanner",
static_cast(
&TransitionPlanner::innerPlanner),
@@ -360,14 +379,28 @@ void exposePathPlanners() {
.def("addPathOptimizer", &TransitionPlanner::addPathOptimizer,
DocClassMethod(addPathOptimizer));
+ // DocNamespace(hpp::manipulation)
+ // DocClass(ManipulationPlanner)
boost::python::class_>(
- "ManipulationPlanner",
- boost::python::init());
+ "ManipulationPlanner", DocClassDoc(),
+ boost::python::init())
+ .def("oneStep", &plannerOneStep,
+ DocClassMethod(oneStep));
+ // DocNamespace(hpp::manipulation::pathPlanner)
+ // DocClass(StatesPathFinder)
boost::python::class_>(
- "StatesPathFinder", boost::python::init());
+ "StatesPathFinder", DocClassDoc(),
+ boost::python::init())
+ .def("startSolve", &plannerStartSolve,
+ DocClassMethod(startSolve))
+ .def("tryConnectInitAndGoals",
+ &plannerTryConnectInitAndGoals,
+ DocClassMethod(tryConnectInitAndGoals))
+ .def("oneStep", &plannerOneStep,
+ DocClassMethod(oneStep));
// DocClass(EndEffectorTrajectory)
boost::python::class_())
.def(boost::python::init())
+ .def("startSolve", &plannerStartSolve,
+ DocClassMethod(startSolve))
+ .def("tryConnectInitAndGoals",
+ &plannerTryConnectInitAndGoals,
+ DocClassMethod(tryConnectInitAndGoals))
+ .def("oneStep", &plannerOneStep,
+ DocClassMethod(oneStep))
.def("nRandomConfig",
static_cast(
&EndEffectorTrajectory::nRandomConfig),
diff --git a/src/pyhpp/manipulation/problem.cc b/src/pyhpp/manipulation/problem.cc
index 57856cff..d818f291 100644
--- a/src/pyhpp/manipulation/problem.cc
+++ b/src/pyhpp/manipulation/problem.cc
@@ -33,11 +33,14 @@
#include <../src/pyhpp/manipulation/steering-method.hh>
#include
#include
+#include
#include
#include
#include
+#include
#include
#include
+#include
#include
// DocNamespace(hpp::manipulation)
@@ -54,6 +57,39 @@ using namespace boost::python;
namespace pyhpp {
namespace manipulation {
+namespace {
+
+struct GraphPathValidation : pyhpp::core::PathValidation {
+ GraphPathValidation(const pyhpp::core::PyWPathValidationPtr_t& pathValidation)
+ : GraphPathValidation(check(pathValidation), 0) {}
+
+ private:
+ GraphPathValidation(const pyhpp::core::PyWPathValidationPtr_t& pathValidation,
+ int)
+ : pyhpp::core::PathValidation(
+ hpp::manipulation::GraphPathValidation::create(pathValidation->obj),
+ graphFactory(pathValidation->factory), pathValidation->tolerance) {}
+
+ static pyhpp::core::PyWPathValidationPtr_t check(
+ const pyhpp::core::PyWPathValidationPtr_t& pathValidation) {
+ if (!pathValidation || !pathValidation->factory)
+ throw std::invalid_argument("Path validation has no factory.");
+ return pathValidation;
+ }
+
+ static hpp::core::PathValidationBuilder_t graphFactory(
+ const hpp::core::PathValidationBuilder_t& innerFactory) {
+ return [innerFactory](const hpp::core::DevicePtr_t& robot,
+ const hpp::core::value_type& tolerance)
+ -> hpp::core::PathValidationPtr_t {
+ return hpp::manipulation::GraphPathValidation::create(
+ innerFactory(robot, tolerance));
+ };
+ }
+};
+
+} // namespace
+
Problem::Problem(const PyWDevicePtr_t& robot)
: pyhpp::core::Problem(
hpp::manipulation::Problem::create(robot->asManipulationDevice())) {}
@@ -110,6 +146,38 @@ pyhpp::core::PyWSteeringMethodPtr_t Problem::steeringMethod() const {
return std::shared_ptr(sm);
}
+pyhpp::core::PyWPathValidationPtr_t Problem::pyPathValidation() const {
+ if (pathValidation_ && pathValidation_->obj == obj->pathValidation())
+ return pathValidation_;
+ return std::make_shared(obj->pathValidation());
+}
+
+void Problem::pyPathValidation(
+ const pyhpp::core::PyWPathValidationPtr_t& pathValidation) {
+ hpp::manipulation::ProblemPtr_t problem = asManipulationProblem();
+ auto obstacleUser = HPP_DYNAMIC_PTR_CAST(hpp::core::ObstacleUserInterface,
+ pathValidation->obj);
+ if (obstacleUser)
+ for (const auto& obstacle : problem->collisionObstacles())
+ obstacleUser->addObstacle(obstacle);
+
+ problem->pathValidation(pathValidation->obj);
+ pathValidation_ = pathValidation;
+}
+
+pyhpp::core::PyWPathValidationPtr_t Problem::pyPathValidationFactory() const {
+ return std::make_shared(
+ asManipulationProblem()->pathValidationFactory());
+}
+
+void Problem::pyPathValidationFactory(
+ const pyhpp::core::PyWPathValidationPtr_t& pathValidation) {
+ if (!pathValidation || !pathValidation->factory)
+ throw std::invalid_argument("Path validation has no factory.");
+ asManipulationProblem()->setPathValidationFactory(pathValidation->factory,
+ pathValidation->tolerance);
+}
+
// PathValidationPtr_t Problem::pathValidation() const {
// return obj->pathValidation();
// }
@@ -145,6 +213,10 @@ pyhpp::core::PyWSteeringMethodPtr_t Problem::steeringMethod() const {
// }
void exposeProblem() {
+ class_>(
+ "GraphPathValidation", init(
+ (arg("pathValidation"))));
+
// DocClass(Problem)
class_>("Problem", DocClassDoc(),
init())
@@ -173,6 +245,26 @@ void exposeProblem() {
"Set the problem steering method directly. Unlike steeringMethod, "
"this does not wrap the given steering method in a manipulation "
"graph steering method.")
+ .def(
+ "pathValidation",
+ static_cast(
+ &Problem::pyPathValidation),
+ "Get the path validation object.")
+ .def("pathValidation",
+ static_cast(
+ &Problem::pyPathValidation),
+ (arg("pathValidation")), "Set the path validation object.")
+ .def(
+ "pathValidationFactory",
+ static_cast(
+ &Problem::pyPathValidationFactory),
+ "Create a path validation using the edge-validation factory.")
+ .def("pathValidationFactory",
+ static_cast(
+ &Problem::pyPathValidationFactory),
+ (arg("pathValidation")), "Set the edge-validation factory.")
// .PYHPP_DEFINE_GETTER_SETTER_CONST_REF(Problem, pathValidation,
// PathValidationPtr_t) .PYHPP_DEFINE_METHOD(Problem,
// manipulationSteeringMethod) .PYHPP_DEFINE_METHOD(Problem,
diff --git a/src/pyhpp/manipulation/problem.hh b/src/pyhpp/manipulation/problem.hh
index d2fbf0bc..db2775e1 100644
--- a/src/pyhpp/manipulation/problem.hh
+++ b/src/pyhpp/manipulation/problem.hh
@@ -62,6 +62,12 @@ struct Problem : public pyhpp::core::Problem {
pyhpp::core::PyWSteeringMethodPtr_t steeringMethod() const;
void fullSteeringMethod(
const pyhpp::core::PyWSteeringMethodPtr_t& steeringMethod);
+ pyhpp::core::PyWPathValidationPtr_t pyPathValidation() const;
+ void pyPathValidation(
+ const pyhpp::core::PyWPathValidationPtr_t& pathValidation);
+ pyhpp::core::PyWPathValidationPtr_t pyPathValidationFactory() const;
+ void pyPathValidationFactory(
+ const pyhpp::core::PyWPathValidationPtr_t& pathValidation);
// PathValidationPtr_t pathValidation() const;
// void pathValidation (const PathValidationPtr_t &pathValidation);
// SteeringMethodPtr_t manipulationSteeringMethod() const;
diff --git a/tests/integration/construction-set-m-rrt.py b/tests/integration/construction-set-m-rrt.py
index e3c64346..91629a60 100644
--- a/tests/integration/construction-set-m-rrt.py
+++ b/tests/integration/construction-set-m-rrt.py
@@ -408,6 +408,7 @@ def forbidExcept(g: str, h: T.List[str]) -> T.List[Rule]:
problem.steeringMethod(Straight(problem))
problem.pathValidation(Progressive(robot, 0.02))
+ problem.pathValidationFactory(Progressive(robot, 0.02))
problem.pathProjector(
ProgressiveProjector(problem.distance(), problem.steeringMethod(), 0.05)
)
@@ -447,6 +448,7 @@ def forbidExcept(g: str, h: T.List[str]) -> T.List[Rule]:
problem.steeringMethod(Straight(problem))
problem.pathValidation(Progressive(robot, 0.02))
+ problem.pathValidationFactory(Progressive(robot, 0.02))
problem.pathProjector(
ProgressiveProjector(problem.distance(), problem.steeringMethod(), 0.05)
)
diff --git a/tests/integration/ur3-spheres-spf.py b/tests/integration/ur3-spheres-spf.py
index d49818d9..20b7619d 100644
--- a/tests/integration/ur3-spheres-spf.py
+++ b/tests/integration/ur3-spheres-spf.py
@@ -281,6 +281,7 @@
problem.steeringMethod(Straight(problem))
problem.pathValidation(Dichotomy(robot, 0))
+problem.pathValidationFactory(Dichotomy(robot, 0))
# need to set path projector due to implicit constraints added above
problem.pathProjector(
diff --git a/tests/integration/ur3-spheres.py b/tests/integration/ur3-spheres.py
index 0b7c3bdf..5db12b04 100644
--- a/tests/integration/ur3-spheres.py
+++ b/tests/integration/ur3-spheres.py
@@ -260,6 +260,7 @@
problem.steeringMethod(Straight(problem))
problem.pathValidation(Dichotomy(robot, 0))
+problem.pathValidationFactory(Dichotomy(robot, 0))
problem.pathProjector(
ProgressiveProjector(problem.distance(), problem.steeringMethod(), 0.01)
)
diff --git a/tests/unit/test_doxygen_xml_parser.py b/tests/unit/test_doxygen_xml_parser.py
new file mode 100644
index 00000000..91edced4
--- /dev/null
+++ b/tests/unit/test_doxygen_xml_parser.py
@@ -0,0 +1,63 @@
+#!/usr/bin/env python3
+#
+# Copyright (c) 2026 CNRS
+# Author: Paul Sardin
+#
+
+import tempfile
+import unittest
+from pathlib import Path
+
+from doc.doxygen_xml_parser import ClassDoc
+
+
+DOXYGEN_XML = """\
+
+
+ hpp::core::Planner
+
+ Class brief .
+
+
+ Class details .
+
+
+
+ startSolve
+
+ Start solving .
+
+
+ Use the child planner .
+
+
+
+
+
+"""
+
+
+class TestDoxygenXmlParser(unittest.TestCase):
+ def setUp(self):
+ self.temporary_directory = tempfile.TemporaryDirectory()
+ filename = Path(self.temporary_directory.name) / "planner.xml"
+ filename.write_text(DOXYGEN_XML)
+ self.class_doc = ClassDoc(filename)
+
+ def tearDown(self):
+ self.temporary_directory.cleanup()
+
+ def test_nested_class_description(self):
+ self.assertEqual(
+ self.class_doc.getClassDoc(), ("Class brief.", "Class details.")
+ )
+
+ def test_nested_method_description(self):
+ self.assertEqual(
+ self.class_doc.getClassMethodDoc("startSolve"),
+ ("Start solving.", "Use the child planner.", ["self"]),
+ )
+
+
+if __name__ == "__main__":
+ unittest.main()