dkx 2.2.0__tar.gz → 2.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (221) hide show
  1. {dkx-2.2.0 → dkx-2.3.0}/PKG-INFO +39 -42
  2. {dkx-2.2.0 → dkx-2.3.0}/README.md +36 -40
  3. {dkx-2.2.0 → dkx-2.3.0}/dkx/__init__.py +84 -5
  4. {dkx-2.2.0 → dkx-2.3.0}/dkx/plotting.py +136 -9
  5. {dkx-2.2.0 → dkx-2.3.0}/dkx/representative.py +142 -11
  6. {dkx-2.2.0 → dkx-2.3.0}/dkx/run.py +127 -0
  7. {dkx-2.2.0 → dkx-2.3.0}/dkx/solve.py +20 -3
  8. {dkx-2.2.0 → dkx-2.3.0}/dkx/validation/release.py +1 -1
  9. {dkx-2.2.0 → dkx-2.3.0}/dkx.egg-info/PKG-INFO +39 -42
  10. {dkx-2.2.0 → dkx-2.3.0}/dkx.egg-info/SOURCES.txt +7 -0
  11. {dkx-2.2.0 → dkx-2.3.0}/dkx.egg-info/requires.txt +2 -1
  12. {dkx-2.2.0 → dkx-2.3.0}/pyproject.toml +12 -2
  13. {dkx-2.2.0 → dkx-2.3.0}/tests/test_benchmark_artifact_policy.py +1 -1
  14. {dkx-2.2.0 → dkx-2.3.0}/tests/test_benchmark_doc_claims.py +1 -1
  15. {dkx-2.2.0 → dkx-2.3.0}/tests/test_collisionality_artifact.py +1 -1
  16. dkx-2.3.0/tests/test_compilation_cache_is_not_size_capped.py +68 -0
  17. dkx-2.3.0/tests/test_environment_guard.py +57 -0
  18. {dkx-2.2.0 → dkx-2.3.0}/tests/test_er_trajectory_sweep.py +1 -1
  19. {dkx-2.2.0 → dkx-2.3.0}/tests/test_er_trajectory_sweep_artifact.py +1 -1
  20. dkx-2.3.0/tests/test_example_docstrings_stay_short.py +33 -0
  21. dkx-2.3.0/tests/test_examples_tree_contract.py +287 -0
  22. {dkx-2.2.0 → dkx-2.3.0}/tests/test_generate_autodiff_sensitivity_validation.py +2 -2
  23. {dkx-2.2.0 → dkx-2.3.0}/tests/test_generate_fortran_suite_benchmark_summary.py +2 -2
  24. {dkx-2.2.0 → dkx-2.3.0}/tests/test_generate_sfincs_paper_figs.py +2 -2
  25. {dkx-2.2.0 → dkx-2.3.0}/tests/test_generate_validation_dashboard.py +2 -2
  26. {dkx-2.2.0 → dkx-2.3.0}/tests/test_generate_w7x_high_nu_performance.py +1 -1
  27. {dkx-2.2.0 → dkx-2.3.0}/tests/test_getting_started_examples.py +1 -1
  28. {dkx-2.2.0 → dkx-2.3.0}/tests/test_paper_benchmark_bootstrap_loop.py +2 -2
  29. {dkx-2.2.0 → dkx-2.3.0}/tests/test_paper_benchmark_electron_root.py +2 -2
  30. {dkx-2.2.0 → dkx-2.3.0}/tests/test_paper_benchmark_monoenergetic.py +4 -4
  31. {dkx-2.2.0 → dkx-2.3.0}/tests/test_paper_benchmark_w7x_ambipolar.py +2 -2
  32. dkx-2.3.0/tests/test_plot_pdf_paging_is_documented_correctly.py +36 -0
  33. dkx-2.3.0/tests/test_readme_quickstart_runs.py +53 -0
  34. {dkx-2.2.0 → dkx-2.3.0}/tests/test_release_gate_metadata.py +1 -1
  35. dkx-2.3.0/tests/test_representative_run_works_when_installed.py +91 -0
  36. dkx-2.3.0/tests/test_run_function_is_not_shadowed_by_run_module.py +67 -0
  37. {dkx-2.2.0 → dkx-2.3.0}/tests/test_validation_artifacts.py +1 -1
  38. {dkx-2.2.0 → dkx-2.3.0}/tests/test_validation_deferred_lane_gates.py +3 -3
  39. {dkx-2.2.0 → dkx-2.3.0}/tests/test_validation_manifest_schema.py +1 -1
  40. {dkx-2.2.0 → dkx-2.3.0}/tests/test_validity.py +1 -1
  41. dkx-2.2.0/tests/test_examples_tree_contract.py +0 -459
  42. {dkx-2.2.0 → dkx-2.3.0}/LICENSE +0 -0
  43. {dkx-2.2.0 → dkx-2.3.0}/dkx/__main__.py +0 -0
  44. {dkx-2.2.0 → dkx-2.3.0}/dkx/ambipolar.py +0 -0
  45. {dkx-2.2.0 → dkx-2.3.0}/dkx/api.py +0 -0
  46. {dkx-2.2.0 → dkx-2.3.0}/dkx/batch.py +0 -0
  47. {dkx-2.2.0 → dkx-2.3.0}/dkx/bootstrap.py +0 -0
  48. {dkx-2.2.0 → dkx-2.3.0}/dkx/bounce_averaged.py +0 -0
  49. {dkx-2.2.0 → dkx-2.3.0}/dkx/cli.py +0 -0
  50. {dkx-2.2.0 → dkx-2.3.0}/dkx/coarse_precond.py +0 -0
  51. {dkx-2.2.0 → dkx-2.3.0}/dkx/collisions.py +0 -0
  52. {dkx-2.2.0 → dkx-2.3.0}/dkx/collocation.py +0 -0
  53. {dkx-2.2.0 → dkx-2.3.0}/dkx/compare.py +0 -0
  54. {dkx-2.2.0 → dkx-2.3.0}/dkx/console.py +0 -0
  55. {dkx-2.2.0 → dkx-2.3.0}/dkx/constants.py +0 -0
  56. {dkx-2.2.0 → dkx-2.3.0}/dkx/drift_kinetic.py +0 -0
  57. {dkx-2.2.0 → dkx-2.3.0}/dkx/er.py +0 -0
  58. {dkx-2.2.0 → dkx-2.3.0}/dkx/impurity.py +0 -0
  59. {dkx-2.2.0 → dkx-2.3.0}/dkx/input_compat.py +0 -0
  60. {dkx-2.2.0 → dkx-2.3.0}/dkx/inputs.py +0 -0
  61. {dkx-2.2.0 → dkx-2.3.0}/dkx/io.py +0 -0
  62. {dkx-2.2.0 → dkx-2.3.0}/dkx/magnetic_geometry.py +0 -0
  63. {dkx-2.2.0 → dkx-2.3.0}/dkx/moments.py +0 -0
  64. {dkx-2.2.0 → dkx-2.3.0}/dkx/momentum_correction.py +0 -0
  65. {dkx-2.2.0 → dkx-2.3.0}/dkx/monoenergetic.py +0 -0
  66. {dkx-2.2.0 → dkx-2.3.0}/dkx/multigrid.py +0 -0
  67. {dkx-2.2.0 → dkx-2.3.0}/dkx/namelist.py +0 -0
  68. {dkx-2.2.0 → dkx-2.3.0}/dkx/paths.py +0 -0
  69. {dkx-2.2.0 → dkx-2.3.0}/dkx/phase_space.py +0 -0
  70. {dkx-2.2.0 → dkx-2.3.0}/dkx/phi1.py +0 -0
  71. {dkx-2.2.0 → dkx-2.3.0}/dkx/profiling.py +0 -0
  72. {dkx-2.2.0 → dkx-2.3.0}/dkx/sensitivity.py +0 -0
  73. {dkx-2.2.0 → dkx-2.3.0}/dkx/shaing_callen.py +0 -0
  74. {dkx-2.2.0 → dkx-2.3.0}/dkx/solver_trace.py +0 -0
  75. {dkx-2.2.0 → dkx-2.3.0}/dkx/sparse_precond.py +0 -0
  76. {dkx-2.2.0 → dkx-2.3.0}/dkx/species.py +0 -0
  77. {dkx-2.2.0 → dkx-2.3.0}/dkx/units.py +0 -0
  78. {dkx-2.2.0 → dkx-2.3.0}/dkx/validation/__init__.py +0 -0
  79. {dkx-2.2.0 → dkx-2.3.0}/dkx/validation/artifacts.py +0 -0
  80. {dkx-2.2.0 → dkx-2.3.0}/dkx/validation/data_fetch.py +0 -0
  81. {dkx-2.2.0 → dkx-2.3.0}/dkx/validation/equilibria_manifest.json +0 -0
  82. {dkx-2.2.0 → dkx-2.3.0}/dkx/validation/fortran.py +0 -0
  83. {dkx-2.2.0 → dkx-2.3.0}/dkx/validation/lanes.py +0 -0
  84. {dkx-2.2.0 → dkx-2.3.0}/dkx/validation/panels.py +0 -0
  85. {dkx-2.2.0 → dkx-2.3.0}/dkx/validation/policy.py +0 -0
  86. {dkx-2.2.0 → dkx-2.3.0}/dkx/validation/series.py +0 -0
  87. {dkx-2.2.0 → dkx-2.3.0}/dkx/validity.py +0 -0
  88. {dkx-2.2.0 → dkx-2.3.0}/dkx/variational.py +0 -0
  89. {dkx-2.2.0 → dkx-2.3.0}/dkx/vmec_ascii.py +0 -0
  90. {dkx-2.2.0 → dkx-2.3.0}/dkx/workflows/__init__.py +0 -0
  91. {dkx-2.2.0 → dkx-2.3.0}/dkx/workflows/geometry_adapters.py +0 -0
  92. {dkx-2.2.0 → dkx-2.3.0}/dkx/workflows/optimization.py +0 -0
  93. {dkx-2.2.0 → dkx-2.3.0}/dkx/workflows/scans.py +0 -0
  94. {dkx-2.2.0 → dkx-2.3.0}/dkx/writer.py +0 -0
  95. {dkx-2.2.0 → dkx-2.3.0}/dkx/xgrid.py +0 -0
  96. {dkx-2.2.0 → dkx-2.3.0}/dkx.egg-info/dependency_links.txt +0 -0
  97. {dkx-2.2.0 → dkx-2.3.0}/dkx.egg-info/entry_points.txt +0 -0
  98. {dkx-2.2.0 → dkx-2.3.0}/dkx.egg-info/top_level.txt +0 -0
  99. {dkx-2.2.0 → dkx-2.3.0}/setup.cfg +0 -0
  100. {dkx-2.2.0 → dkx-2.3.0}/tests/test_api_contracts.py +0 -0
  101. {dkx-2.2.0 → dkx-2.3.0}/tests/test_audit_suite_output_keys.py +0 -0
  102. {dkx-2.2.0 → dkx-2.3.0}/tests/test_audit_suite_runtime_drift.py +0 -0
  103. {dkx-2.2.0 → dkx-2.3.0}/tests/test_batch.py +0 -0
  104. {dkx-2.2.0 → dkx-2.3.0}/tests/test_bootstrap_objective.py +0 -0
  105. {dkx-2.2.0 → dkx-2.3.0}/tests/test_bounce_averaged.py +0 -0
  106. {dkx-2.2.0 → dkx-2.3.0}/tests/test_classical_transport_physics_gates.py +0 -0
  107. {dkx-2.2.0 → dkx-2.3.0}/tests/test_cli_solve_mode.py +0 -0
  108. {dkx-2.2.0 → dkx-2.3.0}/tests/test_cli_validation_io_fast_coverage.py +0 -0
  109. {dkx-2.2.0 → dkx-2.3.0}/tests/test_coarse_precond_constants.py +0 -0
  110. {dkx-2.2.0 → dkx-2.3.0}/tests/test_collision_physics_gates.py +0 -0
  111. {dkx-2.2.0 → dkx-2.3.0}/tests/test_collisions_improved_sugama.py +0 -0
  112. {dkx-2.2.0 → dkx-2.3.0}/tests/test_collocation.py +0 -0
  113. {dkx-2.2.0 → dkx-2.3.0}/tests/test_compare_reference_corruption.py +0 -0
  114. {dkx-2.2.0 → dkx-2.3.0}/tests/test_create_production_benchmark_inputs.py +0 -0
  115. {dkx-2.2.0 → dkx-2.3.0}/tests/test_data_fetch.py +0 -0
  116. {dkx-2.2.0 → dkx-2.3.0}/tests/test_domain_package_import_contracts.py +0 -0
  117. {dkx-2.2.0 → dkx-2.3.0}/tests/test_drift_kinetic.py +0 -0
  118. {dkx-2.2.0 → dkx-2.3.0}/tests/test_er.py +0 -0
  119. {dkx-2.2.0 → dkx-2.3.0}/tests/test_er_scan_and_ambipolar.py +0 -0
  120. {dkx-2.2.0 → dkx-2.3.0}/tests/test_example_qa_bootstrap.py +0 -0
  121. {dkx-2.2.0 → dkx-2.3.0}/tests/test_examples_bootstrap_dkx.py +0 -0
  122. {dkx-2.2.0 → dkx-2.3.0}/tests/test_examples_optimization.py +0 -0
  123. {dkx-2.2.0 → dkx-2.3.0}/tests/test_examples_pedagogic.py +0 -0
  124. {dkx-2.2.0 → dkx-2.3.0}/tests/test_examples_tutorials.py +0 -0
  125. {dkx-2.2.0 → dkx-2.3.0}/tests/test_examples_workflow_browser.py +0 -0
  126. {dkx-2.2.0 → dkx-2.3.0}/tests/test_export_f_canonical_parity.py +0 -0
  127. {dkx-2.2.0 → dkx-2.3.0}/tests/test_figure_provenance.py +0 -0
  128. {dkx-2.2.0 → dkx-2.3.0}/tests/test_finite_beta_vmec_example.py +0 -0
  129. {dkx-2.2.0 → dkx-2.3.0}/tests/test_float64_single_owner.py +0 -0
  130. {dkx-2.2.0 → dkx-2.3.0}/tests/test_fokker_planck_phi1_reduces_to_no_phi1.py +0 -0
  131. {dkx-2.2.0 → dkx-2.3.0}/tests/test_fortran_mpi_wrapper.py +0 -0
  132. {dkx-2.2.0 → dkx-2.3.0}/tests/test_generate_readme_fast_branch_audit.py +0 -0
  133. {dkx-2.2.0 → dkx-2.3.0}/tests/test_generated_deck_coverage.py +0 -0
  134. {dkx-2.2.0 → dkx-2.3.0}/tests/test_geometry_scheme11_parity.py +0 -0
  135. {dkx-2.2.0 → dkx-2.3.0}/tests/test_gpu_solver_path_artifacts.py +0 -0
  136. {dkx-2.2.0 → dkx-2.3.0}/tests/test_h5_parity.py +0 -0
  137. {dkx-2.2.0 → dkx-2.3.0}/tests/test_helper_module_coverage.py +0 -0
  138. {dkx-2.2.0 → dkx-2.3.0}/tests/test_impurity.py +0 -0
  139. {dkx-2.2.0 → dkx-2.3.0}/tests/test_input_compat.py +0 -0
  140. {dkx-2.2.0 → dkx-2.3.0}/tests/test_input_namelist_writer.py +0 -0
  141. {dkx-2.2.0 → dkx-2.3.0}/tests/test_inputs_console.py +0 -0
  142. {dkx-2.2.0 → dkx-2.3.0}/tests/test_jax_geometry_adapters.py +0 -0
  143. {dkx-2.2.0 → dkx-2.3.0}/tests/test_kinetic_operator_fortran_parity.py +0 -0
  144. {dkx-2.2.0 → dkx-2.3.0}/tests/test_magnetic_drift_diagonal.py +0 -0
  145. {dkx-2.2.0 → dkx-2.3.0}/tests/test_magnetic_drifts_parity.py +0 -0
  146. {dkx-2.2.0 → dkx-2.3.0}/tests/test_magnetic_geometry.py +0 -0
  147. {dkx-2.2.0 → dkx-2.3.0}/tests/test_momentum_correction.py +0 -0
  148. {dkx-2.2.0 → dkx-2.3.0}/tests/test_monoenergetic_database.py +0 -0
  149. {dkx-2.2.0 → dkx-2.3.0}/tests/test_multigrid.py +0 -0
  150. {dkx-2.2.0 → dkx-2.3.0}/tests/test_namelist.py +0 -0
  151. {dkx-2.2.0 → dkx-2.3.0}/tests/test_optimization_comparison.py +0 -0
  152. {dkx-2.2.0 → dkx-2.3.0}/tests/test_optimization_evidence.py +0 -0
  153. {dkx-2.2.0 → dkx-2.3.0}/tests/test_optimization_ladder.py +0 -0
  154. {dkx-2.2.0 → dkx-2.3.0}/tests/test_optimization_neoclassical_objectives.py +0 -0
  155. {dkx-2.2.0 → dkx-2.3.0}/tests/test_optimization_promotion.py +0 -0
  156. {dkx-2.2.0 → dkx-2.3.0}/tests/test_optimization_public_scripts_cli.py +0 -0
  157. {dkx-2.2.0 → dkx-2.3.0}/tests/test_optimization_real_artifacts.py +0 -0
  158. {dkx-2.2.0 → dkx-2.3.0}/tests/test_optimization_wave3_coverage.py +0 -0
  159. {dkx-2.2.0 → dkx-2.3.0}/tests/test_optimization_workflow.py +0 -0
  160. {dkx-2.2.0 → dkx-2.3.0}/tests/test_output_coordinate_physics_gates.py +0 -0
  161. {dkx-2.2.0 → dkx-2.3.0}/tests/test_output_formats.py +0 -0
  162. {dkx-2.2.0 → dkx-2.3.0}/tests/test_output_h5_constraintscheme34_parity.py +0 -0
  163. {dkx-2.2.0 → dkx-2.3.0}/tests/test_output_h5_magdrift_schemes_parity.py +0 -0
  164. {dkx-2.2.0 → dkx-2.3.0}/tests/test_output_h5_nonstelsym_vmec_scheme5_parity.py +0 -0
  165. {dkx-2.2.0 → dkx-2.3.0}/tests/test_output_h5_scheme11_parity.py +0 -0
  166. {dkx-2.2.0 → dkx-2.3.0}/tests/test_output_h5_scheme13_parity.py +0 -0
  167. {dkx-2.2.0 → dkx-2.3.0}/tests/test_output_h5_scheme1_parity.py +0 -0
  168. {dkx-2.2.0 → dkx-2.3.0}/tests/test_output_h5_scheme2_parity.py +0 -0
  169. {dkx-2.2.0 → dkx-2.3.0}/tests/test_output_h5_scheme4_parity.py +0 -0
  170. {dkx-2.2.0 → dkx-2.3.0}/tests/test_output_h5_scheme4_quick2species_parity.py +0 -0
  171. {dkx-2.2.0 → dkx-2.3.0}/tests/test_output_h5_scheme5_parity.py +0 -0
  172. {dkx-2.2.0 → dkx-2.3.0}/tests/test_output_h5_xgrid_schemes_parity.py +0 -0
  173. {dkx-2.2.0 → dkx-2.3.0}/tests/test_output_key_coverage_regressions.py +0 -0
  174. {dkx-2.2.0 → dkx-2.3.0}/tests/test_output_npz_canonical_parity.py +0 -0
  175. {dkx-2.2.0 → dkx-2.3.0}/tests/test_package_metadata.py +0 -0
  176. {dkx-2.2.0 → dkx-2.3.0}/tests/test_phase_space.py +0 -0
  177. {dkx-2.2.0 → dkx-2.3.0}/tests/test_phase_timing.py +0 -0
  178. {dkx-2.2.0 → dkx-2.3.0}/tests/test_phi1.py +0 -0
  179. {dkx-2.2.0 → dkx-2.3.0}/tests/test_plotting.py +0 -0
  180. {dkx-2.2.0 → dkx-2.3.0}/tests/test_preconditioner_xi_semantics.py +0 -0
  181. {dkx-2.2.0 → dkx-2.3.0}/tests/test_profile_write_output_trace.py +0 -0
  182. {dkx-2.2.0 → dkx-2.3.0}/tests/test_profiling.py +0 -0
  183. {dkx-2.2.0 → dkx-2.3.0}/tests/test_profiling_helpers.py +0 -0
  184. {dkx-2.2.0 → dkx-2.3.0}/tests/test_public_docs_wording_contract.py +0 -0
  185. {dkx-2.2.0 → dkx-2.3.0}/tests/test_public_facades_and_paths.py +0 -0
  186. {dkx-2.2.0 → dkx-2.3.0}/tests/test_reference_goldens_lightweight.py +0 -0
  187. {dkx-2.2.0 → dkx-2.3.0}/tests/test_remaining_public_contracts.py +0 -0
  188. {dkx-2.2.0 → dkx-2.3.0}/tests/test_repo_size_policy.py +0 -0
  189. {dkx-2.2.0 → dkx-2.3.0}/tests/test_representative.py +0 -0
  190. {dkx-2.2.0 → dkx-2.3.0}/tests/test_research_lane_policy.py +0 -0
  191. {dkx-2.2.0 → dkx-2.3.0}/tests/test_rhsmode1_current_closure.py +0 -0
  192. {dkx-2.2.0 → dkx-2.3.0}/tests/test_rhsmode1_phi1_write_output_end_to_end.py +0 -0
  193. {dkx-2.2.0 → dkx-2.3.0}/tests/test_rhsmode1_write_output_end_to_end.py +0 -0
  194. {dkx-2.2.0 → dkx-2.3.0}/tests/test_rosenbluth_quadrature.py +0 -0
  195. {dkx-2.2.0 → dkx-2.3.0}/tests/test_run_rhsmode1.py +0 -0
  196. {dkx-2.2.0 → dkx-2.3.0}/tests/test_run_transport.py +0 -0
  197. {dkx-2.2.0 → dkx-2.3.0}/tests/test_runtime_helper_coverage.py +0 -0
  198. {dkx-2.2.0 → dkx-2.3.0}/tests/test_scans_progress_and_recycle.py +0 -0
  199. {dkx-2.2.0 → dkx-2.3.0}/tests/test_sensitivity.py +0 -0
  200. {dkx-2.2.0 → dkx-2.3.0}/tests/test_shaing_callen.py +0 -0
  201. {dkx-2.2.0 → dkx-2.3.0}/tests/test_solve.py +0 -0
  202. {dkx-2.2.0 → dkx-2.3.0}/tests/test_solver_path_artifacts.py +0 -0
  203. {dkx-2.2.0 → dkx-2.3.0}/tests/test_solver_trace.py +0 -0
  204. {dkx-2.2.0 → dkx-2.3.0}/tests/test_solver_trace_canonical_parity.py +0 -0
  205. {dkx-2.2.0 → dkx-2.3.0}/tests/test_source_tree_consolidation.py +0 -0
  206. {dkx-2.2.0 → dkx-2.3.0}/tests/test_sparse_precond.py +0 -0
  207. {dkx-2.2.0 → dkx-2.3.0}/tests/test_species_constants.py +0 -0
  208. {dkx-2.2.0 → dkx-2.3.0}/tests/test_tier1_refinement.py +0 -0
  209. {dkx-2.2.0 → dkx-2.3.0}/tests/test_tier2_memory_guard.py +0 -0
  210. {dkx-2.2.0 → dkx-2.3.0}/tests/test_transport_matrix_write_output_end_to_end.py +0 -0
  211. {dkx-2.2.0 → dkx-2.3.0}/tests/test_units.py +0 -0
  212. {dkx-2.2.0 → dkx-2.3.0}/tests/test_upstream_scanplot2_smoke.py +0 -0
  213. {dkx-2.2.0 → dkx-2.3.0}/tests/test_utils_smoke.py +0 -0
  214. {dkx-2.2.0 → dkx-2.3.0}/tests/test_validation_figures.py +0 -0
  215. {dkx-2.2.0 → dkx-2.3.0}/tests/test_validation_math.py +0 -0
  216. {dkx-2.2.0 → dkx-2.3.0}/tests/test_validation_petsc_and_upstream_helpers.py +0 -0
  217. {dkx-2.2.0 → dkx-2.3.0}/tests/test_validation_policy_coverage.py +0 -0
  218. {dkx-2.2.0 → dkx-2.3.0}/tests/test_variational_bounds.py +0 -0
  219. {dkx-2.2.0 → dkx-2.3.0}/tests/test_vmec_wout_ascii.py +0 -0
  220. {dkx-2.2.0 → dkx-2.3.0}/tests/test_vmec_wout_conventions.py +0 -0
  221. {dkx-2.2.0 → dkx-2.3.0}/tests/test_vmex_workflow.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dkx
