282 """Run the full isentropic cold-gas analysis and return results."""
286 P0 = cfg.inlet_pressure
287 T0 = cfg.inlet_temperature
290 eps = cfg.expansion_ratio
291 Pa = cfg.ambient_pressure
292 eta = cfg.nozzle_efficiency
295 M_exit, converged = solve_mach_robust(eps, g, supersonic=
True)
298 f
"Mach solver did not converge for ε={eps:.4f}, γ={g:.4f}"
303 T_exit = T0 / (1.0 + (g - 1.0) / 2.0 * M_exit ** 2)
306 P_exit = P0 * (1.0 + (g - 1.0) / 2.0 * M_exit ** 2) ** (-g / (g - 1.0))
309 v_exit_ideal = M_exit * np.sqrt(g * R * T_exit)
310 v_exit = v_exit_ideal * eta
314 critical_pr = ((g + 1.0) / 2.0) ** (g / (g - 1.0))
315 actual_pr = P0 / Pa
if Pa > 0
else float(
'inf')
316 is_choked = actual_pr >= critical_pr
320 mass_flow_factor = (2.0 / (g + 1.0)) ** ((g + 1.0) / (2.0 * (g - 1.0)))
321 mdot = P0 * A_t * np.sqrt(g / (R * T0)) * mass_flow_factor
324 momentum_thrust = mdot * v_exit
325 pressure_thrust = (P_exit - Pa) * A_e
326 thrust = momentum_thrust + pressure_thrust
329 Isp = thrust / (mdot * G0)
if mdot > 0
else 0.0
333 torque = thrust * arm
336 I_rod = (1.0 / 12.0) * cfg.rocket_mass * cfg.rocket_length ** 2
337 alpha = torque / I_rod
if I_rod > 0
else 0.0
342 specific_impulse=Isp,
343 momentum_thrust=momentum_thrust,
344 pressure_thrust=pressure_thrust,
346 exit_velocity=v_exit,
347 exit_temperature=T_exit,
348 exit_pressure=P_exit,
351 throat_mach=1.0
if is_choked
else 0.0,
353 critical_pressure_ratio=critical_pr,
357 chamber_diameter=cfg.chamber_diameter,
358 chamber_length=cfg.chamber_length,
360 total_mass_flow=mdot * cfg.num_thrusters,
373 do_plot: bool =
True,
374 color_segments: bool =
True,
376 """Generate the converging-diverging nozzle + chamber contour.
378 Two paths are available:
380 **Solved path** (preferred): when ``config.volume_chamber > 0``, calls
381 :func:`engine.core.chamber_geometry_solver.solved_chamber_plot` which is
382 purely geometric — no CEA or thermodynamic iteration required. Requires:
383 ``A_throat``, ``A_exit``, ``volume_chamber``, ``chamber_diameter``, and
384 ``chamber_length`` (``Lstar`` is optional / informational).
386 **Fallback path**: when ``volume_chamber`` is not set, the original
387 hand-rolled contour (cylindrical section + 45° contraction + Rao bell) is
388 used for backward-compatibility.
393 Number of points per contour segment.
395 Whether to save a plot to ``payload/cgt_nozzle.png``.
396 color_segments : bool
397 Colour-code sections in the plot (fallback path only).
401 pts : np.ndarray, shape (N, 2)
402 Contour points (axial x [m], radius y [m]).
404 import matplotlib.pyplot
as plt
412 if cfg._use_solved_plot:
415 lstar_val = cfg.Lstar
if cfg.Lstar > 0
else cfg._effective_volume_chamber / cfg.A_throat
416 pts, _table, _lengths = solved_chamber_plot(
417 area_throat=cfg.A_throat,
418 area_exit=cfg.A_exit,
419 volume_chamber=cfg._effective_volume_chamber,
421 chamber_diameter=cfg.chamber_diameter,
422 length=cfg.chamber_length,
428 pts, nozzle_x_first, nozzle_y_first = generate_nozzle(
429 cfg.A_throat, cfg.A_exit, steps=steps
432 r_c = cfg.chamber_diameter / 2.0
433 x_cyl_start = nozzle_x_first + nozzle_y_first - r_c
435 x_contraction = np.linspace(x_cyl_start, nozzle_x_first, steps)
436 y_contraction = r_c - x_contraction + x_cyl_start
438 x_cyl_end = x_cyl_start - cfg.chamber_length
439 x_cyl = np.linspace(x_cyl_end, x_cyl_start, steps)
440 y_cyl = np.full_like(x_cyl, r_c)
443 np.column_stack((x_cyl, y_cyl)),
444 np.column_stack((x_contraction[1:], y_contraction[1:])),
452 x_mm = pts[:, 0] * 1e3
453 y_mm = pts[:, 1] * 1e3
454 x_span = x_mm.max() - x_mm.min()
455 y_span = 2.0 * y_mm.max()
458 fig_w = x_span * (1.0 + 2 * margin)
459 fig_h = y_span * (1.0 + 2 * margin)
460 scale = 10.0 /
max(fig_w, fig_h)
461 fig_w_in =
max(fig_w * scale, 4.0)
462 fig_h_in =
max(fig_h * scale, 2.0)
464 fig, ax = plt.subplots(figsize=(fig_w_in, fig_h_in))
465 ax.plot(x_mm, y_mm,
"k-", linewidth=2)
466 ax.plot(x_mm, -y_mm,
"k-", linewidth=2)
467 ax.axhline(0, color=
"grey", linewidth=0.5, linestyle=
"--")
468 ax.set_aspect(
"equal", adjustable=
"box")
469 ax.set_xlabel(
"Axial position [mm]")
470 ax.set_ylabel(
"Radius [mm]")
471 path_label =
"solved" if cfg._use_solved_plot
else "hand-rolled"
472 ax.set_title(f
"Cold Gas Thruster — Full Contour ({path_label})")
473 ax.grid(
True, alpha=0.3)
476 f
"Vol = {cfg._effective_volume_chamber*1e6:.3f} cm³\n"
477 if cfg._use_solved_plot
else ""
480 f
"Chamber ∅ {cfg.chamber_diameter*1e3:.1f} mm\n"
481 f
"Throat ∅ {cfg.throat_diameter*1e3:.2f} mm\n"
482 f
"Exit ∅ {cfg.exit_diameter*1e3:.2f} mm\n"
483 f
"ε = {cfg.expansion_ratio:.2f}\n"
488 transform=ax.transAxes,
489 fontsize=8, va=
"top",
490 bbox=dict(boxstyle=
"round", facecolor=
"white", alpha=0.85),
493 os.makedirs(
"payload", exist_ok=
True)
494 fig.savefig(
"payload/cgt_nozzle.png", dpi=150, bbox_inches=
"tight")