feat(motor,flight): tank geometry + fluid density inputs, KML export - #72
feat(motor,flight): tank geometry + fluid density inputs, KML export#72aasitvora99 wants to merge 16 commits into
Conversation
Exposes structured drawing geometry that mirrors rocketpy.Rocket.draw(), so clients can redraw a rocket using the same shape math rocketpy uses without server-side rendering or duplicated geometry logic in the UI. Response carries per-surface shape_x/shape_y arrays, body tube segments, motor patch polygons (nozzle, chamber, grains, tanks, outline), rail button positions, sensors, t=0 CG/CP, and drawing bounds. Coordinates are already transformed into the draw frame rocketpy uses. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Align MotorModel and MotorTank with RocketPy's actual constructor requirements so invalid motors are rejected at the API boundary with a clear error rather than crashing deep inside RocketPy at simulate time. - MotorModel.validate_dry_inertia_for_kind: SOLID / LIQUID / HYBRID motors in RocketPy require dry_inertia with no default. Only GenericMotor accepts (0, 0, 0). Reject the default tuple for every kind except GENERIC with a message the user can act on. - MotorTank.discretize: change to Optional[int] = 100 to match the RocketPy Tank classes' default. Forms can now omit the field and still submit successfully. - stub_motor_dump fixture: use dry_inertia=[0.1, 0.1, 0.1] so tests that override motor_kind to SOLID / LIQUID / HYBRID still pass the new validator without each having to add a dry_inertia override locally.
RocketPy's rocket.draw() does not draw a combustion chamber for GenericMotor because _MotorPlots._generate_combustion_chamber reads grain-only attributes (grain_initial_height, grain_outer_radius, etc.) that GenericMotor lacks — it only emits a nozzle. Users who populate chamber_radius / chamber_height / chamber_position then saw no chamber in the jarvis playground. Add a GenericMotor branch in RocketService._build_motor_geometry that constructs an equivalent rectangular chamber patch from the chamber_* fields. Vertex ordering mirrors _generate_combustion_chamber so the patch flows through _generate_motor_region for outline assembly the same way a SolidMotor chamber does. Patch is emitted with role='chamber', flowing through the existing drawingMotorSchema + GeometryRocket renderer without frontend changes.
…nh/rocket-drawing-geometry
…eneric kinds Two related fixes surfaced during jarvis form refactor. 1. MotorModel.burn_time: float → Optional[float] = None. RocketPy's LiquidMotor / HybridMotor / SolidMotor all auto-detect burn_time from the thrust_source array span — forcing clients to supply it was wrong. GenericMotor still requires it; the service now raises a 422 at the API boundary when the GENERIC path receives None, instead of letting rocketpy error deeper in construction. 2. MotorService.from_motor_model: nozzle_position previously landed in motor_core only for the GenericMotor branch; Liquid / Hybrid / Solid silently ignored the user's value and took rocketpy's default of 0. Now forwarded via motor_core for every kind (conditionally, so a null still falls back to rocketpy's default). Removed the redundant nozzle_position kwarg on the GenericMotor constructor call. 3. Optional-forwarding convention: motor_core only carries burn_time / nozzle_position when the client actually supplied them, so rocketpy picks its own default otherwise instead of receiving None for number-typed args. Verified against real rocketpy: - LIQUID, burn_time=None → LiquidMotor auto-detects burn window - LIQUID, burn_time=2.0 → LiquidMotor builds with explicit window - LIQUID, nozzle_position=0.3 → LiquidMotor.nozzle_position == 0.3 - LIQUID, nozzle_position=None → LiquidMotor.nozzle_position == 0 - GENERIC, burn_time=None → 422 'burn_time is required for generic motors.' All 173 unit tests pass.
… guard Paired API-side work for the jarvis tank/fluid migration (branch feat/tank-fluid-schema-migration in jarvis-ts). Mirrors what that repo now sends on the wire: Schema (src/models/sub/tanks.py): - MotorTank.geometry is a discriminated union on geometry_kind: * custom → legacy piecewise (TankGeometry) * cylindrical → CylindricalTank(radius, height, spherical_caps) * spherical → SphericalTank(radius) - TankFluids.density accepts float or List[(T_K, rho)] temperature samples; pressure dependence deferred. - New validate_tank_kind_fields model_validator mirrors the validate_dry_inertia_for_kind pattern from motor.py — rejects payloads whose tank_kind omits required kind-specific fields at the API boundary with a kind-named 422 instead of letting rocketpy crash deeper in construction. - discretize is now optional (defaults to 100). Service (src/services/motor.py): - _build_rocketpy_tank_geometry dispatches on geometry_kind to TankGeometry/CylindricalTank/SphericalTank. - _build_rocketpy_fluid instantiates a real rocketpy.Fluid and wraps sampled density in a 1D Function-of-temperature callable; scalars pass through. (Eliminates the duck-typed Pydantic-into-rocketpy pattern that worked by accident.) Inverse path (src/services/flight.py): - _extract_fluid_density collapses Function-valued density back to a scalar at rocketpy's reference state (273.15 K, 101325 Pa) — lossy round-trip is documented; samples-roundtrip not supported in this iteration. - Geometry inverse always emits the 'custom' segment list shape; all three rocketpy geometry subclasses expose a piecewise .geometry dict so one code path covers them uniformly. Tests: - test_motors_route.py: 8 new cases covering each geometry_kind, sampled density, invalid discriminator, and all four tank_kind guard paths (MASS / MASS_FLOW / LEVEL / ULLAGE missing sub-fields). - tests/unit/test_services/test_motor_service.py: new suite exercising the adapter end-to-end against real rocketpy for each geometry×variant combination plus sampled density roundtrip. Routes (src/routes/motor.py): - POST /motors docstring gained an example payload showing the discriminated geometry union and sampled density shape. Gitignore: - Added .context and .pylint.d/ to keep local tooling artifacts out of the tree. Full suite: 173/173 pass.
📝 WalkthroughWalkthroughThe changes add a flight KML download endpoint and export service. They also add structured motor tank geometry, sampled fluid density, tank-kind validation, optional burn time handling, RocketPy conversion, documentation, fixtures, and tests. ChangesFlight KML export
Motor tank modeling
Estimated code review effort: 4 (Complex) | ~45 minutes Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
…luid-density # Conflicts: # src/services/motor.py # src/services/rocket.py # src/views/rocket.py
…n tests pylint W0613 (unused-argument) on 5 schema-validation tests failed the build step (pylint exit 4), blocking this PR and the stacked KML PR #73. These tests only assert a 422 from schema validation, so they never touch the controller; and mock_controller_instance is @pytest.fixture(autouse=True), so it still runs for every test regardless — the parameter was vestigial. No behavior change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
CI unblocked ✅ — and why this PR matters for the Jarvis betaPushed a small commit that gets the build green again: the Why this is on the critical path: the Jarvis web client's motor wire-schema was written against this PR's tank model. Against Remaining (your call, @aasitvora99): mark this out of draft → merge → then #73 (stacked on this) → and deploy to — pushed as part of a Claude Code audit of the beta (Gui). |
simplified docs
GabrielBarberini
left a comment
There was a problem hiding this comment.
Took the liberty to put it as ready for review, CI is green and I patched the docs verbosity. LGTM
Co-authored-by: Gui-FernandesBR <guilherme_fernandes@usp.br> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: Gabriel Barberini <gabrielbarberinirc@gmail.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (1)
tests/unit/test_routes/conftest.py (1)
93-149: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueFixtures are correct; consider deduplicating the repeated field-clearing dict.
stub_level_tank_dump,stub_ullage_tank_dump, andstub_mass_tank_dumpeach repeat the same six-key dict that clearsgas_mass_flow_rate_in,gas_mass_flow_rate_out,liquid_mass_flow_rate_in,liquid_mass_flow_rate_out,initial_liquid_mass, andinitial_gas_mass. Extract this into a module-level constant or small helper function to reduce duplication across the three fixtures.♻️ Proposed refactor
+_CLEAR_MASS_FLOW_FIELDS = { + 'gas_mass_flow_rate_in': None, + 'gas_mass_flow_rate_out': None, + 'liquid_mass_flow_rate_in': None, + 'liquid_mass_flow_rate_out': None, + 'initial_liquid_mass': None, + 'initial_gas_mass': None, +} + + `@pytest.fixture` def stub_level_tank_dump(stub_tank_dump): stub_tank_dump.update( { 'tank_kind': TankKinds.LEVEL, 'liquid_height': 0, - 'gas_mass_flow_rate_in': None, - 'gas_mass_flow_rate_out': None, - 'liquid_mass_flow_rate_in': None, - 'liquid_mass_flow_rate_out': None, - 'initial_liquid_mass': None, - 'initial_gas_mass': None, + **_CLEAR_MASS_FLOW_FIELDS, } ) return stub_tank_dump🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/unit/test_routes/conftest.py` around lines 93 - 149, Deduplicate the repeated kind-specific field-clearing mapping used by stub_level_tank_dump, stub_ullage_tank_dump, and stub_mass_tank_dump by extracting it into a module-level constant or small helper. Reuse that shared definition in all three fixtures while preserving each fixture’s tank_kind and kind-specific values.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/controllers/flight.py`:
- Around line 166-168: Update the controller method around get_flight_by_id so
the complete synchronous FlightService.from_flight_model(...).get_flight_kml()
export runs in a worker thread after the awaited database read. Preserve the
returned KML result while ensuring temp-file creation, reading, and deletion do
not execute on the event-loop thread.
In `@src/models/motor.py`:
- Around line 23-27: Update MotorModel.validate_motor_kind to reject GENERIC
motors when burn_time is None, alongside the existing motor-kind requirements.
Ensure this validation occurs during POST model construction before
MotorRepository.create_motor persists model_dump(exclude_none=True), while
preserving validation behavior for other motor kinds.
In `@src/services/flight.py`:
- Around line 230-244: Update _extract_fluid_density and its
import_flight_from_rpy caller to preserve temperature-dependent densities by
converting representable Function values into DensityInput (T_K, density)
samples; do not collapse them to a scalar at 273.15 K. If a density function
cannot be represented by the API’s sampled schema, reject the import explicitly
rather than persisting an inaccurate scalar.
- Around line 262-271: Update _extract_tanks() and the geometry_segments
construction to preserve position-dependent tank geometry during import: do not
sample each function only at position 0 or collapse cylindrical/spherical shapes
into constant-radius custom segments. Retain the discriminated
cylindrical/spherical representation, or serialize the complete variable-radius
custom geometry so spherical tank radii remain height-dependent and downstream
.rpy motor simulations preserve volume.
---
Nitpick comments:
In `@tests/unit/test_routes/conftest.py`:
- Around line 93-149: Deduplicate the repeated kind-specific field-clearing
mapping used by stub_level_tank_dump, stub_ullage_tank_dump, and
stub_mass_tank_dump by extracting it into a module-level constant or small
helper. Reuse that shared definition in all three fixtures while preserving each
fixture’s tank_kind and kind-specific values.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 7949a846-1add-4204-8a34-1385caa8432d
📒 Files selected for processing (13)
.gitignoresrc/controllers/flight.pysrc/models/motor.pysrc/models/sub/tanks.pysrc/routes/flight.pysrc/routes/motor.pysrc/services/flight.pysrc/services/motor.pytests/unit/test_routes/conftest.pytests/unit/test_routes/test_flights_route.pytests/unit/test_routes/test_motors_route.pytests/unit/test_services/__init__.pytests/unit/test_services/test_motor_service.py
| flight = await self.get_flight_by_id(flight_id) | ||
| flight_service = FlightService.from_flight_model(flight.flight) | ||
| return flight_service.get_flight_kml() |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "Files:"
git ls-files | rg '(^|/)flight\.py$|api\.py$|rocketpy|kml|FlightService' || true
echo
echo "src/controllers/flight.py outline:"
ast-grep outline src/controllers/flight.py 2>/dev/null || true
echo
echo "Relevant src/controllers/flight.py lines:"
cat -n src/controllers/flight.py | sed -n '1,230p'
echo
echo "Search FlightService/get_flight_kml:"
rg -n "class FlightService|def get_flight_kml|from_flight_model|RocketPy|KML|rocket" src tests -S || trueRepository: RocketPy-Team/Infinity-API
Length of output: 50382
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "Relevant src/routes/flight.py lines:"
cat -n src/routes/flight.py | sed -n '290,350p'
echo
echo "Relevant src/services/flight.py outline and contents:"
wc -l src/services/flight.py
ast-grep outline src/services/flight.py 2>/dev/null || true
cat -n src/services/flight.py | sed -n '1,340p'
echo
echo "Focused RocketPy KML usage in src/services/flight.py:"
rg -n -C 8 "kml|KML|Flight\(|save_kml|plot|export" src/services/flight.py src/controllers/flight.py || true
echo
echo "Check package versions/deps mentioning rocketpy:"
fd -a 'requirements|pyproject|poetry|Pipfile|environment|uv.lock' . | sed 's#^\./##' | sort
rg -n "rocketpy|rocketpy-dev|FastAPI|starlette|uvicorn|aiohttp|asyncio\.to_thread" -S --glob '!node_modules' --glob '!dist' --glob '!build' . | head -n 200Repository: RocketPy-Team/Infinity-API
Length of output: 46105
Move KML generation off the event-loop thread.
get_flight_kml still runs FlightService.from_flight_model(...).get_flight_kml() synchronously after awaiting the database read, and the controller is awaited by the FastAPI route handler. The export creates a temp file, reads it, and deletes it synchronously, so a long KML export blocks the shared event loop.
Run the complete synchronous export path in a worker thread.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/controllers/flight.py` around lines 166 - 168, Update the controller
method around get_flight_by_id so the complete synchronous
FlightService.from_flight_model(...).get_flight_kml() export runs in a worker
thread after the awaited database read. Preserve the returned KML result while
ensuring temp-file creation, reading, and deletion do not execute on the
event-loop thread.
| # burn_time is optional for Liquid/Hybrid/Solid motors — rocketpy | ||
| # auto-detects the burn window from the thrust_source array span. | ||
| # GenericMotor still requires it; the motor service re-raises an | ||
| # explicit error when the GENERIC path receives None. | ||
| burn_time: Optional[float] = None |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Description: Check whether MotorController.post_motor builds the rocketpy motor synchronously.
fd -e py motor.py --full-path src/controllers | xargs -I{} sh -c 'echo "--- {} ---"; cat -n {}'Repository: RocketPy-Team/Infinity-API
Length of output: 3177
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "Tracked controller files:"
git ls-files 'src/controllers/*.py'
echo "--- MotorController methods ---"
rg -n "def (create|create_?motor|post|get|update|delete|add|upsert|fetch)|motor_id|motor\.motor|MotorModel|get_motor_by_id" src/controllers/motor.py
echo "--- motor.py line count ---"
wc -l src/controllers/motor.pyRepository: RocketPy-Team/Infinity-API
Length of output: 1224
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "--- src/controllers/interface.py relevant sections ---"
wc -l src/controllers/interface.py
sed -n '1,220p' src/controllers/interface.py | cat -n
echo "--- src/api.py imports and registered motor/controller routes ---"
wc -l src/api.py
rg -n "MotorController|motor|controllers|router|BaseController|create" src/api.py -C 3
echo "--- MotorModel and service validation snippets ---"
wc -l src/models/motor.py src/services/motor.py
sed -n '1,130p' src/models/motor.py | cat -n
sed -n '220,260p' src/services/motor.py | cat -nRepository: RocketPy-Team/Infinity-API
Length of output: 14542
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "--- create_motor methods in motor controllers/repositories ---"
rg -n "def create_motor|create_motor\\(" src/controllers src/repositories src/routes -C 6
echo "--- motor router outline and relevant snippets ---"
wc -l src/routes/motor.py
sed -n '1,220p' src/routes/motor.py | cat -n
echo "--- motor repository filenames and create_motor snippets ---"
git ls-files 'src/repositories/*motor*' 'src/repositories/motor.py'
fd -e py 'motor|Motor' src/repositories -x sh -c 'echo "--- $1 ---"; rg -n "create_motor|create_model|NAME" "$1" -C 5' sh {}Repository: RocketPy-Team/Infinity-API
Length of output: 8123
Enforce GENERIC burn_time in the model validator.
POST /motors builds MotorModel, then MotorRepository.create_motor persists motor.model_dump(exclude_none=True). The GENERIC burn_time check lives only in MotorService.from_motor_model, so a malformed GENERIC motor can be stored and fail later. Add the same POST-level check used for solid/grain parameters and other motor-kind requirements in MotorModel.validate_motor_kind.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/models/motor.py` around lines 23 - 27, Update
MotorModel.validate_motor_kind to reject GENERIC motors when burn_time is None,
alongside the existing motor-kind requirements. Ensure this validation occurs
during POST model construction before MotorRepository.create_motor persists
model_dump(exclude_none=True), while preserving validation behavior for other
motor kinds.
| @staticmethod | ||
| def _extract_fluid_density(fluid): | ||
| """Project a rocketpy Fluid's density back onto the API schema. | ||
|
|
||
| The API accepts either a scalar or a list of (T_K, density) | ||
| samples. Rocketpy may store density as either a raw scalar or a | ||
| ``Function`` wrapping a 2D ``(T, P) -> density`` callable. A | ||
| full sample round-trip is not supported in this iteration; | ||
| Function-valued densities are collapsed to a scalar evaluated | ||
| at rocketpy's default reference (273.15 K, 101325 Pa). | ||
| """ | ||
| density = fluid.density | ||
| if isinstance(density, Function): | ||
| return float(density(273.15, 101325)) | ||
| return density |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "Files matching flight.py:"
fd -a 'flight.py$' . || true
echo
echo "Outline src/services/flight.py:"
if [ -f src/services/flight.py ]; then
wc -l src/services/flight.py
ast-grep outline src/services/flight.py --view expanded | sed -n '1,220p'
fi
echo
echo "Relevant sections:"
for start in 1 180 210 280 380 460 540; do
end=$((start+120))
echo "--- src/services/flight.py $start-$end"
sed -n "${start},${end}p" src/services/flight.py
done
echo
echo "Search DensityInput and extract_models:"
rg -n "DensityInput|extract_models|import_flight_from_rpy|_extract_fluid_density|density_input|fluid_dens|fluid" src tests | sed -n '1,240p'Repository: RocketPy-Team/Infinity-API
Length of output: 33062
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "src/controllers/flight.py relevant section:"
sed -n '180,240p' src/controllers/flight.py | cat -n
echo
echo "src/services/motor.py fluid construction:"
sed -n '55,100p' src/services/motor.py | cat -n
sed -n '250,275p' src/services/motor.py | cat -n
echo
echo "MotorService round-trip usages:"
rg -n "from_models|from_motor_model|extract_models|get_models|_build_rocketpy_fluid|Fluid\\(" src/services/motor.py src/services/flight.py src/services/*.py | sed -n '1,220p'
echo
echo "Search for create flight from model round-trip:"
rg -n "FlightService\\.(from_flight_model|extract_models)|import_flight_from_rpy|from_motor_model" src tests | sed -n '1,220p'Repository: RocketPy-Team/Infinity-API
Length of output: 10363
Preserve temperature-sampled fluid density during import.
_extract_fluid_density collapses function-backed tank gas and liquid density to one value at 273.15 K, then import_flight_from_rpy persists that scalar. A later simulation rebuilds fluids from those persisted samples and ignores the original temperature-dependent density. Extract representable (T_K, density) samples into DensityInput, or reject the import for non-representable density functions instead of changing the flight model.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/services/flight.py` around lines 230 - 244, Update _extract_fluid_density
and its import_flight_from_rpy caller to preserve temperature-dependent
densities by converting representable Function values into DensityInput (T_K,
density) samples; do not collapse them to a scalar at 273.15 K. If a density
function cannot be represented by the API’s sampled schema, reject the import
explicitly rather than persisting an inaccurate scalar.
| # Geometry round-trip is lossy: even if the client originally | ||
| # sent a cylindrical/spherical geometry, we discretise it back | ||
| # to the generic piecewise form on read. Every rocketpy tank | ||
| # geometry exposes its internal piecewise dict via | ||
| # `tank.geometry.geometry`, so this path covers all three | ||
| # geometry subclasses uniformly. | ||
| geometry_segments = [ | ||
| (bounds, float(func(0))) | ||
| for bounds, func in tank.geometry.geometry.items() | ||
| ] |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== relevant files =="
git ls-files | rg '(^src/services/flight.py$|rocketpy|tank|geometry)' | sed -n '1,120p'
echo
echo "== outline src/services/flight.py around import/export =="
ast-grep outline src/services/flight.py --view expanded 2>/dev/null | sed -n '1,220p' | rg -n 'def|class|flight|tank|import|serde|geometry' || true
echo
echo "== lines 220-310 =="
sed -n '220,310p' src/services/flight.py | nl -ba -v220
echo
echo "== search geometry classes/config =="
rg -n "class .*Tank|Geometry|geometry\.geometry|TankGeometry|Tank|tank\.geometry|rocketpy" src tests -g '*.py' | sed -n '1,240p'Repository: RocketPy-Team/Infinity-API
Length of output: 1357
🌐 Web query:
rocketpy geometry TankGeometry geometry attribute cylindrical spherical piecewise
💡 Result:
In RocketPy, the TankGeometry class is used to define the geometry of an axi-symmetric tank by specifying its radius as a function of its height along the axis of symmetry [1][2]. The geometry is defined using a dictionary provided to the geometry_dict parameter [3][4]. This dictionary allows for the construction of complex, piecewise defined shapes [1][5]. Key details regarding the TankGeometry attribute: 1. The geometry attribute is a dictionary where each key is a disjoint tuple (lower_bound, upper_bound) representing a domain along the tank's symmetry axis in meters [1][3]. 2. The corresponding values are functions (typically Python lambdas or RocketPy.Function objects) that map the tank's height to its radius at that specific section [1][2]. 3. Because the dictionary can contain multiple disjoint domains, the tank's overall radius profile is effectively a piecewise-defined function [1][5][6]. While users can manually define geometries using this dictionary approach for arbitrary shapes, RocketPy also provides convenience classes such as CylindricalTank and SphericalTank for standard configurations [3][4]. For example, a spherical tank can be defined using the geometry dictionary by specifying a radius function such as lambda h: (radius2 - h2)(1/2) over the domain (-radius, radius) [1][2]. The internal logic of TankGeometry then computes the overall radius, area, and volume functions based on these inputs [1][5].
Citations:
- 1: https://docs.rocketpy.org/en/latest/reference/classes/motors/geometries/TankGeometry.html
- 2: https://docs.rocketpy.org/en/latest/_modules/rocketpy/motors/tank_geometry.html
- 3: https://docs.rocketpy.org/en/latest/user/motors/tanks.html
- 4: https://docs.rocketpy.org/en/develop/user/motors/tanks.html
- 5: https://docs.rocketpy.org/en/develop/reference/classes/motors/geometries/TankGeometry.html
- 6: https://docs.rocketpy.org/en/develop/_modules/rocketpy/motors/tank_geometry.html
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== src/services/flight.py lines 247-330 =="
sed -n '247,330p' src/services/flight.py
echo
echo "== src/models/sub/tanks.py =="
cat -n src/models/sub/tanks.py
echo
echo "== imports and serialization helpers =="
sed -n '1,120p' src/services/flight.py
echo
echo "== tank field serialization references =="
rg -n "MotorTank|tank|geometry|_extract_tanks|rpy|from_rpy" src -g '*.py' | sed -n '1,260p'Repository: RocketPy-Team/Infinity-API
Length of output: 28255
🏁 Script executed:
#!/bin/bash
set -euo pipefail
python3 - <<'PY'
import ast
from pathlib import Path
src = Path('src/services/flight.py').read_text()
tree = ast.parse(src, filename='src/services/flight.py')
for node in tree.body:
if node.name == 'FlightService':
for cls in node.body:
if isinstance(cls, ast.FunctionDef) and cls.name == '_extract_tanks':
src_snippet = src.splitlines()[cls.lineno-1:cls.end_lineno]
print(f'function: {cls.name} lines {cls.lineno}-{cls.end_lineno}')
lines = [
('geometry_segments =', any('geometry_segments = [' in line for line in src_snippet)),
('evaluating func(0) explicitly:', any('func(0)' in line for line in src_snippet)),
('custom geometry_kind:', any('geometry_kind": "custom"' in line for line in src_snippet)),
('tank geometry dict:', any('tank.geometry.geometry.items()' in line for line in src_snippet)),
]
for label, ok in lines:
print(f'{label}: {ok!s}')
print('target block:')
for i, line in enumerate(src_snippet, start=cls.lineno):
print(f'{i}: {line}')
PYRepository: RocketPy-Team/Infinity-API
Length of output: 442
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== inspect _build_rocketpy_tank_geometry =="
sed -n '40,62p' src/services/motor.py
echo
echo "== inspect _generate_positioned_tanks if present in installed rocketpy =="
python3 - <<'PY'
import inspect
try:
import rocketpy
from rocketpy import LevelBasedTank, MassBasedTank, UllageBasedTank
print('rocketpy imported', getattr(rocketpy, '__version__', ''))
for name, cls in [('LevelBasedTank', LevelBasedTank), ('MassBasedTank', MassBasedTank), ('UllageBasedTank', UllageBasedTank)]:
print(f'\n{name}:')
print(inspect.signature(cls))
print('methods:', sorted([m for m in dir(cls) if not m.startswith('_') and callable(getattr(cls, m))]))
if hasattr(cls, 'plots') and hasattr(getattr(cls, 'plots'), '_generate_positioned_tanks'):
print(inspect.getsource(cls.plots._generate_positioned_tanks))
except Exception as exc:
print(type(exc).__name__ + ': ' + str(exc))
PYRepository: RocketPy-Team/Infinity-API
Length of output: 1101
Preserve position-dependent tank geometry during import.
_extract_tanks() collapses each shape into a geometry_kind: "custom" segment whose radius is sampled at position == 0. For spherical tanks, the radius depends on tank height, so the imported geometry changes volume and later .rpy-loaded motor simulations. Preserve the discriminated cylindrical/spherical forms, or serialize the full variable-radius custom geometry instead of collapsing it to a constant-radius segment.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/services/flight.py` around lines 262 - 271, Update _extract_tanks() and
the geometry_segments construction to preserve position-dependent tank geometry
during import: do not sample each function only at position 0 or collapse
cylindrical/spherical shapes into constant-radius custom segments. Retain the
discriminated cylindrical/spherical representation, or serialize the complete
variable-radius custom geometry so spherical tank radii remain height-dependent
and downstream .rpy motor simulations preserve volume.
Summary
Two capabilities that clients could not express through the API before:
GET /flights/{flight_id}/kml.These ship together because the KML work (#73) branched off this branch and was merged back into it.
Tank geometry and fluid density
Previously
geometryhad to be an explicit list of((z_start, z_end), radius)segments, so modelling a plain cylinder meant precomputing the discretisation client-side.densitywas a single float, which left cryogenic propellants unrepresentable.MotorTank.geometryis now a discriminated union ongeometry_kind:geometry_kindcylindricalradius,height,spherical_capsCylindricalTanksphericalradiusSphericalTankcustomgeometry(piecewise segments)TankGeometryTankFluids.densitytakes either a scalar or[(temperature_K, density)]samples. Samples get linearly interpolated into a rocketpyFunctionand wrapped as a(T, P)callable, sinceFluidexpects density to depend on both. Pressure dependence is out of scope here, and the wrapper ignores that argument deliberately.Breaking change:
geometryis an object now, not a bare list. Payloads sending a raw segment list must wrap it as{"geometry_kind": "custom", "geometry": [...]}.Validation moved to the API boundary
MotorTankrejects payloads wheretank_kindand the supplied fields disagree, naming exactly what is missing (LEVELwithoutliquid_height,MASSwithoutliquid_mass/gas_mass, and so on). Before, these reached rocketpy and failed duringTankconstruction with an opaque trace.burn_timeis optional for solid, liquid, and hybrid motors, where rocketpy infers the burn window from thethrust_sourcespan.GenericMotorgenuinely requires it, so that path raises an explicit 422.nozzle_positionis only forwarded when the client supplies it, letting rocketpy's default apply otherwise.KML trajectory export
GET /flights/{flight_id}/kmlresponds withapplication/vnd.google-earth.kml+xmland aContent-Dispositionattachment header, so browsers save it asflight_{id}.kml.Implementation note for reviewers: rocketpy's
FlightDataExporter.export_kmlonly writes to a file path, it does not return bytes. The service writes to aNamedTemporaryFile, reads it back, and unlinks in afinally.Round-trip is lossy in two places
Reading a flight back projects rocketpy state onto the API schema, and two projections cannot be inverted:
customwith discretised segments, even when it was submitted ascylindricalorspherical.Function-valued density collapses to a scalar evaluated at rocketpy's reference point (273.15 K, 101325 Pa).Both are documented in comments at the call sites. Callers that need to preserve their original input should keep it themselves rather than reading it back.
Tests
tests/unit/test_services/test_motor_service.py(new) covers geometry adapter dispatch across all 3 kinds plus the unsupported case, fluid adapter for scalar and sampled density, andfrom_motor_modelfor liquid motors.tests/unit/test_routes/test_motors_route.pyadds 3 tests for the new geometry kinds and sampled density, and 5 asserting eachtank_kindrejects payloads missing its required fields.tests/unit/test_routes/test_flights_route.pycovers the KML success, 404, and 500 paths.Summary by CodeRabbit