3
- Version: 2.2.0
3
+ Version: 2.3.0
4
4
  Summary: Neoclassical transport solver with CPU/GPU and differentiable JAX workflows
5
5
  Project-URL: Homepage, https://github.com/uwplasma/DKX
6
6
  Project-URL: Documentation, https://sfincs-jax.readthedocs.io/
@@ -10,7 +10,7 @@ Requires-Python: >=3.10
10
10
  Description-Content-Type: text/markdown
11
11
  License-File: LICENSE
12
12
  Requires-Dist: jax
13
- Requires-Dist: numpy
13
+ Requires-Dist: numpy>=2.1
14
14
  Requires-Dist: scipy
15
15
  Requires-Dist: h5py
16
16
  Requires-Dist: netCDF4
@@ -23,6 +23,7 @@ Requires-Dist: pytest; extra == "dev"
23
23
  Requires-Dist: pytest-cov; extra == "dev"
24
24
  Requires-Dist: pytest-xdist; extra == "dev"
25
25
  Requires-Dist: pytest-split; extra == "dev"
26
+ Requires-Dist: pypdf; extra == "dev"
26
27
  Provides-Extra: docs
27
28
  Requires-Dist: sphinx; extra == "docs"
28
29
  Requires-Dist: sphinx-rtd-theme; extra == "docs"
