Conversation
Introduce a shared private abstract base so free flight and aeroelasticity can both generate SteadyProblems dynamically per time step rather than reimplementing step-by-step list management and seeding logic independently. Add movement and steady_problems NotImplementedError stubs to CoreUnsteadyProblem so the base UVLM solver can access them on any subclass without knowing the concrete type. Widen the solver unsteady_problem parameter accordingly and guard direct base instantiation via a type(self) check, which preserves rejection of misuse while letting future coupled solver subclasses pass their own variants through super(). Co-Authored-By: JonahJ27 <jobomcbean@gmail.com>
Both upcoming free flight and aeroelasticity features need step by step geometry orchestration on top of the UVLM run loop. Adding a shared coupled parent solver lets both features inherit the orchestration instead of duplicating it. Introduce three hooks on the base UVLM solver so the coupled subclass can share run() rather than copying it: per step bound vortex init, per step array reinit, and a pre shed hook. The coupled subclass overrides these plus _get_steady_problem_at, routing dynamic geometry dispatch through _CoupledUnsteadyProblem. Name the class publicly inside a private module (Core* convention) so future public subclasses can inherit without cross module private attribute imports. Co-Authored-By: JonahJ27 <jobomcbean@gmail.com>
Complete the test coverage for the Coupled* middle layer extracted in prior commits on this branch. Cover _CoupledUnsteadyProblem construction and accessors, the coupled solver's init validation and dispatch to the problem's get_steady_problem accessor, and the base solver's new _CoupledUnsteadyProblem rejection plus three hook defaults.
Align _CoupledUnsteadyProblem with the _movement/movement idiom used throughout the package so external readers touch only the tuple-view property while subclass initialize_next_problem overrides write through the private backing list. Document the new class in CLASSES_AND_IMMUTABILITY.md, flagging that the steady_problems view retains the inherited read-only contract but loses the temporal- invariance guarantee held by UnsteadyProblem. Update the solver-classes paragraph to include the fourth solver class.
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #163 +/- ##
==========================================
- Coverage 91.74% 91.72% -0.03%
==========================================
Files 34 35 +1
Lines 6784 6837 +53
==========================================
+ Hits 6224 6271 +47
- Misses 560 566 +6 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
This PR extracts an abstract
Coupled*middle layer from the problem and solver hierarchies so that upcoming feature work (single-airplane free flight and aeroelasticity) can build on a shared step-by-step orchestration layer instead of each feature reimplementing the loop. The middle layer is intentionally incomplete on its own: it establishes the shape of the contract (initialize_next_problemas the step-to-step hook) and the machinery to run the parent UVLM loop coupled to that contract, but concrete subclasses land in follow-up PRs.Much of this code was manually extracted from @JonahJ27's work on PR #156 rather than written from scratch. @JonahJ27 is tagged as co-author on the commits. In addition, I've nailed down a few other design decisions, added unit tests for the new classes and attributes, and updated the docs.
Motivation
Two features, aeroelasticity (PR #156) and free flight (branch
feature/free_flight), both need the UVLM solver to advance one step at a time, build the next step'sSteadyProblemfrom the previous step's results, and loop. Their current branches each define aCoupledUnsteadyProblem/ coupled solver pair with incompatible interfaces, and each duplicates the UVLMrun()method to inject coupling. Without a shared middle layer, merging either feature forces the other to rewrite to match. Extracting the shared pieces here removes that double cost and unblocks both features to rebase onto main cleanly.The foundation this builds on landed in PR #160:
Core*movement classes in_core.py,CoreUnsteadyProblem, and_get_steady_problem_at(step)dispatch on the parent UVLM solver. This PR adds the next layer up.Relevant Issues
Incidentally, this PR addresses the unit testing portion of #136.
Changes
_CoupledUnsteadyProblem(CoreUnsteadyProblem)toproblems.pyas an abstract middle-layer class. Owns the mutable_steady_problems: list[SteadyProblem]backing slot, seeds step 0 frominitial_airplanesandinitial_operating_point, exposes aget_steady_problem(step)accessor with dynamic bounds, and provides property overrides formovementandsteady_problems(the latter returns a read-only tuple view of the backing list).initialize_next_problem(solver)raisesNotImplementedError. Subclasses must implement it.CoupledUnsteadyRingVortexLatticeMethodSolver(UnsteadyRingVortexLatticeMethodSolver)in a new private modulepterasoftware/_coupled_unsteady_ring_vortex_lattice_method.py. Inherits the parent'srun()andinitialize_step_geometry()unchanged and overrides three hooks to inject coupled behavior (see next bullet). Declares__slots__ = ()to avoid silently defeating the parent's__slots__. Exposes a_coupled_problemproperty that narrows the inheritedunsteady_problemto_CoupledUnsteadyProblemviatyping.cast.UnsteadyRingVortexLatticeMethodSolverso the coupled subclass can share the parent's run loop rather than duplicating it:_initialize_step_vortices(step): default initializes bound vortices for all steps on step 0 and is a no-op afterwards; coupled subclasses override to initialize per step._reinitialize_step_arrays_hook(): no-op extension point for feature subclasses to reset step-specific arrays._pre_shed_hook(step): default no-op; coupled subclasses call_CoupledUnsteadyProblem.initialize_next_problem(self)here between steps.UnsteadyRingVortexLatticeMethodSolver.__init__to acceptCoreUnsteadyProblem(previously requiredUnsteadyProblem) so subclasses can pass their own variants throughsuper().__init__. Added atype(self) is UnsteadyRingVortexLatticeMethodSolver and not isinstance(..., UnsteadyProblem)guard that still rejects direct misuse of the base solver with a_CoupledUnsteadyProblem.NotImplementedErrorproperty stubs formovementandsteady_problemsonCoreUnsteadyProblemso that the shared UVLM solver code can reach through eitherUnsteadyProblemor a coupled subclass without knowing which.tests/unit/test_coupled_unsteady_problem.py(19 tests): construction, property views,get_steady_problemdynamic bounds,initialize_next_problemNotImplementedError, immutability.tests/unit/test_coupled_unsteady_ring_vortex_lattice_method.py(7 tests): init accept / reject,_coupled_problemcast,_get_steady_problem_atdispatch, empty__slots__.tests/unit/test_unsteady_ring_vortex_lattice_method.py(7 tests):_CoupledUnsteadyProblemrejection on the base solver plus the three new hook defaults.tests/unit/__init__.pyand addedmake_basic_coupled_unsteady_problem_fixture/make_coupled_unsteady_ring_solver_fixtureto the shared fixture modules._CoupledUnsteadyProblemsection todocs/CLASSES_AND_IMMUTABILITY.mdwith its attribute classification and a note flagging that thesteady_problemstuple view is frozen per call but successive calls may return different-length tuples as the solver initializes new steps. Widened the Solver Classes paragraph for the fourth solver.Notes for the aeroelasticity rebase
@JonahJ27: Flagging the structural differences between this extracted middle layer and the version currently on
feature/aeroelasticity-ptera-merge, so nothing surprises you during the rebase:_CoupledUnsteadyProblem(leading underscore) and lives in publicproblems.py. YourAeroelasticUnsteadyProblemshould extend_CoupledUnsteadyProblem. The underscore indicates it's internal scaffolding, not a user-facing class. Import path:from pterasoftware.problems import _CoupledUnsteadyProblem.pterasoftware/_coupled_unsteady_ring_vortex_lattice_method.py(private module, public class name). YourAeroelasticSolverimports shift tofrom pterasoftware._coupled_unsteady_ring_vortex_lattice_method import CoupledUnsteadyRingVortexLatticeMethodSolver._CoupledUnsteadyProblem.initialize_next_problemraisesNotImplementedError. You already plan to override it inAeroelasticUnsteadyProblem, so no change required. Just noting that the no-op default from PR add coupled unsteady problem and solver #161 is gone.solver.steady_problems_data_storage: list[...]worked, but putting the growing list on_CoupledUnsteadyProbleminstead has two benefits. The problem already has to exposesteady_problemsto satisfy the inheritedUnsteadyProblemcontract used byoutput.py,_serialization.py, and the solver's own_get_steady_problem_atdispatch, so keeping a parallel accumulator on the solver was redundant._CoupledUnsteadyProblemnow owns the backing list atself._steady_problems, and the publicsteady_problemstuple property views it. Yourinitialize_next_problemoverride onAeroelasticUnsteadyProblemappends newly constructedSteadyProblems toself._steady_problems.run()shouldn't need duplicating: the parent UVLMrun()is now hook-friendly. The coupled solver overrides three hooks instead of copying the run loop:_initialize_step_vortices(step): bound-vortex init for the current step. The coupled base already implements this by delegating to_initialize_panel_vortices_at(step). Override further inAeroelasticSolveronly if you need additional per-step vortex work._reinitialize_step_arrays_hook(): reset step-specific solver arrays between steps. The coupled base is a no-op; this is the place to put aeroelastic per-step state resets if you need them._pre_shed_hook(step): called after loads are computed, before the wake shed. The coupled base calls_coupled_problem.initialize_next_problem(self)and re-initializes the next step's vortices here. Any work you had in the middle of your duplicatedrun()between load computation and wake shed will likely move here. Any SLEP / moment-processing work that needs to happen specifically for aeroelasticity stays insideAeroelasticSolver.__slots__on subclasses:CoupledUnsteadyRingVortexLatticeMethodSolverdeclares__slots__ = (), which means yourAeroelasticSolvermust declare its own__slots__(either()or the tuple of any new slots it needs). Leaving__slots__undeclared on the subclass gives the instance a__dict__and silently defeats the parent's slots.CoupledUnsteadyRingVortexLatticeMethodSolverexposes_coupled_problemas atyping.castnarrowed view of the inheritedunsteady_problemattribute.AeroelasticSolvercan follow the same pattern to expose an_aeroelastic_problemproperty narrowed toAeroelasticUnsteadyProblem, avoiding repeatedcastcalls in method bodies.CoreUnsteadyProblem:UnsteadyRingVortexLatticeMethodSolver.__init__widened to accept anyCoreUnsteadyProblemso subclasses can chain throughsuper().__init__. Atype(self) is ...guard still rejects direct instantiation of the base solver with a_CoupledUnsteadyProblem. I don't think you'll need to modify either. Tag me in a comment if there's an edge case I didn't consider.CoreUnsteadyProblemproperty stubs:movementandsteady_problemsnow raiseNotImplementedErroron theCoreUnsteadyProblembase and are overridden by bothUnsteadyProblemand_CoupledUnsteadyProblem.AeroelasticUnsteadyProbleminherits the_CoupledUnsteadyProblemoverrides, so no action needed.AeroelasticUnsteadyProblem: the new_CoupledUnsteadyProblemsection indocs/CLASSES_AND_IMMUTABILITY.mdis the shapeAeroelasticUnsteadyProblemshould follow. Treat its "Immutable" table as the starting point; your aeroelastic class mainly addsAeroelasticStructuralModeland any per-step structural state.Dependency Updates
None.
Change Magnitude
Moderate: Medium-sized change that adds or modifies a feature without large-scale impact.
Checklist (check each item when completed or not applicable)
mainand is up to date with the upstreammainbranch.--in-place --black). See the style guide for type hints and docstrings for more details.pterasoftwarepackage use type hints. See the style guide for type hints and docstrings for more details.testspackage.testspackage.black,codespell, andisortGitHub actions.mypyGitHub action.testsGitHub actions.