92 """Triangulation topology of an nx-by-nr logical node grid (node id = i*nr + j):
93 each logical quad cell split into 2 triangles along the same diagonal.
95 Shared by the plain tensor-product meshes below AND the wall-mapped meshes of
96 ``contour.py`` (where r depends on x): the topology is identical, only the node
97 coordinates differ — so the triangulation convention lives in exactly one place.
99 def nid(i: int, j: int) -> int:
103 for i
in range(nx - 1):
104 for j
in range(nr - 1):
105 n00, n10 = nid(i, j), nid(i + 1, j)
106 n01, n11 = nid(i, j + 1), nid(i + 1, j + 1)
108 tris.append((n00, n10, n11))
109 tris.append((n00, n11, n01))
110 return np.asarray(tris, dtype=np.int64)
114 """Shared builder: structured triangulation over an arbitrary (not necessarily
115 uniform) axial node grid ``xs`` crossed with a radial node grid ``rs``.
117 Both ``cylinder_mesh`` (uniform ``xs`` spacing) and ``two_zone_duct_mesh`` (two
118 uniformly-spaced runs of ``xs`` stitched together, with a shared column at the
119 zone interface) delegate to this.
121 nx, nr = len(xs), len(rs)
122 X, Rr = np.meshgrid(xs, rs, indexing=
"ij")
123 nodes = np.column_stack([X.ravel(), Rr.ravel()])
128 """Structured mesh of the rectangle [0, L] x [0, R] (uniform cylinder, case V1).
130 ``nx``, ``nr`` are node counts (not cell counts) along the axial and radial
131 directions. Each of the ``(nx-1)*(nr-1)`` rectangular cells is split into 2
132 triangles sharing the same diagonal, so refinement is simply increasing nx, nr.
135 raise ValueError(
"cylinder_mesh needs at least 2 nodes per direction")
137 raise ValueError(
"L and R must be positive")
143 ) -> tuple[MeridionalMesh, np.ndarray]:
144 """Structured mesh of a duct [0, L1+L2] x [0, R] with an exact node column at the
145 zone interface ``x = L1`` (verification case V2, temperature-jump duct).
147 Why an exact shared column, and not two independent grids glued approximately:
148 the pressure field itself is physically continuous across a sound-speed
149 discontinuity (only its second derivative jumps — see the derivation in
150 ``validation/v2_temperature_jump.py``), so the standard, exact way to represent
151 that in FEM is a single shared node at the interface, with each side's *element*
152 (not node) carrying its own material property. Building ``xs`` as two runs of
153 ``linspace`` that share their common endpoint (rather than gluing two separately-
154 spaced grids) guarantees that shared column exists exactly, with no search or
155 tolerance-based node-snapping needed.
157 Returns ``(mesh, zone_of_element)`` where ``zone_of_element`` is an ``(n_tri,)``
158 int array, 1 for elements entirely in ``[0, L1]`` and 2 for elements entirely in
159 ``[L1, L1+L2]`` — unambiguous because no element straddles the shared column by
162 if nx1 < 2
or nx2 < 2
or nr < 2:
163 raise ValueError(
"two_zone_duct_mesh needs at least 2 nodes per direction/zone")
164 if L1 <= 0
or L2 <= 0
or R <= 0:
165 raise ValueError(
"L1, L2, and R must be positive")
167 xs1 = np.linspace(0.0, L1, nx1)
168 xs2 = np.linspace(L1, L1 + L2, nx2)
169 xs = np.concatenate([xs1, xs2[1:]])
170 rs = np.linspace(0.0, R, nr)
173 interface_col = nx1 - 1
176 for i
in range(len(xs) - 1):
177 zone = 1
if (i < interface_col)
else 2
178 zone_of_element += [zone, zone] * n_r_cells
179 return mesh, np.asarray(zone_of_element, dtype=np.int64)