@@ -92,45 +93,41 @@ throughout: 37.3 s on W7-X and 45.8 s on a finite-β precise-QA equilibrium, the
92
93
  latter split 16.4 s for the 21-point scan, 6.0 s for the `|B|` solve, and 22.5 s
93
94
  for the radial scan's 50 drift-kinetic solves.
94
95
 
95
- The same solve from Python (mirrors
96
- [`examples/getting_started/run_tokamak.py`](examples/getting_started/run_tokamak.py),
97
- which also builds the namelist from dicts and plots the result):
96
+ The same solve from Python. No input file, no argument parsing — the
97
+ parameters are the ones a deck carries, passed as keywords
98
+ ([`examples/1_basics/run_tokamak.py`](examples/1_basics/run_tokamak.py)):
98
99
 
99
100
  ```python
100
- from pathlib import Path
101
- from dkx.run import run_profile
102
-
103
- deck = Path("input.namelist")
104
- deck.write_text("""\
105
- &geometryParameters
106
- geometryScheme = 1 ! circular tokamak: BHat = 1 + 0.1 cos(theta)
107
- inputRadialCoordinate = 3
108
- rN_wish = 0.3
109
- B0OverBBar = 1.0 GHat = 1.0 IHat = 0.0 iota = 1.31
110
- epsilon_t = 0.1 epsilon_h = 0.0 psiAHat = 0.045 aHat = 0.1
111
- /
112
- &speciesParameters
113
- Zs = 1 mHats = 1.0 nHats = 1.0 THats = 0.5
114
- dNHatdrHats = -6.0 dTHatdrHats = -3.0
115
- /
116
- &physicsParameters
117
- Delta = 4.5694d-3 alpha = 1.0 nu_n = 8.4774d-3
118
- Er = 0.0 collisionOperator = 1 ! pitch-angle scattering
119
- /
120
- &resolutionParameters
121
- Ntheta = 15 Nzeta = 1 Nxi = 8 NL = 4 Nx = 6
122
- solverTolerance = 1d-10
123
- /
124
- """)
125
-
126
- run = run_profile(deck, solve_method="auto", out_path=Path("sfincsOutput.h5"))
101
+ import dkx
102
+
103
+ run = dkx.run(
104
+ geometryScheme=1, # circular tokamak: BHat = 1 + 0.1 cos(theta)
105
+ inputRadialCoordinate=3, rN_wish=0.3,
106
+ B0OverBBar=1.0, GHat=1.0, IHat=0.0, iota=1.31,
107
+ epsilon_t=0.1, epsilon_h=0.0, psiAHat=0.045, aHat=0.1,
108
+ Zs=[1.0], mHats=[1.0], nHats=[1.0], THats=[0.5],
109
+ dNHatdrHats=[-6.0], dTHatdrHats=[-3.0],
110
+ Ntheta=15, Nzeta=1, Nxi=8, NL=4, Nx=6,
111
+ Delta=4.5694e-3, alpha=1.0, nu_n=8.4774e-3,
112
+ Er=0.0, collisionOperator=1, # 1 = pitch-angle scattering, 0 = Fokker-Planck
113
+ )
127
114
  print("particle flux:", float(run.moments["particleFlux_vm_psiHat"][0]))
128
115
  print("bootstrap current <j.B>:", float(run.moments["FSABjHat"]))
129
116
  ```
