148 length_cylindrical: float, gamma: float,
149 theta_contraction_deg: float = 45.0,
150 arc_factor: float = 1.5,
151 truncate_area_ratio: Optional[float] =
None
152 ) -> ChamberAcousticContour:
153 """Build the acoustic contour from the same inputs the hardware geometry uses
154 (``ChamberGeometryConfig``: chamber_diameter, A_throat, length_cylindrical;
155 the 45 deg cone and 1.5*R_t arc are the hardware defaults from
156 ``chamber_geometry_solver.solved_chamber_plot``).
158 ``truncate_area_ratio`` selects the acoustic truncation plane (module docstring):
159 None = convergence-start plane; else the plane where A(x)/A_t equals it.
161 R_c = 0.5 * chamber_diameter
162 R_t = float(np.sqrt(A_throat / np.pi))
164 raise ValueError(f
"throat radius {R_t:.4f} m >= chamber radius {R_c:.4f} m")
165 theta = np.radians(theta_contraction_deg)
168 r_tangent = R_t * (1.0 + f * (1.0 - np.cos(theta)))
170 raise ValueError(
"entrance arc alone exceeds the chamber radius; "
171 "contraction ratio too small for this arc_factor/theta")
172 L_cone = (R_c - r_tangent) / np.tan(theta)
173 x_cone_end = length_cylindrical + L_cone
174 x_throat = x_cone_end + f * R_t * np.sin(theta)
176 CR = (R_c / R_t) ** 2
177 if truncate_area_ratio
is None:
178 x_end = length_cylindrical
181 if not (1.05 <= truncate_area_ratio < CR):
183 f
"truncate_area_ratio must be in [1.05, CR={CR:.2f}) — the throat plane "
184 f
"(ratio 1) is sonic and outside Helmholtz validity")
185 r_trunc = R_t * np.sqrt(truncate_area_ratio)
186 if r_trunc >= r_tangent:
188 x_end = length_cylindrical + (R_c - r_trunc) / np.tan(theta)
192 dx = np.sqrt(rho ** 2 - ((1.0 + f) * R_t - r_trunc) ** 2)
193 x_end = x_throat - dx
194 area_ratio_end = truncate_area_ratio
198 L_cyl=length_cylindrical, x_cone_end=x_cone_end,
199 x_throat=x_throat, x_end=x_end,
200 area_ratio_end=area_ratio_end, mach_end=mach_end)
207def _segment_xs(contour: ChamberAcousticContour, n_axial: int) -> np.ndarray:
208 """Axial node columns: per-segment linspaces sharing endpoints, counts allocated
209 proportionally to segment length (minimum 2 columns per nonempty segment), so the
210 cylinder/cone and cone/arc breaks — and x_end itself — are exact node columns.
212 breaks = [0.0,
min(contour.L_cyl, contour.x_end)]
213 if contour.x_end > contour.L_cyl:
214 breaks.append(
min(contour.x_cone_end, contour.x_end))
215 if contour.x_end > contour.x_cone_end:
216 breaks.append(contour.x_end)
217 breaks = np.array(sorted(set(breaks)))
219 lengths = np.diff(breaks)
220 total = lengths.sum()
222 for k, (a, b)
in enumerate(zip(breaks[:-1], breaks[1:])):
223 n_seg =
max(2, int(round(n_axial * (b - a) / total)) + 1)
224 seg = np.linspace(a, b, n_seg)
225 xs_parts.append(seg
if k == 0
else seg[1:])
226 return np.concatenate(xs_parts)
229def contour_mesh(contour: ChamberAcousticContour, n_axial: int, nr: int) -> MeridionalMesh:
230 """Wall-mapped structured mesh of the acoustic domain.
232 Logical node (i, j) -> (x_i, r_wall(x_i) * j/(nr-1)): row j=0 is exactly the axis,
233 row j=nr-1 exactly the wall, and every column is a constant-x line (so
234 ``nodes_at_x(contour.x_end)`` finds the admittance-plane column exactly, as the
235 boundary assembly requires). Topology shared with the P0 meshes via
236 ``_grid_triangles`` — nothing downstream (assembly, axis conditions, solvers)
237 changes for a mapped domain.
239 if nr < 2
or n_axial < 4:
240 raise ValueError(
"need nr >= 2 and n_axial >= 4")
242 r_wall = contour.r_wall(xs)
243 eta = np.linspace(0.0, 1.0, nr)
244 X = np.repeat(xs, nr)
245 Rr = np.outer(r_wall, eta).ravel()
246 nodes = np.column_stack([X, Rr])
247 return MeridionalMesh(nodes=nodes, triangles=_grid_triangles(len(xs), nr))
ChamberAcousticContour build_chamber_contour(*, float chamber_diameter, float A_throat, float length_cylindrical, float gamma, float theta_contraction_deg=45.0, float arc_factor=1.5, Optional[float] truncate_area_ratio=None)