Tutorial: Tees — Tapping a Routed Conduit ========================================= A **tee** is a conduit that runs from its own port to *anywhere along another conduit*: a branch on a supply line, a drain joining a collector, a pipe leaving a header. You declare where the tee starts and which conduit it taps; the router chooses where along that conduit it taps, and lays the junction: - the tee is routed **after the conduit it taps**, whatever the order you give; - it taps the conduit **on its axis**, square to it, on a straight part; - you choose how free it is: **anywhere** along the conduit, at **sites you declare**, or among **sites routed one by one** for you to compare; - the optimizer reroutes each tee on the **optimized geometry** of its conduit before optimizing the tee itself. The examples come from the window case of ``scripts/route_planning/``: four conduits, and seven tees on them, one of which taps another tee. .. contents:: On this page :local: :depth: 1 Overview -------- .. code-block:: python from routing.core.core import TapSite, TeeSpecification from routing.core.route_planner import RoutePlanner .. list-table:: :header-rows: 1 :widths: 42 58 * - You write - What it does * - ``TeeSpecification(port, tapped, pipe_type)`` - A tee from ``port`` to the conduit ``tapped``: its specification, or a conduit already routed * - ``TapSite(abscissa)``, ``TapSite.at(conduit, point)`` - A place along a conduit where a tee may tap, in metres from the conduit's start port * - ``TeeSpecification(..., tap_sites=[...])`` - The tee taps at those sites only * - ``TeeSpecification(..., explored_step=0.3)`` - The tee is routed once per site, one every 0.3 m; it stands by the cheapest route, the others are kept as alternatives * - ``RoutePlanning.from_specifications(specifications)`` - The routing order — each tee after the conduit it taps — and one solution per conduit; ``dependents`` lists the tees built on a conduit * - ``RoutePlanner.generate(specifications)`` - Routes the conduits, each tee after the conduit it taps * - ``RoutePlanner.optimize(costs, settings)`` - Optimizes them in the same order, each tee rerouted on its optimized conduit * - ``RoutePlanner.explore(tee, step)`` - Routes a tee once per site on the conduits as they stand, laying nothing * - ``solution.pick(index)``, ``solution.pick_nearest(point)`` - Retains one alternative of a conduit; for a tee, the site where it taps * - ``result.summary()``, ``result.solution(name)`` - One line per conduit; a solution by its name Declaring a Tee --------------- A :class:`~routing.core.core.TeeSpecification` has a port, like any conduit end, a pipe type, and the conduit it taps — here the :class:`~routing.core.core.Specification` of that conduit: .. code-block:: python import routing.core.legacy_piping as piping from routing.core.core import PipeType, Port, Specification, TeeSpecification thin_type = PipeType( section=piping.Section(radius_equivalent=0.005), name="tee_thin", radius_of_curvature_ratio_min=1.5, length_before_turn_min=0.0, type_="Type1", ) o7 = Specification(ports["O7"].start, ports["O7"].end, pipe_types["O7"], "O7") s1 = Specification(ports["S1"].start, ports["S1"].end, pipe_types["S1"], "S1") tee_o7_thin = TeeSpecification( Port((0.12, -0.4, 0.18), (0, 1, 0), 0.05, name="tee_O7_thin_port"), o7, # the conduit it taps thin_type, name="tee_O7_thin", ) tee_s1_thin = TeeSpecification(Port((0.65, 0.8, 0.45), (0, -1, 0), 0.05), s1, thin_type, name="tee_S1_thin") # a tee may tap a tee tee_of_tee = TeeSpecification(Port((0.5, 0.8, 0.45), (0, -1, 0), 0.05), tee_s1_thin, thin_type, name="tee_of_tee") The port is the tee's own end: its direction points into the routing space and its length is the straight run the tee keeps there, as for any port. Like the other ports, it must lie in the routing space: include it in the volumes ``CadMap.build_bounding_volume`` bounds. The other end is built by the routing, on the conduit tapped: - the **tap port** stands on the axis of the conduit, square to it; - the tee reaches it in a straight **leg**, over the radius of the conduit, the ``length_before_turn_min`` of its own pipe type and its curvature radius; - a leg never enters another conduit routed before the tee: a tap point whose leg would cross one is not offered to the search. Where a Tee Taps ---------------- A tee taps the **straight parts** of its conduit: never the port lines at the conduit's ends, and never closer to a corner than the conduit's curvature radius, where the conduit bends. Along those parts, it is as free as you say. **Anywhere (the default).** The tee is routed to the cheapest place along its conduit, in one search toward every tap point at once. **At declared sites.** ``tap_sites`` restricts the tee to those places. A :class:`~routing.core.core.TapSite` is a curvilinear abscissa: metres along the conduit, from its start port. .. code-block:: python from routing.core.core import TapSite # tee_p_big taps P 0.3 m from P's start port, and nowhere else tee_p_big = TeeSpecification( Port((0.72, -0.4, 0.3), (0, 1, 0), 0.05), p, big_type, tap_sites=[TapSite(0.3)], name="tee_P_big" ) Once the conduit is routed, ``TapSite.at(conduit, point)`` gives the site nearest a point of space, and ``TapSite.every(conduit, step)`` one site every ``step`` metres: .. code-block:: python site = TapSite.at(result.solution("P"), (0.6, -0.1, 0.25)) print(f"{site.abscissa:.3f} m along P") **Explored at generation.** ``explored_step`` routes the tee once per site, one site every ``explored_step`` metres. Each route becomes an alternative of the tee, named after its site (``tee_S1_mid@0.32``); the tee stands by the cheapest, and you may pick another one later (see `Picking a Site`_). .. code-block:: python tee_s1_mid = TeeSpecification( Port((1.0, -0.3, 0.1), (-1, 0, 0), 0.05), s1, mid_type, explored_step=0.3, name="tee_S1_mid" ) .. note:: A conduit too short or too bent to offer a straight part of two curvature radii has no site: a tee on it is **not routed**, and neither is a tee on a conduit that is not routed. Like any conduit no route joins, the tee is kept in the result with ``is_routed`` false. Automatic Mode -------------- Give the tees with the other specifications, in any order. The planner routes each tee after the conduit it taps, keeps your order otherwise, and says so at ``INFO`` level — this is ``scripts/route_planning/window_tees.py``: .. code-block:: python from routing.core.route_planner import RoutePlanner # tees before their conduits, the tee of a tee first of all entries = [ tee_of_tee, o7, tee_p_big, tee_tuy_mid, tuy, p, tee_o7_thin, tee_s1_thin, s1, tee_o7_mid, tee_s1_mid, ] planner = RoutePlanner(cadmap, finder) result = planner.generate(entries) result = planner.optimize(costs, settings) print(result.summary()) .. code-block:: text tee_TUY_mid is routed after the conduit it taps. tee_P_big is routed after the conduit it taps. tee_S1_thin is routed after the conduit it taps. tee_of_tee is routed after the conduit it taps. ... conduit taps routed paths cost optimized well O7 yes 1 224.3 7628.3 True TUY yes 1 239.4 6908.3 True tee_TUY_mid TUY yes 1 519.7 4541.1 True P yes 1 594.6 9568.9 True tee_P_big P yes 1 1033.7 3346.1 True tee_O7_thin O7 yes 1 3.0 1114.4 True S1 yes 1 90.3 18659.2 True tee_S1_thin S1 yes 1 29.5 2034.2 True tee_of_tee tee_S1_thin yes 1 26.4 3125.1 True tee_O7_mid O7 yes 1 524.6 3960.8 True tee_S1_mid S1 yes 8 95.7 6508.4 True ``tee_S1_mid`` holds eight alternatives: one per site it was explored at. A tee whose conduit is missing from the list, or tees tapping one another in a cycle, raise ``ValueError`` before anything is routed. To read the order before routing, plan the list yourself with a :class:`~routing.core.route_planner.RoutePlanning` and pass the plan to ``generate``. It holds one solution per conduit, in routing order, planned but not routed yet; ``dependents`` lists the tees built on a conduit, directly or through other tees: .. code-block:: python from routing.core.route_planner import RoutePlanning planning = RoutePlanning.from_specifications(entries) print([solution.name for solution in planning.solutions]) # ['O7', 'TUY', 'tee_TUY_mid', 'P', 'tee_P_big', 'tee_O7_thin', 'S1', 'tee_S1_thin', 'tee_of_tee', 'tee_O7_mid', 'tee_S1_mid'] print([tee.name for tee in planning.dependents(planning.solutions[0])]) # ['tee_O7_thin', 'tee_O7_mid']: the tees built on O7 result = planner.generate(planning) See :doc:`../user_guide/core_concepts` for the rules of the order and the names. ``optimize`` walks the same order. The packs are rebuilt from each optimized geometry, so a conduit is optimized against the conduits finalized before it, and a tee is **routed again on the optimized conduit** — its tap point moves with the conduit — before its own optimization. The junction is not counted as a clash: the leg sits inside the conduit it taps by construction. Picking a Site -------------- Every :class:`~routing.core.core.Solution` keeps its routes as **alternatives** and stands by one of them: the cheapest generated path, until you pick another one. For a tee each alternative ends on its own tap port, so picking an alternative picks the site: .. code-block:: python tee = result.solution("tee_S1_mid") for index, alternative in enumerate(tee.alternatives): print(index, alternative.name, alternative.grid_path.cost, alternative.output_port.coordinates) tee.pick(3) # by index, after a look tee.pick_nearest((0.5, 0.2, 0.45)) # or the alternative tapping nearest a point result = planner.optimize(costs, settings) At the next ``optimize`` the tee is routed again at the site it was picked at, on its conduit as it stands. If the optimized conduit no longer lets it tap there, it taps at the nearest site in reach, with a warning. Picking a conduit that tees tap works the same way: at the next ``optimize``, the tees follow the alternative the conduit stands by. Holding the Cycle by Hand ------------------------- The automatic mode routes and optimizes everything at once. To check the conduits before tapping them, run the cycle in steps — this is ``scripts/route_planning/window_tees_by_hand.py``. 1. **The conduits tapped, first.** Generate them with two paths each, retain one, optimize: .. code-block:: python planner = RoutePlanner(cadmap, finder, n_iterations=2) result = planner.generate([o7, p, tuy, s1]) result.solution("O7").pick(1) # stand by the second path of O7 result = planner.optimize(costs, settings) ``n_iterations`` is the number of paths searched per conduit (k shortest paths), at generation and when ``optimize`` routes a tee again; ``generate(n_iterations=...)`` sets it too. 2. **The tees, on the conduits as they stand.** A tee may tap a conduit already routed: pass its :class:`~routing.core.core.Solution` instead of its specification. A tee on a tee of the same batch taps that tee's specification, planned before it: .. code-block:: python planner.n_iterations = 0 # one route per tee tee_o7_thin = TeeSpecification(port_o7_thin, result.solution("O7"), thin_type, name="tee_O7_thin") tee_s1_thin = TeeSpecification(port_s1_thin, result.solution("S1"), thin_type, name="tee_S1_thin") tee_of_tee = TeeSpecification(port_tee_of_tee, tee_s1_thin, thin_type, name="tee_of_tee") tees = [tee_o7_thin, tee_s1_thin, tee_of_tee] result = planner.generate(tees) result = planner.optimize(costs, settings, solutions=[result.solution(tee.name) for tee in tees]) ``solutions`` restricts the optimization: the conduits left out are kept as they stand. 3. **A tee explored before it is laid.** ``explore`` routes a tee once per site on the conduits as they stand, and lays nothing; the routes are the alternatives of the solution it returns. Look at them, pick one, and optimize that solution: it is laid at its site. .. code-block:: python from volmdlr.model import VolumeModel tee_s1_mid = TeeSpecification(port_s1_mid, result.solution("S1"), mid_type, name="tee_S1_mid") explored = planner.explore(tee_s1_mid, step=0.3) VolumeModel(explored.volmdlr_primitives() + result.solution("S1").volmdlr_primitives()).display_3d() explored.pick_nearest((0.5, 0.2, 0.45)) result = planner.optimize(costs, settings, solutions=[explored]) Reading the Result ------------------ .. code-block:: python tee = result.solution("tee_O7_thin") tee.is_routed # False when no route joins it tee.terminals.tapped.name # "O7": the conduit it taps tee.output_port # the tap port of the alternative it stands by, on O7's axis tee.best_pipe # its optimized geometry, else its generated one tee.is_well_optimized # an optimized geometry passed the optimizer's checks ``result.summary()`` prints one line per conduit: the conduit it taps, whether it is routed, its number of alternatives, its generated cost, its optimized cost, and whether it is well optimized. See :doc:`../user_guide/results_access` for the alternatives of a solution. Good to Know ------------ - **The k shortest paths of a tee end on one tap port.** ``n_iterations`` varies the path, not the site; to compare sites, use ``explored_step`` or ``explore``. - **Forced zones apply to tees.** A tee passes the forced zones of the map first, then taps its conduit (see :doc:`shaped_zones`). - **A port facing its conduit.** When the tee's port already faces the conduit at the right distance, the route is a single cell: the port, the leg, the tap port. - **The optimizer is stochastic.** Optimized costs change from run to run: compare the structure of two runs, not their decimals. - **Harness generation does not know tees yet.** :meth:`~routing.core.route_planner.RoutePlanner.generate_harness` reads a tap port as the end of a conduit, not as a junction. Complete Example ---------------- The inputs — the window map, the four conduits, the three tee pipe types, the seven tees and their disordered list — live in ``scripts/route_planning/window_tees_base.py``. The automatic mode is then: .. code-block:: python from volmdlr.model import VolumeModel from window_tees_base import cadmap, costs, entries, finder, settings from routing.core.route_planner import RoutePlanner planner = RoutePlanner(cadmap, finder) result = planner.generate(entries) result = planner.optimize(costs, settings) print(result.summary()) VolumeModel(result.volmdlr_primitives()).display_3d() See ``scripts/route_planning/window_tees.py`` for the automatic mode and ``scripts/route_planning/window_tees_by_hand.py`` for the cycle by hand. Next Steps ---------- - :doc:`../user_guide/core_concepts` — specifications, solutions and the planner - :doc:`../user_guide/results_access` — alternatives, refinements and the pick - :doc:`../user_guide/optimization` — the optimizer and the order it follows - :doc:`../user_guide/migration_v3_0_0` — what changed for code written before tees