130
117
 
131
- `run_profile` prints the Fortran-parity console flow, writes `sfincsOutput.h5`/`.nc`
132
- keyed by the SFINCS output names, and returns the state, solver statistics, and
133
- all moments in memory.
118
+ `dkx.run` reads `RHSMode` from the case and dispatches, so the same call covers
119
+ a profile-gradient run and a transport matrix. It also takes an
120
+ `input.namelist` path — `dkx.run("input.namelist")` — and overrides on top of
121
+ one, which is what a convergence scan is:
122
+
123
+ ```python
124
+ dkx.run("input.namelist", Ntheta=25) # same deck, finer grid
125
+ dkx.run(..., out="sfincsOutput.h5") # write a file; the suffix picks the format
126
+ ```
127
+
128
+ Results come back in memory: `run.moments` is keyed by the SFINCS output names,
129
+ and `run.solve_result` carries the residual, the iteration count, and whether it
130
+ converged.
134
131
 
135
132
  ## Parity with SFINCS Fortran v3
136
133
 
@@ -166,7 +163,7 @@ flow corrections, an extended-collisionality Sugama operator, monoenergetic
166
163
  database mode, batched GPU scans, a bounce-averaged 1/ν surrogate) — lives in
167
164
  [docs/feature_matrix.rst](docs/feature_matrix.rst).
