161 runner: PintleEngineRunner,
162 target_thrust_kN: float,
164 initial_guess_psi: Optional[Tuple[float, float]] =
None,
165 max_iterations: int = 50,
166 tolerance: float = 0.01,
167) -> Tuple[Tuple[float, float], Dict[str, Any], Dict[str, Any]]:
168 """Solve for tank pressures that achieve both target thrust AND target O/F ratio.
170 This uses a 2D Newton-Raphson iteration to find (P_tank_O, P_tank_F) such that:
171 - Thrust(P_O, P_F) = target_thrust_kN
172 - MR(P_O, P_F) = target_MR
176 runner : PintleEngineRunner
177 Engine pipeline runner
178 target_thrust_kN : float
181 Desired mixture ratio (O/F)
182 initial_guess_psi : tuple, optional
183 Starting guess for (P_tank_O, P_tank_F) in psi
184 If None, uses (1305, 974) as default
186 Maximum Newton iterations
188 Convergence tolerance (relative error)
192 (P_tank_O_psi, P_tank_F_psi) : tuple
193 Solution tank pressures [psi]
195 Full pipeline results at solution
197 Iteration history and convergence info
199 from scipy.optimize
import fsolve
201 if initial_guess_psi
is None:
202 initial_guess_psi = (1305.0, 974.0)
204 P_O_guess, P_F_guess = initial_guess_psi
216 def residual(P_psi: np.ndarray) -> np.ndarray:
217 """Residual function for fsolve: [thrust_error, MR_error]"""
218 P_O_psi, P_F_psi = P_psi
221 if P_O_psi <= 0
or P_F_psi <= 0:
222 return np.array([1e6, 1e6])
225 P_O_pa = P_O_psi * PSI_TO_PA
226 P_F_pa = P_F_psi * PSI_TO_PA
227 res = runner.evaluate(P_O_pa, P_F_pa)
229 thrust_kN = res[
"F"] / 1000.0
233 thrust_error = (thrust_kN - target_thrust_kN) / target_thrust_kN
if target_thrust_kN > 0
else 1.0
234 MR_error = (MR - target_MR) / target_MR
if target_MR > 0
else 1.0
237 history[
"P_tank_O"].append(P_O_psi)
238 history[
"P_tank_F"].append(P_F_psi)
239 history[
"thrust"].append(thrust_kN)
240 history[
"MR"].append(MR)
241 history[
"thrust_error"].append(thrust_error)
242 history[
"MR_error"].append(MR_error)
244 return np.array([thrust_error, MR_error])
246 except Exception
as exc:
248 return np.array([1e6, 1e6])
252 from scipy.optimize
import root
259 x0=np.array([P_O_guess, P_F_guess]),
261 options={
'xtol': tolerance,
'maxfev': max_iterations * 10},
264 if not result.success:
268 x0=np.array([P_O_guess, P_F_guess]),
270 options={
'xtol': tolerance,
'max_nfev': max_iterations * 10},
274 ier = 1
if result.success
else 0
275 msg = result.message
if hasattr(result,
'message')
else str(result.status)
277 P_O_sol, P_F_sol = solution
281 if ier != 1
or not result.success:
284 test_results = runner.evaluate(P_O_sol * PSI_TO_PA, P_F_sol * PSI_TO_PA)
285 test_thrust = test_results[
"F"] / 1000.0
286 test_MR = test_results[
"MR"]
287 thrust_err = abs(test_thrust - target_thrust_kN) / target_thrust_kN
if target_thrust_kN > 0
else 1.0
288 MR_err = abs(test_MR - target_MR) / target_MR
if target_MR > 0
else 1.0
291 if thrust_err < tolerance * 2.0
and MR_err < tolerance * 2.0:
296 f
"Root finder did not converge: {msg}",
299 "final_pressures": (P_O_sol, P_F_sol),
300 "target_thrust": target_thrust_kN,
301 "target_MR": target_MR,
302 "actual_thrust": test_thrust,
303 "actual_MR": test_MR,
306 except Exception
as e:
308 f
"Root finder did not converge: {msg}",
311 "final_pressures": (P_O_sol, P_F_sol),
312 "target_thrust": target_thrust_kN,
313 "target_MR": target_MR,
318 final_results = runner.evaluate(P_O_sol * PSI_TO_PA, P_F_sol * PSI_TO_PA)
319 final_thrust = final_results[
"F"] / 1000.0
320 final_MR = final_results[
"MR"]
322 thrust_error_pct = abs((final_thrust - target_thrust_kN) / target_thrust_kN) * 100
323 MR_error_pct = abs((final_MR - target_MR) / target_MR) * 100
327 "iterations": len(history[
"thrust"]),
328 "final_thrust": final_thrust,
329 "final_MR": final_MR,
330 "thrust_error_pct": thrust_error_pct,
331 "MR_error_pct": MR_error_pct,
335 return (P_O_sol, P_F_sol), final_results, diagnostics
337 except Exception
as exc:
339 f
"Failed to solve for thrust and O/F: {exc}",
342 "target_thrust": target_thrust_kN,
343 "target_MR": target_MR,
344 "initial_guess": initial_guess_psi,
350 runner: PintleEngineRunner,
351 target_thrust_kN: float,
352 base_pressures_psi: Tuple[float, float],
353 scale_bounds: Tuple[float, float] = (0.2, 5.0),
354 num_samples: int = 60,
355) -> Tuple[Tuple[float, float], Dict[str, Any], Dict[str, Any]]:
356 """Solve for tank pressures that achieve the target thrust.
358 The solution scales the baseline tank pressures by a factor 'scale'.
359 The function also returns diagnostic information useful for UIs.
362 base_O_psi, base_F_psi = base_pressures_psi
363 if base_O_psi <= 0
or base_F_psi <= 0:
364 raise ValueError(
"Baseline tank pressures must be positive.")
366 base_pressures_pa = (base_O_psi * PSI_TO_PA, base_F_psi * PSI_TO_PA)
369 baseline_results = runner.evaluate(*base_pressures_pa)
370 except Exception
as exc:
372 f
"Baseline evaluation failed: {exc}",
374 "baseline_pressed": base_pressures_pa,
375 "target_thrust": target_thrust_kN,
378 baseline_thrust = baseline_results[
"F"] / 1000.0
380 scale_min, scale_max = scale_bounds
381 if scale_min <= 0
or scale_max <= 0:
382 raise ValueError(
"Scale bounds must be positive.")
385 sample_scales = np.linspace(scale_min, scale_max, num_samples)
386 sample_scales = np.unique(np.append(sample_scales, 1.0))
391 cache: Dict[float, Dict[str, Any]] = {}
392 invalid_samples: list[Tuple[float, str]] = []
394 for scale
in sample_scales:
395 P_tank_O = scale * base_pressures_pa[0]
396 P_tank_F = scale * base_pressures_pa[1]
398 res = runner.evaluate(P_tank_O, P_tank_F)
399 except Exception
as exc:
400 invalid_samples.append((scale, str(exc)))
401 cache[scale] = {
"error": str(exc)}
402 thrust_samples.append(np.nan)
403 diff_samples.append(np.nan)
407 thrust = res[
"F"] / 1000.0
408 thrust_samples.append(thrust)
409 diff_samples.append(thrust - target_thrust_kN)
411 thrust_array = np.asarray(thrust_samples, dtype=float)
412 diff_array = np.asarray(diff_samples, dtype=float)
413 finite_mask = np.isfinite(thrust_array)
415 if not np.any(finite_mask):
417 "baseline_thrust": baseline_thrust,
418 "scale_bounds": scale_bounds,
419 "sample_scales": sample_scales,
420 "sample_thrusts": thrust_samples,
421 "invalid_samples": invalid_samples,
424 "All sampled scale evaluations failed. Consider adjusting baseline pressures or scale bounds.",
428 valid_scales = sample_scales[finite_mask]
429 valid_thrusts = thrust_array[finite_mask]
430 valid_diffs = diff_array[finite_mask]
432 thrust_min = float(np.min(valid_thrusts))
433 thrust_max = float(np.max(valid_thrusts))
436 "baseline_thrust": baseline_thrust,
437 "min_thrust": thrust_min,
438 "max_thrust": thrust_max,
439 "scale_bounds": scale_bounds,
440 "sample_scales": sample_scales,
441 "sample_thrusts": thrust_samples,
442 "invalid_samples": invalid_samples,
445 if target_thrust_kN < thrust_min
or target_thrust_kN > thrust_max:
447 f
"Target thrust {target_thrust_kN:.2f} kN is outside achievable range "
448 f
"[{thrust_min:.2f}, {thrust_max:.2f}] kN for scale bounds {scale_bounds}. "
449 f
"Baseline thrust: {baseline_thrust:.2f} kN.",
453 bracket: Optional[Tuple[float, float]] =
None
454 for idx
in range(len(valid_scales) - 1):
455 diff_i = valid_diffs[idx]
456 diff_j = valid_diffs[idx + 1]
458 if abs(diff_i) < 1e-3:
459 scale = valid_scales[idx]
460 P_tank_O_solution = scale * base_pressures_pa[0]
461 P_tank_F_solution = scale * base_pressures_pa[1]
462 results = cache.get(scale)
464 results = runner.evaluate(P_tank_O_solution, P_tank_F_solution)
465 return (P_tank_O_solution, P_tank_F_solution), results, diagnostics
467 if diff_i * diff_j <= 0:
468 bracket = (valid_scales[idx], valid_scales[idx + 1])
473 "Failed to bracket target thrust. Consider expanding scale bounds.",
482 args=(runner, base_pressures_pa, target_thrust_kN),
487 except Exception
as exc:
490 P_tank_O_solution = scale * base_pressures_pa[0]
491 P_tank_F_solution = scale * base_pressures_pa[1]
492 results = runner.evaluate(P_tank_O_solution, P_tank_F_solution)
494 return (P_tank_O_solution, P_tank_F_solution), results, diagnostics
Tuple[Tuple[float, float], Dict[str, Any], Dict[str, Any]] solve_for_thrust_and_MR(PintleEngineRunner runner, float target_thrust_kN, float target_MR, Optional[Tuple[float, float]] initial_guess_psi=None, int max_iterations=50, float tolerance=0.01)
Tuple[Tuple[float, float], Dict[str, Any], Dict[str, Any]] solve_for_thrust(PintleEngineRunner runner, float target_thrust_kN, Tuple[float, float] base_pressures_psi, Tuple[float, float] scale_bounds=(0.2, 5.0), int num_samples=60)