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 |
|---|---|
|
A tee from |
|
A place along a conduit where a tee may tap, in metres from the conduit’s start port |
|
The tee taps at those sites only |
|
The tee is routed once per site, one every 0.3 m; it stands by the cheapest route, the others are kept as alternatives |
|
The routing order — each tee after the conduit it taps — and one solution per conduit;
|
|
Routes the conduits, each tee after the conduit it taps |
|
Optimizes them in the same order, each tee rerouted on its optimized conduit |
|
Routes a tee once per site on the conduits as they stand, laying nothing |
|
Retains one alternative of a conduit; for a tee, the site where it taps |
|
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_minof 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.
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_iterationsis the number of paths searched per conduit (k shortest paths), at generation and whenoptimizeroutes a tee again;generate(n_iterations=...)sets it too.The tees, on the conduits as they stand. A tee may tap a conduit already routed: pass its
Solutioninstead 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])
solutionsrestricts the optimization: the conduits left out are kept as they stand.A tee explored before it is laid.
exploreroutes 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_iterationsvaries the path, not the site; to compare sites, useexplored_steporexplore.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#
User Guide: Core Concepts — specifications, solutions and the planner
User Guide: Accessing Routing Results — alternatives, refinements and the pick
User Guide: Path Geometry Optimization — the optimizer and the order it follows
Migrating to v3.0.0 — what changed for code written before tees