168
165
 
169
- *Reproduce with the drivers in [`examples/parity/`](examples/parity/).*
166
+ *Reproduce with the drivers in [`tools/parity/`](tools/parity/).*
170
167
 
171
168
  ### Interpreting a difference
172
169
 
@@ -269,7 +266,7 @@ matched-deck SFINCS Fortran v3 cross-check points at solver precision. The
269
266
  quasi-helically symmetric HSX case shows the suppressed 1/ν branch that W7-X and
270
267
  TJ-II retain.
271
268
 
272
- *Reproduce with `python examples/paper_benchmarks/monoenergetic_icnts_w7x.py`
269
+ *Reproduce with `python tools/paper_benchmarks/monoenergetic_icnts_w7x.py`
273
270
  (and the `_tjii` / `_hsx` companions).*
274
271
 
275
272
  ## Low-collisionality bootstrap convergence
@@ -283,7 +280,7 @@ asymptote — the 1/ν-regime offset does not decay without orbit precession —
283
280
  a small finite `EStar` detaches below `nuPrime ~ 1e-3` and flattens back toward
284
281
  the asymptote, the E×B-precession dip.
285
282
 
286
- *Reproduce with `python examples/paper_benchmarks/shaing_callen_convergence.py`.*
283
+ *Reproduce with `python tools/paper_benchmarks/shaing_callen_convergence.py`.*
287
284
 
288
285
  ## Ambipolar Er and electron roots
289
286
 
@@ -296,8 +293,8 @@ sign, and follows the physical branch by radial continuity to reproduce the
296
293
  electron-root → ion-root crossover near `ρ ~ 0.6`, matching the reference `E_r`
297
294
  within the digitization uncertainty (~1.5 kV/m mean difference).
298
295
 
299
- *Reproduce with `python examples/paper_benchmarks/w7x_ambipolar_er.py` and
300
- `python examples/paper_benchmarks/electron_root_optimization.py`.*
296
+ *Reproduce with `python tools/paper_benchmarks/w7x_ambipolar_er.py` and
297
+ `python tools/paper_benchmarks/electron_root_optimization.py`.*
301
298
 
302
299
  ## Impurity transport
303
300
 
@@ -310,7 +307,7 @@ diagnostic recovers the exact `-Z` density-peaking coefficient and the classical
310
307
  1/2 collisional screening coefficient, with an autodiff ion-temperature-gradient
311
308
  derivative verified against finite differences.
312
309
 
313
- *Reproduce with `python examples/paper_benchmarks/impurity_transport.py`.*
310
+ *Reproduce with `python tools/paper_benchmarks/impurity_transport.py`.*
314
311
 
315
312
  ## Kinetic-in-the-loop bootstrap
316
313
 
@@ -324,7 +321,7 @@ error the kinetic-in-the-loop iteration removes by construction — and one
324
321
  `jax.value_and_grad` differentiates the total bootstrap current through the
325
322
  equilibrium → Boozer → kinetic chain.
326
323
 
327
- *Reproduce with `python examples/paper_benchmarks/bootstrap_consistency_kinetic_loop.py`
324
+ *Reproduce with `python tools/paper_benchmarks/bootstrap_consistency_kinetic_loop.py`
328
325
  (needs the optional `vmex` + `booz_xform_jax` companions).*
329
326
 
330
327
  To put that inside an optimizer instead of after it, `dkx.bootstrap.KineticBootstrapCurrent`
@@ -62,45 +62,41 @@ throughout: 37.3 s on W7-X and 45.8 s on a finite-β precise-QA equilibrium, the
62
62
  latter split 16.4 s for the 21-point scan, 6.0 s for the `|B|` solve, and 22.5 s
63
63
  for the radial scan's 50 drift-kinetic solves.
64
64
 
65
- The same solve from Python (mirrors
66
- [`examples/getting_started/run_tokamak.py`](examples/getting_started/run_tokamak.py),
67
- which also builds the namelist from dicts and plots the result):
65
+ The same solve from Python. No input file, no argument parsing — the
66
+ parameters are the ones a deck carries, passed as keywords
67
+ ([`examples/1_basics/run_tokamak.py`](examples/1_basics/run_tokamak.py)):
68
68
 
69
69
  ```python
70
- from pathlib import Path
71
- from dkx.run import run_profile
72
-
73
- deck = Path("input.namelist")
74
- deck.write_text("""\
75
- &geometryParameters
76
- geometryScheme = 1 ! circular tokamak: BHat = 1 + 0.1 cos(theta)
77
- inputRadialCoordinate = 3
78
- rN_wish = 0.3
79
- B0OverBBar = 1.0 GHat = 1.0 IHat = 0.0 iota = 1.31
80
- epsilon_t = 0.1 epsilon_h = 0.0 psiAHat = 0.045 aHat = 0.1
81
- /
82
- &speciesParameters
83
- Zs = 1 mHats = 1.0 nHats = 1.0 THats = 0.5
84
- dNHatdrHats = -6.0 dTHatdrHats = -3.0
85
- /
86
- &physicsParameters
87
- Delta = 4.5694d-3 alpha = 1.0 nu_n = 8.4774d-3
88
- Er = 0.0 collisionOperator = 1 ! pitch-angle scattering
89
- /
90
- &resolutionParameters
91
- Ntheta = 15 Nzeta = 1 Nxi = 8 NL = 4 Nx = 6
92
- solverTolerance = 1d-10
93
- /
94
- """)
95
-
96
- run = run_profile(deck, solve_method="auto", out_path=Path("sfincsOutput.h5"))
70
+ import dkx
71
+
72
+ run = dkx.run(
73
+ geometryScheme=1, # circular tokamak: BHat = 1 + 0.1 cos(theta)
74
+ inputRadialCoordinate=3, rN_wish=0.3,
75
+ B0OverBBar=1.0, GHat=1.0, IHat=0.0, iota=1.31,
76
+ epsilon_t=0.1, epsilon_h=0.0, psiAHat=0.045, aHat=0.1,
77
+ Zs=[1.0], mHats=[1.0], nHats=[1.0], THats=[0.5],
78
+ dNHatdrHats=[-6.0], dTHatdrHats=[-3.0],
79
+ Ntheta=15, Nzeta=1, Nxi=8, NL=4, Nx=6,
80
+ Delta=4.5694e-3, alpha=1.0, nu_n=8.4774e-3,
81
+ Er=0.0, collisionOperator=1, # 1 = pitch-angle scattering, 0 = Fokker-Planck
82
+ )
97
83
  print("particle flux:", float(run.moments["particleFlux_vm_psiHat"][0]))
98
84
  print("bootstrap current <j.B>:", float(run.moments["FSABjHat"]))
99
85
  ```
