Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions python/pysimple/_bminmax.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,15 @@
from __future__ import annotations


def needs_bminmax_cache(num_surf: int, ntcut: int, class_plot: bool) -> bool:
def classification_enabled(ntcut: int, class_plot: bool, fast_class: bool) -> bool:
"""Match the Fortran params.classification_enabled predicate."""
return int(ntcut) > 0 or bool(class_plot) or bool(fast_class)


def needs_bminmax_cache(
num_surf: int, ntcut: int, class_plot: bool, fast_class: bool = False
) -> bool:
"""Match the Fortran cache-reader predicate."""
if int(ntcut) > 0 or bool(class_plot):
if classification_enabled(ntcut, class_plot, fast_class):
return int(num_surf) > 1
return int(num_surf) != 1
15 changes: 15 additions & 0 deletions src/params.f90
Original file line number Diff line number Diff line change
Expand Up @@ -707,6 +707,21 @@ subroutine apply_config_aliases
end if
end subroutine apply_config_aliases

pure logical function classification_enabled()
!> Whether a run collects orbit classifications at all. Three call sites
!> must agree: the bmin/bmax cache the classifier needs to separate
!> trapped from passing, the dispatch to trace_orbit_with_classifiers,
!> and the class_parts.dat writer.
!>
!> fast_class belongs here. Without it, fast_class alone (tcut <= 0 and
!> class_plot = .False.) silently traced ordinary orbits and wrote no
!> classification output, even though the flag documents the opposite.
!> That combination is the only one giving fast classification without
!> the Minkowski fractal cut, since the cut fires at kt == ntcut and the
!> early exit in classification.f90 requires .not. class_plot.
classification_enabled = (ntcut > 0) .or. class_plot .or. fast_class
end function classification_enabled

subroutine reset_seed_if_deterministic
! for run with fixed random seed
integer :: seedsize
Expand Down
12 changes: 6 additions & 6 deletions src/simple_main.f90
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ module simple_main
boundary_event_time_width, integmode, relerr, trace_time, class_plot, &
fast_class, generate_start_only, ntcut, iclass, bmin, bmax, zstart, &
zend, trap_par, perp_inv, sbeg, ntimstep, should_skip, &
reset_seed_if_deterministic, field_input, isw_field_type, reuse_batch, &
reset_seed_if_deterministic, classification_enabled, &
field_input, isw_field_type, reuse_batch, &
max_consecutive_warning_holds, &
coord_input, wall_input, wall_units, wall_hit, wall_hit_cart, &
wall_query_rho_lcfs, &
Expand Down Expand Up @@ -1473,9 +1474,8 @@ subroutine init_bminmax
end subroutine init_bminmax

logical function needs_bminmax_cache()
! Match the current readers. Classifier semantics are kept unchanged
! here; broad classifier changes belong in the classifier PR.
if ((ntcut > 0) .or. class_plot) then
! Match the current readers.
if (classification_enabled()) then
needs_bminmax_cache = num_surf > 1
else
needs_bminmax_cache = num_surf /= 1
Expand Down Expand Up @@ -1835,7 +1835,7 @@ subroutine trace_orbit(anorb, ipart, orbit_traj, orbit_times)

if (swcoll) call reset_seed_if_deterministic

if (ntcut > 0 .or. class_plot) then
if (classification_enabled()) then
call trace_orbit_with_classifiers(anorb, ipart, class_result)
if (class_plot) then
call write_classification_results(ipart, class_result)
Expand Down Expand Up @@ -3168,7 +3168,7 @@ subroutine write_results

call write_spectre_crossing_events

if (ntcut > 0 .or. class_plot) then
if (classification_enabled()) then
open (newunit=unit, file='class_parts.dat', recl=1024)
do i = 1, ntestpart
write (unit, *) i, zstart(1, i), perp_inv(i), iclass(:, i)
Expand Down
12 changes: 12 additions & 0 deletions test/tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -503,6 +503,18 @@ if(SIMPLE_ENABLE_PYTHON_TOOLS)
set_tests_properties(test_bminmax_driver PROPERTIES
LABELS "integration;system"
TIMEOUT 120)

add_test(NAME test_fast_class_driver
COMMAND ${CMAKE_COMMAND} -E env
CTEST_BINARY_DIRECTORY=${CMAKE_CURRENT_BINARY_DIR}
${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/test_fast_class_driver.py
$<TARGET_FILE:simple.x>
${WOUT_FILE}
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR})
set_tests_properties(test_fast_class_driver PROPERTIES
LABELS "integration;system"
TIMEOUT 120)
endif()

