Core Concepts ============= This page explains the main objects you interact with in every routing workflow. Understanding these classes and how they fit together is the foundation for using ``routing`` effectively. .. contents:: On this page :local: :depth: 2 .. image:: ../../_static/index-images/user_guide.svg :align: center Overview -------- Every routing workflow follows the same structure: 1. **Define inputs**: :class:`~routing.core.core.Port`, :class:`~routing.core.core.PipeType`, :class:`~routing.core.core.Specification` 2. **Define the routing space**: :class:`~routing.core.core.RuledVolume`, :class:`~routing.core.cadmap.CadMap` 3. **Find paths**: :class:`~routing.core.route_planner.RoutePlanner` 4. **Read results**: :class:`~routing.core.route_planner.RoutingResults` Here is how they relate: .. code-block:: text Port (start) ──┐ ├──► Specification ──┐ Port (end) ──┘ │ PipeType ──────────────────────────►│ │ VolumeModel ──► RuledVolume ──────►CadMap ──► RoutePlanner.generate(specs) ──► RoutingResults DesignRules ──►────────────────────►│ │ (optional) ──────── RoutePlanner.optimize() ──► refined RoutingResults Port ---- A :class:`~routing.core.core.Port` is a fixed connection point in 3D space where a pipe starts or ends. It has a position, an orientation, and an optional minimum straight length. .. code-block:: python from routing.core.core import Port port = Port( coordinates=(0.05, -0.4, 0.1), # (x, y, z) in metres direction=(0, 1, 0), # pipe exits in the +Y direction length=0.1, # 100 mm straight before the first turn name="inlet_port", ) **Parameters:** - **coordinates** *(tuple of 3 floats)*: The (x, y, z) position of the port in the global coordinate frame (metres). - **direction** *(tuple of 3 floats)*: The direction the pipe must leave this port (for a start port) or arrive (for an end port). Does not need to be a unit vector — it is normalized automatically. - **length** *(float, default 0.0)*: Minimum straight segment length at the port before the first direction change. Required for pipes with minimum bend radius constraints. - **len_adaptable_with_environment** *(bool, default False)*: When ``True``, the router will automatically extend the port length if the immediate environment would cause collisions. This attribute is not used in generic mode, it is an attribute for custom algorithms. - **permitted_departure_angle** *(list of float, optional)*: If set, restricts the departure direction to a list of allowed angles (in radians). Leave as ``None`` to permit all directions. This attribute is not used in generic mode, it is an attribute for custom algorithms. **Importing ports from Excel:** For large assemblies, define ports in an Excel spreadsheet and import them: .. code-block:: python # Excel columns: name | coordinates_x | coordinates_y | coordinates_z # | direction_x | direction_y | direction_z | length with open("ports.xlsx", "rb") as f: ports = Port.from_xlsx_stream(f) PipeType -------- A :class:`~routing.core.core.PipeType` defines the physical properties of a class of pipe. You reuse the same ``PipeType`` for all pipes of that specification. .. code-block:: python from piping_3d import piping from routing.core.core import PipeType section = piping.Section(radius_equivalent=0.015) # 30 mm outer diameter pipe_type = PipeType( section=section, name="HydraulicLine", radius_of_curvature_ratio_min=1.5, # bend radius ≥ 1.5 × diameter length_before_turn_min=0.030, # 30 mm straight before any turn color=(0, 100, 200), # visualization color (RGB 0–255) type_="Hydraulic", # group label for pooling ) **Key parameters:** - **section**: A ``piping.Section`` object specifying the outer radius of the pipe (``radius_equivalent``). - **radius_of_curvature_ratio_min** *(float)*: Minimum bend radius as a multiple of the pipe diameter. Common values: 1.5 for flexible hoses, 3–5 for rigid pipes. - **length_before_turn_min** *(float)*: Minimum straight length required before any direction change. Set to 0 for fully flexible tubing. - **type_** *(str)*: A group label. Pipes with the same ``type_`` can be pooled together (see :doc:`pooling`). - **min_slope / max_slope** *(float, optional)*: For gravity-drained pipes, set ``min_slope`` to the minimum required slope (e.g. ``0.02`` = 2% grade). See :doc:`design_rules_reference` for ``GravityRule``. - **priority_index** *(int, optional)*: Routing priority. Lower index = higher priority (routed first when ordering matters). This attribute is not used in generic mode, it is an attribute for custom algorithms. - **color**: RGB tuple in range (0–255). Used by the visualization layer. **Compact creation with** ``Specification.from_complete_definition``: If you only have a few pipes and don't need to reuse ``PipeType`` objects, you can skip creating them explicitly: .. code-block:: python from routing.core.core import Specification spec = Specification.from_complete_definition( start=port_start, end=port_end, radius=0.015, # pipe outer radius name="HydraulicLine_1", radius_of_curvature_ratio=1.5, min_straight_length=0.030, pooling_type="Hydraulic", color=(0, 100, 200), ) Specification ------------- A :class:`~routing.core.core.Specification` is a routing requirement: it pairs a start port, end port, and pipe type into one object that ``RoutePlanner.generate()`` processes. .. code-block:: python from routing.core.core import Specification # Standard construction spec = Specification( start=port_start, end=port_end, pipe_type=pipe_type, name="HydraulicLine_route_1", ) # Shorthand: use pipe_type.name automatically spec = Specification.from_ports(port_start, port_end, pipe_type) Create one ``Specification`` per pipe to route. Pass a list of them to ``generate()``. RuledVolume ----------- A :class:`~routing.core.core.RuledVolume` wraps a 3D CAD volume (a ``VolumeModel`` from volmdlr) and attaches a list of :ref:`design rules ` to it. The design rules control how the routing algorithm weights the space near that volume. .. code-block:: python from routing.core.core import RuledVolume from routing.core.design_rules import WeightingRule from volmdlr.model import VolumeModel # A weighting rule: routes prefer to stay close to this volume weighting_rule = WeightingRule( function="linear", min_length=0.0, max_length=0.2, min_value=1, max_value=10, ) # Wrap the CAD volume ruled_volume = RuledVolume.from_volume_model( volume_model=VolumeModel([my_cad_primitive]), design_rules=[weighting_rule], name="main_structure", ) **Factory methods:** - ``RuledVolume.from_volume_model(volume_model, design_rules, name)`` — the standard way to create a ``RuledVolume`` from a volmdlr ``VolumeModel``. - ``RuledVolume.from_ruled_volume(existing_rv, design_rules)`` — copy an existing ``RuledVolume`` and override its global rules. **in_border** parameter: Set ``in_border=True`` to use this volume for computing the automatic bounding box of the routing space. In practice you need to set this manually when using ``CadMap.build_bounding_volume()`` in order to use the specified volume's bounding box into the construction of the main bounding box. .. _design-rules: See :doc:`design_rules_reference` for all available design rule types. CadMap ------ :class:`~routing.core.cadmap.CadMap` is the central routing space object. It: 1. Takes all your ``RuledVolume`` objects and voxelizes the free space between them 2. Computes distance fields from the CAD surfaces to every voxel 3. Applies the design rules to compute a weight matrix 4. Stores the result as a compact grid ready for pathfinding .. code-block:: python from routing.core.cadmap import CadMap from routing.core.design_rules import TurnPenalizer voxel_size = 0.021 # 21 mm — about 0.7× the largest pipe diameter # Global rules applied to all pipes global_rules = [TurnPenalizer(cost=3.0)] # Auto-compute a bounding box from the volumes bounding_box = CadMap.build_bounding_volume( ruled_volumes=[ruled_vol_1, ruled_vol_2], border_rules=[], voxel_size=voxel_size, voxel_margin=1, ) # Build the CadMap (this does the voxelization + distance computation) cadmap = CadMap.from_project_inputs( design_rules=global_rules, ruled_volumes=[ruled_vol_1, ruled_vol_2], bounding_box=bounding_box, voxel_size=voxel_size, exact_distances=True, ) **Key parameters of** ``from_project_inputs``: .. list-table:: :header-rows: 1 :widths: 25 15 60 * - Parameter - Default - Description * - ``design_rules`` - ``[]`` - Global rules applied to every pipe type (e.g. ``TurnPenalizer``) * - ``ruled_volumes`` - required - List of ``RuledVolume`` objects to voxelize against * - ``bounding_box`` - ``None`` - Bounding ``RuledVolume``; auto-computed if ``None`` * - ``voxel_size`` - ``0.1`` - Voxel edge length in metres. Use 1.5–3× the largest pipe radius. * - ``exact_distances`` - ``False`` - Exact mesh distances (accurate, slower) vs voxel approximation (fast) * - ``weights_operator`` - ``"min"`` - How to combine weights from multiple volumes. ``"min_distance"`` is recommended. * - ``voxel_inside`` - ``False`` - Set ``True`` to fill enclosed cavities (route through solid bodies) * - ``build_cad`` - ``False`` - Build visual box primitives per voxel (expensive; use only for debugging) **Choosing voxel_size:** - Start with ``voxel_size = 1.5 × largest_pipe_radius`` - Halve it if paths look inaccurate or miss narrow passages - Double it if voxelization takes too long **Memory:** ``cadmap.nbytes`` reports total memory usage. Large assemblies with fine voxels can require several GB. RoutePlanner ------------ :class:`~routing.core.route_planner.RoutePlanner` coordinates the pathfinding and optimization. It takes a ``CadMap`` and a ``SmartFinder`` and exposes two main methods: ``generate()`` and ``optimize()``. .. code-block:: python from routing.core.finders import SmartFinder from routing.core.route_planner import RoutePlanner finder = SmartFinder( algorithm="AStar", # "AStar", "ThetaStar", "Dijkstra" heuristic="manhattan", # "manhattan", "euclidean", "octile" diagonal="never", # "never", "always", "only_when_no_obstacle" ) route_planner = RoutePlanner(cadmap, finder) **generate() — find routing paths:** .. code-block:: python routing_result = route_planner.generate( specifications=[spec1, spec2, spec3], n_iterations=1, # k-shortest paths; 1 = only the best path per pipe steady_mode=False, # True = continue even if some pipes fail to route ) Pipes are processed in the order they appear in ``specifications``. Earlier pipes influence the grid weights for later pipes (through the pooling and history mechanisms). **optimize() — refine path geometry:** .. code-block:: python from routing.core.optimizer import Costs, Settings routing_result = route_planner.optimize( costs=Costs(length=1000, sideways=100, short_line=5000, gravity=5000, bend=250, constraints=5000, interferences=1e8), settings=Settings(voxel_size=0.01, max_shift=0.1, n_iterations=3, max_iter=1000), picked_path="best", # "best" (lowest cost path) or "last" (most recent) max_refinements=0, # extra passes to fix constraint violations ) See :doc:`optimization` for a full guide to the optimizer. RoutingResults -------------- :class:`~routing.core.route_planner.RoutingResults` is the output of ``generate()`` and ``optimize()``. It contains all routing solutions and exposes visualization methods: .. code-block:: python # Plot all generated paths (before optimization) routing_result.plot_data_generated().plot() # Plot all optimized paths routing_result.plot_data_optimized().plot() # Collect pipe bundles (groups of pipes routed together) packs = routing_result.collect_packs(only_optimized=False) routing_result.specific_packs_cad_view(packs) # Get bifurcation points (where bundles split) bifurcation_pts = routing_result.cadmap.get_bifurcation_points(packs, tolerance=0.04) See :doc:`results_access` for a complete guide to reading and exporting results. Next Steps ---------- - :doc:`../tutorials/basic_routing` — hands-on tutorial with the full routing workflow - :doc:`design_rules_reference` — all design rule types explained - :doc:`pooling` — bundle pipes into organized packs - :doc:`optimization` — tune the geometry optimizer - :doc:`multi_scale` — route large assemblies with multiple voxel sizes