100
86
 
101
- `run_profile` prints the Fortran-parity console flow, writes `sfincsOutput.h5`/`.nc`
102
- keyed by the SFINCS output names, and returns the state, solver statistics, and
103
- all moments in memory.
87
+ `dkx.run` reads `RHSMode` from the case and dispatches, so the same call covers
88
+ a profile-gradient run and a transport matrix. It also takes an
89
+ `input.namelist` path — `dkx.run("input.namelist")` — and overrides on top of
90
+ one, which is what a convergence scan is:
91
+
92
+ ```python
93
+ dkx.run("input.namelist", Ntheta=25) # same deck, finer grid
94
+ dkx.run(..., out="sfincsOutput.h5") # write a file; the suffix picks the format
95
+ ```
96
+
97
+ Results come back in memory: `run.moments` is keyed by the SFINCS output names,
98
+ and `run.solve_result` carries the residual, the iteration count, and whether it
99
+ converged.
104
100
 
105
101
  ## Parity with SFINCS Fortran v3
106
102
 
@@ -136,7 +132,7 @@ flow corrections, an extended-collisionality Sugama operator, monoenergetic
136
132
  database mode, batched GPU scans, a bounce-averaged 1/ν surrogate) — lives in
137
133
  [docs/feature_matrix.rst](docs/feature_matrix.rst).
138
134
 
139
- *Reproduce with the drivers in [`examples/parity/`](examples/parity/).*
135
+ *Reproduce with the drivers in [`tools/parity/`](tools/parity/).*
140
136
 
141
137
  ### Interpreting a difference
142
138
 
@@ -239,7 +235,7 @@ matched-deck SFINCS Fortran v3 cross-check points at solver precision. The
239
235
  quasi-helically symmetric HSX case shows the suppressed 1/ν branch that W7-X and
240
236
  TJ-II retain.
241
237
 
242
- *Reproduce with `python examples/paper_benchmarks/monoenergetic_icnts_w7x.py`
238
+ *Reproduce with `python tools/paper_benchmarks/monoenergetic_icnts_w7x.py`
243
239
  (and the `_tjii` / `_hsx` companions).*
244
240
 
245
241
  ## Low-collisionality bootstrap convergence
@@ -253,7 +249,7 @@ asymptote — the 1/ν-regime offset does not decay without orbit precession —
253
249
  a small finite `EStar` detaches below `nuPrime ~ 1e-3` and flattens back toward
254
250
  the asymptote, the E×B-precession dip.
255
251
 
256
- *Reproduce with `python examples/paper_benchmarks/shaing_callen_convergence.py`.*
252
+ *Reproduce with `python tools/paper_benchmarks/shaing_callen_convergence.py`.*
257
253
 
258
254
  ## Ambipolar Er and electron roots
259
255
 
@@ -266,8 +262,8 @@ sign, and follows the physical branch by radial continuity to reproduce the
266
262
  electron-root → ion-root crossover near `ρ ~ 0.6`, matching the reference `E_r`
267
263
  within the digitization uncertainty (~1.5 kV/m mean difference).
268
264
 
269
- *Reproduce with `python examples/paper_benchmarks/w7x_ambipolar_er.py` and
270
- `python examples/paper_benchmarks/electron_root_optimization.py`.*
265
+ *Reproduce with `python tools/paper_benchmarks/w7x_ambipolar_er.py` and
266
+ `python tools/paper_benchmarks/electron_root_optimization.py`.*
271
267
 
272
268
  ## Impurity transport
273
269
 
@@ -280,7 +276,7 @@ diagnostic recovers the exact `-Z` density-peaking coefficient and the classical
280
276
  1/2 collisional screening coefficient, with an autodiff ion-temperature-gradient
281
277
  derivative verified against finite differences.
282
278
 
283
- *Reproduce with `python examples/paper_benchmarks/impurity_transport.py`.*
279
+ *Reproduce with `python tools/paper_benchmarks/impurity_transport.py`.*
284
280
 
285
281
  ## Kinetic-in-the-loop bootstrap
286
282
 
@@ -294,7 +290,7 @@ error the kinetic-in-the-loop iteration removes by construction — and one
294
290
  `jax.value_and_grad` differentiates the total bootstrap current through the
295
291
  equilibrium → Boozer → kinetic chain.
296
292
 
297
- *Reproduce with `python examples/paper_benchmarks/bootstrap_consistency_kinetic_loop.py`
293
+ *Reproduce with `python tools/paper_benchmarks/bootstrap_consistency_kinetic_loop.py`
298
294
  (needs the optional `vmex` + `booz_xform_jax` companions).*
299
295
 
300
296
  To put that inside an optimizer instead of after it, `dkx.bootstrap.KineticBootstrapCurrent`
@@ -10,12 +10,49 @@ from __future__ import annotations
10
10
  # CLI invocations unless the user explicitly disables it. This improves cold-start
11
11
  # performance without requiring environment configuration.
12
12
  import os
13
+ import sys as _sys
14
+ import types as _types
13
15
  import tempfile
14
16
 
15
17
  # Suppress low-value XLA/PjRt C++ warning chatter by default. Users can still
16
18
  # override this before importing dkx if they need backend debug logs.
17
19
  os.environ.setdefault("TF_CPP_MIN_LOG_LEVEL", "2")
18
20
 
21
+
22
+ def _check_numpy() -> None:
23
+ """Fail with the actual problem instead of a NameError from numpy internals.
24
+
25
+ ``jax`` requires ``numpy>=2.1``. Installing dkx into an environment that
26
+ holds a conda-managed numpy 1.x -- the Anaconda base environment is the
27
+ common case -- resolves without complaint and then dies on the first
28
+ ``import jax`` with::
29
+
30
+ File numpy/core/getlimits.py, line 606, in smallest_normal
31
+ NameError: name 'isnan' is not defined
32
+
33
+ raised while ``ml_dtypes`` probes ``bfloat16``'s ``finfo``. That traceback
34
+ names neither dkx, nor jax, nor numpy's version as the cause, so it costs a
35
+ user real time. Checking here is cheap: numpy is imported either way, and
36
+ this runs before any jax import.
37
+ """
38
+ import numpy # noqa: PLC0415
39
+
40
+ major = int(numpy.__version__.split(".", 1)[0])
41
+ if major < 2:
42
+ raise ImportError(
43
+ f"dkx needs numpy>=2.1 (jax's own requirement); found {numpy.__version__}. "
44
+ "numpy 1.x crashes inside ml_dtypes at 'import jax'. Fix the "
45
+ "environment with:\n"
46
+ " pip install -U 'numpy>=2.1' jax\n"
47
+ "or, better, install into a clean environment rather than the "
48
+ "Anaconda base one:\n"
49
+ " conda create -n dkx python=3.11 -y && conda activate dkx\n"
50
+ " pip install dkx"
51
+ )
52
+
53
+
54
+ _check_numpy()
55
+
19
56
  _distributed_runtime_initialized = False
20
57
 
21
58
 
@@ -132,19 +169,28 @@ if _disable_cache not in {"1", "true", "yes", "on"}:
132
169
  except OSError:
133
170
  return False
134
171
 