add_executable (test_coordinates.x test_coordinates.f90)
Expand Down
17 changes: 10 additions & 7 deletions test/tests/bminmax_cache_cases.tsv
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
1 0 F F
0 0 F T
2 0 F T
0 0 T F
2 0 T T
0 1 F F
2 1 F T
1 0 F F F
0 0 F F T
2 0 F F T
0 0 T F F
2 0 T F T
0 1 F F F
2 1 F F T
1 0 F T F
0 0 F T F
2 0 F T T
8 changes: 5 additions & 3 deletions test/tests/test_bminmax_lifecycle.f90
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ program test_bminmax_lifecycle
use find_bminmax_sub, only: get_bminmax, init_bminmax_arrays
use magfie_sub, only: dp, magfie
use new_vmec_stuff_mod, only: nper
use params, only: class_plot, ntcut, num_surf
use params, only: class_plot, fast_class, ntcut, num_surf
use simple_main, only: needs_bminmax_cache
use test_bminmax_backend, only: test_magfie_backend
use test_utils, only: check, check_close
Expand All @@ -65,7 +65,7 @@ program test_bminmax_lifecycle
subroutine test_cache_gate_cases(errors)
integer, intent(inout) :: errors
character(len=256) :: cases_path
character(len=1) :: class_plot_token, expected_token
character(len=1) :: class_plot_token, fast_class_token, expected_token
integer :: unit_id, ios, case_num
logical :: expected, actual

Expand All @@ -77,7 +77,7 @@ subroutine test_cache_gate_cases(errors)

do
read(unit_id, *, iostat=ios) num_surf, ntcut, class_plot_token, &
expected_token
fast_class_token, expected_token
if (ios < 0) exit
if (ios > 0) then
print *, "ERROR: failed to read bminmax cache case", case_num + 1
Expand All @@ -87,6 +87,7 @@ subroutine test_cache_gate_cases(errors)

case_num = case_num + 1
class_plot = class_plot_token == "T"
fast_class = fast_class_token == "T"
expected = expected_token == "T"
actual = needs_bminmax_cache()
call check(actual .eqv. expected, "bminmax cache predicate case failed", &
Expand All @@ -99,6 +100,7 @@ subroutine test_cache_gate_cases(errors)

ntcut = 0
class_plot = .false.
fast_class = .false.
num_surf = 1
end subroutine test_cache_gate_cases

Expand Down
4 changes: 2 additions & 2 deletions test/tests/test_bminmax_python.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@ def iter_cases(path: Path):
for line in path.read_text(encoding="utf-8").splitlines():
if not line.strip():
continue
num_surf, ntcut, class_plot, expected = line.split()
num_surf, ntcut, class_plot, fast_class, expected = line.split()
yield (
(int(num_surf), int(ntcut), class_plot == "T"),
(int(num_surf), int(ntcut), class_plot == "T", fast_class == "T"),
expected == "T",
)

Expand Down
80 changes: 80 additions & 0 deletions test/tests/test_fast_class_driver.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
#!/usr/bin/env python3
"""Driver coverage for standalone fast classification.

fast_class documents itself as "quit immediately after fast classification".
Standalone (tcut <= 0, class_plot = .False.) it is the only configuration that
classifies orbits without the Minkowski fractal cut, which fires at
kt == ntcut, and without disabling the early exit, which requires
.not. class_plot.

The oracle is the classification output itself: a classifying run owes a
class_parts.dat with one row per test particle, and a non-classifying run owes
none. Both expectations are independent of how the dispatch predicate is
spelled.
"""

from __future__ import annotations

import sys
from pathlib import Path

from simple_driver import assert_file_absent, assert_row_count, run_simple_case

NTESTPART = 4

COMMON = """
&config
multharm = 3
contr_pp = -1d10
trace_time = 1d-4
ntestpart = 4
nper = 1
npoiper = 4
npoiper2 = 32
ntimstep = 4
num_surf = 1
sbeg = 0.3
notrace_passing = 1
netcdffile = 'wout.nc'
isw_field_type = 2
integmode = 3
deterministic = .True.
/
"""

# (name, extra namelist lines, classification expected)
CASES = (
("fast_class_standalone", "fast_class = .True.\ntcut = -1d0", True),
("fast_class_with_cut", "fast_class = .True.\ntcut = 5d-5", True),
("fast_class_with_plot", "fast_class = .True.\nclass_plot = .True.", True),
("fast_class_absent", "tcut = -1d0", False),
)


def config_with(additions: str) -> str:
return COMMON.replace(
"deterministic = .True.",
f"deterministic = .True.\n{additions}",
)


def main() -> int:
if len(sys.argv) != 3:
print("usage: test_fast_class_driver.py SIMPLE_X WOUT_FILE", file=sys.stderr)
return 2

simple_x = Path(sys.argv[1]).resolve()
wout = Path(sys.argv[2]).resolve()

for name, additions, classifies in CASES:
case_dir = run_simple_case(simple_x, wout, name, config_with(additions))
if classifies:
assert_row_count(case_dir, "class_parts.dat", NTESTPART)
else:
assert_file_absent(case_dir, "class_parts.dat")

return 0


if __name__ == "__main__":
raise SystemExit(main())
Loading