Refactor classification.f90 to eliminate code duplication with CutDetector
Problem Statement
The trace_orbit_with_classifiers subroutine in src/classification.f90 is a 416-line monolithic routine that reimplements cut detection logic that already exists in the CutDetector type (src/cut_detector.f90). This creates:
- Code duplication: ~200 lines of identical tip/period detection logic
- Maintenance burden: Bug fixes must be applied in two places
- Technical debt: Prevents clean Python API for classification
- Testing difficulty: Classification logic intertwined with integration and I/O
Current Architecture
trace_orbit_with_classifiers (classification.f90:48-416)
Does everything in one routine:
- Orbit integration (like
trace_orbit)
- Banana tip detection (v_parallel sign change)
- Periodic boundary crossing detection (toroidal cuts)
- Stencil interpolation (Lagrange polynomials)
- J_parallel classification (
check_orbit_type)
- Topological ideal/non-ideal classification
- Minkowski fractal dimension computation (
fract_dimension)
- Dynamic array management (reallocating tip/period buffers)
- File I/O (writing 10+
fort.* files)
- Early exit logic (
fast_class, class_plot, regularity detection)
Structure:
subroutine trace_orbit_with_classifiers(anorb, ipart)
! Lines 48-151: Setup, coordinate conversion, early exits
! Lines 154-207: Allocate stencil & cut storage arrays
! Lines 210-401: MAIN LOOP
! do it=2,ntimstep ! Outer loop
! do ktau=1,ntau ! Inner loop
! call orbit_timestep_sympl(...) ! Integration
!
! ! === DUPLICATED FROM trace_to_cut ===
! ! Update stencil (lines 241-249)
! ! Detect tip (lines 252-254)
! ! Interpolate tip (lines 256-263)
! ! Detect period (lines 305-314)
! ! Interpolate period (lines 316-323)
!
! ! === CLASSIFICATION (unique) ===
! ! classify at tip (lines 288-299)
! ! Minkowski at ntcut (lines 348-382)
!
! ! === FILE I/O (unique) ===
! ! write fort.* files (lines 379-387)
! enddo
! enddo
! Lines 403-416: Cleanup
end subroutine
CutDetector::trace_to_cut (cut_detector.f90:70-152)
Clean, focused function:
- Integrates orbit until next tip or periodic crossing
- Returns
var_cut(6) = [s, θ, φ, |v|, v_∥, J_∥]
- Returns
cut_type (0=tip, 1=periodic)
- Used by
examples/orbits_and_cuts.py for visualization
Key difference: Returns one cut at a time, caller manages loop.
Code Duplication Analysis
Identical Logic in Both Routines
| Feature |
trace_orbit_with_classifiers |
CutDetector::trace_to_cut |
| Parameters |
nplagr=6, nder=0, npl_half=3 |
nplagr=6, nder=0, nplagr/2=3 |
| Stencil update |
Lines 241-249 |
Lines 91-100 |
| Tip detection |
Lines 252-254 |
Lines 106-108 |
| Tip interpolation |
Lines 256-263 |
Lines 111-117 |
| Period detection |
Lines 305-314 |
Lines 126-135 |
| Period interpolation |
Lines 316-323 |
Lines 138-144 |
| Parallel invariant |
Lines 241, 286 |
Lines 91, 117 |
| Stencil rotation |
ipoi=cshift(ipoi,1) |
self%ipoi=cshift(self%ipoi,1) |
Total duplicated lines: ~200 (lines 241-263 + 305-343 in classification.f90)
What trace_orbit_with_classifiers Adds
Beyond cut detection, it provides:
-
Cut storage (lines 267-285, 325-343):
! Dynamic arrays that grow as cuts are collected
real(dp), allocatable :: zpoipl_tip(:,:) ! (2, nfp_tip)
real(dp), allocatable :: zpoipl_per(:,:) ! (2, nfp_per)
! Reallocate when full (13 OpenMP critical sections!)
-
J_parallel & topological classification (lines 288-299):
fpr_in = [var_tip(1), var_tip(iangvar), var_tip(6)]
call check_orbit_type(nturns, nfp_cot, fpr_in, ideal, ijpar, ierr_cot)
iclass(1,ipart) = ijpar ! 0=unclassified, 1=regular, 2=stochastic
iclass(2,ipart) = ideal ! 0=unclassified, 1=ideal, 2=non-ideal
-
Minkowski/fractal dimension (lines 348-382):
if(kt == ntcut) then
call fract_dimension(ifp_tip, zpoipl_tip(:,1:ifp_tip), fraction)
if(fraction > 0.2d0) then
regular = .False.
iclass(3,ipart) = 2 ! Stochastic
else
iclass(3,ipart) = 1 ! Regular
endif
endif
-
File output (scattered):
fort.20000 - trapped-passing boundary (line 101)
fort.10000 - forced regular passing (line 146)
fort.10011/10012 - regular passing/trapped (lines 437, 439)
fort.10021/10022 - stochastic passing/trapped (lines 443, 445)
fort.40012/40022/40032 - J_parallel classes (lines 456-460)
fort.50012/50022/50032 - topological classes (lines 470-474)
-
Early exit conditions:
fast_class=.true. → exit after first tip (line 297)
class_plot=.true. + Minkowski done → exit (line 380)
regular=.true. → skip integration, just count (lines 211-220)
Why This Matters
Current Issues
-
Python API blocked: Tests (test_classification_api.py, test_simple_api.py, test_batch_api.py) expect high-level classify_fast() API, but implementation requires:
- Managing global params (
class_plot, fast_class, tcut, ntcut)
- Parsing
fort.* files (no data structure return)
- Working directory changes (fort units are global)
-
Maintenance cost: Bug fix in cut detection must be applied twice:
- Fixed in
trace_to_cut for visualization
- Separately fixed in
trace_orbit_with_classifiers for classification
-
Testing: Can't test classification logic independently from:
- Integration
- File I/O
- OpenMP parallelization
-
Extensibility: Adding new classification methods requires:
- Modifying 416-line routine
- Understanding nested loops, dynamic allocation, OpenMP critical sections
Impact on Users
- Fortran users: Works fine, just hard to maintain
- Python users: Cannot access classification features without:
- File I/O workarounds
- Managing working directory
- Parsing text files
Proposed Refactoring Plan
Phase 1: Extract Cut Storage (Low Risk)
Goal: Simplify memory management, prepare for CutDetector integration.
New type:
! In classification.f90
type :: ClassificationCutStorage
integer :: max_cuts
integer :: n_tips, n_periods
! Fixed-size arrays (no dynamic reallocation)
real(dp), allocatable :: tips(:,:) ! (2, max_cuts) - [s, θ]
real(dp), allocatable :: periods(:,:) ! (2, max_cuts) - [s, θ]
real(dp), allocatable :: tip_data(:,:) ! (6, max_cuts) - full var_tip
contains
procedure :: init_storage
procedure :: add_tip
procedure :: add_period
procedure :: is_full
end type ClassificationCutStorage
Benefits:
- Eliminates 13 OpenMP critical sections for reallocation
- Pre-allocate based on expected number of cuts
- OpenMP-friendly (no mid-loop allocation)
Changes to trace_orbit_with_classifiers:
! Replace lines 162-167, 267-285, 325-343
type(ClassificationCutStorage) :: storage
call storage%init_storage(max_cuts=1000)
! In loop, replace reallocation with:
if(.not. storage%is_full()) then
call storage%add_tip(var_tip)
endif
Estimated effort: 2-3 hours
Risk: Low (isolated change, preserves existing logic)
Phase 2: Use CutDetector for Detection (Medium Risk)
Goal: Eliminate duplicated cut detection code.
Key insight: trace_to_cut integrates until next cut, but trace_orbit_with_classifiers integrates fixed timesteps. Need to bridge this:
subroutine trace_orbit_with_classifiers_v2(anorb, ipart)
type(CutDetector) :: cutty
type(ClassificationCutStorage) :: storage
real(dp) :: var_cut(6)
integer :: cut_type, ierr, n_cuts
integer(8) :: kt, kt_start, kt_cut
! === SETUP (keep lines 48-151 mostly as-is) ===
! Initialize cutty instead of local variables
call cutty%init(fper, z)
call storage%init_storage(max_cuts=1000)
! === MAIN LOOP: Replace lines 210-401 ===
n_cuts = 0
kt = 0
do while(n_cuts < storage%max_cuts .and. kt < ntimstep*ntau)
kt_start = kt
! Use CutDetector instead of inline logic
call trace_to_cut_with_counter(cutty, anorb%si, anorb%f, z, &
var_cut, cut_type, kt_cut, ierr)
kt = kt + kt_cut
if(ierr /= 0) exit ! Particle lost
! Update confined counters (need to backfill timesteps)
do it = (kt_start/ntau)+1, (kt/ntau)
if(passing) then
!$omp atomic
confpart_pass(it) = confpart_pass(it) + 1.d0
else
!$omp atomic
confpart_trap(it) = confpart_trap(it) + 1.d0
endif
enddo
n_cuts = n_cuts + 1
! === CLASSIFICATION (keep) ===
if(cut_type == 0) then ! Tip
call storage%add_tip(var_cut)
! J_parallel & topological classification
fpr_in = [var_cut(1), var_cut(iangvar), var_cut(6)]
call check_orbit_type(nturns, nfp_cot, fpr_in, ideal, ijpar, ierr_cot)
iclass(1,ipart) = ijpar
iclass(2,ipart) = ideal
if(fast_class) exit
elseif(cut_type == 1) then ! Periodic crossing
call storage%add_period(var_cut)
endif
! === MINKOWSKI AT TIME CUTOFF ===
if(kt >= ntcut) then
if(storage%n_tips > 0) then
call fract_dimension(storage%n_tips, storage%tips(:,1:storage%n_tips), fraction)
if(fraction > 0.2d0) then
regular = .False.
iclass(3,ipart) = 2
else
iclass(3,ipart) = 1
endif
endif
if(class_plot) then
call output_minkowsky_class(ipart, regular, passing)
exit
endif
! Continue but skip integration (keep regularity shortcut)
regular = .True.
endif
enddo
! === CLEANUP (keep lines 403-416) ===
! Write files if class_plot
if(class_plot .and. .not. passing) then
call output_jpar_class(ipart, ijpar)
call output_topological_class(ipart, ideal)
endif
zend(:,ipart) = z
times_lost(ipart) = kt*dtaumin/v0
end subroutine
New helper needed:
! In cut_detector.f90
subroutine trace_to_cut_with_counter(self, si, f, z, var_cut, cut_type, n_steps, ierr)
! Same as trace_to_cut, but also returns n_steps taken
integer(8), intent(out) :: n_steps
! ...
n_steps = 0
do i=1, nstep_max
n_steps = n_steps + 1
call tstep(...)
! ... rest as before
enddo
end subroutine
Benefits:
- ✅ Eliminates ~200 lines of duplicate code
- ✅ Reuses tested cut detection from
CutDetector
- ✅ Single place to fix bugs
- ✅ Easier to understand (separation of concerns)
Challenges:
- ⚠️ Need to track timesteps correctly for
confpart_pass/trap arrays
- ⚠️
CutDetector has allocatable components → not thread-safe
- Solution: Make thread-private or use
!$omp critical around trace_to_cut
- ⚠️ Behavior change: currently updates
confpart every ntau steps, new version would update at each cut
- Solution: Backfill timesteps between cuts (see loop above)
Testing strategy:
- Run existing
simple.x test cases with fast_class=.true.
- Compare
iclass(:,:) arrays bit-for-bit
- Compare
fort.* file outputs
- Check
confpart_pass/trap arrays match
Estimated effort: 1-2 days
Risk: Medium (needs careful testing, OpenMP considerations)
Phase 3: Separate Classification Logic (High Value)
Goal: Make classification logic reusable, testable, and accessible from Python.
New module: src/orbit_classification.f90
module orbit_classification
use params, only: dp => real64
implicit none
private
public :: classify_from_tips, ClassificationResult
type :: ClassificationResult
integer :: ijpar ! 0=unclassified, 1=regular, 2=stochastic
integer :: ideal ! 0=unclassified, 1=ideal, 2=non-ideal
integer :: minkowski ! 0=unclassified, 1=regular, 2=stochastic
end type
contains
! Pure function: no side effects, no file I/O, thread-safe
function classify_from_tips(tip_cuts, n_tips, nturns, tcut, dtaumin, v0) &
result(classes)
real(dp), intent(in) :: tip_cuts(:,:) ! (6, n_tips) from CutDetector
integer, intent(in) :: n_tips
integer, intent(in) :: nturns
real(dp), intent(in) :: tcut, dtaumin, v0
type(ClassificationResult) :: classes
integer :: nfp_cot, ierr_cot
real(dp) :: fpr_in(3), fraction
integer :: i, iangvar
! Initialize
classes%ijpar = 0
classes%ideal = 0
classes%minkowski = 0
if(n_tips == 0) return
iangvar = 2 ! Use theta angle
! === J_PARALLEL & TOPOLOGICAL CLASSIFICATION ===
! Process each tip sequentially (like trace_orbit_with_classifiers)
nfp_cot = 0
do i = 1, n_tips
fpr_in(1) = tip_cuts(1, i) ! s
fpr_in(2) = tip_cuts(iangvar, i) ! theta
fpr_in(3) = tip_cuts(6, i) ! parallel invariant
call check_orbit_type(nturns, nfp_cot, fpr_in, &
classes%ideal, classes%ijpar, ierr_cot)
! Early exit logic could be added here if needed
enddo
! === MINKOWSKI FRACTAL DIMENSION ===
if(n_tips > 0) then
! Extract (s, theta) for fractal dimension
call fract_dimension(n_tips, tip_cuts(1:2, 1:n_tips), fraction)
if(fraction > 0.2d0) then
classes%minkowski = 2 ! Stochastic
else
classes%minkowski = 1 ! Regular
endif
endif
end function classify_from_tips
! Helper for batch processing
subroutine classify_batch(tip_cuts_batch, n_tips_batch, nturns, &
n_particles, classes_out)
real(dp), intent(in) :: tip_cuts_batch(:,:,:) ! (6, max_tips, n_particles)
integer, intent(in) :: n_tips_batch(:) ! (n_particles)
integer, intent(in) :: nturns, n_particles
type(ClassificationResult), intent(out) :: classes_out(:) ! (n_particles)
integer :: ipart
!$omp parallel do
do ipart = 1, n_particles
classes_out(ipart) = classify_from_tips( &
tip_cuts_batch(:, 1:n_tips_batch(ipart), ipart), &
n_tips_batch(ipart), nturns, 0.0d0, 0.0d0, 0.0d0)
enddo
!$omp end parallel do
end subroutine classify_batch
end module orbit_classification
Update trace_orbit_with_classifiers:
subroutine trace_orbit_with_classifiers_v3(anorb, ipart)
use orbit_classification, only: classify_from_tips, ClassificationResult
type(ClassificationResult) :: classes
! === COLLECT CUTS (using Phase 2 code) ===
! ... (Phase 2 loop, but without inline classification)
! === CLASSIFY FROM COLLECTED CUTS ===
classes = classify_from_tips(storage%tip_data, storage%n_tips, &
nturns, tcut, dtaumin, v0)
iclass(1,ipart) = classes%ijpar
iclass(2,ipart) = classes%ideal
iclass(3,ipart) = classes%minkowski
! === FILE OUTPUT (optional) ===
if(class_plot) then
call write_classification_files(ipart, classes, storage, passing)
endif
! === CLEANUP ===
! ...
end subroutine
Benefits:
- ✅ Python-accessible: Can wrap
classify_from_tips directly
- ✅ Testable: Pure function, no side effects
- ✅ Reusable: Works with any cut data source
- ✅ Composable: Can chain with other analysis
- ✅ Thread-safe: No global state, no I/O in classification
Python API enabled:
import pysimple
pysimple.init(vmec_file, trace_time=0.015)
particles = pysimple.sample_surface(n=16, surface=0.4)
# Collect cuts (Phase 2)
cuts_data = pysimple.collect_orbit_cuts(particles, max_cuts=100)
# Classify (Phase 3 - NEW!)
results = pysimple.classify_orbits(cuts_data)
# Results dict:
# {
# 'j_parallel': np.array([1, 2, 0, ...]), # per-particle
# 'topology': np.array([1, 0, 2, ...]), # per-particle
# 'minkowski': np.array([1, 2, 1, ...]), # per-particle
# 'n_tips': np.array([45, 67, 32, ...]) # per-particle
# }
Estimated effort: 2-3 days
Risk: Medium (need to preserve exact classification behavior)
Phase 4: File I/O Separation (Optional Polish)
Goal: Decouple file writing from classification logic.
New module: src/classification_io.f90
module classification_io
use orbit_classification, only: ClassificationResult
implicit none
private
public :: write_classification_files, ClassificationFileWriter
type :: ClassificationFileWriter
logical :: class_plot
integer :: units(10) ! Fort unit numbers
logical :: units_open(10)
contains
procedure :: open_files
procedure :: close_files
procedure :: write_particle
end type
contains
subroutine write_classification_files(ipart, classes, storage, passing)
integer, intent(in) :: ipart
type(ClassificationResult), intent(in) :: classes
type(ClassificationCutStorage), intent(in) :: storage
logical, intent(in) :: passing
! Write to appropriate fort.* files based on classification
! (Move all write() statements here from trace_orbit_with_classifiers)
select case(classes%ijpar)
case(0)
write(iaaa_jer, *) zstart(2,ipart), zstart(5,ipart), trap_par(ipart)
case(1)
write(iaaa_jre, *) zstart(2,ipart), zstart(5,ipart), trap_par(ipart)
case(2)
write(iaaa_jst, *) zstart(2,ipart), zstart(5,ipart), trap_par(ipart)
end select
! ... similar for topology, minkowski
end subroutine
end module classification_io
Benefits:
- ✅ Clean separation: classification logic vs. output formatting
- ✅ Can disable I/O for Python API (just return data structures)
- ✅ Easier to support multiple output formats (HDF5, NetCDF, etc.)
Estimated effort: 4-6 hours
Risk: Low (pure refactoring, preserves file formats)
Migration Strategy
Backward Compatibility
During refactoring, maintain both versions:
! classification.f90
subroutine trace_orbit_with_classifiers(anorb, ipart)
! Current implementation (keep as legacy)
! Mark as deprecated in comments
end subroutine
subroutine trace_orbit_with_classifiers_v2(anorb, ipart)
! New implementation (Phase 2+)
end subroutine
Switch controlled by compile flag:
option(USE_LEGACY_CLASSIFICATION "Use old classification routine" OFF)
Testing:
! New test: test/tests/test_classification_parity.f90
! Compare outputs of both versions for same inputs
Gradual Rollout
-
Phase 1 (Week 1): Implement ClassificationCutStorage
- Test with current
trace_orbit_with_classifiers
- Verify identical results
-
Phase 2 (Week 2-3): Implement trace_orbit_with_classifiers_v2 using CutDetector
- Run in parallel with legacy version
- Compare outputs, fix discrepancies
-
Phase 3 (Week 4-5): Extract orbit_classification module
- Implement Python wrappers
- Update tests to use new API
-
Phase 4 (Week 6): Polish
- Extract I/O to
classification_io
- Update documentation
- Deprecate legacy version
-
Phase 5 (Week 7+): Remove legacy after soak period
- Delete old
trace_orbit_with_classifiers
- Clean up compile flags
Testing Requirements
Unit Tests (New)
-
test_classification_cut_storage.f90:
- Test
ClassificationCutStorage type
- Test overflow handling
- Test thread-safety
-
test_classify_from_tips.f90:
- Test
classify_from_tips with known inputs
- Compare against
check_orbit_type direct calls
- Test edge cases (0 tips, 1 tip, many tips)
-
test_cut_detector_consistency.f90:
- Verify
CutDetector::trace_to_cut gives same cuts as inline version
- Use fixed seed, identical initial conditions
Integration Tests
test_classification_parity.f90:
- Run both legacy and new
trace_orbit_with_classifiers
- Compare
iclass(:,:) arrays
- Compare
fort.* file contents
- Test with multiple VMEC files, surface values
Regression Tests
- Golden record comparison:
- Save outputs from current version (commit hash)
- Re-run after each phase
- Verify bit-for-bit identical results
Python Tests
test_python_classification_api.py:
- Test
pysimple.collect_orbit_cuts()
- Test
pysimple.classify_orbits()
- Compare with Fortran
simple.x outputs
- Re-enable currently skipped tests
Success Criteria
Must Have
Nice to Have
Metrics
- Code reduction: ~200 lines eliminated (duplication)
- Module count: +2 (orbit_classification, classification_io)
- Test coverage: +6 test files
- Python API: +2 functions (
collect_orbit_cuts, classify_orbits)
Open Questions
-
OpenMP threading: Should CutDetector be thread-safe (thread-private) or use critical sections?
- Proposal: Make thread-private copy per particle in
trace_parallel
-
Timestep tracking: How to maintain exact confpart_pass/trap behavior when using trace_to_cut?
- Proposal: Add
n_steps output to trace_to_cut_with_counter
-
File I/O from Python: Should Python API support writing fort.* files or only return data?
- Proposal: Return data by default, add
legacy_files=True option
-
Backward compatibility period: How long to keep legacy version?
- Proposal: 2 release cycles (~6 months)
-
Performance impact: Will function call overhead of trace_to_cut slow down classification?
- Need: Benchmark Phase 2 against current version
- Mitigation: Inlining hints, profiling
Related Issues
References
src/classification.f90: Current implementation
src/cut_detector.f90: Reusable cut detection
examples/orbits_and_cuts.py: Example usage of trace_to_cut
src/check_orbit_type_sub.f90: J_parallel classification logic
Implementation Checklist
Phase 1: Cut Storage
Phase 2: Use CutDetector
Phase 3: Extract Classification
Phase 4: I/O Separation (Optional)
Phase 5: Cleanup
Refactor classification.f90 to eliminate code duplication with CutDetector
Problem Statement
The
trace_orbit_with_classifierssubroutine insrc/classification.f90is a 416-line monolithic routine that reimplements cut detection logic that already exists in theCutDetectortype (src/cut_detector.f90). This creates:Current Architecture
trace_orbit_with_classifiers(classification.f90:48-416)Does everything in one routine:
trace_orbit)check_orbit_type)fract_dimension)fort.*files)fast_class,class_plot, regularity detection)Structure:
CutDetector::trace_to_cut(cut_detector.f90:70-152)Clean, focused function:
var_cut(6)= [s, θ, φ, |v|, v_∥, J_∥]cut_type(0=tip, 1=periodic)examples/orbits_and_cuts.pyfor visualizationKey difference: Returns one cut at a time, caller manages loop.
Code Duplication Analysis
Identical Logic in Both Routines
trace_orbit_with_classifiersCutDetector::trace_to_cutnplagr=6,nder=0,npl_half=3nplagr=6,nder=0,nplagr/2=3ipoi=cshift(ipoi,1)self%ipoi=cshift(self%ipoi,1)Total duplicated lines: ~200 (lines 241-263 + 305-343 in classification.f90)
What
trace_orbit_with_classifiersAddsBeyond cut detection, it provides:
Cut storage (lines 267-285, 325-343):
J_parallel & topological classification (lines 288-299):
Minkowski/fractal dimension (lines 348-382):
File output (scattered):
fort.20000- trapped-passing boundary (line 101)fort.10000- forced regular passing (line 146)fort.10011/10012- regular passing/trapped (lines 437, 439)fort.10021/10022- stochastic passing/trapped (lines 443, 445)fort.40012/40022/40032- J_parallel classes (lines 456-460)fort.50012/50022/50032- topological classes (lines 470-474)Early exit conditions:
fast_class=.true.→ exit after first tip (line 297)class_plot=.true.+ Minkowski done → exit (line 380)regular=.true.→ skip integration, just count (lines 211-220)Why This Matters
Current Issues
Python API blocked: Tests (
test_classification_api.py,test_simple_api.py,test_batch_api.py) expect high-levelclassify_fast()API, but implementation requires:class_plot,fast_class,tcut,ntcut)fort.*files (no data structure return)Maintenance cost: Bug fix in cut detection must be applied twice:
trace_to_cutfor visualizationtrace_orbit_with_classifiersfor classificationTesting: Can't test classification logic independently from:
Extensibility: Adding new classification methods requires:
Impact on Users
Proposed Refactoring Plan
Phase 1: Extract Cut Storage (Low Risk)
Goal: Simplify memory management, prepare for CutDetector integration.
New type:
Benefits:
Changes to
trace_orbit_with_classifiers:Estimated effort: 2-3 hours
Risk: Low (isolated change, preserves existing logic)
Phase 2: Use CutDetector for Detection (Medium Risk)
Goal: Eliminate duplicated cut detection code.
Key insight:
trace_to_cutintegrates until next cut, buttrace_orbit_with_classifiersintegrates fixed timesteps. Need to bridge this:New helper needed:
Benefits:
CutDetectorChallenges:
confpart_pass/traparraysCutDetectorhas allocatable components → not thread-safe!$omp criticalaround trace_to_cutconfparteveryntausteps, new version would update at each cutTesting strategy:
simple.xtest cases withfast_class=.true.iclass(:,:)arrays bit-for-bitfort.*file outputsconfpart_pass/traparrays matchEstimated effort: 1-2 days
Risk: Medium (needs careful testing, OpenMP considerations)
Phase 3: Separate Classification Logic (High Value)
Goal: Make classification logic reusable, testable, and accessible from Python.
New module:
src/orbit_classification.f90Update
trace_orbit_with_classifiers:Benefits:
classify_from_tipsdirectlyPython API enabled:
Estimated effort: 2-3 days
Risk: Medium (need to preserve exact classification behavior)
Phase 4: File I/O Separation (Optional Polish)
Goal: Decouple file writing from classification logic.
New module:
src/classification_io.f90Benefits:
Estimated effort: 4-6 hours
Risk: Low (pure refactoring, preserves file formats)
Migration Strategy
Backward Compatibility
During refactoring, maintain both versions:
Switch controlled by compile flag:
Testing:
Gradual Rollout
Phase 1 (Week 1): Implement
ClassificationCutStoragetrace_orbit_with_classifiersPhase 2 (Week 2-3): Implement
trace_orbit_with_classifiers_v2usingCutDetectorPhase 3 (Week 4-5): Extract
orbit_classificationmodulePhase 4 (Week 6): Polish
classification_ioPhase 5 (Week 7+): Remove legacy after soak period
trace_orbit_with_classifiersTesting Requirements
Unit Tests (New)
test_classification_cut_storage.f90:ClassificationCutStoragetypetest_classify_from_tips.f90:classify_from_tipswith known inputscheck_orbit_typedirect callstest_cut_detector_consistency.f90:CutDetector::trace_to_cutgives same cuts as inline versionIntegration Tests
test_classification_parity.f90:trace_orbit_with_classifiersiclass(:,:)arraysfort.*file contentsRegression Tests
Python Tests
test_python_classification_api.py:pysimple.collect_orbit_cuts()pysimple.classify_orbits()simple.xoutputsSuccess Criteria
Must Have
Nice to Have
Metrics
collect_orbit_cuts,classify_orbits)Open Questions
OpenMP threading: Should
CutDetectorbe thread-safe (thread-private) or use critical sections?trace_parallelTimestep tracking: How to maintain exact
confpart_pass/trapbehavior when usingtrace_to_cut?n_stepsoutput totrace_to_cut_with_counterFile I/O from Python: Should Python API support writing
fort.*files or only return data?legacy_files=TrueoptionBackward compatibility period: How long to keep legacy version?
Performance impact: Will function call overhead of
trace_to_cutslow down classification?Related Issues
test_classification_api.py,test_simple_api.py,test_batch_api.pyReferences
src/classification.f90: Current implementationsrc/cut_detector.f90: Reusable cut detectionexamples/orbits_and_cuts.py: Example usage oftrace_to_cutsrc/check_orbit_type_sub.f90: J_parallel classification logicImplementation Checklist
Phase 1: Cut Storage
ClassificationCutStoragetypeinit_storage,add_tip,add_periodmethodstrace_orbit_with_classifiersPhase 2: Use CutDetector
trace_to_cut_with_countertocut_detector.f90trace_orbit_with_classifiers_v2confpartarraysPhase 3: Extract Classification
orbit_classification.f90moduleclassify_from_tipsfunctionclassify_batchfor paralleltrace_orbit_with_classifiers_v2to use new modulePhase 4: I/O Separation (Optional)
classification_io.f90modulelegacy_filesoptionPhase 5: Cleanup