408def setup_flight(config, thrust_curve, mdot_lox, mdot_fuel, plot_results=False, liftoff_mass=None,
411 Build and fly the vehicle in RocketPy from a thrust curve and the two propellant flows.
413 ``liftoff_mass`` [kg]: the vehicle as weighed on the rail, loaded and pressed. When given, the
414 airframe's mass is set so the vehicle lifts off at exactly this mass.
416 ``ullage_gas_kg`` [kg]: the gas in the tanks' ullages at T-0 when the caller knows it better
417 than this config's tanks do (Layer X burns the drawing's tanks). The difference from the
418 config-sized ullage is carried as a lumped mass with the airframe: it stays aboard all flight.
420 Returns a dict: apogee (AGL), apogee_asl, max_velocity (vertical), flight (RocketPy Flight),
421 truncation_info, mass_caps, mass_budget, flight_report (see flight_report), params (the config).
423 burn_time = float(config.thrust.burn_time)
426 rho_lox = config.fluids[
'oxidizer'].density
427 rho_rp1 = config.fluids[
'fuel'].density
430 m_lox0 = config.lox_tank.mass
431 m_rp10 = config.fuel_tank.mass
437 lox_max_mass, lox_tank_volume, lox_ff, _ = resolve_lox_tank_limits(config, rho_lox)
438 lox_requested = m_lox0
439 if m_lox0 > lox_max_mass:
441 f
"[flight_sim] Capping LOX mass: {m_lox0:.2f} -> {lox_max_mass:.2f} kg "
442 f
"(tank vol: {lox_tank_volume * 1000:.1f}L, {lox_ff * 100:.0f}% fill)"
444 m_lox0 = lox_max_mass
446 "requested_kg": lox_requested,
447 "effective_kg": m_lox0,
448 "max_fill_kg": lox_max_mass,
449 "tank_volume_m3": lox_tank_volume,
450 "was_capped": lox_requested > m_lox0 + 1e-6,
451 "fill_factor": lox_ff,
454 rp1_max_mass, rp1_tank_volume, fuel_ff, _ = resolve_fuel_tank_limits(config, rho_rp1)
455 fuel_requested = m_rp10
456 if m_rp10 > rp1_max_mass:
458 f
"[flight_sim] Capping Fuel mass: {m_rp10:.2f} -> {rp1_max_mass:.2f} kg "
459 f
"(tank vol: {rp1_tank_volume * 1000:.1f}L, {fuel_ff * 100:.0f}% fill)"
461 m_rp10 = rp1_max_mass
462 mass_caps[
"fuel"] = {
463 "requested_kg": fuel_requested,
464 "effective_kg": m_rp10,
465 "max_fill_kg": rp1_max_mass,
466 "tank_volume_m3": rp1_tank_volume,
467 "was_capped": fuel_requested > m_rp10 + 1e-6,
468 "fill_factor": fuel_ff,
481 n_grid =
max(5001, int(burn_time * 2000) + 1)
482 t_grid = np.linspace(0.0, burn_time, n_grid)
483 mdot_O_grid =
_sample(mdot_lox, t_grid)
484 mdot_F_grid =
_sample(mdot_fuel, t_grid)
488 t_dep = {k: v
for k, v
in t_dep.items()
if v
is not None and v < burn_time}
490 cutoff_reason =
min(t_dep, key=t_dep.get)
491 effective_burn_time = t_dep[cutoff_reason]
492 if effective_burn_time <= 0.0:
495 "error": f
"{cutoff_reason} tank is empty at ignition.",
500 "truncation_info": {
"truncated":
True,
"cutoff_time": 0.0,
"reason": cutoff_reason},
503 f
"{cutoff_reason} tank runs dry at t={effective_burn_time:.4f} s of a {burn_time:.4f} s curve; "
504 "thrust and both flows stop there."
506 print(f
"[flight_sim] {truncation_msg}")
510 "cutoff_time": effective_burn_time,
511 "reason": cutoff_reason,
512 "message": truncation_msg,
515 effective_burn_time = burn_time
516 truncation_info = {
"truncated":
False}
521 if callable(thrust_curve)
and not isinstance(thrust_curve, list):
525 cg = ensure_chamber_geometry(config)
529 if not config.environment:
530 raise ValueError(
"Flight simulation requires 'environment' configuration")
531 if not config.rocket:
532 raise ValueError(
"Flight simulation requires 'rocket' configuration")
533 if not config.lox_tank:
534 raise ValueError(
"Flight simulation requires 'lox_tank' configuration")
535 if not config.fuel_tank:
536 raise ValueError(
"Flight simulation requires 'fuel_tank' configuration")
541 rocket_inertia = config.rocket.inertia
542 rocket_radius = config.rocket.radius
546 hasattr(config.rocket,
'airframe_mass')
and config.rocket.airframe_mass
is not None and
547 hasattr(config.rocket,
'propulsion_dry_mass')
and config.rocket.propulsion_dry_mass
is not None
552 airframe_mass = config.rocket.airframe_mass
553 motor_position = getattr(config.rocket,
'motor_position', 0.5)
556 has_detailed_breakdown = (
557 hasattr(config.rocket,
'engine_mass')
and config.rocket.engine_mass
is not None
560 if has_detailed_breakdown:
563 engine_mass = config.rocket.engine_mass
564 engine_cm_offset = getattr(config.rocket,
'engine_cm_offset', 0.15)
565 lox_tank_structure_mass = getattr(config.rocket,
'lox_tank_structure_mass',
None)
or 0.0
566 fuel_tank_structure_mass = getattr(config.rocket,
'fuel_tank_structure_mass',
None)
or 0.0
567 copv_dry_mass = getattr(config.rocket,
'copv_dry_mass',
None)
or 0.0
570 lox_tank_pos = config.lox_tank.ox_tank_pos
571 fuel_tank_pos = config.fuel_tank.fuel_tank_pos
572 copv_pos = config.press_tank.pres_tank_pos
if config.press_tank
else 0.0
575 motor_dry_mass = engine_mass + lox_tank_structure_mass + fuel_tank_structure_mass + copv_dry_mass
576 propulsion_dry_mass = motor_dry_mass
580 if motor_dry_mass > 0:
582 engine_mass * engine_cm_offset +
583 lox_tank_structure_mass * lox_tank_pos +
584 fuel_tank_structure_mass * fuel_tank_pos +
585 copv_dry_mass * copv_pos
588 weighted_cm = engine_cm_offset
599 def compute_component_inertia(mass, cm_pos, system_cm, radius, height=None):
600 """Compute inertia contribution of a cylindrical component."""
602 return [0.0, 0.0, 0.0]
605 d = cm_pos - system_cm
609 I_local_axial = 0.5 * mass * radius**2
613 if height
is not None and height > 0:
614 I_local_transverse = (1.0/12.0) * mass * (3 * radius**2 + height**2)
616 I_local_transverse = 0.25 * mass * radius**2
620 I_transverse_total = I_local_transverse + mass * d**2
622 return [I_transverse_total, I_transverse_total, I_local_axial]
625 engine_r = rocket_radius * 0.6
627 I_engine = compute_component_inertia(engine_mass, engine_cm_offset, weighted_cm, engine_r, engine_h)
630 lox_tank_r = config.lox_tank.lox_radius
631 lox_tank_h = config.lox_tank.lox_h
632 I_lox_tank = compute_component_inertia(lox_tank_structure_mass, lox_tank_pos, weighted_cm, lox_tank_r, lox_tank_h)
635 fuel_tank_r = config.fuel_tank.rp1_radius
636 fuel_tank_h = config.fuel_tank.rp1_h
637 I_fuel_tank = compute_component_inertia(fuel_tank_structure_mass, fuel_tank_pos, weighted_cm, fuel_tank_r, fuel_tank_h)
640 if copv_dry_mass > 0
and config.press_tank:
641 copv_r = config.press_tank.press_radius
642 copv_h = config.press_tank.press_h
643 I_copv = compute_component_inertia(copv_dry_mass, copv_pos, weighted_cm, copv_r, copv_h)
645 I_copv = [0.0, 0.0, 0.0]
649 I_engine[0] + I_lox_tank[0] + I_fuel_tank[0] + I_copv[0],
650 I_engine[1] + I_lox_tank[1] + I_fuel_tank[1] + I_copv[1],
651 I_engine[2] + I_lox_tank[2] + I_fuel_tank[2] + I_copv[2],
655 engine_cm_offset = weighted_cm
657 print(f
"Using DETAILED mass model (RocketPy native):")
658 print(f
" Engine + plumbing: {engine_mass:.2f} kg at {getattr(config.rocket, 'engine_cm_offset', 0.15):.2f}m above nozzle")
659 print(f
" LOX tank structure: {lox_tank_structure_mass:.2f} kg at {lox_tank_pos:.2f}m (motor coords)")
660 print(f
" Fuel tank structure: {fuel_tank_structure_mass:.2f} kg at {fuel_tank_pos:.2f}m (motor coords)")
661 if copv_dry_mass > 0:
662 print(f
" COPV structure: {copv_dry_mass:.2f} kg at {copv_pos:.2f}m (motor coords)")
663 print(f
" Combined dry mass CM: {weighted_cm:.3f}m above nozzle")
664 print(f
" Motor dry inertia: [{motor_inertia[0]:.4f}, {motor_inertia[1]:.4f}, {motor_inertia[2]:.4f}] kg·m²")
665 print(f
" Total propulsion dry: {propulsion_dry_mass:.2f} kg")
668 propulsion_dry_mass = config.rocket.propulsion_dry_mass
669 propulsion_cm_offset = getattr(config.rocket,
'propulsion_cm_offset', 0.3)
670 motor_dry_mass = propulsion_dry_mass
671 engine_cm_offset = propulsion_cm_offset
674 prop_r = rocket_radius * 0.8
677 I_transverse = (1.0/12.0) * motor_dry_mass * (3 * prop_r**2 + prop_h**2)
678 I_axial = 0.5 * motor_dry_mass * prop_r**2
679 motor_inertia = [I_transverse, I_transverse, I_axial]
681 print(f
"Using SIMPLE propulsion model:")
682 print(f
" Propulsion dry mass: {propulsion_dry_mass:.2f} kg (lumped)")
683 print(f
" Propulsion CM offset: {propulsion_cm_offset:.2f} m above nozzle")
684 print(f
" Propulsion inertia (estimated): [{motor_inertia[0]:.4f}, {motor_inertia[1]:.4f}, {motor_inertia[2]:.4f}] kg·m²")
686 rocket_mass = airframe_mass
689 cm_wo_motor = getattr(config.rocket,
'cm_wo_motor',
None)
690 if cm_wo_motor
is None:
692 cm_wo_motor = motor_position + 1.5
694 total_dry_mass = airframe_mass + propulsion_dry_mass
695 print(f
" Airframe mass: {airframe_mass:.2f} kg")
696 print(f
" Airframe inertia: [{rocket_inertia[0]:.2f}, {rocket_inertia[1]:.2f}, {rocket_inertia[2]:.2f}] kg·m²")
697 print(f
" Total dry mass: {total_dry_mass:.2f} kg")
700 if config.rocket.mass
is None:
701 raise ValueError(
"Rocket configuration must include 'airframe_mass' + 'propulsion_dry_mass' (new) or 'mass' + 'motor' (legacy)")
702 rocket_mass = config.rocket.mass
704 if config.rocket.motor
is None:
705 raise ValueError(
"Legacy config requires 'motor' section with 'dry_mass'")
706 motor_dry_mass = config.rocket.motor.dry_mass
707 motor_inertia = config.rocket.motor_inertia
if config.rocket.motor_inertia
else [0.1, 0.1, 0.1]
709 cm_wo_motor = config.rocket.cm_wo_motor
if config.rocket.cm_wo_motor
else 1.0
710 motor_position = getattr(config.rocket,
'motor_position', 0.5)
711 engine_cm_offset = 0.0
713 print(f
"Using LEGACY mass model:")
714 print(f
" Rocket mass (airframe): {rocket_mass:.2f} kg")
715 print(f
" Motor dry mass: {motor_dry_mass:.2f} kg")
716 print(f
" Total dry mass: {rocket_mass + motor_dry_mass:.2f} kg")
721 date=config.environment.date,
722 latitude=config.environment.latitude,
723 longitude=config.environment.longitude,
724 elevation=config.environment.elevation,
727 atmos = getattr(config.environment,
'atmosphere_model',
'standard_atmosphere')
or 'standard_atmosphere'
728 if str(atmos).lower() ==
'forecast':
730 env.set_atmospheric_model(type=
'Forecast', file=
'GFS')
731 except Exception
as e:
733 print(f
"[flight_sim] GFS forecast unavailable ({e}); falling back to standard atmosphere.")
734 env.set_atmospheric_model(type=
'standard_atmosphere')
736 env.set_atmospheric_model(type=
'standard_atmosphere')
738 env.set_elevation(config.environment.elevation)
747 stack = built_stack(config)
748 lox_geom = CylindricalTank(radius=config.lox_tank.lox_radius, height=stack[
"lox_h"], spherical_caps=
False)
749 rp1_geom = CylindricalTank(radius=config.fuel_tank.rp1_radius, height=stack[
"fuel_h"], spherical_caps=
False)
750 for name, section, h_attr
in ((
"LOX", config.lox_tank,
"lox_h"), (
"fuel", config.fuel_tank,
"rp1_h")):
751 h_cfg = float(getattr(section, h_attr))
752 h_used = stack[
"lox_h" if name ==
"LOX" else "fuel_h"]
753 if abs(h_used - h_cfg) > 1e-6 * h_cfg:
754 report_warnings.append(
755 f
"{name} tank: tank_volume_m3 and pi r^2 {h_attr} disagree; flown at the volume "
756 f
"(height {h_used:.4f} m, config {h_attr} {h_cfg:.4f} m)"
761 ox_name = getattr(config.fluids[
'oxidizer'],
'name',
None)
or "Oxidizer"
762 fuel_name = getattr(config.fluids[
'fuel'],
'name',
None)
or "Fuel"
763 lox = Fluid(name=ox_name, density=rho_lox)
764 rp1 = Fluid(name=fuel_name, density=rho_rp1)
776 for name, section, m_liq, rho_liq, V
in (
777 (
"LOX", config.lox_tank, m_lox0, rho_lox, lox_geom.total_volume),
778 (
"fuel", config.fuel_tank, m_rp10, rho_rp1, rp1_geom.total_volume),
780 V_liq = m_liq / rho_liq
784 f
"{name} tank: {V_liq * 1e3:.4f} L of liquid in a {V * 1e3:.4f} L tank leaves no ullage"
788 "tank_volume_L": V * 1e3,
789 "liquid_volume_L": V_liq * 1e3,
790 "ullage_volume_L": V_ull * 1e3,
791 "gas": str(section.ullage_gas),
792 "pressure_pa": P_abs,
793 "temperature_K": T_g,
794 "gas_density_kg_m3": rho_g,
795 "initial_gas_kg": 0.999 * V_ull * rho_g,
796 "fluid": Fluid(name=f
"{section.ullage_gas} ullage", density=rho_g),
798 m_ullage_gas = ullage[
"LOX"][
"initial_gas_kg"] + ullage[
"fuel"][
"initial_gas_kg"]
803 gases = sorted({ullage[
"LOX"][
"gas"], ullage[
"fuel"][
"gas"]})
804 pressurant_gas = gases[0]
if len(gases) == 1
else " / ".join(gases)
806 gas_label =
" / ".join(_GAS_LABEL.get(g.strip().lower(), g)
for g
in gases)
810 if config.press_tank:
811 m_pressurant = getattr(config.press_tank,
'initial_gas_mass',
None)
or 0.0
825 free_L = getattr(config.press_tank,
'free_volume_L',
None)
826 if free_L
and free_L > 0:
827 V_copv = float(free_L)/1000.0
828 press_h_eff = V_copv/(np.pi*config.press_tank.press_radius**2)
830 press_h_eff = config.press_tank.press_h
831 V_copv = np.pi*config.press_tank.press_radius**2*press_h_eff
836 copv_gas = Fluid(name=f
"{gas_label} COPV", density=m_pressurant/(V_copv*0.999))
837 press_geom = CylindricalTank(
838 radius=config.press_tank.press_radius,
859 mdot_pressurant_avg = 0.0
861 print(f
" Pressurant ({gas_label}): {m_pressurant:.3f} kg in {V_copv*1000:.2f} L "
862 f
"({m_pressurant/V_copv:.0f} kg/m3), carried as dead mass (not expelled)")
867 refilled = m_pressurant > 0
868 t_flux = np.concatenate([t_grid[t_grid < effective_burn_time], [effective_burn_time]])
870 def _tank_masses(name, m0, cum):
871 cum_f = np.interp(t_flux, t_grid, cum)
872 liquid = np.clip(m0 - cum_f, 1.0e-9,
None)
873 gas = ullage[name][
"initial_gas_kg"] + (
874 ullage[name][
"gas_density_kg_m3"] / (rho_lox
if name ==
"LOX" else rho_rp1) * (m0 - liquid)
if refilled
else 0.0
876 return Function(np.column_stack((t_flux, liquid))), Function(np.column_stack((t_flux, np.broadcast_to(gas, t_flux.shape)))), liquid
878 lox_liquid, lox_gas, lox_liq_arr = _tank_masses(
"LOX", m_lox0, cum_O)
879 fuel_liquid, fuel_gas, fuel_liq_arr = _tank_masses(
"fuel", m_rp10, cum_F)
880 oxidizer_tank = MassBasedTank(
883 flux_time=effective_burn_time,
885 gas=ullage[
"LOX"][
"fluid"],
886 liquid_mass=lox_liquid,
890 fuel_tank = MassBasedTank(
893 flux_time=effective_burn_time,
895 gas=ullage[
"fuel"][
"fluid"],
896 liquid_mass=fuel_liquid,
903 ullage[n][
"gas_density_kg_m3"] / rho * (m0 - liq[-1])
904 for n, rho, m0, liq
in ((
"LOX", rho_lox, m_lox0, lox_liq_arr), (
"fuel", rho_rp1, m_rp10, fuel_liq_arr))
906 if refill_kg > m_pressurant - 1.0e-4:
908 f
"COPV holds {m_pressurant:.3f} kg; holding tank pressure over this burn needs "
909 f
"{refill_kg:.3f} kg into the ullages. The regulated curve cannot be flown with this COPV."
911 lockup_kg =
max(u[
"gas_density_kg_m3"]
for u
in ullage.values()) * V_copv
912 if refill_kg > m_pressurant - lockup_kg:
913 report_warnings.append(
914 f
"COPV refill {refill_kg:.3f} kg exceeds the {m_pressurant - lockup_kg:.3f} kg above tank "
915 "pressure (isothermal): the regulator would lose lockup before burnout"
919 pressurant_tank =
None
920 if config.press_tank
and m_pressurant > 0:
922 refill_rate = mdot_pressurant_avg + (
923 ullage[
"LOX"][
"gas_density_kg_m3"] / rho_lox * np.interp(t_flux, t_grid, mdot_O_grid)
924 + ullage[
"fuel"][
"gas_density_kg_m3"] / rho_rp1 * np.interp(t_flux, t_grid, mdot_F_grid)
926 mdot_pressurant = Function(np.column_stack((t_flux, refill_rate)))
928 pressurant_tank = MassFlowRateBasedTank(
929 name=f
"Pressurant ({gas_label}) Tank",
931 flux_time=effective_burn_time,
938 initial_liquid_mass=
max(0.0, m_pressurant - 1.0e-4),
939 initial_gas_mass=1.0e-4,
940 liquid_mass_flow_rate_in=0.0,
941 liquid_mass_flow_rate_out=mdot_pressurant,
942 gas_mass_flow_rate_in=0.0,
943 gas_mass_flow_rate_out=0.0,
951 p_ref = getattr(config.thrust,
"reference_pressure_pa",
None)
955 p_ref = float(compute_ambient_pressure_from_elevation(float(config.environment.elevation)))
960 liquid_motor = LiquidMotor(
961 thrust_source=thrust_curve,
962 center_of_dry_mass_position=engine_cm_offset,
963 dry_inertia=motor_inertia,
964 dry_mass=motor_dry_mass,
965 burn_time=(0.0, effective_burn_time),
966 nozzle_radius=math.sqrt(A_e / math.pi),
968 coordinate_system_orientation=
"nozzle_to_combustion_chamber",
969 reference_pressure=float(p_ref),
978 liquid_motor.add_tank(fuel_tank, position=config.fuel_tank.fuel_tank_pos)
979 liquid_motor.add_tank(oxidizer_tank, position=config.lox_tank.ox_tank_pos)
982 if pressurant_tank
is not None:
983 liquid_motor.add_tank(pressurant_tank, position=config.press_tank.pres_tank_pos)
984 print(f
" Added pressurant tank at position {config.press_tank.pres_tank_pos:.2f}m")
990 drag_curves = resolve_drag_curves(config, A_e, stack)
997 extra_gas = (float(ullage_gas_kg) - m_ullage_gas)
if ullage_gas_kg
is not None else 0.0
998 carried = motor_dry_mass + m_lox0 + m_rp10 + m_pressurant + m_ullage_gas + extra_gas
999 airframe_source =
"config"
1000 if liftoff_mass
is not None:
1001 airframe = float(liftoff_mass) - carried
1004 f
"A liftoff mass of {float(liftoff_mass):.2f} kg leaves nothing for the airframe: the motor "
1005 f
"dry mass, propellants and gases alone are {carried:.2f} kg"
1007 if float(rocket_mass) > 0.0:
1008 rocket_inertia = [float(i) * airframe / float(rocket_mass)
for i
in rocket_inertia]
1009 print(f
" Liftoff mass {float(liftoff_mass):.2f} kg given: airframe {airframe:.2f} kg "
1010 f
"(config {float(rocket_mass):.2f} kg), inertia scaled with it")
1011 rocket_mass = airframe
1012 airframe_source =
"liftoff mass"
1013 airframe_only = float(rocket_mass)
1015 print(f
" Ullage gas at T-0 {float(ullage_gas_kg):.3f} kg given ({m_ullage_gas:.3f} kg in these tanks): "
1016 f
"{extra_gas:+.3f} kg carried with the airframe")
1017 rocket_mass = rocket_mass + extra_gas
1019 "airframe_kg": airframe_only,
1020 "airframe_source": airframe_source,
1021 "motor_dry_kg": float(motor_dry_mass),
1022 "oxidizer_kg": float(m_lox0),
1023 "fuel_kg": float(m_rp10),
1024 "pressurant_kg": float(m_pressurant),
1025 "pressurant_gas": pressurant_gas,
1026 "ullage_gas_kg": float(m_ullage_gas + extra_gas),
1027 "liftoff_kg": float(airframe_only + carried),
1030 radius=rocket_radius,
1032 inertia=rocket_inertia,
1033 center_of_mass_without_motor=cm_wo_motor,
1034 coordinate_system_orientation=
"tail_to_nose",
1039 power_off_drag=Function(drag_curves.rocketpy(power_on=
False),
"Mach Number",
1040 "Drag Coefficient with Power Off",
"linear",
"constant"),
1041 power_on_drag=Function(drag_curves.rocketpy(power_on=
True),
"Mach Number",
1042 "Drag Coefficient with Power On",
"linear",
"constant"),
1044 rocket_length_cfg = getattr(config.rocket,
"rocket_length",
None)
1045 if rocket_length_cfg
is not None and abs(float(rocket_length_cfg) - stack[
"length"]) > 1e-3:
1046 report_warnings.append(
1047 f
"rocket.rocket_length {float(rocket_length_cfg):.3f} m, but the stack flown is {stack['length']:.3f} m "
1048 "tail to nose tip (tank positions + avionics_payload_length_m); drag and inertia use the stack"
1052 fins = getattr(config.rocket,
"fins",
None)
1053 if fins
is not None:
1054 rocket.add_trapezoidal_fins(
1056 root_chord=fins.root_chord,
1057 tip_chord=fins.tip_chord,
1059 position=fins.fin_position,
1062 report_warnings.append(
"rocket.fins is not set: flown without fins, so the vehicle has no aerodynamic stability")
1065 rocket.add_motor(liquid_motor, position=motor_position)
1073 body_diameter = 2.0 * rocket_radius
1074 nose_kind = getattr(config.rocket,
'nose_kind',
None)
or "vonKarman"
1075 nose_length = stack[
"nose_length"]
1076 print(f
" Nosecone: {nose_kind}, length {nose_length:.3f} m "
1077 f
"(fineness {nose_length / body_diameter:.2f}:1 on Ø{body_diameter:.3f} m)")
1078 rocket.add_nose(length=nose_length, kind=nose_kind, position=stack[
"nose_tip"])
1080 upper = getattr(config.rocket,
"rail_button_upper_pos_m",
None)
1081 lower = getattr(config.rocket,
"rail_button_lower_pos_m",
None)
1082 if upper
is not None and lower
is not None:
1083 rocket.set_rail_buttons(upper_button_position=float(upper), lower_button_position=float(lower))
1087 if isinstance(thrust_curve, list):
1088 initial_thrust = thrust_curve[0][1]
if thrust_curve
else 0.0
1089 elif hasattr(thrust_curve,
'__call__'):
1090 initial_thrust = float(thrust_curve(0.0))
1092 initial_thrust = float(thrust_curve)
1096 total_initial_mass = rocket_mass + motor_dry_mass + m_lox0 + m_rp10 + m_pressurant + m_ullage_gas
1097 initial_twr = initial_thrust / (total_initial_mass * g0)
1099 print(f
"\nMass Summary:")
1100 print(f
" Airframe: {rocket_mass:.2f} kg")
1101 print(f
" Motor dry (engine + tank structures): {motor_dry_mass:.2f} kg")
1102 print(f
" LOX propellant: {m_lox0:.2f} kg")
1103 print(f
" Fuel propellant: {m_rp10:.2f} kg")
1104 if m_pressurant > 0:
1105 print(f
" Pressurant gas ({gas_label}): {m_pressurant:.3f} kg")
1106 print(f
" Ullage gas at T-0: {m_ullage_gas:.3f} kg")
1107 print(f
" TOTAL: {total_initial_mass:.2f} kg")
1108 print(f
"\nInitial thrust: {initial_thrust:.1f} N")
1109 print(f
"Initial T/W ratio: {initial_twr:.3f}")
1111 if initial_twr < 1.0:
1113 f
"Thrust-to-weight ratio ({initial_twr:.3f}) is less than 1.0! "
1114 f
"The rocket cannot take off. Either increase thrust or reduce mass. "
1115 f
"Current: thrust={initial_thrust:.1f} N, mass={total_initial_mass:.2f} kg, "
1116 f
"requires thrust > {total_initial_mass * g0:.1f} N"
1121 max_flight_time =
max(300.0, effective_burn_time * 30)
1125 with warnings.catch_warnings():
1126 warnings.filterwarnings(
1128 message=
".*must be within the domain of the Function.*",
1129 category=UserWarning,
1134 rail_length=float(config.environment.rail_length_m),
1135 inclination=float(config.environment.launch_inclination_deg),
1136 heading=float(config.environment.launch_heading_deg),
1138 max_time=max_flight_time,
1139 terminate_on_apogee=
True,
1143 elevation = float(config.environment.elevation)
1144 apogee_asl = float(flight.apogee)
1145 apogee_agl = apogee_asl - elevation
1149 vz_source = flight.vz.get_source()
1150 max_v = float(np.max(vz_source[:, 1]))
1154 if truncation_info.get(
"truncated"):
1155 truncation_info[
"cutoff_time"] = float(liquid_motor.burn_out_time)
1162 "reference_pressure_pa": float(p_ref),
1163 "nozzle_exit_area_m2": float(A_e),
1164 "wet_mass_kg": float(total_initial_mass),
1165 "stack_length_m": float(stack[
"length"]),
1166 "ullage": {k: {kk: vv
for kk, vv
in v.items()
if kk !=
"fluid"}
for k, v
in ullage.items()},
1167 "copv_refill_kg": float(refill_kg),
1168 "warnings": report_warnings,
1173 flight.flight_report = report
1175 print(f
"Apogee AGL [m]: {apogee_agl:.2f} (ASL: {apogee_asl:.2f}, elevation: {elevation:.2f})")
1176 if max_v
is not None:
1177 print(f
"Max velocity [m/s]: {max_v:.2f}")
1180 "apogee": apogee_agl,
1181 "apogee_asl": apogee_asl,
1182 "elevation": elevation,
1183 "max_velocity": max_v,
1184 "thrust_curve": thrust_curve,
1187 "truncation_info": truncation_info,
1188 "mass_caps": mass_caps,
1189 "mass_budget": mass_budget,
1190 "flight_report": report,