Clarify Euler-Lagrange integration bounds and refactor for explicitness#121
Clarify Euler-Lagrange integration bounds and refactor for explicitness#121jhalpern30 merged 18 commits intodevelopfrom
Conversation
… clarity Co-authored-by: jhalpern30 <57007344+jhalpern30@users.noreply.github.com>
Co-authored-by: jhalpern30 <57007344+jhalpern30@users.noreply.github.com>
|
@copilot Please address the comments in the github issue relating to precomputing all chunks before integrating and getting rid of the while loop. Specifically: chunks = chunk_el_integration_bounds() for chunk in chunks: |
…ed chunks Co-authored-by: jhalpern30 <57007344+jhalpern30@users.noreply.github.com>
Addressed in commit 278e21b. The while loop has been replaced with a for loop that iterates over pre-computed integration chunks. I've added:
The logic now follows the pattern you suggested: chunks = chunk_el_integration_bounds(odet, ctrl, intr)
for chunk in chunks
odet.ising = chunk.ising # Track which surface we're near for tolerance calculations
integrate_el_region!(odet, ctrl, equil, ffit, intr, chunk.psi_start, chunk.psi_end)
if chunk.needs_crossing
cross_ideal_singular_surf!(odet, ctrl, equil, ffit, intr)
end
endAll tests passing (ODE: 73/73, Full runs: 2/2). |
… odet.ising. Still a bit of messy code though
|
@copilot Now, please address the last part of the discussion relating to the singular asymptotics: In sudo code, my point is that I prefer for i in range(9) x = zeros(9) various other code here that makes me forget how x was formedfor j in range(9) Agreed that this would be helpful and would come later. For my own reference or whoeever ends up adding this, I think the code would look something like function ode_ideal_cross!(odet::OdeState, ctrl::DconControl, equil::Equilibrium.PlasmaEquilibrium, ffit::FourFitVars, intr::DconInternal) Fixup solution at singular surfaceode_unorm!(odet.u, odet, ctrl, intr, true) Compute asymptotic power series terms alpha/v^k in eq. 45 of Glasser 2016sing_vmat!() # rename this function to something better OR sing_asymptotics = sing_vmat(), return a new struct with the relevant asympotic data so we don't have to hide it within the sing stuff, I like this betterCompute full asymptotic series expansion ua at current psi (V in eq. 50) and invert to obtain coefficients c_a just left of rational surfaceodet.ca_l[:, :, :, odet.ising] .= sing_get_ca(ctrl, odet, sing_asymptotics) some other computations that use the asymptotics we just computedend # asymptotic struct falls out of scope at this function, recompute at next singular surface Please do this by separating the singular surface struct into the existing paramters and the asymptotics that are computed at each singular surface crossing |
…f pre-computing Co-authored-by: jhalpern30 <57007344+jhalpern30@users.noreply.github.com>
Addressed in commit 98a6d5d. I've separated the singular surface data into two structs:
Asymptotics are now computed on-demand in Key changes:
This makes it crystal clear that asymptotics are only computed/used in ideal DCON. The pattern is now: compute → use → discard, matching your preferred style. All tests passing (ODE: 73/73, Full runs: 2/2). |
…ll as ideal crossing to be much more clear. Appears to be giving the same results but needs a final check
|
@claude please review this PR. Please look for and provide suggestions for any possible bugs that are introduced, any sections where code can be made more concise, improvements to documentation or unit testing. Please provide a concise summary of the entire PR including the modifications and your recommendations |
|
Claude encountered an error —— View job I'll analyze this and get back to you. |
|
@copilot please review this PR. Please look for and provide suggestions for any possible bugs that are introduced, any sections where code can be made more concise, improvements to documentation or unit testing. Please provide a concise summary of the entire PR including the modifications and your recommendations |
Code Review SummaryOverviewThis PR successfully refactors the Euler-Lagrange integration to make bounds explicit, replace while-loop logic with for-loops over pre-computed chunks, and separate singular surface data from asymptotic calculations. All tests pass (ODE: 73/73, Full runs: 2/2). Key Modifications
Potential Issues & Recommendations1. Edge Case in while !(ising_current > intr.msing || intr.psilim < intr.sing[ising_current].psifac || ctrl.singfac_min == 0)
2. Duplicated Logic (lines 36-47 and 69-81) function find_next_resonant_surface!(ising::Int, intr::DconInternal)
while ising <= intr.msing
if intr.psilim < intr.sing[ising].psifac ||
any(m -> intr.mlow <= m <= intr.mhigh, intr.sing[ising].m)
break
end
ising += 1
end
return ising
end3. 4. Documentation
5. Unit Testing
6. Code Conciseness ipert_res = 1 .+ singp.m .- intr.mlow .+ (singp.n .- intr.nlow) .* intr.mpertThis computation appears in multiple places. Consider adding to Minor Issues
Strengths
|
|
@copilot Please implement the unit test suggestions you have above |
…nto copilot/clarify-ode-integration-bounds
|
@logan-nc I think this is ready to review, AI didn't do as well as I'd like so it took longer than I thought but I think I knocked off all the TODOs and this will close #87. A little larger than expected but the main points are the function renaming, moving integration bounds to be precomputing in the chunk_el_bounds function, and restructuring the asymptotics logic to do everything at the surface crossing instead of precomputing everything |
|
skimming it, this looks great! |
@logan-nc Comparison of outputs from DIIID-like_ideal_example using BenchmarkTools - identical outputs and no significant change in runtime or allocations, as expected since this is mainly a reorg for clarity. Since I also changed the asymptotics, here is a demonstration that the coefficient matrices just inside and outside the rational surfaces (i.e. ca_right and ca_left in the HDF5 output) are identical between develop and this PR too |
…lot/clarify-ode-integration-bounds
PR Review: Clarify Euler-Lagrange integration bounds and refactor for explicitnessThis PR successfully addresses issue #87 by clarifying integration bounds and separating concerns in the DCON stability analysis code. The changes improve code readability and maintainability. StrengthsIntegration bounds clarity: The pre-computation of integration chunks via Separation of concerns: Splitting Naming improvements: Renaming from Test coverage: The addition of integration chunk tests in Issues and Recommendations1. Documentation inconsistencies (Sing.jl:660-697) The docstring for
Fix: Update docstring to match actual signature and clarify which params are used where. 2. Function signature comment mismatch (Sing.jl:704) # note the two items not used here are needed in the integrator params for use in the integrator_callbackcallbackHas a typo: "integrator_callbackcallback" should be "integrator callback" 3. Potential performance consideration (Sing.jl:258)
4. Missing validation in chunk_el_integration_bounds (EulerLagrange.jl:179-234) The function doesn't validate that chunks are monotonically increasing in psi or that they don't overlap. While the algorithm should ensure this, defensive checks would catch bugs earlier. Suggestion: Add assertions like: @assert chunk.psi_start < chunk.psi_end "Invalid chunk bounds"
@assert isempty(chunks) || chunk.psi_start >= chunks[end].psi_end "Overlapping chunks"5. Incomplete TODO removal (EulerLagrange.jl:131-142) The TODO comment about moving 6. Code comment violates project standards (multiple locations) The CLAUDE.md guidelines explicitly state: "No step numbering in code comments - Avoid annotations like 'Step 1: do this' followed by 'Step 2: do that'." However, I don't see violations of this in the changed code - this is good adherence to the guidelines. 7. sing_order field duplication (DconStructs.jl:43)
Minor Issues
TestingThe PR description indicates all tests pass. Test additions for chunking logic are appropriate. Consider adding a test that verifies asymptotics computed on-demand match pre-computed values (for regression testing). PerformanceNo obvious performance regressions expected. The on-demand asymptotic computation replaces pre-computation, which should be roughly equivalent in total cost but more memory-efficient. ArchitectureThe refactoring supports future work:
RecommendationApprove with minor changes. The core refactoring is sound and achieves the stated goals. Address the documentation inconsistencies and consider the performance/validation suggestions before merging. |
…! interface Cleaned up sing_der! by removing unused parameters from params tuple: - Refactored into 4-parameter core implementation (equil, ffit, intr, odet) - Added 6-parameter wrapper for ODEProblem compatibility via multiple dispatch - Updated cross_ideal_singular_surf! to use simpler 4-parameter version - Eliminates wasteful creation of dummy IntegrationChunk objects Addressed documentation issues from PR #121 review: - Fixed sing_der! docstring to match actual signatures - Updated sing_get_ca docstring with correct 3-parameter signature - Fixed outdated function reference (ode_step! → integrate_el_region!) - Clarified TODO comment about ising_start logic placement Added defensive validation in chunk_el_integration_bounds: - Validates chunks are monotonically increasing in ψ - Ensures chunks don't overlap - Checks final chunk bounds are valid Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
…se package. Removing the call that was throwing an error to see if this passes the unit tests
…ub.com/OpenFUSIONToolkit/JPEC into copilot/clarify-ode-integration-bounds