172
+ # Versioned because the cache is bounded now (see the cap below) and
173
+ # JAX's LRU keeps a sidecar "-atime" file per entry. A directory
174
+ # filled in before the cap existed has none, so every eviction pass
175
+ # tries to touch a file that was never written and warns -- measured,
176
+ # 12 warnings on a single small solve against a legacy cache, and 0
177
+ # against a fresh one. Starting a new directory is what makes the
178
+ # bound work; the old one simply stops being written to.
179
+ _CACHE_DIR_NAME = "jax_compilation_cache"
180
+
135
181
  cache_override = os.environ.get("DKX_COMPILATION_CACHE_DIR", "").strip()
136
182
  if cache_override:
137
183
  default_cache_dir = cache_override
138
184
  else:
139
185
  xdg_cache = os.environ.get("XDG_CACHE_HOME", "").strip()
140
186
  if xdg_cache:
141
- default_cache_dir = os.path.join(xdg_cache, "dkx", "jax_compilation_cache")
187
+ default_cache_dir = os.path.join(xdg_cache, "dkx", _CACHE_DIR_NAME)
142
188
  else:
143
- default_cache_dir = os.path.join(os.path.expanduser("~"), ".cache", "dkx", "jax_compilation_cache")
189
+ default_cache_dir = os.path.join(os.path.expanduser("~"), ".cache", "dkx", _CACHE_DIR_NAME)
144
190
  try:
145
191
  os.makedirs(default_cache_dir, exist_ok=True)
146
192
  except OSError:
147
- default_cache_dir = os.path.join(tempfile.gettempdir(), "dkx", "jax_compilation_cache")
193
+ default_cache_dir = os.path.join(tempfile.gettempdir(), "dkx", _CACHE_DIR_NAME)
148
194
  try:
149
195
  os.makedirs(default_cache_dir, exist_ok=True)
150
196
  except OSError:
@@ -153,7 +199,7 @@ if _disable_cache not in {"1", "true", "yes", "on"}:
153
199
  # Some environments (CI sandboxes, read-only homes) can create the directory but
154
200
  # cannot write compilation entries. Fall back to a tempdir cache to avoid noisy
155
201
  # warnings and degraded cold-start performance.
156
- default_cache_dir = os.path.join(tempfile.gettempdir(), "dkx", "jax_compilation_cache")
202
+ default_cache_dir = os.path.join(tempfile.gettempdir(), "dkx", _CACHE_DIR_NAME)
157
203
  try:
158
204
  os.makedirs(default_cache_dir, exist_ok=True)
159
205
  except OSError:
@@ -207,6 +253,20 @@ try:
207
253
  )
208
254
  except ValueError:
209
255
  pass
256
+ # The cache is deliberately NOT size-capped. Setting
257
+ # jax_compilation_cache_max_size turns on jax's LRU, which writes a
258
+ # sidecar "-atime" file per entry and does size bookkeeping on every
259
+ # write. With the zero thresholds above -- which exist so even small
260
+ # kernels are cached -- that is measurably expensive: on
261
+ # tests/test_monoenergetic_database.py, 32 s and 1792 files capped
262
+ # against 20 s and 896 uncapped, a 60% penalty on every run. CI proved
263
+ # it at scale, nine of ten coverage shards crossing a 10-minute timeout
264
+ # they had been finishing in four to eight.
265
+ #
266
+ # So the cache grows without bound. That is a disk-space cost -- it
267
+ # reached 782 MB over 64k entries on a development machine -- and the
268
+ # remedy is to delete the directory, which loses nothing but compile
269
+ # time. Paying 60% on every run to avoid it is the worse trade.
210
270
  except Exception:
211
271
  # Keep import lightweight for tooling that inspects the package without JAX.
212
272
  pass
@@ -233,6 +293,7 @@ from .inputs import SfincsInput, load_sfincs_input # noqa: E402
233
293
  # Heavy flagship entry points (they import the JAX solve stack) are exported
234
294
  # lazily via PEP 562 module __getattr__ so `import dkx` stays cheap.
