Skip to content

ENH: model unbonded solid-motor grain CM shift (#340) - #1138

Merged
Gui-FernandesBR merged 3 commits into
RocketPy-Team:developfrom
thatrandomasiandev:enh/340-unglued-solid-grains
Aug 15, 2026
Merged

Gui-FernandesBR merged 3 commits into
RocketPy-Team:developfrom
thatrandomasiandev:enh/340-unglued-solid-grains

Conversation

@thatrandomasiandev

Copy link
Copy Markdown

Summary

  • Adds opt-in SolidMotor(grains_bonded=True) (default preserves current fixed-grain / BATES behavior).
  • When grains_bonded=False, applies a first-order inertial packing model: grains pack against the aft (nozzle-side) face of the initial grain stack, so propellant CM shifts toward the nozzle as grain_height regresses: CM(t) = grains_center_of_mass_position - _csys * (n/2) * (h0 - h(t)). Parallel-axis propellant_I_11 uses the packed pitch.
  • Documents that this is not a DEM / rigid-body grain dynamics model (no friction, rattling, or acceleration threshold). Inter-grain grain_separation (spacers) is retained; only grain-height loss shortens the stack. With only_radial_burn=True, height is fixed so CM does not shift.

Fixes #340

Follow-ups (out of scope)

  • Acceleration-dependent settling / ignition transient before packing completes
  • Full multi-body / DEM grain contact model
  • Optional collapse of soft spacers (grain_separation → 0 when packing)

Test plan

  • PYTEST_DISABLE_PLUGIN_AUTOLOAD=1 pytest tests/unit/motors/test_solidmotor.py
  • Bonded path matches prior fixed CM
  • Unbonded multi-grain CM moves aft vs bonded mid-burn; ignition CM matches
  • grains_bonded round-trips through to_dict / from_dict

@thatrandomasiandev
thatrandomasiandev requested a review from a team as a code owner August 11, 2026 02:08
@Gui-FernandesBR

Copy link
Copy Markdown
Member

I don't believe the solution could be that simple... But we can give it a try

Blank line before evaluate_geometry, wrap the propellant_I_11 assignment
that ran past 88 columns, and let the expected_cm expression in the test
break the way the formatter wants. No behaviour change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@codecov

codecov Bot commented Aug 15, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 84.34%. Comparing base (e0ff281) to head (ded59fe).
⚠️ Report is 72 commits behind head on develop.

Additional details and impacted files
@@             Coverage Diff             @@
##           develop    #1138      +/-   ##
===========================================
+ Coverage    82.18%   84.34%   +2.15%     
===========================================
  Files          122      130       +8     
  Lines        16355    17268     +913     
===========================================
+ Hits         13441    14564    +1123     
+ Misses        2914     2704     -210     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@Gui-FernandesBR

Copy link
Copy Markdown
Member

Following up on my "I don't believe the solution could be that simple" comment — I worked through it and it does hold up. Recording the check so it is on the record:

The CM expression is exactly derivable. With grains packed against the fixed aft face and instantaneous pitch p = h + s, grain k's centre sits at z_k = z_aft + h/2 + k*(h + s). Averaging over k = 0..N-1:

mean(z_k)          = z_aft + h/2  + (h  + s)*(N-1)/2
mean(z_k) bonded   = z_aft + h0/2 + (h0 + s)*(N-1)/2
difference         = (h - h0)/2 + (h - h0)*(N-1)/2 = -(N/2) * (h0 - h)

which is precisely the implemented grains_center_of_mass_position - _csys * (grain_number/2) * (grain_initial_height - grain_height). The instantaneous pitch in the parallel-axis term is the right companion to that, and propellant_I_22 returns self.propellant_I_11, so the transverse components stay consistent for free.

The default path is unchanged. For grains_bonded=True, _grain_pitch_squared_sum() returns (h0 + s)**2 * sum(offsets**2), which is algebraically the same as the old np.sum(d**2) with d = linspace(-iv, iv, n) * (h0 + s). So existing motors get bit-identical numbers.

The remaining caveats are modelling choices rather than errors, and the docstring already states them: grain_separation is retained while the stack shortens, and there is no acceleration-dependent settling — aft packing is what happens under boost, and after burnout grain_height stops regressing so the CM freezes anyway, which lands on the right answer for the wrong reason. Fine as a documented first-order model.

@Gui-FernandesBR
Gui-FernandesBR merged commit ffb5a58 into RocketPy-Team:develop Aug 15, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ENH: Support for solid motor designs without glued/bonded grains

2 participants