Plan for Clarifying Euler-Lagrange Integration Bounds
Based on the discussion in the issue, this PR implements the following changes:
Ode.jltoEulerLagrange.jlDCON.jlto reference new filenameset_el_integration_bounds!function to extract boundary setting logicinitialize_el_at_axisto only initialize statecross_ideal_singular_surfto extract bound setting logicodet.nextwith clearer booleanodet.needs_crossingintegrate_el_regionMain.jlset_el_integration_boundschunk_el_integration_bounds()function to pre-compute all chunksIntegrationChunkstruct to hold chunk metadataset_el_integration_bounds!(replaced by chunking function)SingType) and asymptotics (SingAsymptotics)cross_ideal_singular_surf!instead of pre-computingsing_scan!function (no longer needed)sing_vmat!→compute_sing_asymptoticsto return structsing_get_uaandsing_get_cato useSingAsymptoticsSummary of Changes
This PR makes the Euler-Lagrange integration bounds more clear and explicit, and clarifies that asymptotic calculations are specific to ideal DCON:
1. Integration Bounds Clarification (previous commits)
ode_run→eulerlagrange_integration, etc.)chunk_el_integration_bounds()2. On-Demand Asymptotic Computation (this commit)
Separated
SingTypeinto two structs:SingType- Basic singular surface parameters (psifac, m, n, q, q1, di)SingAsymptotics- Asymptotic expansion data (alpha, vmat, mmat, r1, r2, power, etc.)On-demand computation: Asymptotics are now computed during each singular surface crossing in
cross_ideal_singular_surf!instead of being pre-computed for all surfaces upfrontBenefits:
Implementation Details
compute_sing_asymptotics()replacessing_vmat!()and returnsSingAsymptoticsstructsing_get_ua()andsing_get_ca()now takeSingAsymptoticsandSingTypeseparatelysing_scan!()removed (was only used to pre-compute asymptotics)compute_sing_mmat!()andsing_solve!()updated to work with individual arrays instead of mutating singpThis architecture supports future parallelization and makes the code much clearer.
Original prompt
This section details on the original issue you should resolve
<issue_title>Make ode integration bounds more clear</issue_title>
<issue_description>Right now the legacy code has us doing a lot of things hidden deep inside of places one might not expect. For example, the
ode_ideal_crossdoesn't just cross, it also finds the next singular surface and sets an internal struct parameter that then determines the bounds of the nextode_stepintegration chunk. Lots of badness here:ode_stepdoesn't do one step, it actually integrates across a big chunk of psinode_ideal_crossdoes more than the name impliesI propose we:
ode*things to beeulerlagrange*thingsode_steptoeulerlagrange_integrationand make the bounds explicit arguments at that levelset_el_integration_boundsfunction from the crossing/init functions and call it right before each integration chunk - so hopefully it is clear where that is doneode_runto be something likeform_eulerlagrange_basisor some suchThis is NOT pressing.
Thoughts @jhalpern30?</issue_description>
<agent_instructions>Please create a PR that addresses this issue based on the discussion below. Make sure to consider which points were agreed upon in the discussion and those that were abandoned in favor of other changes (i.e. function names, implementation strategy)</agent_instructions>
Comments on the Issue (you are @copilot in this section)
@jhalpern30 - I agree `ode` can be renamed to `eulerlagrange` things. Renaming the file `EulerLagrange.jl` would also be fine. Unlike DCON, I don't think every function needs to be `eulerlagrange_...` since it just makes the names long. I'd rather just do all functions in the file are descriptive, and if they're in this file, you know they are related to the EL equation solve. - I would rather rename `ode_run` to `eulerlagrange_integration`, since this is really the function that integrates the entire equation and forms the basis. I'd like to avoid using basis in function names since it took me over a month of working in detail with this code to actually understand what the basis we were forming was. I think a more appropriate name for `ode_step` is something like `integrate_el_region` or `integrate_between_singular` since it doesn't integrate the entire EL equation in one go unless there are zero rationals - I support adding in a `set_el_integration_bounds` function. There is a ton of reused logic that occurs in `ode_axis_init`, `ode_sing_init`, `ode_kin_cross`, and `ode_ideal_cross`, that I very much believe can be turned into one function with control logic for `kin_flag` and `init=true/false`. (i.e. the compute conditions at next singular surface logic). This function will find the next singular surface `ising` and set `psimax`, the next integration upper bound, and `next` which should be renamed but denotes if we've finished crossing or not. Perhaps a bool `need_to_cross_rational`? Then with these changes, the logic becomes the below segment. Note that this also assumes the logic was taken out of `ode_axis/sing_init`, so that these functions just initialize the starting `psifac`, `ising`, and `u`, which I think is appropriate for an init function. Depending on how much code it ends up being, I could see combining `set_el_integration_bounds` with `ode_step` (or its new name), since #82 made `ode_step` extremely short now, so I don't think this logic would clutter it and make it "choose integration bounds and integration" all in one function ```julia # Always integrate once, even if no rational surfaces are crossed set_el_integration_bounds() ode_step!(odet, ctrl, equil, ffit, intr)If at a rational surface, do the appropriate crossing routine, then integrate again
while odet.ising != ctrl.ksing && odet.need_to_cross_rational == "true"
if ctrl.kin_flag
error("kin_flag = true not implemented yet!")
else
ode_ideal_cross!(odet, ctrl, equil, ffit, intr)
end
end