235
295
  _LAZY_EXPORTS = {
296
+ "plot": ("dkx.plotting", "plot"),
236
297
  "run_profile": ("dkx.run", "run_profile"),
237
298
  "run_transport_matrix": ("dkx.run", "run_transport_matrix"),
238
299
  "run_from_namelist": ("dkx.run", "run_from_namelist"),
@@ -246,6 +307,8 @@ _LAZY_EXPORTS = {
246
307
 
247
308
 
248
309
  def __getattr__(name: str):
310
+ if name == "run":
311
+ return _lazy_run_module()
249
312
  try:
250
313
  module_name, attr = _LAZY_EXPORTS[name]
251
314
  except KeyError:
@@ -261,6 +324,20 @@ def __dir__() -> list[str]:
261
324
  return sorted(set(globals()) | set(_LAZY_EXPORTS))
262
325
 
263
326
 
327
+ # ``dkx/run.py`` is a module and ``dkx.run(case)`` is a call. Rather than pick
328
+ # one -- the function breaks ``dkx.run.run_profile`` and every monkeypatch that
329
+ # targets it by path, the module breaks the call -- ``dkx/run.py`` makes itself
330
+ # callable, so ``dkx.run`` is always the module and always invocable. The name
331
+ # is resolved here rather than listed in _LAZY_EXPORTS because what it yields
332
+ # is the module itself, not an attribute of one.
333
+ def _lazy_run_module():
334
+ import importlib # noqa: PLC0415
335
+
336
+ module = importlib.import_module(f"{__name__}.run")
337
+ globals()["run"] = module
338
+ return module
339
+
340
+
264
341
 
265
342
  def require_float64() -> None:
266
343
  """Raise unless JAX is in float64 mode.
@@ -309,9 +386,11 @@ __all__ = [
309
386
  "run_ambipolar_brent",
310
387
  "run_from_namelist",
311
388
  "run_monoenergetic_database",
389
+ "plot",
390
+ "run",
312
391
  "run_profile",
313
392
  "run_transport_matrix",
314
393
  "write_output",
315
394
  ]
316
395
 
317
- __version__ = "2.2.0"
396
+ __version__ = "2.3.0"
@@ -54,6 +54,29 @@ def _add_heatmap(ax, data: dict[str, object], key: str, *, title: str | None = N
54
54
  return True
55
55
 
56
56
 
57
+
58
+ def _theta_zeta_oriented(b_hat: np.ndarray, data: dict[str, object]) -> np.ndarray:
59
+ """Return ``BHat`` as ``(theta, zeta)`` whatever order it was stored in.
60
+
61
+ The output file stores it ``(zeta, theta)`` (Fortran layout) while the
62
+ operator hands it over ``(theta, zeta)``, and ``imshow`` puts axis 0 on the
63
+ vertical. Labelling without checking is how every axisymmetric figure came
64
+ out with its variation on the axis marked zeta: the tokamak's Nzeta=1 strip
65
+ was drawn horizontally and called zeta when it was theta.
66
+
67
+ Orientation is decided by matching the array's dimensions against the run's
68
+ own ``Ntheta``/``Nzeta`` rather than by assuming an order. When the two are
69
+ equal there is nothing to disambiguate, and the stored layout is kept.
70
+ """
71
+ b_hat = np.asarray(b_hat)
72
+ if b_hat.ndim != 2:
73
+ return b_hat
74
+ n_theta = int(np.asarray(data.get("Ntheta", 0)).reshape(-1)[0]) if "Ntheta" in data else 0
75
+ n_zeta = int(np.asarray(data.get("Nzeta", 0)).reshape(-1)[0]) if "Nzeta" in data else 0
76
+ if n_theta and n_zeta and n_theta != n_zeta and b_hat.shape == (n_zeta, n_theta):
77
+ return b_hat.T
78
+ return b_hat
79
+
57
80
  def _add_profile(ax, data: dict[str, object], x: np.ndarray, key: str, *, title: str | None = None) -> bool:
58
81
  if key not in data:
59
82
  return False
@@ -220,8 +243,9 @@ def plot_sfincs_output_summary(
220
243
  axes[0].set_ylabel("FSABFlow_vs_x")
221
244
  axes[0].grid(True, alpha=0.25)
222
245
  else:
223
- theta = np.asarray(data.get("theta", np.arange(b_hat.shape[0]))).ravel()
224
- axes[0].plot(theta, b_hat[:, 0], lw=1.8)
246
+ oriented = _theta_zeta_oriented(b_hat, data)
247
+ theta = np.asarray(data.get("theta", np.arange(oriented.shape[0]))).ravel()
248
+ axes[0].plot(theta, oriented[:, 0], lw=1.8)
225
249
  axes[0].set_title("BHat(theta, zeta=0)")
226
250
  axes[0].set_xlabel("theta")
227
251
  axes[0].set_ylabel("BHat")
@@ -252,15 +276,118 @@ def plot_sfincs_output_summary(
252
276
  )
253
277
  axes[1].set_title("Run summary")
254
278
 
255
- im = axes[2].imshow(b_hat, aspect="auto", origin="lower")
256
- axes[2].set_title("BHat(theta, zeta)")
257
- axes[2].set_xlabel("zeta index")
258
- axes[2].set_ylabel("theta index")
259
- axes[2].set_xticks([0, max(0, b_hat.shape[-1] - 1)])
260
- axes[2].set_xticklabels(["0", f"{float(zeta[-1]):.2f}"])
279
+ oriented = _theta_zeta_oriented(b_hat, data)
280
+ im = axes[2].imshow(oriented, aspect="auto", origin="lower")
281
+ axes[2].set_title(r"$\hat B(\theta, \zeta)$")
282
+ axes[2].set_xlabel(r"$\zeta$ index")
283
+ axes[2].set_ylabel(r"$\theta$ index")
284
+ if zeta.size and oriented.shape[-1] == zeta.size:
285
+ axes[2].set_xticks([0, max(0, oriented.shape[-1] - 1)])
286
+ axes[2].set_xticklabels(["0", f"{float(zeta[-1]):.2f}"])
261
287
  fig.colorbar(im, ax=axes[2], fraction=0.046, pad=0.04)
262
288
 
263
- fig.suptitle(f"SFINCS output summary: {Path(input_h5).name}", y=1.03)
289
+ # y=1.03 put the title outside the figure, where constrained_layout does
290
+ # not reserve room for it, so it landed on the subplot titles. Let the
291
+ # layout engine place it.
292
+ fig.suptitle(f"SFINCS output summary: {Path(input_h5).name}")
264
293
  fig.savefig(output_png, dpi=200, bbox_inches="tight")
265
294
  plt.close(fig)
266
295
  return output_png.resolve()
296
+
297
+
298
+
299
+ def _shown(written: Path) -> Path:
300
+ """Display an already-written figure, for IDE and notebook users.
301
+
302
+ The panel builders save and close their own figure, so there is nothing
303
+ left to ``plt.show()``. Re-reading the PNG and showing it is what puts it
304
+ in Spyder's plots pane, and it costs one imread.
305
+ """
306
+ try:
307
+ image = plt.imread(written)
308
+ except Exception: # pragma: no cover - unreadable/exotic format
309
+ return written
310
+ figure = plt.figure(figsize=(13.5, 8.0))
311
+ axis = figure.add_axes((0.0, 0.0, 1.0, 1.0))
312
+ axis.imshow(image)
313
+ axis.axis("off")
314
+ # block=False so a script run from a terminal is not held open by a
315
+ # window nobody is there to close; Spyder and Jupyter render regardless.
316
+ plt.show(block=False)
317
+ return written
318
+
319
+
320
+ def plot(source, out="dkx_panels.png", *, style="panels", show=False):
321
+ """Plot a run or an output file. One call, one figure, returns its path.
322
+
323
+ ``source`` is whatever you already have:
324
+
325
+ - the object :func:`dkx.run` returned, when the run was given ``out=``;
326
+ - a path to an ``sfincsOutput`` file, DKX's or Fortran SFINCS's --- the
327
+ layout is the same, so both work;
328
+ - a directory containing one.
329
+
330
+ The figure is the same six-panel one ``dkx --plot`` writes: the
331
+ monoenergetic coefficients, ``|B|`` on the surface, the bootstrap current
332
+ and the species fluxes. ``out`` picks the format by suffix.
333
+
334
+ ``style="summary"`` selects the compact three-panel page instead. Only
335
+ that style expands with a ``.pdf`` suffix, into a four-page diagnostics
336
+ book; ``style="panels"`` writes its one figure whatever the suffix.
337
+
338
+ For a figure this does not draw, read the numbers off ``run.moments`` and
339
+ use matplotlib directly --- ``examples/1_basics/plot_custom.py`` shows
340
+ that, and it is not a fallback so much as the normal way to make a figure
341
+ for a paper.
342
+
343
+ Args:
344
+ source: a run object, an output-file path, or a directory.
345
+ out: destination path; the suffix selects png or pdf. A ``.pdf``
346
+ is multi-page only under ``style="summary"``.
347
+ style: ``"panels"`` (default) for the ``dkx --plot`` figure, or
348
+ ``"summary"`` for the compact three-panel page.
349
+ show: also display the figure. For Spyder, Jupyter or any IDE where
350
+ you want it in the plots pane rather than only on disk; a no-op
351
+ under a headless backend.
352
+
353
+ Returns:
354
+ The :class:`~pathlib.Path` actually written.
355
+ """
356
+ out = Path(out)
357
+ out.parent.mkdir(parents=True, exist_ok=True)
358
+
359
+ path = getattr(source, "output_path", None) or (
360
+ source if isinstance(source, (str, Path)) else None)
361
+ if path is not None:
362
+ path = Path(path)
363
+ if path.is_dir():
364
+ candidates = sorted(
365
+ p for p in path.iterdir()
366
+ if p.suffix in {".h5", ".nc", ".npz"} and "sfincsOutput" in p.name
367
+ )
368
+ if not candidates:
369
+ raise FileNotFoundError(f"no sfincsOutput file in {path}")
370
+ path = candidates[0]
371
+ if style == "summary":
372
+ written = plot_sfincs_output_summary(input_h5=path, output_png=out)
373
+ return _shown(written) if show else written
374
+ if style != "panels":
375
+ raise ValueError(f"style must be 'panels' or 'summary'; got {style!r}")
376
+ from dkx.representative import plot_output_file # noqa: PLC0415
377
+
378
+ written = plot_output_file(path, out)
379
+ return _shown(written) if show else written
380
+
381
+ if hasattr(source, "moments"):
382
+ raise ValueError(
383
+ "this run has no output file to plot: pass out=... to dkx.run so the "
384
+ "solve writes one, e.g.\n"
385
+ ' run = dkx.run(case, out="sfincsOutput.h5")\n'
386
+ " dkx.plot(run)\n"
387
+ "Plotting cannot reuse the in-memory result because the panels read "
388
+ "the output file's full dataset, not just the moments."
389
+ )
390
+ raise TypeError(
391
+ "plot() takes the object dkx.run() returned, a path to an sfincsOutput "
392
+ f"file, or a directory holding one; got {type(source).__name__}"
393
+ )