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.

Overview#

from routing.core.core import TapSite, TeeSpecification
from routing.core.route_planner import RoutePlanner

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 TeeSpecification has a port, like any conduit end, a pipe type, and the conduit it taps — here the Specification of that conduit:

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 TapSite is a curvilinear abscissa: metres along the conduit, from its start port.

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:

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).

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:

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())
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 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:

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 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 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:

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:

    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 Solution instead of its specification. A tee on a tee of the same batch taps that tee’s specification, planned before it:

    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.

    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#

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 User Guide: Accessing Routing Results 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 Tutorial: Routing 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. 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:

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#