STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
test_chamber_geometry_volume.py
Go to the documentation of this file.
1"""The drawn chamber must actually hold the commanded L* x A_t.
2
3Guards the convergent-volume term in chamber_length_calc. The old form used
4(eps^(1/3) - 1) instead of (eps^(3/2) - 1), understating the cone by ~18x at
5typical contraction ratios and padding the barrel to compensate.
6"""
7import math
8import numpy as np
9import pytest
10
12 chamber_length_calc,
13 contraction_length_horizontal_calc,
14 theta_default,
15)
16
17
18def _tangency_radius(R_t, theta):
19 """Radius where the 1.5*R_t entrance arc is tangent to a cone of half-angle theta."""
20 return R_t * (1.0 + 1.5 * (1.0 - math.cos(theta)))
21
22
23def _drawn_volume(L_cyl, L_con, R_c, R_t, theta, n=200001):
24 """Volume of the solid of revolution ACTUALLY generated: cylinder, cone to the arc
25 tangency radius, then the 1.5*R_t entrance arc down to the throat.
26
27 This used to draw a straight cone all the way to R_t, which is not what the generator
28 makes -- so it reproduced the very convention the volume arithmetic got wrong and could
29 not catch the entrance-arc bug. See the real-contour tests at the bottom of this file.
30 """
31 r_tan = _tangency_radius(R_t, theta)
32 x1 = np.linspace(0.0, L_cyl, n // 3)
33 r1 = np.full_like(x1, R_c)
34 x2 = np.linspace(L_cyl, L_cyl + L_con, n // 3)
35 r2 = R_c + (r_tan - R_c) * (x2 - L_cyl) / L_con
36 t = np.linspace(-(math.pi / 2 + theta), -math.pi / 2, n // 3)
37 x3 = 1.5 * R_t * np.cos(t)
38 x3 = x3 - x3[0] + (L_cyl + L_con)
39 r3 = 1.5 * R_t * np.sin(t) + 2.5 * R_t
40 x = np.concatenate([x1, x2, x3])
41 r = np.concatenate([r1, r2, r3])
42 trapz = getattr(np, "trapezoid", None) or np.trapz
43 return float(trapz(math.pi * r * r, x))
44
45
46@pytest.mark.parametrize("D_c,Lstar", [
47 (0.127, 1.30), (0.127, 1.00), (0.127, 0.80),
48 (0.1143, 1.00), (0.1397, 1.00), (0.1524, 1.20),
49])
50@pytest.mark.parametrize("theta", [math.radians(30.0), math.radians(45.0)])
52 A_t = 0.0018729167346808366
53 A_c = math.pi / 4 * D_c ** 2
54 R_c, R_t = D_c / 2, math.sqrt(A_t / math.pi)
55 V_cmd = Lstar * A_t
56
57 L_cyl = chamber_length_calc(V_cmd, A_t, A_c / A_t, theta)
58 L_con = contraction_length_horizontal_calc(A_c, _tangency_radius(R_t, theta), theta)
59 assert L_cyl > 0, "degenerate case: no cylindrical section"
60
61 V_drawn = _drawn_volume(L_cyl, L_con, R_c, R_t, theta)
62 # 0.1 % covers trapezoid discretisation only
63 assert V_drawn == pytest.approx(V_cmd, rel=1e-3), (
64 f"drawn L* = {V_drawn / A_t:.4f} but {Lstar} was commanded"
65 )
66
67
69 """The pre-fix formula overshoots by ~10 % -- proves this test has teeth."""
70 A_t = 0.0018729167346808366
71 D_c = 0.127
72 A_c = math.pi / 4 * D_c ** 2
73 eps = A_c / A_t
74 R_t = math.sqrt(A_t / math.pi)
75 V_cmd = 1.30 * A_t
76
77 old_L_cyl = (V_cmd / A_t - (1 / 3) * R_t * (1 / math.tan(theta_default))
78 * (eps ** (1 / 3) - 1)) / eps
79 L_con = contraction_length_horizontal_calc(
80 A_c, _tangency_radius(R_t, theta_default), theta_default)
81 V_old = _drawn_volume(old_L_cyl, L_con, D_c / 2, R_t, theta_default)
82 assert V_old / V_cmd > 1.05, "old formula should overshoot volume by >5 %"
83
84
85# ---------------------------------------------------------------------------------------
86# The tests above integrate a SYNTHETIC contour (cylinder + straight cone to R_t). That is
87# a valid guard for the eps exponent, but it reproduces the generator's old convention and
88# therefore could never catch the second volume bug: the cone does not reach R_t, it reaches
89# the tangency radius of the 1.5*R_t entrance arc, and the arc carries its own volume.
90# Layer 1 passed R_throat as the entrance radius and chamber_length_calc credited a straight
91# frustum all the way down, so chambers were drawn 3.7-10.2% over the commanded L*.
92#
93# These tests integrate the REAL generated contour instead. That is the only thing that can
94# catch a disagreement between what the volume arithmetic assumes and what actually gets drawn.
95# ---------------------------------------------------------------------------------------
96
97from engine.core.chamber_geometry import ( # noqa: E402
98 chamber_geometry_calc,
99 area_throat_calc,
100 entrance_arc_volume,
101 force_coeffcient_default,
102)
103
104_PC, _F, _D_EXIT = 2.965e6, 8000.0, 0.1165
105
106
107def _real_drawn_lstar(D_c: float, Lstar: float, theta: float) -> float:
108 """L* of the contour chamber_geometry_calc ACTUALLY generates, face to throat."""
109 pts, _, _ = chamber_geometry_calc(
110 pc_design=_PC, thrust_design=_F, diameter_inner=D_c,
111 diameter_exit=_D_EXIT, l_star=Lstar, steps=3000, theta=theta)
112 p = np.asarray(pts)
113 throat = int(np.argmin(p[:, 1]))
114 seg = p[:throat + 1]
115 trapz = getattr(np, "trapezoid", None) or np.trapz
116 vol = float(trapz(math.pi * seg[:, 1] ** 2, seg[:, 0]))
117 return vol / area_throat_calc(_PC, _F, force_coeffcient_default)
118
119
120@pytest.mark.parametrize("Lstar", [0.85, 1.00, 1.15, 1.30])
121@pytest.mark.parametrize("theta_deg", [45.0, 40.0, 35.0, 30.0])
123 """The chamber that gets DRAWN must hold the volume that was asked for."""
124 drawn = _real_drawn_lstar(0.127, Lstar, math.radians(theta_deg))
125 rel = abs(drawn - Lstar) / Lstar
126 assert rel < 2.0e-3, (
127 f"commanded L* = {Lstar} at theta = {theta_deg} deg, but the generated contour "
128 f"integrates to L* = {drawn:.4f} ({rel*100:+.2f}%). The volume arithmetic in "
129 f"chamber_length_calc disagrees with what chamber_geometry_calc draws."
130 )
131
132
134 """Guard the guard: if the arc term were dropped the test above must fail, not pass."""
135 A_t = area_throat_calc(_PC, _F, force_coeffcient_default)
136 R_t = math.sqrt(A_t / math.pi)
137 V_arc = entrance_arc_volume(R_t, math.radians(45.0))
138 # at L* = 1.0 the whole chamber is 1.0 * A_t; the arc is a per-cent-level slice of it
139 frac = V_arc / (1.0 * A_t)
140 assert 0.005 < frac < 0.10, (
141 f"entrance arc is {frac*100:.2f}% of a L*=1.0 chamber - outside the range that makes "
142 f"this correction meaningful; check entrance_arc_volume()"
143 )
144
145
147 """r_tan = R_t(1 + 1.5(1-cos theta)) must exceed R_t, or the cone would run past the arc."""
148 for deg in (25.0, 30.0, 45.0):
149 f = 1.0 + 1.5 * (1.0 - math.cos(math.radians(deg)))
150 assert f > 1.0, f"tangency factor {f} at {deg} deg is not above the throat radius"
151 assert abs((1.0 + 1.5 * (1.0 - math.cos(math.radians(45.0)))) - 1.43934) < 1e-4
test_drawn_volume_matches_commanded_lstar(D_c, Lstar, theta)
float _real_drawn_lstar(float D_c, float Lstar, float theta)
test_generated_contour_has_the_commanded_lstar(Lstar, theta_deg)
_drawn_volume(L_cyl, L_con, R_c, R_t, theta, n=200001)