diff --git a/python/helios/helios_python.cpp b/python/helios/helios_python.cpp index 6cbdf06fd..902e4288a 100755 --- a/python/helios/helios_python.cpp +++ b/python/helios/helios_python.cpp @@ -67,11 +67,13 @@ PYBIND11_MAKE_OPAQUE(std::vector); #include #include #include -#include +#include #include -#include -#include -#include +#include +#include +#include +#include +#include @@ -83,8 +85,7 @@ namespace pyhelios{ py::bind_vector>(m, "StringVector"); py::bind_vector>(m, "MeasurementVector"); py::bind_vector>(m, "TrajectoryVector"); - py::bind_vector>(m, "DoubleVector"); // CHECK THIS!!!! : you'll need to make sure that you correctly handle the vector's indexing and modification methods. - + py::bind_vector>(m, "DoubleVector"); py::implicitly_convertible(); logging::makeQuiet(); @@ -105,16 +106,19 @@ namespace pyhelios{ m.def("logging_time", &logging::makeTime, "Set the logging verbosity level to time"); m.def("default_rand_generator_seed", &setDefaultRandomnessGeneratorSeed, "Set the seed for the default randomness generator"); - - py::class_ aabb(m, "AABB"); + + py::class_> aabb(m, "AABB"); aabb - .def_static("create", []() { return std::make_unique(); }, py::return_value_policy::take_ownership) + .def_static("create", []() { return std::make_shared(); }, py::return_value_policy::take_ownership) + .def_property_readonly("min_vertex", [](AABB &aabb) { return &(aabb.vertices[0]); }, py::return_value_policy::reference) .def_property_readonly("max_vertex", [](AABB &aabb) { return &(aabb.vertices[1]); }, py::return_value_policy::reference) .def("__str__", &AABB::toString); - py::class_> abstract_beam_deflector(m, "AbstractBeamDeflector"); + py::class_> abstract_beam_deflector(m, "AbstractBeamDeflector"); abstract_beam_deflector + .def(py::init(), + py::arg("scanAngleMax_rad"), py::arg("scanFreqMax_Hz"), py::arg("scanFreqMin_Hz")) .def_readwrite("scan_freq_max",&AbstractBeamDeflector::cfg_device_scanFreqMax_Hz) .def_readwrite("scan_freq_min",&AbstractBeamDeflector::cfg_device_scanFreqMin_Hz) .def_readwrite("scan_angle_max",&AbstractBeamDeflector::cfg_device_scanAngleMax_rad) @@ -129,8 +133,9 @@ namespace pyhelios{ &AbstractBeamDeflector::getEmitterRelativeAttitudeByReference) .def_property_readonly("optics_type", &AbstractBeamDeflector::getOpticsType); - py::class_ primitive(m, "Primitive"); + py::class_> primitive(m, "Primitive"); primitive + .def(py::init<>()) .def_property_readonly("scene_part", [](Primitive &prim) { return prim.part.get(); }) .def_property_readonly("material", [](Primitive &prim) { @@ -169,18 +174,18 @@ namespace pyhelios{ }); - py::class_ detailed_voxel(m, "DetailedVoxel", py::base()); + py::class_, Primitive> detailed_voxel(m, "DetailedVoxel"); detailed_voxel .def(py::init<>()) - .def(py::init, std::vector>(), - py::arg("x"), py::arg("y"), py::arg("z"), py::arg("halfVoxelSize"), py::arg("intValues"), py::arg("doubleValues")) + .def(py::init, std::vector>(), + py::arg("center"), py::arg("VoxelSize"), py::arg("intValues"), py::arg("doubleValues")) .def_property("nb_echos", &DetailedVoxel::getNbEchos, &DetailedVoxel::setNbEchos) .def_property("nb_sampling", &DetailedVoxel::getNbSampling, &DetailedVoxel::setNbSampling) .def_property_readonly("number_of_double_values", &DetailedVoxel::getNumberOfDoubleValues) - .def_property("maxPad", &DetailedVoxel::getMaxPad, &DetailedVoxel::setMaxPad) - .def("doubleValue", &DetailedVoxel::getDoubleValue, "Get the value at index") - .def("doubleValue", &DetailedVoxel::setDoubleValue, "Set the value at index", py::arg("index"), py::arg("value")); + .def_property("max_pad", &DetailedVoxel::getMaxPad, &DetailedVoxel::setMaxPad) + .def("get_double_value", &DetailedVoxel::getDoubleValue, "Get the value at index") + .def("set_double_value", &DetailedVoxel::setDoubleValue, "Set the value at index", py::arg("index"), py::arg("value")); py::class_> abstract_detector(m, "AbstractDetector"); @@ -189,14 +194,12 @@ namespace pyhelios{ std::shared_ptr, double, double, - double, - std::shared_ptr> + double >(), py::arg("scanner"), py::arg("accuracy_m"), py::arg("rangeMin_m"), - py::arg("rangeMax_m") = std::numeric_limits::max(), - py::arg("errorDistanceExpr") = nullptr) + py::arg("rangeMax_m") = std::numeric_limits::max()) .def_readwrite("accuracy", &AbstractDetector::cfg_device_accuracy_m) .def_readwrite("range_min", &AbstractDetector::cfg_device_rangeMin_m) @@ -207,7 +210,7 @@ namespace pyhelios{ [](AbstractDetector &self, double lasScale) { self.getFMS()->write.setMeasurementWriterLasScale(lasScale);}); - py::class_ triangle(m, "Triangle", py::base()); + py::class_, Primitive> triangle(m, "Triangle"); triangle .def(py::init(), py::arg("v0"), py::arg("v1"), py::arg("v2")) .def("__str__", &Triangle::toString) @@ -229,9 +232,7 @@ namespace pyhelios{ .def(py::init<>()) .def(py::init()) .def_readwrite("gps_time", &Trajectory::gpsTime) - .def_property("position", - [](const Trajectory &t) { return t.position; }, - [](Trajectory &t, const glm::dvec3 &pos) { t.position = pos; }) + .def_readwrite("position", &Trajectory::position) .def_readwrite("roll", &Trajectory::roll) .def_readwrite("pitch", &Trajectory::pitch) .def_readwrite("yaw", &Trajectory::yaw); @@ -462,6 +463,8 @@ namespace pyhelios{ py::class_> survey(m, "Survey", py::module_local()); survey .def(py::init<>()) + + .def_readwrite("scanner", &Survey::scanner) .def("calculate_length", &Survey::calculateLength) .def_property_readonly("length", &Survey::getLength) .def_property("name", @@ -489,7 +492,7 @@ namespace pyhelios{ .def_property("strip", &Leg::getStrip, &Leg::setStrip) .def("belongs_to_strip", &Leg::isContainedInAStrip); - py::class_ scene_part(m, "ScenePart"); + py::class_> scene_part(m, "ScenePart"); scene_part .def(py::init<>()) .def(py::init(), @@ -534,7 +537,7 @@ namespace pyhelios{ } }) - + .def_property("primitives", &ScenePart::getPrimitives, &ScenePart::setPrimitives) .def("primitive", [](ScenePart& self, size_t index) -> Primitive* { if (index < self.mPrimitives.size()) { return self.mPrimitives[index]; @@ -610,12 +613,10 @@ namespace pyhelios{ .def_readwrite("speed_m_s", &PlatformSettings::movePerSec_m) .def_property_readonly("base_template", &PlatformSettings::getTemplate, py::return_value_policy::reference) - .def_property_readonly("position", &PlatformSettings::getPosition) + .def_property("position", &PlatformSettings::getPosition, [](PlatformSettings& self, const glm::dvec3& pos) { self.setPosition(pos); }) //&PlatformSettings::setPosition) .def("cherryPick", &PlatformSettings::cherryPick, py::arg("cherries"), py::arg("fields"), py::arg("templateFields") = nullptr) - .def("set_position", py::overload_cast(&PlatformSettings::setPosition)) - .def("set_position", py::overload_cast(&PlatformSettings::setPosition)) .def("has_template", &PlatformSettings::hasTemplate) .def("to_string", &PlatformSettings::toString); @@ -624,12 +625,13 @@ namespace pyhelios{ .def(py::init<>()) .def(py::init(), py::arg("scene")) + .def_readwrite("scene_parts", &Scene::parts) .def_property("bbox", &Scene::getBBox, &Scene::setBBox) .def_property("bbox_crs", &Scene::getBBoxCRS, &Scene::setBBoxCRS) .def_property_readonly("num_primitives", [](const ScenePart& self) -> size_t { return self.mPrimitives.size();}) - .def_property_readonly("AABB", &Scene::getAABB, py::return_value_policy::reference) + .def_property_readonly("aabb", &Scene::getAABB, py::return_value_policy::reference) .def_property_readonly("shift", &Scene::getShift) .def_property("dyn_scene_step", @@ -720,8 +722,8 @@ namespace pyhelios{ .def_property_readonly("last_ground_check", [](const Platform &self) { return self.lastGroundCheck;}) - .def_property_readonly("position", &Platform::getPosition)//, &Platform::setPosition) - .def_property_readonly("attitude", &Platform::getAttitude)//, &Platform::setAttitude) + .def_property_readonly("position", &Platform::getPosition) + .def_property_readonly("attitude", &Platform::getAttitude) .def_property_readonly("absolute_mount_position", &Platform::getAbsoluteMountPosition) .def_property_readonly("absolute_mount_attitude", &Platform::getAbsoluteMountAttitude) @@ -791,7 +793,14 @@ namespace pyhelios{ py::class_> scanner(m, "Scanner"); scanner - .def(py::init<>()) + .def(py::init const&, bool, bool, bool, bool, bool>(), + py::arg("id"), + py::arg("pulseFreqs"), + py::arg("writeWaveform") = false, + py::arg("writePulse") = false, + py::arg("calcEchowidth") = false, + py::arg("fullWaveNoise") = false, + py::arg("platformNoiseDisabled") = false) .def(py::init(), py::arg("scanner")) .def("initialize_sequential_generators", &Scanner::initializeSequentialGenerators) @@ -950,21 +959,17 @@ namespace pyhelios{ py::overload_cast<>(&Scanner::getDeviceId, py::const_), py::overload_cast(&Scanner::setDeviceId)) - .def_property("scanner_head", - py::overload_cast<>(&Scanner::getScannerHead), - py::overload_cast>(&Scanner::setScannerHead)) + .def_property_readonly("scanner_head", + py::overload_cast<>(&Scanner::getScannerHead)) - .def_property("beam_deflector", - py::overload_cast<>(&Scanner::getBeamDeflector), - py::overload_cast>(&Scanner::setBeamDeflector)) + .def_property_readonly("beam_deflector", + py::overload_cast<>(&Scanner::getBeamDeflector)) - .def_property("detector", - py::overload_cast<>(&Scanner::getDetector), - py::overload_cast>(&Scanner::setDetector)) + .def_property_readonly("detector", + py::overload_cast<>(&Scanner::getDetector)) - .def_property("supported_pulse_freqs_hz", - py::overload_cast<>(&Scanner::getSupportedPulseFreqs_Hz), - py::overload_cast&>(&Scanner::setSupportedPulseFreqs_Hz)) + .def_property_readonly("supported_pulse_freqs_hz", + py::overload_cast<>(&Scanner::getSupportedPulseFreqs_Hz)) .def_property("num_time_bins", py::overload_cast<>(&Scanner::getNumTimeBins, py::const_), diff --git a/python/helios/scene.py b/python/helios/scene.py deleted file mode 100755 index 0b75b3704..000000000 --- a/python/helios/scene.py +++ /dev/null @@ -1,49 +0,0 @@ -# from typing import Optional, List -# from pydantic import BaseModel -# import numpy as np -# from helios.scenepart import ScenePart, ObjectType - - -# class Scene(BaseModel): -# kdgrove_factory: Optional[KDGroveFactory] -# kdgrove: Optional[KDGrove] -# bbox: Optional[AABB] -# bbox_crs: Optional[AABB] -# kd_raycaster: Optional[KDGroveRaycaster] -# primitives: List[Primitive] -# _scene_parts: List[ScenePart] -# material: Optional[Material] = None - - -# @property -# def scene_parts(self) -> List[ScenePart]: -# return self._scene_parts - -# @scene_parts.setter -# def scene_parts(self, scene_parts: List[ScenePart]): -# self._scene_parts = scene_parts - -# def has_moving_objects(self) -> bool: -# return any(scene_part.object_type == ObjectType.DYN_MOVING_OBJECT for scene_part in self._scene_parts) - -# def add_scene_part(self, scene_part: ScenePart): -# if scene_part not in self._scene_parts: -# self._scene_parts.append(scene_part) - -# def show(self, annotate_ids: bool = False, show_bboxes: bool = False, show_crs: bool = False, show_kd: bool = False, show_kd_raycaster: bool = False): -# # visualize scene prior to simulation (e.g. with open3D/PyVista) -# pass - -# def get_scene_part(self, scenepart_id: int) -> ScenePart: -# try: -# return self._scene_parts[scenepart_id] -# except IndexError: -# raise ValueError(f"No ScenePart with this id") - -# def delete_scene_part(self, scenepart_id: int)-> bool: -# if 0 <= scenepart_id < len(self._scene_parts): -# del self._scene_parts[scenepart_id] -# return True -# return False - - \ No newline at end of file diff --git a/python/pyhelios/leg.py b/python/pyhelios/leg.py new file mode 100644 index 000000000..e4e54048f --- /dev/null +++ b/python/pyhelios/leg.py @@ -0,0 +1,38 @@ +from pyhelios.utils import Validatable, ValidatedCppManagedProperty +from pyhelios.platforms import PlatformSettings +from pyhelios.scanner import ScannerSettings +from typing import Optional, List, Tuple, Annotated, Type, Any + +import _helios + + + +class ScanningStrip(Validatable): + def __init__(self, strip_id: Optional[str] = "", legs: Optional[List['Leg']] = None) -> None: + + self._cpp_object = _helios.ScanningStrip(strip_id) + self.strip_id = strip_id + + + strip_id: Optional[str] = ValidatedCppManagedProperty("strip_id") + +class Leg(Validatable): + def __init__(self, platform_settings: Optional[PlatformSettings] = None, scanner_settings: Optional[ScannerSettings] = None, + scanning_strip: Optional[ScanningStrip] = None, length: Optional[float] = 0.0, serial_id: Optional[int] = 0, belongs_to_strip: Optional[bool] = False) -> None: + + self._cpp_object = _helios.Leg() + self.platform_settings = platform_settings + self.scanner_settings = scanner_settings + self.strip = scanning_strip + + + self.length = length + self.serial_id = serial_id + + + platform_settings: Optional[PlatformSettings] = ValidatedCppManagedProperty("platform_settings") + scanner_settings: Optional[ScannerSettings] = ValidatedCppManagedProperty("scanner_settings") + scanning_strip: Optional[ScanningStrip] = ValidatedCppManagedProperty("scanning_strip") + length: Optional[float] = ValidatedCppManagedProperty("length") + serial_id: Optional[int] = ValidatedCppManagedProperty("serial_id") + diff --git a/python/pyhelios/platforms.py b/python/pyhelios/platforms.py new file mode 100644 index 000000000..56f7a29c9 --- /dev/null +++ b/python/pyhelios/platforms.py @@ -0,0 +1,67 @@ +from pyhelios.utils import Validatable, ValidatedCppManagedProperty +from pyhelios.primitives import Rotation, Primitive, AABB +from pyhelios.scene import ScenePart, Scene + +from typing import Optional, List, Tuple, Annotated, Type, Any +import _helios + + +class PlatformSettings(Validatable): + def __init__(self, id: Optional[str] = "", x: Optional[float] = 0.0, y: Optional[float] = 0.0, z: Optional[float] = 0.0, is_on_ground: Optional[bool] = False, + position: Optional[List[float]] = [0, 0, 0], is_yaw_angle_specified: Optional[bool] = False, yaw_angle: Optional[float] = 0.0, + is_stop_and_turn: Optional[bool] = True, is_smooth_turn: Optional[bool] = False, speed_m_s: Optional[float] = 70.0) -> None: + + self._cpp_object = _helios.PlatformSettings() + self.id = id + self.x = x + self.y = y + self.z = z + self.is_on_ground = is_on_ground + self.position = position + self.is_yaw_angle_specified = is_yaw_angle_specified + self.yaw_angle = yaw_angle + self.is_stop_and_turn = is_stop_and_turn + self.is_smooth_turn = is_smooth_turn + self.speed_m_s = speed_m_s + + id: Optional[str] = ValidatedCppManagedProperty("id") + x: Optional[float] = ValidatedCppManagedProperty("x") + y: Optional[float] = ValidatedCppManagedProperty("y") + z: Optional[float] = ValidatedCppManagedProperty("z") + is_on_ground: Optional[bool] = ValidatedCppManagedProperty("is_on_ground") + position: Optional[List[float]] = ValidatedCppManagedProperty("position") + is_yaw_angle_specified: Optional[bool] = ValidatedCppManagedProperty("is_yaw_angle_specified") + yaw_angle: Optional[float] = ValidatedCppManagedProperty("yaw_angle") + is_stop_and_turn: Optional[bool] = ValidatedCppManagedProperty("is_stop_and_turn") + is_smooth_turn: Optional[bool] = ValidatedCppManagedProperty("is_smooth_turn") + speed_m_s: Optional[float] = ValidatedCppManagedProperty("speed_m_s") + + +class Platform(Validatable): + def __init__(self, platform_settings: Optional[PlatformSettings] = None, last_check_z: Optional[float] = 0.0, dmax: Optional[float] = 0.0, is_orientation_on_leg_init: Optional[bool] = False, + is_on_ground: Optional[bool] = False, is_stop_and_turn: Optional[bool] = True, settings_speed_m_s: Optional[float] = 70.0, + is_slowdown_enabled: Optional[bool] = False, is_smooth_turn: Optional[bool] = False) -> None: + + self._cpp_object = _helios.Platform() + + self.last_check_z = last_check_z + self.dmax = dmax + self.is_orientation_on_leg_init = is_orientation_on_leg_init + self.is_on_ground = is_on_ground + self.is_stop_and_turn = is_stop_and_turn + self.settings_speed_m_s = settings_speed_m_s + self.is_slowdown_enabled = is_slowdown_enabled + self.is_smooth_turn = is_smooth_turn + + + + last_check_z: Optional[float] = ValidatedCppManagedProperty("last_check_z") + dmax: Optional[float] = ValidatedCppManagedProperty("dmax") + is_orientation_on_leg_init: Optional[bool] = ValidatedCppManagedProperty("is_orientation_on_leg_init") + is_on_ground: Optional[bool] = ValidatedCppManagedProperty("is_on_ground") + is_stop_and_turn: Optional[bool] = ValidatedCppManagedProperty("is_stop_and_turn") + settings_speed_m_s: Optional[float] = ValidatedCppManagedProperty("settings_speed_m_s") + is_slowdown_enabled: Optional[bool] = ValidatedCppManagedProperty("is_slowdown_enabled") + is_smooth_turn: Optional[bool] = ValidatedCppManagedProperty("is_smooth_turn") + + diff --git a/python/pyhelios/primitives.py b/python/pyhelios/primitives.py new file mode 100644 index 000000000..ff5a800a4 --- /dev/null +++ b/python/pyhelios/primitives.py @@ -0,0 +1,167 @@ +from pyhelios.utils import Validatable, ValidatedCppManagedProperty + +from pydantic import ConfigDict +from typing import Optional, List +import numpy as np + +import _helios + +class Rotation(Validatable): + model_config = ConfigDict(arbitrary_types_allowed=True) + def __init__(self, q0: Optional[float] = .0, q1: Optional[float] = .0, q2: Optional[float] = .0, q3: Optional[float] = .0): + + self._cpp_object = _helios.Rotation(q0, q1, q2, q3, False) + self.q0 = q0 + self.q1 = q1 + self.q2 = q2 + self.q3 = q3 + + q0: Optional[float] = ValidatedCppManagedProperty("q0") + q1: Optional[float] = ValidatedCppManagedProperty("q1") + q2: Optional[float] = ValidatedCppManagedProperty("q2") + q3: Optional[float] = ValidatedCppManagedProperty("q3") + + +class Vertex(Validatable): + def __init__(self, pos: Optional[List[float]]=[]) -> None: + self._cpp_object = _helios.Vertex() + + +class DetailedVoxel(Validatable): + def __init__(self, center: Optional[List[float]], voxel_size: Optional[float], int_values: Optional[List[int]] = None, float_values: Optional[List[float]] = None) -> None: #def __init__(self, nb_echos: Optional[int] = 0, nb_sampling: Optional[int] = 0, max_pad: Optional[float] = .0): #: + + self._cpp_object = _helios.DetailedVoxel((center, voxel_size, int_values, float_values) if int_values is not None and float_values is not None else ()) + + self.nb_echos = self._cpp_object.nb_echos + + self.nb_sampling = self._cpp_object.nb_sampling + self.max_pad = self._cpp_object.max_pad + + nb_echos: Optional[int] = ValidatedCppManagedProperty("nb_echos") + nb_sampling: Optional[int] = ValidatedCppManagedProperty("nb_sampling") + max_pad: Optional[float] = ValidatedCppManagedProperty("max_pad") + + +class Triangle(Validatable): + def __init__(self, v0: Optional[Vertex], v1: Optional[Vertex], v2: Optional[Vertex]) -> None: + self._cpp_object = _helios.Triangle(v0._cpp_object, v1._cpp_object, v2._cpp_object) + + +class Material(Validatable): + def __init__(self, name: Optional[str], mat_file_path: Optional[str], is_ground: Optional[bool] = False, + use_vertex_colors: Optional[bool] = False, reflectance: Optional[float] = None, specularity: Optional[float] = .0, ) -> None: + self._cpp_object = _helios.Material() + self.name = name + self.mat_file_path = mat_file_path + self.is_ground = is_ground + self.use_vertex_colord = use_vertex_colors + self.reflectance = reflectance + self.specularity = specularity + + name: Optional[str] = ValidatedCppManagedProperty("name") + mat_file_path: Optional[str] = ValidatedCppManagedProperty("mat_file_path") + is_ground: Optional[bool] = ValidatedCppManagedProperty("is_ground") + use_vertex_colors: Optional[bool] = ValidatedCppManagedProperty("use_vertex_color") + reflectance: Optional[float] = ValidatedCppManagedProperty("reflectance") + specularity: Optional[float] = ValidatedCppManagedProperty("specularity") + + +class AABB(Validatable): + def __init__(self) -> None: + self._cpp_object = _helios.AABB.create() + + +class Primitive(Validatable): + def __init__(self, material: Optional[Material] = None, aabb: Optional[AABB] = None, + detailed_voxel: Optional[DetailedVoxel] = None, vertices: Optional[List[Vertex]] = None, triangles: Optional[List[Triangle]] = None, + scene_parts: Optional[List['ScenePart']] = None) -> None: + self._cpp_object = _helios.Primitive() + if scene_parts is not None: + from pyhelios.scene import ScenePart + self.scene_parts = scene_parts + else: + self.scene_parts = [] + + +class Trajectory(Validatable): + def __init__(self, gps_time: Optional[float] = .0, position: Optional[List[float]] = [0, 0, 0], roll: Optional[float] = .0, pitch: Optional[float] = .0, yaw: Optional[float] = .0) -> None: + self._cpp_object = _helios.Trajectory() + self.position = position + self.roll = roll + self.pitch = pitch + self.yaw = yaw + + gps_time: Optional[float] = ValidatedCppManagedProperty("gps_time") + position: Optional[List[float]] = ValidatedCppManagedProperty("position") + roll: Optional[float] = ValidatedCppManagedProperty("roll") + pitch: Optional[float] = ValidatedCppManagedProperty("pitch") + yaw: Optional[float] = ValidatedCppManagedProperty("yaw") + + +class Measurement(Validatable): + def __init__(self, hit_object_id: Optional[str] = "", position: Optional[List[float]] = [0, 0, 0], beam_direction: Optional[List[float]] = [0, 0, 0], + beam_origin: Optional[List[float]] = [0, 0, 0], distance: Optional[float] = .0, intensity: Optional[float] = .0, + echo_width: Optional[float] = .0, return_number: Optional[int] = 0, pulse_return_number: Optional[int] = 0, + fullwave_index: Optional[int] = 0, classification: Optional[int] = 0, gps_time: Optional[float] = .0): + + self._cpp_object = _helios.Measurement() + self.hit_object_id = hit_object_id + self.position = position + self.beam_direction = beam_direction + self.beam_origin = beam_origin + self.distance = distance + self.intensity = intensity + self.echo_width = echo_width + self.return_number = return_number + self.pulse_return_number = pulse_return_number + self.fullwave_index = fullwave_index + self.classification = classification + self.gps_time = gps_time + + hit_object_id: Optional[str] = ValidatedCppManagedProperty("hit_object_id") + position: Optional[List[float]] = ValidatedCppManagedProperty("position") + beam_direction: Optional[List[float]] = ValidatedCppManagedProperty("beam_direction") + beam_origin: Optional[List[float]] = ValidatedCppManagedProperty("beam_origin") + distance: Optional[float] = ValidatedCppManagedProperty("distance") + intensity: Optional[float] = ValidatedCppManagedProperty("intensity") + echo_width: Optional[float] = ValidatedCppManagedProperty("echo_width") + return_number: Optional[int] = ValidatedCppManagedProperty("return_number") + pulse_return_number: Optional[int] = ValidatedCppManagedProperty("pulse_return_number") + fullwave_index: Optional[int] = ValidatedCppManagedProperty("fullwave_index") + classification: Optional[int] = ValidatedCppManagedProperty("classification") + gps_time: Optional[float] = ValidatedCppManagedProperty("gps_time") + + +class FWFSettings(Validatable): + def __init__(self, bin_size_ns: Optional[float] = .0, beam_sample_quality: Optional[int] = 0) -> None: + self._cpp_object = _helios.FWFSettings() + self.bin_size_ns = bin_size_ns + self.beam_sample_quality = beam_sample_quality + + bin_size_ns: Optional[float] = ValidatedCppManagedProperty("bin_size_ns") + beam_sample_quality: Optional[int] = ValidatedCppManagedProperty("beam_sample_quality") + + +class AbstractBeamDeflector(Validatable): + def __init__(self, scan_angle_max: Optional[float] = .0, scan_freq_max: Optional[float] = .0, scan_freq_min: Optional[float] = .0, scan_freq: Optional[float] = .0, scan_angle: Optional[float] = .0, + vertical_angle_min: Optional[float] = .0, vertical_angle_max: Optional[float] = .0, current_beam_angle: Optional[float] = .0, angle_diff_rad: Optional[float] = .0) -> None: + self._cpp_object = _helios.AbstractBeamDeflector(scan_angle_max, scan_freq_max, scan_freq_min) + self.scan_angle_max = scan_angle_max + self.scan_freq_max = scan_freq_max + self.scan_freq_min = scan_freq_min + self.scan_freq = scan_freq + self.scan_angle = scan_angle + self.vertical_angle_min = vertical_angle_min + self.vertical_angle_max = vertical_angle_max + self.current_beam_angle = current_beam_angle + self.angle_diff_rad = angle_diff_rad + + scan_angle_max: Optional[float] = ValidatedCppManagedProperty("scan_angle_max") + scan_freq_max: Optional[float] = ValidatedCppManagedProperty("scan_freq_max") + scan_freq_min: Optional[float] = ValidatedCppManagedProperty("scan_freq_min") + scan_freq: Optional[float] = ValidatedCppManagedProperty("scan_freq") + scan_angle: Optional[float] = ValidatedCppManagedProperty("scan_angle") + vertical_angle_min: Optional[float] = ValidatedCppManagedProperty("vertical_angle_min") + vertical_angle_max: Optional[float] = ValidatedCppManagedProperty("vertical_angle_max") + current_beam_angle: Optional[float] = ValidatedCppManagedProperty("current_beam_angle") + angle_diff_rad: Optional[float] = ValidatedCppManagedProperty("angle_diff_rad") diff --git a/python/pyhelios/scanner.py b/python/pyhelios/scanner.py new file mode 100644 index 000000000..311037cdb --- /dev/null +++ b/python/pyhelios/scanner.py @@ -0,0 +1,88 @@ +from pyhelios.utils import Validatable, ValidatedCppManagedProperty +from pyhelios.primitives import FWFSettings, AbstractBeamDeflector, FWFSettings +from typing import Optional, List, Tuple, Annotated, Type, Any + +import _helios + +class ScannerSettings(Validatable): + def __init__(self, id: Optional[str] = "", is_active: Optional[bool] = True, head_rotation: Optional[float] = .0, + rotation_start_angle: Optional[float] = .0, rotation_stop_angle: Optional[float] = .0, pulse_frequency: Optional[int] = 0, + scan_angle: Optional[float] = .0, min_vertical_angle: Optional[float] = .0, max_vertical_angle: Optional[float] = .0, + scan_frequency: Optional[float] = .0, beam_divergence_angle: Optional[float] = .003, trajectory_time_interval: Optional[float] = .0, + vertical_resolution: Optional[float] = .0, horizontal_resolution: Optional[float] = .0) -> None: + + self._cpp_object = _helios.ScannerSettings() + self.id = id + self.is_active = is_active + self.head_rotation = head_rotation + self.rotation_start_angle = rotation_start_angle + self.rotation_stop_angle = rotation_stop_angle + self.pulse_frequency = pulse_frequency + self.scan_angle = scan_angle + self.min_vertical_angle = min_vertical_angle + self.max_vertical_angle = max_vertical_angle + self.scan_frequency = scan_frequency + self.beam_divergence_angle = beam_divergence_angle + self.trajectory_time_interval = trajectory_time_interval + self.vertical_resolution = vertical_resolution + self.horizontal_resolution = horizontal_resolution + + id: Optional[str] = ValidatedCppManagedProperty("id") + is_active: Optional[bool] = ValidatedCppManagedProperty("is_active") + head_rotation: Optional[float] = ValidatedCppManagedProperty("head_rotation") + rotation_start_angle: Optional[float] = ValidatedCppManagedProperty("rotation_start_angle") + rotation_stop_angle: Optional[float] = ValidatedCppManagedProperty("rotation_stop_angle") + pulse_frequency: Optional[int] = ValidatedCppManagedProperty("pulse_frequency") + scan_angle: Optional[float] = ValidatedCppManagedProperty("scan_angle") + min_vertical_angle: Optional[float] = ValidatedCppManagedProperty("min_vertical_angle") + max_vertical_angle: Optional[float] = ValidatedCppManagedProperty("max_vertical_angle") + scan_frequency: Optional[float] = ValidatedCppManagedProperty("scan_frequency") + beam_divergence_angle: Optional[float] = ValidatedCppManagedProperty("beam_divergence_angle") + trajectory_time_interval: Optional[float] = ValidatedCppManagedProperty("trajectory_time_interval") + vertical_resolution: Optional[float] = ValidatedCppManagedProperty("vertical_resolution") + horizontal_resolution: Optional[float] = ValidatedCppManagedProperty("horizontal_resolution") + +class ScannerHead(Validatable): + def __init__(self, rotate_per_sec_max: Optional[float], rotation_axis: Optional[List[float]] = [1., 0., 0.], rotate_per_sec: Optional[float] = .0, rotate_stop: Optional[float] = .0, rotate_start: Optional[float] = .0, + rotate_range: Optional[float] = .0, current_rotate_angle: Optional[float] = .0) -> None: + self._cpp_object = _helios.ScannerHead(rotation_axis, rotate_per_sec_max) + self.rotate_per_sec_max = rotate_per_sec_max + self.rotate_per_sec = rotate_per_sec + self.rotate_stop = rotate_stop + self.rotate_start = rotate_start + self.rotate_range = rotate_range + self.current_rotate_angle = current_rotate_angle + + rotate_per_sec_max: Optional[float] = ValidatedCppManagedProperty("rotate_per_sec_max") + rotate_per_sec: Optional[float] = ValidatedCppManagedProperty("rotate_per_sec") + rotate_stop: Optional[float] = ValidatedCppManagedProperty("rotate_stop") + rotate_start: Optional[float] = ValidatedCppManagedProperty("rotate_start") + rotate_range: Optional[float] = ValidatedCppManagedProperty("rotate_range") + current_rotate_angle: Optional[float] = ValidatedCppManagedProperty("current_rotate_angle") + + +class Scanner(Validatable): + def __init__(self, id: Optional[str], scanner_settings: Optional[ScannerSettings] = None, FWF_settings: Optional[FWFSettings] = None, scanner_head: Optional[ScannerHead] = None, + beam_deflector: Optional[AbstractBeamDeflector] = None, detector: Optional['AbstractDetector'] = None, supported_pulse_freqs_hz: Optional[List[int]] = [0], + num_rays: Optional [int] = 0, pulse_length: Optional[float] = .0) -> None: + self._cpp_object = _helios.Scanner(id, supported_pulse_freqs_hz) + self.fwf_settings = FWF_settings + self.num_rays = num_rays + self.pulse_length = pulse_length + + FWF_settings: Optional[FWFSettings] = ValidatedCppManagedProperty("fwf_settings") + num_rays: Optional[int] = ValidatedCppManagedProperty("num_rays") + pulse_length: Optional[float] = ValidatedCppManagedProperty("pulse_length") + + +class AbstractDetector(Validatable): + def __init__(self, scanner: Optional[Scanner], range_max: Optional[float], accuracy: Optional[float] = .0, range_min: Optional[float] = .0) -> None: + self._cpp_object = _helios.AbstractDetector(scanner._cpp_object, accuracy, range_min, range_max) + self.accuracy = accuracy + self.range_min = range_min + self.range_max = range_max + + + accuracy: Optional[float] = ValidatedCppManagedProperty("accuracy") + range_min: Optional[float] = ValidatedCppManagedProperty("range_min") + range_max: Optional[float] = ValidatedCppManagedProperty("range_max") diff --git a/python/pyhelios/scene.py b/python/pyhelios/scene.py new file mode 100755 index 000000000..e7ffaf0b8 --- /dev/null +++ b/python/pyhelios/scene.py @@ -0,0 +1,52 @@ +from pyhelios.utils import Validatable, ValidatedCppManagedProperty +from pyhelios.primitives import Rotation, Primitive, AABB + +from typing import Optional, List, Tuple, Annotated, Type, Any +import _helios + +class ScenePart(Validatable): + + def __init__(self, + id: Optional[str] = "", + origin: Optional[List[float]] = [0.0, 0.0, 0.0], + rotation: Optional[Rotation] = None, + scale: Optional[float] = 1.0, + bound: Optional[List[float]] = None, + primitives: Optional[List[Primitive]] = None) -> None: + + + self._cpp_object = _helios.ScenePart() + self.id = id + self.origin = origin + self.rotation = rotation + self.scale = scale + self.bound = bound + if primitives is not None: + self.primitives = primitives + else: + self.primitives = [] + + + + id: Optional[str] = ValidatedCppManagedProperty("id") + origin: Optional[Tuple[float, float, float]] = ValidatedCppManagedProperty("origin") + rotation: Optional[Rotation] = ValidatedCppManagedProperty("rotation") + scale: Optional[float] = ValidatedCppManagedProperty("scale") + bound: Optional[tuple[float, float, float]] = ValidatedCppManagedProperty("bound") + primitives: Optional[List[Primitive]] = ValidatedCppManagedProperty("primitives") + + +class Scene(Validatable): + + def __init__(self, + scene_parts: Optional[List[ScenePart]], bbox: Optional[AABB] = None, bbox_crs: Optional[AABB] = None) -> None: + + self._cpp_object = _helios.Scene() + self.scene_parts = scene_parts + self.bbox = bbox + self.bbox_crs = bbox_crs + + scene_parts: Optional[List[ScenePart]] = ValidatedCppManagedProperty("scene_parts") + bbox: Optional[AABB] = ValidatedCppManagedProperty("bbox") + bbox_crs: Optional[AABB] = ValidatedCppManagedProperty + diff --git a/python/pyhelios/simulation.py b/python/pyhelios/simulation.py new file mode 100644 index 000000000..34cf073a9 --- /dev/null +++ b/python/pyhelios/simulation.py @@ -0,0 +1,51 @@ +from pyhelios.scene import Scene +from pyhelios.utils import Validatable, ValidatedCppManagedProperty +from pyhelios.scanner import Scanner, AbstractDetector +from pyhelios.primitives import Rotation, Measurement, Trajectory +from typing import Optional, List, Callable +import _helios + +class SimulationCycleCallback: + def __init__(self, callback: Optional[Callable] = None): + # Create an instance of the C++ SimulationCycleCallback and pass the callback object + self._cpp_object = _helios.SimulationCycleCallback(callback) + self._callback = callback # Store the Python callable + + @property + def callback(self) -> Optional[Callable]: + return self._callback + + @callback.setter + def callback(self, value: Optional[Callable]): + if not callable(value) and value is not None: + raise ValueError("The callback must be callable.") + self._callback = value + + + def __call__(self, measurements: List['Measurement'], trajectories: List['Trajectory'], outpath: str): + """ + Invokes the callback function with the measurements, trajectories, and outpath. + """ + if self._callback: + self._callback(measurements, trajectories, outpath) + else: + raise ValueError("No callback function set.") + + def set_callback(self, callback: Callable): + """ + Set a new callback function for the C++ object. + """ + self.callback = callback + +class Simulation: + def __init__(self, final_output: Optional[bool] = True, legacy_energy_model: Optional[bool] = False, export_to_file: Optional[bool] = True, + num_threads: Optional[int] = 0, num_runs: Optional[int] = 1, callback_frequency: Optional[int] = 0, + simulation_frequency: Optional[SimulationCycleCallback] = None, fixed_gps_time: Optional[str] = "", + las_output: Optional[bool] = False, las10_output: Optional[bool] = False, + zip_output: Optional[bool] = False , split_by_channel: Optional[bool] = False, las_scale: Optional[float] = 0.0001, + kdt_factory: Optional[int] = 4, + kdt_jobs: Optional[int] = 0, kdt_SAH_loss_nodes: Optional[int] = 32, parallelization_strategy: Optional[int] = 1, chunk_size: Optional[int] = 32, + warehouse_factor: Optional[int] = 1) -> None: + self._cpp_object = _helios.Simulation() + + diff --git a/python/pyhelios/survey.py b/python/pyhelios/survey.py new file mode 100644 index 000000000..491c26961 --- /dev/null +++ b/python/pyhelios/survey.py @@ -0,0 +1,24 @@ +from pyhelios.scene import Scene +from pyhelios.utils import Validatable, ValidatedCppManagedProperty +from pyhelios.scanner import Scanner, AbstractDetector +from pyhelios.primitives import Rotation +from typing import Optional, List +import _helios + + +class Survey(Validatable): + def __init__(self, name: Optional[str] = "", num_runs: Optional[int] = -1, + sim_speed_factor: Optional[float] = 1., scanner: Optional[Scanner] = None) -> None: + + self._cpp_object = _helios.Survey() + self.name = name + self.num_runs = num_runs + self.sim_speed_factor = sim_speed_factor + self.scanner = scanner + + name: Optional[str] = ValidatedCppManagedProperty("name") + num_runs: Optional[int] = ValidatedCppManagedProperty("num_runs") + sim_speed_factor: Optional[float] = ValidatedCppManagedProperty("sim_speed_factor") + scanner: Optional[Scanner] = ValidatedCppManagedProperty("scanner") + + \ No newline at end of file diff --git a/python/pyhelios/utils.py b/python/pyhelios/utils.py new file mode 100644 index 000000000..8e5abac6e --- /dev/null +++ b/python/pyhelios/utils.py @@ -0,0 +1,36 @@ +from pydantic import validate_call, GetCoreSchemaHandler +from pydantic_core import core_schema +from typing import Optional, Type, Any + +class Validatable: + @classmethod + def __get_pydantic_core_schema__(cls, source: Type[Any], handler: GetCoreSchemaHandler): + return core_schema.no_info_after_validator_function(cls._validate, core_schema.any_schema()) + + @classmethod + def _validate(cls, value): + if not isinstance(value, cls): + raise ValueError(f"Expected {cls.__name__}, got {type(value).__name__}") + return value + + +class ValidatedCppManagedProperty: + def __init__(self, name): + self.name = name + self._values = {} + + def __get__(self, obj, objtype=None): + return self._values.get(obj, None) + + def __set__(self, obj, value): + @validate_call + def _validated_setter(value: obj.__annotations__[self.name]): + if isinstance(value, list): + cpp_value = [getattr(item, "_cpp_object") if hasattr(item, "_cpp_object") else item for item in value] + else: + cpp_value = getattr(value, "_cpp_object") if hasattr(value, "_cpp_object") else value + setattr(obj._cpp_object, self.name, cpp_value) + self._values[obj] = value + + _validated_setter(value) + diff --git a/src/python/AbstractBeamDeflectorWrap.h b/src/python/AbstractBeamDeflectorWrap.h index 07ff124d1..61a299466 100644 --- a/src/python/AbstractBeamDeflectorWrap.h +++ b/src/python/AbstractBeamDeflectorWrap.h @@ -6,12 +6,29 @@ class AbstractBeamDeflectorWrap : public AbstractBeamDeflector { public: using AbstractBeamDeflector::AbstractBeamDeflector; + std::shared_ptr clone() override { + PYBIND11_OVERRIDE_PURE( + std::shared_ptr, // Return type + AbstractBeamDeflector, // Parent class + clone // Function name + ); + } + + void doSimStep() override { + PYBIND11_OVERRIDE_PURE( + void, // Return type + AbstractBeamDeflector, // Parent class + doSimStep // Function name + ); + } + std::string getOpticsType() const override { PYBIND11_OVERRIDE_PURE( - std::string, - AbstractBeamDeflector, - getOpticsType, - ); + std::string, // Return type + AbstractBeamDeflector, // Parent class + getOpticsType // Function name + ); } + }; \ No newline at end of file diff --git a/src/python/PrimitiveWrap.h b/src/python/PrimitiveWrap.h new file mode 100644 index 000000000..0d1074fd8 --- /dev/null +++ b/src/python/PrimitiveWrap.h @@ -0,0 +1,80 @@ +#include +#include + +class PrimitiveWrap : public Primitive { +public: + using Primitive::Primitive; // Inherit constructors + + Primitive* clone() override { + PYBIND11_OVERRIDE_PURE( + Primitive*, // Return type + Primitive, // Parent class + clone, // Name of the function in C++ + ); + } + // Override getAABB method + AABB* getAABB() override { + PYBIND11_OVERRIDE_PURE( + AABB*, // Return type + Primitive, // Parent class + getAABB, // Name of the function in C++ + ); + } + + // Override getCentroid method + glm::dvec3 getCentroid() override { + PYBIND11_OVERRIDE_PURE( + glm::dvec3, // Return type + Primitive, // Parent class + getCentroid, // Name of the function in C++ + ); + } + + // Override getIncidenceAngle_rad method + double getIncidenceAngle_rad(const glm::dvec3& p, const glm::dvec3& d, const glm::dvec3& n) override { + PYBIND11_OVERRIDE_PURE( + double, // Return type + Primitive, // Parent class + getIncidenceAngle_rad, // Name of the function in C++ + p, d, n // Arguments + ); + } + + // Override getRayIntersection method + std::vector getRayIntersection(const glm::dvec3& p, const glm::dvec3& d) override { + PYBIND11_OVERRIDE_PURE( + std::vector, // Return type + Primitive, // Parent class + getRayIntersection, // Name of the function in C++ + p, d // Arguments + ); + } + + // Override getRayIntersectionDistance method + double getRayIntersectionDistance(const glm::dvec3& p, const glm::dvec3& d) override { + PYBIND11_OVERRIDE_PURE( + double, // Return type + Primitive, // Parent class + getRayIntersectionDistance, // Name of the function in C++ + p, d // Arguments + ); + } + + // Override getVertices method + Vertex* getVertices() override { + PYBIND11_OVERRIDE_PURE( + Vertex*, // Return type + Primitive, // Parent class + getVertices // Name of the function in C++ + ); + } + + // Override update method + void update() override { + PYBIND11_OVERRIDE_PURE( + void, // Return type + Primitive, // Parent class + update // Name of the function in C++ + ); + } +}; diff --git a/tests/python/test_bindings.py b/tests/python/test_bindings.py index 7c698d27b..f24471ed7 100755 --- a/tests/python/test_bindings.py +++ b/tests/python/test_bindings.py @@ -96,9 +96,9 @@ def test_primitive_ray_intersection(): assert intersection == expected_intersections[i], f"Intersection at index {i} does not match" ray_origin = (0.5, 0.5, 1.0) ray_dir = (0.0, 0.0, -1.0) - intersections = triangle.ray_intersection(ray_origin, ray_dir) - assert isinstance(intersections, _helios.DoubleVector), "ray_intersection should return a DoubleVector" + intersections = triangle.ray_intersection(ray_origin, ray_dir) assert len(intersections) > 0, "The intersections list should not be empty" + assert intersections[0] == -1 or isinstance(intersections[0], float), "The intersection should be a float or -1" def test_primitive_ray_intersection_distance(): v0 = _helios.Vertex(0.0, 0.0, 0.0) @@ -155,30 +155,31 @@ def test_primitive_vertices(): ray_origin = (0.5, 0.5, 1.0) ray_dir = (0.0, 0.0, -1.0) intersections = triangle.ray_intersection(ray_origin, ray_dir) - assert isinstance(intersections, _helios.DoubleVector), "ray_intersection should return a DoubleVector" + assert len(intersections) > 0, "The intersections list should not be empty" + assert intersections[0] == -1 or isinstance(intersections[0], float), "The intersection should be a float or -1" def test_detailed_voxel_instantiation(): double_values = _helios.DoubleVector([0.1, 0.2, 0.3]) - voxel = _helios.DetailedVoxel(1.0, 2.0, 3.0, 0.5, [1, 2], double_values)#[0.1, 0.2, 0.3]) + voxel = _helios.DetailedVoxel([1.0, 2.0, 3.0], 0.5, [1, 2], double_values)#[0.1, 0.2, 0.3]) assert isinstance(voxel, _helios.DetailedVoxel) assert voxel.nb_echos == 1 assert voxel.nb_sampling == 2 def test_detailed_voxel_properties(): double_values = _helios.DoubleVector([0.1, 0.2, 0.3]) - voxel = _helios.DetailedVoxel(1.0, 2.0, 3.0, 0.5, [1, 2], double_values) + voxel = _helios.DetailedVoxel([1.0, 2.0, 3.0], 0.5, [1, 2], double_values) voxel.nb_echos = 3 assert voxel.nb_echos == 3 voxel.nb_sampling = 4 assert voxel.nb_sampling == 4 assert voxel.number_of_double_values == 3 - voxel.maxPad = 0.6 - assert voxel.maxPad == 0.6 - voxel.doubleValue(1, 0.5) - assert voxel.doubleValue(1) == 0.5 + voxel.max_pad = 0.6 + assert voxel.max_pad == 0.6 + voxel.set_double_value(1, 0.5) + assert voxel.get_double_value(1) == 0.5 def test_trajectory_instantiation(): # Test default constructor @@ -1186,14 +1187,14 @@ def getHeadRelativeEmitterAttitudeByRef(self, idx=0): class TestScannerMethods: @pytest.fixture def scanner(self): - return ExampleScanner() + return ExampleScanner(id = "SCANNER-ID", pulseFreqs = [1, 2, 3]) def test_scanner_construction(self, scanner): # Test default construction assert scanner is not None # Test parameterized construction - scanner1 = ExampleScanner() + scanner1 = scanner scanner = ExampleScanner(scanner1) assert scanner.id == "SCANNER-ID"