diff --git a/.gitignore b/.gitignore index d1d98eec..873d5d11 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ .vscode *.DS_Store *.pdf +!docs/resources/*.pdf *.png *.jld2 .gitattributes diff --git a/CLAUDE.md b/CLAUDE.md index 4e463abe..6744e76f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,7 +4,116 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## Overview -JPEC (Julia Perturbed Equilibrium Code) is a work-in-progress Julia port of the Generalized Perturbed Equilibrium Code (GPEC) suite for magnetohydrodynamic (MHD) equilibrium and stability analysis in fusion plasmas. The codebase is a hybrid Julia/Fortran implementation, with active Julia development alongside legacy Fortran code that is called via ccall. +JPEC (Julia Perturbed Equilibrium Code) is a comprehensive Julia implementation of GPEC-style (Generalized Perturbed Equilibrium Code) MHD analysis for fusion plasmas. The code performs equilibrium reconstruction, ideal MHD stability analysis, and perturbed equilibrium calculations including plasma response and singular surface coupling diagnostics. + +**Relationship to Fortran GPEC**: JPEC is an evolution of the Fortran GPEC code suite, available at https://github.com/PrincetonUniversity/GPEC. When users reference "GPEC", "the Fortran code", or "the original GPEC", they are referring to this Fortran codebase. JPEC reimplements and extends GPEC's functionality in Julia with improved performance and maintainability. + +**Local GPEC Repository**: For code conversion or comparison with the original Fortran implementation, check for a local GPEC repository at `~/Code/gpec`. If not found at this location, ask the user for the correct path. + +JPEC is a hybrid Julia/Fortran implementation with active Julia development alongside legacy Fortran code called via ccall. The Fortran-to-Julia conversion is largely complete, with pure Julia implementations available for all major components. + +**Current Development Focus**: The `perturbed_equilibrium` branch is implementing full GPEC-style perturbed equilibrium functionality, including singular coupling analysis, island formation diagnostics, and mode-space field reconstruction. + +## Key References + +**IMPORTANT**: The papers in `docs/resources/` provide the theoretical foundation for JPEC's algorithms and should be referenced to understand what the code is doing. **Citing equations from these papers in code comments and annotations is strongly encouraged** to maintain traceability between theory and implementation. + +### Vacuum Module + +The Vacuum module implements the methods described in: + +- **Chance et al. (1997)**: "Vacuum calculations in azimuthally symmetric geometry" + - Location: `docs/resources/1997-Chance-Vacuum_calculations_in_azimuthally_symmetric_geometry.pdf` + - Published: Physics of Plasmas **4**, 2161 (1997) + - Link: https://pubs.aip.org/aip/pop/article-abstract/4/6/2161/263192 + - Describes: Fundamental vacuum response calculation method for tokamak geometry + +- **Chance et al. (2007)**: "Calculation of the vacuum Green's function valid even for high toroidal mode numbers in tokamaks" + - Location: `docs/resources/2007-Chance-Calculation of the vacuum Greens function valid even for high toroidal mode numbers in tokamaks.pdf` + - Published: Physics of Plasmas **14**, 052506 (2007) + - Describes: Improved Green's function calculation for high-n modes + +### ForceFreeStates Module + +The ForceFreeStates module (ideal MHD stability analysis) implements methods from: + +- **Glasser (2016)**: "The direct criterion of Newcomb for the ideal MHD stability of an axisymmetric toroidal plasma" + - Location: `docs/resources/2016-Glasser-The_direct_criterion_of_Newcomb_for_the_ideal_MHD_stability_of_an_axisymmetric_toroidal_plasma.pdf` + - Published: Physics of Plasmas **23**, 112506 (2016) + - **Describes: FUNDAMENTAL PAPER - Newcomb's criterion for ideal MHD stability (current implementation)** + +- **Glasser (2018)**: "A Riccati solution for the ideal MHD plasma response with applications to real-time stability control" + - Location: `docs/resources/2018-Glasser-A Riccati solution for the ideal MHD plasma response with applications to real-time stability control.pdf` + - Published: Physics of Plasmas **25**, 032507 (2018) + - Describes: Riccati method for ideal MHD eigenvalue problem + +### PerturbedEquilibrium Module + +The PerturbedEquilibrium module implements GPEC-style perturbed equilibrium calculations from: + +- **Park et al. (2007a)**: "Computation of three-dimensional tokamak and spherical torus equilibria" + - Location: `docs/resources/2007-Park-Computation_of_three-dimensional_tokamak_and_spherical_torus_equilibria-compressed.pdf` + - Published: Physics of Plasmas **14**, 052110 (2007) + - Describes: 3D equilibrium perturbations in toroidal geometry + +- **Park et al. (2007b)**: "Control of Asymmetric Magnetic Perturbations in Tokamaks" + - Location: `docs/resources/2007-Park-Control_of_Asymmetric_Magnetic_Perturbations_in_Tokamaks.pdf` + - Published: Physical Review Letters **99**, 195003 (2007) + - Describes: Plasma response to resonant magnetic perturbations (RMP) + +- **Park et al. (2009)**: "Importance of plasma response to nonaxisymmetric perturbations in tokamaks" + - Location: `docs/resources/2009-Park-Importance_of_plasma_response_to_nonaxisymmetric_perturbations_in_tokamaks-compressed.pdf` + - Published: Physics of Plasmas **16**, 056115 (2009) + - Describes: Self-consistent plasma response calculation + +- **Park et al. (2011)**: "Kinetic energy principle and neoclassical toroidal torque in tokamaks" + - Location: `docs/resources/2011-Park-Physics_of_Plasmas_Kinetic_energy_principle_and_neoclassical_toroidal_torque_in_tokamaks.pdf` + - Published: Physics of Plasmas **18**, 110702 (2011) + - Describes: Energy principle for perturbed equilibria + +- **Park et al. (2017)**: "Self-consistent perturbed equilibrium with neoclassical toroidal torque in tokamaks" + - Location: `docs/resources/2017-Park-Self_consistent_perturbed_equilibrium_with_neoclassical_toroidal_torque_in_toka.pdf` + - Published: Physics of Plasmas **24**, 032505 (2017) + - Describes: Self-consistent coupling with neoclassical effects + +### Resistive DCON Module (Future Work) + +JPEC will eventually implement resistive MHD stability analysis based on: + +- **Glasser (2016)**: "Computation of resistive instabilities by matched asymptotic expansions" + - Location: `docs/resources/2016-Glasser-Computation_of_resistive_instabilities_by_matched_asymptotic_expansions-compressed.pdf` + - Published: Physics of Plasmas **23**, 072505 (2016) + - Describes: Resistive stability analysis and Δ' calculation via matched asymptotic expansions + +- **Glasser (2018)**: "A robust solution for the resistive MHD toroidal Δ′ matrix in near real-time" + - Location: `docs/resources/2018-Glasser-A robust solution for the resistive MHD toroidal Delta-prime matrix in near real-time.pdf` + - Published: Physics of Plasmas **25**, 032501 (2018) + - Describes: Fast computation of Δ' matrix for resistive stability + +- **Wang et al. (2020)**: "Modeling of resistive plasma response in toroidal geometry using an asymptotic matching approach" + - Location: `docs/resources/2020-Wang-Modeling of resistive plasma response in toroidal geometry using an asymptotic matching approach.pdf` + - Published: Physics of Plasmas **27**, 122509 (2020) + - Describes: Asymptotic matching for resistive plasma response + +### PENTRC Module (Future Work) + +JPEC will eventually port the PENTRC (Perturbed Equilibrium Neoclassical Toroidal viscosity in Realistic geometry Code) functionality from the Fortran GPEC suite. This is described in: + +- **Logan & Park (2013)**: "Neoclassical toroidal viscosity in perturbed equilibria with general tokamak geometry" + - Location: `docs/resources/2013-Logan-Neoclassical_toroidal_viscosity_in_perturbed_equilibria_with_general_tokamak_geometry.pdf` + - Published: Physics of Plasmas **20**, 122507 (2013) + - Describes: Neoclassical toroidal viscosity (NTV) in perturbed equilibria + +- **Logan (2015)**: "Electromagnetic Torque in Tokamaks with Toroidal Asymmetries" + - Location: `docs/resources/2015-Logan-Electromagnetic_Torque_in_Tokamaks_with_Toroidal_Asymmetries-compressed.pdf` + - Published: PhD Thesis, Princeton University (2015) + - Describes: Complete PENTRC theory and implementation + +### Additional References + +- **Various (2009)**: "Nonambipolar Transport by Trapped Particles in Tokamaks" + - Location: `docs/resources/2009-Nonambipolar_Transport_by_Trapped_Particles_in_Tokamaks.pdf` + - Describes: Neoclassical transport theory ## Common Commands @@ -21,14 +130,14 @@ julia --project=. -e 'using Pkg; Pkg.activate("."); Pkg.build()' julia --project=. test/runtests.jl test/runtests_spline.jl # Available test files: -# - test/runtests_build.jl -# - test/runtests_spline.jl -# - test/runtests_vacuum_fortran.jl -# - test/runtests_vacuum_julia.jl -# - test/runtests_solovev.jl -# - test/runtests_ode.jl -# - test/runtests_sing.jl -# - test/runtests_fullruns.jl +# - test/runtests_build.jl # Fortran build verification +# - test/runtests_spline.jl # Spline interpolation +# - test/runtests_vacuum_fortran.jl # Fortran vacuum module +# - test/runtests_vacuum_julia.jl # Julia vacuum module +# - test/runtests_solovev.jl # Analytical equilibrium +# - test/runtests_ode.jl # ODE integration +# - test/runtests_sing.jl # Singular surface handling +# - test/runtests_fullruns.jl # End-to-end tests ``` ### Building Documentation @@ -36,6 +145,8 @@ julia --project=. test/runtests.jl test/runtests_spline.jl ```bash # Build documentation locally julia --project=. build_docs_local.jl + +# Documentation hosted at: https://openfusiontoolkit.github.io/JPEC/dev/ ``` ### Development with Revise @@ -97,75 +208,239 @@ julia benchmarks/benchmark_git_branches.jl \ ## Architecture +### Computational Workflow + +JPEC follows a three-stage analysis pipeline: + +1. **Equilibrium** → Solve Grad-Shafranov equation, compute flux surfaces, safety factor q-profile +2. **Stability Analysis** → Solve ideal MHD eigenvalue problem (DCON-style), identify singular surfaces +3. **Perturbed Equilibrium** → Compute plasma response to external fields, analyze singular coupling and island formation + +This workflow is reflected in the modular structure and data flow. + ### Module Structure -JPEC consists of four main modules, each organized in `src/`: +JPEC consists of **seven main modules** organized in `src/`: + +#### Foundation Modules 1. **Splines** (`src/Splines/`) - Numerical interpolation library - `CubicSpline.jl` - 1D cubic spline interpolation - `BicubicSpline.jl` - 2D bicubic spline interpolation - `FourierSpline.jl` - Fourier-based spline interpolation - Supports both pure Julia and Fortran implementations (via `fortran/` subdirectory) + - Status: Mature, both implementations maintained for validation -2. **Equilibrium** (`src/Equilibrium/`) - MHD equilibrium solvers - - Main entry point: `setup_equilibrium(path)` or `setup_equilibrium(config)` - - Supports multiple equilibrium types: efit, chease, chease2, lar (Large Aspect Ratio), sol (Solovev) - - `EquilibriumTypes.jl` - Core data structures (`PlasmaEquilibrium`, `EquilibriumConfig`, etc.) - - `ReadEquilibrium.jl` - Parsing equilibrium files - - `DirectEquilibrium.jl` - Direct equilibrium solver - - `InverseEquilibrium.jl` - Inverse equilibrium solver - - `AnalyticEquilibrium.jl` - Analytical equilibrium solutions - -3. **Vacuum** (`src/Vacuum/`) - Vacuum field calculations - - Hybrid Julia/Fortran implementation actively being converted to pure Julia - - Main function: `mscvac()` (Fortran) and `compute_vacuum_response()` (Julia) - - Fortran interface via ccall to `libvac` shared library - - `Vacuum_data.jl` - Data structures - - `Vacuum_init.jl` - Initialization routines - - `Vacuum_vac.jl` - Core vacuum calculation (Julia conversion in progress) - - `Vacuum_math.jl` - Mathematical utilities - - `fortran/` - Legacy Fortran code compiled to shared library - -4. **DCON** (`src/DCON/`) - Stability analysis - - `DconStructs.jl` - Core data structures - - `Main.jl` - Main entry points - - `Ode.jl` - ODE solver for stability equations - - `Sing.jl` - Singular point handling - - `Fourfit.jl` - Fourier fitting routines - - `FixedBoundaryStability.jl` - Fixed boundary stability analysis - - `Free.jl` - Free boundary stability - - `Mercier.jl` - Mercier stability criterion - -### Fortran Integration +2. **Utilities** (`src/Utilities/`) - Shared computational tools + - `FourierTransforms.jl` - Efficient Fourier transform utilities with pre-computed basis functions + - Provides type-stable functor pattern for repeated transforms + - Used by Vacuum and PerturbedEquilibrium modules -The build system compiles Fortran code into shared libraries: +#### Core Physics Modules -- Build configuration in `deps/build.jl` and `deps/build_helpers.jl` -- OS-specific compiler flags (macOS uses Accelerate framework, Linux uses OpenBLAS) -- Current Fortran modules: Splines, Vacuum -- Platform support: macOS and Linux only (Windows unsupported, use WSL) -- Add new Fortran builds by creating `build_*_fortran()` functions in `deps/build_helpers.jl` +3. **Equilibrium** (`src/Equilibrium/`) - MHD equilibrium solvers + - Main entry point: `setup_equilibrium(path)` or `setup_equilibrium(config)` + - Supports multiple equilibrium types: + - `efit` - EFIT g-file format + - `chease`, `chease2` - CHEASE equilibrium code formats + - `lar` - Large Aspect Ratio analytical model + - `sol` - Solovev analytical equilibrium + - Key files: + - `EquilibriumTypes.jl` - Core data structures + - `ReadEquilibrium.jl` - Parsing equilibrium files + - `DirectEquilibrium.jl` - Direct Grad-Shafranov solver + - `InverseEquilibrium.jl` - Inverse equilibrium solver + - `AnalyticEquilibrium.jl` - Analytical solutions + - Status: Stable and feature-complete + +4. **Vacuum** (`src/Vacuum/`) - Vacuum field calculations and Green's functions + - Computes vacuum response matrices for ideal MHD analysis + - Calculates both **interior** (grri) and **exterior** (grre) Green's functions + - Main functions: + - `compute_vacuum_response()` - Pure Julia implementation + - `mscvac()` - Legacy Fortran interface via ccall + - Key files: + - `VacuumStructs.jl` - Data structures + - `VacuumInternals.jl` - Core algorithms + - `VacuumFromEquilibrium.jl` - Integration with equilibrium data + - Fortran code in `src/Vacuum/fortran/` + - Status: **Pure Julia implementation complete and available** + +5. **ForceFreeStates** (`src/ForceFreeStates/`) - Ideal MHD stability analysis (DCON-style) + - Solves ideal MHD eigenvalue problem with force-free boundary conditions + - Identifies singular surfaces where ξ·∇ψ = 0 + - Key files: + - `ForceFreeStatesStructs.jl` - Core data structures + - `Ode.jl` - ODE solver for Euler-Lagrange equations + - `Sing.jl` - Singular point handling and layer analysis + - `Fourfit.jl` - Fourier fitting routines + - `FixedBoundaryStability.jl` - Fixed boundary analysis + - `Free.jl` - Free boundary stability + - `Mercier.jl` - Mercier stability criterion + - Status: Stable, core DCON functionality implemented + +#### Perturbed Equilibrium Modules + +6. **ForcingTerms** (`src/ForcingTerms/`) - External field specification + - Handles external magnetic field perturbations (coils, RMP, etc.) + - Supports ASCII and HDF5 forcing data formats + - `ForcingMode` data structure specifies amplitude and phase for each (m,n) component + - Status: Complete and functional + +7. **PerturbedEquilibrium** (`src/PerturbedEquilibrium/`) - **GPEC-style plasma response** + - Computes plasma response to external forcing + - Calculates singular coupling metrics at rational surfaces + - Key files: + - `PerturbedEquilibrium.jl` - Main entry point + - `PerturbedEquilibriumStructs.jl` - Data structures + - `ResponseMatrices.jl` - Permeability matrix calculation + - `FieldReconstruction.jl` - Mode-space field reconstruction + - `Response.jl` - Plasma response computation + - `SingularCoupling.jl` - **Singular surface analysis** including: + - Delta prime (Δ') tearing stability parameter + - Resonant flux and currents at rational surfaces + - Island half-widths and Chirikov parameters + - Green's functions at interior flux surfaces + - Surface inductance for singular surfaces + - `Utils.jl` - Helper functions + - Status: **Active development on `perturbed_equilibrium` branch** + +### Configuration + +**Unified Configuration File**: `jpec.toml` + +All JPEC modules are configured via a single TOML file with the following sections: + +- `[Equilibrium]` - Equilibrium solver settings +- `[Wall]` - Wall geometry and vacuum region +- `[ForceFreeStates]` - Stability analysis parameters +- `[PerturbedEquilibrium]` - Perturbed equilibrium settings +- `[ForcingTerms]` - External field specification + +Key parameters: +- `force_termination` - Set to `true` to exit after equilibrium/stability (skip perturbed equilibrium) +- `output_file` - Output filename (default: `jpec.h5`) + +Example configuration files are provided in: +- `examples/Solovev_ideal_example/jpec.toml` +- `examples/DIIID-like_ideal_example/jpec.toml` + +**Note**: Legacy configuration files (`equil.toml`, `vac.in`) are deprecated. ### Data Flow -1. **Equilibrium Setup**: `setup_equilibrium()` reads equilibrium files (TOML config) → parses equilibrium data → runs solver (direct/inverse) → computes global parameters (q-profile, beta, etc.) → diagnoses Grad-Shafranov solution -2. **Vacuum Calculations**: Initialize plasma/wall surfaces → compute vacuum response matrix → return wv, grri, xzpts arrays -3. **Stability Analysis**: Uses equilibrium data and vacuum response to analyze MHD stability +The complete JPEC analysis pipeline: + +1. **Equilibrium Setup**: + - `setup_equilibrium(config)` reads configuration from `jpec.toml` + - Parses equilibrium data (EFIT, CHEASE, or analytical) + - Runs Grad-Shafranov solver (direct or inverse) + - Computes global parameters: q-profile, pressure, current density, β + - Creates bicubic splines for (ψ, θ, φ) → (R, Z, Φ) mapping + - Outputs: `PlasmaEquilibrium` object + +2. **Vacuum Response**: + - Initialize plasma and wall surfaces from equilibrium + - Compute vacuum response matrices (wv, grri, grre) + - Calculate both interior and exterior Green's functions + - Available in pure Julia or via Fortran interface + +3. **Stability Analysis** (ForceFreeStates): + - Solve ideal MHD Euler-Lagrange equations via ODE integration + - Identify singular surfaces where q = m/n + - Compute Δ' at each singular surface + - Calculate potential and kinetic energies + - Check Mercier and ballooning stability criteria + - Outputs: Eigenmode structure ξ(ψ,θ) + +4. **Perturbed Equilibrium** (GPEC-style): + - Load external forcing data (coil fields, RMP configuration) + - Compute plasma response using permeability matrices + - Reconstruct mode-space fields (ξ_modes, b_modes) + - Calculate singular coupling metrics at rational surfaces: + - Δ' (tearing stability parameter) + - Island half-widths + - Chirikov overlap parameter + - Resonant flux and currents + - Outputs: `PerturbedEquilibriumState` with response fields and diagnostics + +5. **Output**: + - All results saved to single HDF5 file (default: `jpec.h5`) + - HDF5 groups: `input/`, `info/`, `equil/`, `splines/`, `locstab/`, `integration/`, `singular/`, `vacuum/`, and perturbed equilibrium data ### Key Data Structures -- `PlasmaEquilibrium` - Main equilibrium container with bicubic splines (rzphi), 1D profiles (sq), and parameters +#### Equilibrium +- `PlasmaEquilibrium` - Main equilibrium container with bicubic splines (rzphi), 1D profiles (sq), and global parameters - `EquilibriumConfig` - Configuration loaded from TOML files + +#### Vacuum - `VacuumInput` - Input parameters for vacuum calculations - `WallShapeSettings` - Wall geometry configuration +#### Stability +- `SingType` - Singular surface data including: + - Rational surface location (ψ, q = m/n) + - Δ' (tearing stability parameter) + - Eigenmode structure at singular surface + - **NEW**: Green's functions (grri, grre) at interior singular surfaces + - Surface inductance + +#### Perturbed Equilibrium +- `PerturbedEquilibriumControl` - User-facing TOML configuration parameters +- `PerturbedEquilibriumInternal` - Internal state with mode arrays +- `PerturbedEquilibriumState` - Results including: + - Response fields (ξ_modes, b_modes) in mode space + - Singular coupling matrices [msing × numpert_total] + - Island diagnostics (half-widths, Chirikov parameters) +- `ForcingMode` - External forcing specification (m, n, amplitude, phase) + +### Module Dependencies + +``` +JPEC +├── Splines (foundation) +├── Utilities (shared tools) +│ └── FourierTransforms +├── Equilibrium (uses Splines) +├── Vacuum (uses Splines, Equilibrium, Utilities) +├── ForcingTerms (data I/O) +├── ForceFreeStates (uses Equilibrium, Vacuum, Splines) +└── PerturbedEquilibrium (uses ForceFreeStates, Vacuum, ForcingTerms, Utilities) +``` + +### Fortran Integration + +The build system compiles Fortran code into shared libraries for performance-critical routines: + +- Build configuration in `deps/build.jl` and `deps/build_helpers.jl` +- OS-specific compiler flags: + - macOS: Uses Accelerate framework + - Linux: Uses OpenBLAS +- Current Fortran modules: Splines, Vacuum +- Compiled libraries: + - `libspline.dylib` / `libspline.so` + - `libvac.dylib` / `libvac.so` +- Platform support: **macOS and Linux only** (Windows unsupported, use WSL) +- Add new Fortran builds by creating `build_*_fortran()` functions in `deps/build_helpers.jl` + +**Fortran-to-Julia Conversion Status**: +- ✅ Vacuum module: Pure Julia implementation complete +- ✅ Splines: Both implementations available for validation +- 🔄 Both implementations maintained for testing and verification + ## Git Workflow This project uses GitFlow: + - Two permanent branches: `main` and `develop` - `main` branch updated only at release-ready stages +- `develop` branch for integration of features - Feature branches off `develop`, merged back with `--no-ff` -- Current work is on `vacuum_julia` branch + +**Current Development**: +- Active branch: `perturbed_equilibrium` - Major feature implementing GPEC-style perturbed equilibrium calculations +- Previous work: `vacuum_julia` branch (merged) - Pure Julia vacuum implementation ### Commit Message Format @@ -174,24 +449,35 @@ CODE - TAG - Detailed message ``` Where: -- CODE: Module name (EQUIL, DCON, VAC, VACUUM, etc.) -- TAG: Type descriptor (WIP, MINOR, IMPROVEMENT, BUG FIX, NEW FEATURE, etc.) +- **CODE**: Module name (EQUIL, VAC, VACUUM, ForceFreeStates, PERTURBED EQUILIBRIUM, etc.) +- **TAG**: Type descriptor (WIP, MINOR, IMPROVEMENT, BUG FIX, NEW FEATURE, REFACTOR, CLEANUP, etc.) Examples: -- `VAC - WIP - Converting vaccal wall code to Julia` +- `PERTURBED EQUILIBRIUM - NEW FEATURE - Implement singular coupling diagnostics` +- `VAC - IMPROVEMENT - Add dual Green's function computation` - `EQUIL - BUG FIX - Fixed separatrix finding for high kappa` -- `DCON - NEW FEATURE - Added Mercier criterion calculation` +- `ForceFreeStates - REFACTOR - Unified singular surface data structure` This format is used for compiling release notes, so tags should be human-readable and descriptive. ## Important Notes -- Julia 1.11 is the target version -- Tests include both Fortran and Julia implementations to ensure parity during conversion +### General +- **Julia version**: 1.11 is the target version +- **Indexing**: The codebase uses 0-based indexing in many places to match Fortran conventions, then converts to 1-based Julia indexing - **No step numbering in code comments** - Avoid annotations like "Step 1: do this" followed by "Step 2: do that". These get out of sync as code changes. Just describe the action without numbering. -- The Vacuum module is actively being converted from Fortran to Julia (see `vacuum_julia` branch) -- When modifying equilibrium code, remember to update diagnostic outputs (gsec.h5, gse.h5, gsei.h5) -- The codebase uses 0-based indexing in many places to match Fortran conventions, then converts to 1-based Julia indexing + +### Output Files +- **Default output**: `jpec.h5` (previously `euler.h5` in older versions) +- **Legacy diagnostic files**: When modifying equilibrium code, remember that older versions output `gsec.h5`, `gse.h5`, `gsei.h5` - these are now consolidated into `jpec.h5` + +### Current Development Priorities +- **Perturbed equilibrium module**: Active development of GPEC-style singular coupling analysis +- **Configuration**: All settings now in unified `jpec.toml` file + +### Performance +- Pure Julia implementations are available for all major components and offer comparable or better performance than Fortran +- Benchmarks available in `benchmark/` directory for Fourier transforms and vacuum calculations - Pre-commit hooks are configured for notebook cleaning and Julia formatting (see `docs/src/set_up.md` for developer setup) ## Git Merge conflict resolution policy @@ -202,4 +488,4 @@ This format is used for compiling release notes, so tags should be human-readabl - If both sides renamed the same symbol differently, prefer the current (ours) branch convention. - When a rename on one side conflicts with a logic change on the other, apply the logic change using the renamed symbol. - If a conflict involves changes to numerical parameters (tolerances, boundary conditions, grid sizes), flag for human review rather than guessing. -- Flag any conflicts where the combination is ambiguous for human review. \ No newline at end of file +- Flag any conflicts where the combination is ambiguous for human review. diff --git a/Project.toml b/Project.toml index 4c1c7c78..3c4b07ca 100644 --- a/Project.toml +++ b/Project.toml @@ -5,6 +5,7 @@ authors = ["Matthew Pharr ", "Rithik Banerjee = 0 + # Positive frequencies + modes[i] = fft_result[m + 1] / mtheta # FFT normalization + else + # Negative frequencies (wrap around) + modes[i] = fft_result[mtheta + m + 1] / mtheta + end + end + return modes +end + +# Test configurations +test_cases = [ + (name="Small (mtheta=128, mpert=10)", mtheta=128, mpert=10, mlow=-5), + (name="Medium (mtheta=256, mpert=20)", mtheta=256, mpert=20, mlow=-10), + (name="Large (mtheta=480, mpert=40)", mtheta=480, mpert=40, mlow=-20), + (name="Very Large (mtheta=1024, mpert=80)", mtheta=1024, mpert=80, mlow=-40), +] + +for test in test_cases + println("\n" * "="^80) + println(test.name) + println("="^80) + + mtheta = test.mtheta + mpert = test.mpert + mlow = test.mlow + mhigh = mlow + mpert - 1 + + # Create test data + theta = range(0, 2π, length=mtheta+1)[1:end-1] + data = sin.(3 .* theta) .+ 0.5 .* cos.(7 .* theta) .+ 0.2 .* sin.(11 .* theta) + + # Initialize FourierTransform + ft = FourierTransform(mtheta, mpert, mlow) + + # Pre-allocate buffers for in-place operations + modes_buffer = zeros(ComplexF64, mpert) + theta_buffer = zeros(ComplexF64, mtheta) + + # Pre-allocate for low-level API + cslth, snlth = compute_fourier_coefficients(mtheta, mpert, mlow) + gij = reshape(data, mtheta, 1) # Matrix form + gil = zeros(Float64, mtheta, mpert) + + # Pre-compute FFTW plan for fair comparison + fft_plan = plan_fft(data) + + println("\n--- Forward Transform ---") + + # 1. Our allocating API + print("FourierTransform (allocating): ") + t1 = @benchmark $ft($data) + display(t1) + modes_alloc = ft(data) + + # 2. Our in-place API + print("\nFourierTransform (in-place): ") + t2 = @benchmark transform!($modes_buffer, $ft, $data) + display(t2) + transform!(modes_buffer, ft, data) + + # 3. FFTW + print("\nFFTW (full DFT): ") + t3 = @benchmark $fft_plan * $data + display(t3) + fft_result = fft_plan * data + fft_modes = extract_modes(fft_result, mlow, mhigh, mtheta) + + # Verify results match (for overlapping modes) + # Note: Our transform uses a different normalization and basis + println("\n--- Accuracy Check ---") + println("FourierTransform allocating vs in-place: ", + @sprintf("%.2e", maximum(abs.(modes_alloc .- modes_buffer)))) + + # Compare magnitudes of modes (since basis might differ) + println("Mode magnitudes comparison (FourierTransform vs FFTW):") + println(" FourierTransform peak: ", @sprintf("%.4f", maximum(abs.(modes_alloc)))) + println(" FFTW peak: ", @sprintf("%.4f", maximum(abs.(fft_modes)))) + + println("\n--- Inverse Transform ---") + + # Use modes from our forward transform + modes_test = modes_alloc + + # 1. Our allocating inverse + print("inverse(ft, modes) [allocating]: ") + t4 = @benchmark inverse($ft, $modes_test) + display(t4) + theta_alloc = inverse(ft, modes_test) + + # 2. Our in-place inverse + print("\ninverse_transform! [in-place]: ") + t5 = @benchmark inverse_transform!($theta_buffer, $ft, $modes_test) + display(t5) + inverse_transform!(theta_buffer, ft, modes_test) + + # 3. IFFT + # Note: IFFT requires full spectrum, not just truncated modes + print("\nIFFT (full DFT, not comparable): ") + full_modes = zeros(ComplexF64, mtheta) + for (i, m) in enumerate(mlow:mhigh) + if m >= 0 + full_modes[m + 1] = modes_test[i] + else + full_modes[mtheta + m + 1] = modes_test[i] + end + end + t6 = @benchmark ifft($full_modes) + display(t6) + + # Accuracy check + println("\n--- Inverse Accuracy Check ---") + println("inverse() allocating vs in-place: ", + @sprintf("%.2e", maximum(abs.(theta_alloc .- theta_buffer)))) + println("Round-trip error (real part): ", + @sprintf("%.2e", maximum(abs.(real.(theta_alloc) .- data)))) + + # Performance summary + println("\n--- Performance Summary ---") + println(@sprintf("Forward transform speedup (in-place vs allocating): %.2fx", + median(t1).time / median(t2).time)) + println(@sprintf("Allocations eliminated: %d → %d", + t1.allocs, t2.allocs)) + + # Compare to FFTW + println(@sprintf("\nFourier vs FFTW (forward): %.2fx %s", + abs(median(t2).time / median(t3).time), + median(t2).time < median(t3).time ? "faster" : "slower")) + println("Note: FFTW computes full DFT (all N modes), we compute truncated series ($mpert modes)") +end + +# Additional test: Matrix transforms (batch processing) +println("\n" * "="^80) +println("Batch Processing Test (Multiple Functions at Once)") +println("="^80) + +mtheta = 256 +mpert = 20 +mlow = -10 +nbatch = 10 # Transform 10 functions simultaneously + +ft = FourierTransform(mtheta, mpert, mlow) +theta = range(0, 2π, length=mtheta+1)[1:end-1] + +# Create batch data +data_matrix = zeros(Float64, mtheta, nbatch) +for i in 1:nbatch + data_matrix[:, i] = sin.(i .* theta) +end + +modes_matrix = zeros(ComplexF64, mpert, nbatch) + +println("\nTransforming $nbatch functions of length $mtheta:") + +print("Allocating (loop): ") +@btime for i in 1:$nbatch + modes = $ft($data_matrix[:, i]) +end + +print("Allocating (matrix):") +@btime $ft($data_matrix) + +print("In-place (loop): ") +modes_buffer = zeros(ComplexF64, mpert) +@btime for i in 1:$nbatch + transform!($modes_buffer, $ft, $data_matrix[:, i]) +end + +print("In-place (matrix): ") +@btime transform!($modes_matrix, $ft, $data_matrix) + +# Low-level matrix API test +println("\n" * "="^80) +println("Low-Level Matrix API Test (for Vacuum module)") +println("="^80) + +mtheta = 128 +mpert = 10 +mlow = 1 + +# Setup for low-level API +cslth, snlth = compute_fourier_coefficients(mtheta, mpert, mlow) +gij = randn(mtheta, mtheta) # Green's function matrix +gil = zeros(Float64, mtheta, mpert) + +println("\nLow-level fourier_transform! with offset support:") +print("With offsets (m00=0, l00=0): ") +@btime fourier_transform!($gil, $gij, $cslth, 0, 0) + +print("With offsets (m00=64, l00=5): ") +gil_large = zeros(Float64, 256, 20) +@btime fourier_transform!($gil_large, $gij, $cslth, 64, 5) + +println("\n" * "="^80) +println("Benchmark Complete") +println("="^80) + +println("\nKey Takeaways:") +println("1. In-place API eliminates allocations with same performance") +println("2. Custom transforms optimized for truncated series (arbitrary mode ranges)") +println("3. FFTW is faster but computes full DFT (all modes, fixed range 0:N-1)") +println("4. Use FourierTransform when you need:") +println(" - Arbitrary mode ranges (e.g., m = -20:20)") +println(" - Truncated series (fewer modes than grid points)") +println(" - Phase-shifted basis functions (n*qa*delta terms)") +println("5. Use FFTW when you need:") +println(" - Full DFT (all modes 0:N-1)") +println(" - Maximum performance for dense spectral data") diff --git a/benchmarks/benchmark_git_branches.jl b/benchmarks/benchmark_git_branches.jl index 406bbd02..0c58e390 100755 --- a/benchmarks/benchmark_git_branches.jl +++ b/benchmarks/benchmark_git_branches.jl @@ -34,7 +34,7 @@ julia benchmarks/benchmark_git_branches.jl --example examples/DIIID-like_ideal_e # Requirements -- Example directory must contain DCON.jl file that can be run via: `using JPEC; JPEC.DCON.Main("path")` +- Example directory must contain jpec.jl file that can be run via: `using JPEC; JPEC.main(["path"])` - Working directory must be clean or changes will be stashed during branch switching # Output Metrics @@ -166,7 +166,7 @@ function run_example_benchmark(example_path, num_runs) cd(example_path) do Pkg.activate("../..") using JPEC - @time JPEC.DCON.Main("./") + @time JPEC.main(["./"]) end # Warm runs for timing @@ -174,7 +174,7 @@ function run_example_benchmark(example_path, num_runs) for i in 1:num_runs println("\n[$((i+1))/$(num_runs+1)] Warm run $i...") runtime = cd(example_path) do - @elapsed JPEC.DCON.Main("./") + @elapsed JPEC.main(["./"]) end push!(runtimes, runtime) println(" Runtime: $(round(runtime, digits=2)) s") diff --git a/build_docs_local.jl b/build_docs_local.jl index e192eb4f..1d34af0a 100644 --- a/build_docs_local.jl +++ b/build_docs_local.jl @@ -8,13 +8,13 @@ Pkg.instantiate() # Build the package Pkg.build() -# Activate the docs environment (don't instantiate yet - JPEC is not registered) +# Activate the docs environment Pkg.activate("docs") -# Add the local package to docs environment BEFORE instantiating -Pkg.develop(PackageSpec(; path=".")) +# Add the local package to docs environment first +Pkg.develop(PackageSpec(path=".")) -# Now instantiate to get Documenter and other deps +# Now instantiate to get other dependencies Pkg.instantiate() # Build the documentation diff --git a/docs/README.md b/docs/README.md index 44a2f400..f5c3fa98 100644 --- a/docs/README.md +++ b/docs/README.md @@ -7,7 +7,7 @@ This directory contains the documentation source files for JPEC.jl. To build the documentation locally: ```bash -julia build_docs_local.jl +julia --project=. build_docs_local.jl ``` Or step by step: diff --git a/docs/make.jl b/docs/make.jl index d2753c72..c5f66fde 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -27,7 +27,8 @@ makedocs(; "Splines" => "splines.md", "Vacuum" => "vacuum.md", "Equilibrium" => "equilibrium.md", - "Utilities" => "utilities.md" + "Utilities" => "utilities.md", + "Perturbed Equilibrium" => "perturbed_equilibrium.md" ], ], checkdocs=:exports diff --git a/docs/resources/1997-Chance-Vacuum_calculations_in_azimuthally_symmetric_geometry.pdf b/docs/resources/1997-Chance-Vacuum_calculations_in_azimuthally_symmetric_geometry.pdf new file mode 100644 index 00000000..40dff285 Binary files /dev/null and b/docs/resources/1997-Chance-Vacuum_calculations_in_azimuthally_symmetric_geometry.pdf differ diff --git a/docs/resources/2007-Chance-Calculation of the vacuum Greens function valid even for high toroidal mode numbers in tokamaks.pdf b/docs/resources/2007-Chance-Calculation of the vacuum Greens function valid even for high toroidal mode numbers in tokamaks.pdf new file mode 100644 index 00000000..c5bdbdfd Binary files /dev/null and b/docs/resources/2007-Chance-Calculation of the vacuum Greens function valid even for high toroidal mode numbers in tokamaks.pdf differ diff --git a/docs/resources/2007-Park-Computation_of_three-dimensional_tokamak_and_spherical_torus_equilibria-compressed.pdf b/docs/resources/2007-Park-Computation_of_three-dimensional_tokamak_and_spherical_torus_equilibria-compressed.pdf new file mode 100644 index 00000000..a47f657a Binary files /dev/null and b/docs/resources/2007-Park-Computation_of_three-dimensional_tokamak_and_spherical_torus_equilibria-compressed.pdf differ diff --git a/docs/resources/2007-Park-Control_of_Asymmetric_Magnetic_Perturbations_in_Tokamaks.pdf b/docs/resources/2007-Park-Control_of_Asymmetric_Magnetic_Perturbations_in_Tokamaks.pdf new file mode 100644 index 00000000..fdaf63b1 Binary files /dev/null and b/docs/resources/2007-Park-Control_of_Asymmetric_Magnetic_Perturbations_in_Tokamaks.pdf differ diff --git a/docs/resources/2009-Nonambipolar_Transport_by_Trapped_Particles_in_Tokamaks.pdf b/docs/resources/2009-Nonambipolar_Transport_by_Trapped_Particles_in_Tokamaks.pdf new file mode 100644 index 00000000..022dab15 Binary files /dev/null and b/docs/resources/2009-Nonambipolar_Transport_by_Trapped_Particles_in_Tokamaks.pdf differ diff --git a/docs/resources/2009-Park-Importance_of_plasma_response_to_nonaxisymmetric_perturbations_in_tokamaks-compressed.pdf b/docs/resources/2009-Park-Importance_of_plasma_response_to_nonaxisymmetric_perturbations_in_tokamaks-compressed.pdf new file mode 100644 index 00000000..746be989 Binary files /dev/null and b/docs/resources/2009-Park-Importance_of_plasma_response_to_nonaxisymmetric_perturbations_in_tokamaks-compressed.pdf differ diff --git a/docs/resources/2011-Park-Physics_of_Plasmas_Kinetic_energy_principle_and_neoclassical_toroidal_torque_in_tokamaks.pdf b/docs/resources/2011-Park-Physics_of_Plasmas_Kinetic_energy_principle_and_neoclassical_toroidal_torque_in_tokamaks.pdf new file mode 100644 index 00000000..82b7c2be Binary files /dev/null and b/docs/resources/2011-Park-Physics_of_Plasmas_Kinetic_energy_principle_and_neoclassical_toroidal_torque_in_tokamaks.pdf differ diff --git a/docs/resources/2013-Logan-Neoclassical_toroidal_viscosity_in_perturbed_equilibria_with_general_tokamak_geometry.pdf b/docs/resources/2013-Logan-Neoclassical_toroidal_viscosity_in_perturbed_equilibria_with_general_tokamak_geometry.pdf new file mode 100644 index 00000000..7675cb44 Binary files /dev/null and b/docs/resources/2013-Logan-Neoclassical_toroidal_viscosity_in_perturbed_equilibria_with_general_tokamak_geometry.pdf differ diff --git a/docs/resources/2015-Logan-Electromagnetic_Torque_in_Tokamaks_with_Toroidal_Asymmetries-compressed.pdf b/docs/resources/2015-Logan-Electromagnetic_Torque_in_Tokamaks_with_Toroidal_Asymmetries-compressed.pdf new file mode 100644 index 00000000..849acaae Binary files /dev/null and b/docs/resources/2015-Logan-Electromagnetic_Torque_in_Tokamaks_with_Toroidal_Asymmetries-compressed.pdf differ diff --git a/docs/resources/2016-Glasser-Computation_of_resistive_instabilities_by_matched_asymptotic_expansions-compressed.pdf b/docs/resources/2016-Glasser-Computation_of_resistive_instabilities_by_matched_asymptotic_expansions-compressed.pdf new file mode 100644 index 00000000..7107cd34 Binary files /dev/null and b/docs/resources/2016-Glasser-Computation_of_resistive_instabilities_by_matched_asymptotic_expansions-compressed.pdf differ diff --git a/docs/resources/2016-Glasser-The_direct_criterion_of_Newcomb_for_the_ideal_MHD_stability_of_an_axisymmetric_toroidal_plasma.pdf b/docs/resources/2016-Glasser-The_direct_criterion_of_Newcomb_for_the_ideal_MHD_stability_of_an_axisymmetric_toroidal_plasma.pdf new file mode 100644 index 00000000..391877cc Binary files /dev/null and b/docs/resources/2016-Glasser-The_direct_criterion_of_Newcomb_for_the_ideal_MHD_stability_of_an_axisymmetric_toroidal_plasma.pdf differ diff --git a/docs/resources/2017-Park-Self_consistent_perturbed_equilibrium_with_neoclassical_toroidal_torque_in_toka.pdf b/docs/resources/2017-Park-Self_consistent_perturbed_equilibrium_with_neoclassical_toroidal_torque_in_toka.pdf new file mode 100644 index 00000000..a44ba873 Binary files /dev/null and b/docs/resources/2017-Park-Self_consistent_perturbed_equilibrium_with_neoclassical_toroidal_torque_in_toka.pdf differ diff --git a/docs/resources/2018-Glasser-A Riccati solution for the ideal MHD plasma response with applications to real-time stability control.pdf b/docs/resources/2018-Glasser-A Riccati solution for the ideal MHD plasma response with applications to real-time stability control.pdf new file mode 100644 index 00000000..31a768c1 Binary files /dev/null and b/docs/resources/2018-Glasser-A Riccati solution for the ideal MHD plasma response with applications to real-time stability control.pdf differ diff --git a/docs/resources/2018-Glasser-A robust solution for the resistive MHD toroidal Delta-prime matrix in near real-time.pdf b/docs/resources/2018-Glasser-A robust solution for the resistive MHD toroidal Delta-prime matrix in near real-time.pdf new file mode 100644 index 00000000..fe8411bc Binary files /dev/null and b/docs/resources/2018-Glasser-A robust solution for the resistive MHD toroidal Delta-prime matrix in near real-time.pdf differ diff --git a/docs/resources/2020-Wang-Modeling of resistive plasma response in toroidal geometry using an asymptotic matching approach.pdf b/docs/resources/2020-Wang-Modeling of resistive plasma response in toroidal geometry using an asymptotic matching approach.pdf new file mode 100644 index 00000000..d180c347 Binary files /dev/null and b/docs/resources/2020-Wang-Modeling of resistive plasma response in toroidal geometry using an asymptotic matching approach.pdf differ diff --git a/docs/src/equilibrium.md b/docs/src/equilibrium.md index a502cafa..1ed945fa 100644 --- a/docs/src/equilibrium.md +++ b/docs/src/equilibrium.md @@ -19,7 +19,7 @@ Key responsibilities of the module: (global parameters, q-profile finding, separatrix finding, GSE checks). The module exposes a small public API that covers setup, configuration, -and common analyses used by other JPEC components (e.g. `DCON`, vacuum +and common analyses used by other JPEC components (e.g. `ForceFreeStates`, vacuum interfaces). ## API Reference @@ -35,7 +35,6 @@ Modules = [JPEC.Equilibrium] provided). - `EquilibriumControl` — low-level control parameters (grid, jacobian type, tolerances, etc.). -- `EquilibriumOutput` — options controlling what output is written. - `PlasmaEquilibrium` — the runtime structure containing spline fields, geometry, profiles, and computed diagnostics (q-profile, separatrix, etc.). @@ -65,7 +64,7 @@ Basic example: read a TOML config and build an equilibrium using JPEC # Build from a TOML file (searches relative paths if needed) -pe = JPEC.Equilibrium.setup_equilibrium("docs/examples/dcon.toml") +pe = JPEC.Equilibrium.setup_equilibrium("docs/examples/ForceFreeStates.toml") println("Magnetic axis: ", pe.params.r0, ", ", pe.params.z0) println("q(0) = ", pe.params.q0) diff --git a/docs/src/index.md b/docs/src/index.md index 4cbd24e7..bfb0b81d 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -19,12 +19,43 @@ Pkg.add("JPEC") ## Quick Start +### Running JPEC as a Script + +JPEC includes an executable script in the project root for easy command-line usage: + +```bash +./jpec path/to/directory +``` + +This will: +1. Read the `jpec.toml` configuration file from the specified directory +2. Set up the equilibrium +3. Compute force-free states (stability analysis) +4. Optionally compute perturbed equilibrium response (if configured) + +If no directory is provided, JPEC will use the current directory (`./`): + +```bash +./jpec +``` + +### Using JPEC as a Library + +You can also use JPEC programmatically in your own Julia code: + ```julia using JPEC -# Great question +# Run full analysis from a directory containing jpec.toml +JPEC.main(["path/to/directory"]) + +# Or set up equilibrium only +using JPEC.Equilibrium +equil = setup_equilibrium("path/to/jpec.toml") ``` +See the [Examples](@ref) section for detailed usage examples. + ## Modules ```@contents @@ -46,7 +77,7 @@ To assist with release note compilation, please follow the commit message format ``` CODE - TAG - Detailed message ``` -where CODE is EQUIL, DCON, VAC, etc. and TAGs are descriptors like WIP, MINOR, IMPROVEMENT, BUG FIX, NEW FEATURE, etc. +where CODE is Equil, ForceFreeStates, Vacuum, etc. and TAGs are descriptors like WIP, MINOR, IMPROVEMENT, BUG FIX, NEW FEATURE, etc. Additionally, please see [this](https://docs.google.com/document/d/1XAOTz1IV8ErZAAk-iSuEuddNOLB5XcoVZsAbPKRUUuA) google doc for more details on using the GitHub. diff --git a/docs/src/perturbed_equilibrium.md b/docs/src/perturbed_equilibrium.md new file mode 100644 index 00000000..e7db5e04 --- /dev/null +++ b/docs/src/perturbed_equilibrium.md @@ -0,0 +1,19 @@ +# Perturbed Equilibrium + +The `PerturbedEquilibrium` module computes the plasma response to external magnetic perturbations. + +## Types + +```@docs +JPEC.PerturbedEquilibrium.PerturbedEquilibriumControl +JPEC.PerturbedEquilibrium.PerturbedEquilibriumInternal +JPEC.PerturbedEquilibrium.ForcingMode +JPEC.PerturbedEquilibrium.PerturbedEquilibriumState +``` + +## Functions + +```@docs +JPEC.PerturbedEquilibrium.compute_perturbed_equilibrium +JPEC.PerturbedEquilibrium.write_outputs_to_HDF5 +``` diff --git a/docs/src/set_up.md b/docs/src/set_up.md index ff61a6bb..cd65de23 100644 --- a/docs/src/set_up.md +++ b/docs/src/set_up.md @@ -169,6 +169,74 @@ Clone it from GitHub directly to your virtual machine. ## On macOS +(To be completed) + +## Running JPEC + +Once JPEC is installed and built, you can run it in two ways: + +### As a Command-Line Script + +JPEC includes an executable script (`jpec`) in the project root directory. To run JPEC on a directory containing a `jpec.toml` configuration file: + +```bash +./jpec path/to/directory +``` + +**Example:** +```bash +# Run JPEC on one of the included examples +./jpec examples/DIIID-like_ideal_example + +# Run in the current directory (must contain jpec.toml) +./jpec +``` + +The script will: +1. Read configuration from `jpec.toml` in the specified directory +2. Load or generate the equilibrium based on the `[Equilibrium]` section +3. Compute force-free states (stability analysis) based on the `[ForceFreeStates]` section +4. If a `[PerturbedEquilibrium]` section exists, compute the plasma response to external perturbations +5. Write output to HDF5 files as configured + +**Early Termination:** You can stop execution early by setting: +- `force_termination = true` in `[Equilibrium]` to stop after equilibrium setup +- `force_termination = true` in `[ForceFreeStates]` to stop after stability analysis (before perturbed equilibrium) + +### As a Julia Library + +You can also use JPEC programmatically in your own Julia scripts or notebooks: + +```julia +using JPEC + +# Run the full JPEC analysis pipeline +JPEC.main(["path/to/directory"]) + +# Or access individual modules +using JPEC.Equilibrium +using JPEC.Vacuum +using JPEC.ForceFreeStates + +# Set up equilibrium only +equil = Equilibrium.setup_equilibrium("path/to/jpec.toml") + +# Access equilibrium data +println("q at axis: ", equil.params.q0) +println("Beta-N: ", equil.params.betan) +``` + +### Configuration Files + +JPEC uses TOML configuration files (`jpec.toml`) with the following main sections: + +- **`[Equilibrium]`**: Equilibrium solver settings (input file, grid resolution, coordinate system, etc.) +- **`[Wall]`**: Wall geometry for vacuum calculations (shape, size, position) +- **`[ForceFreeStates]`**: Stability analysis settings (mode numbers, tolerances, flags) +- **`[PerturbedEquilibrium]`**: Plasma response settings (forcing data, output options) + +See the example directories for complete configuration file templates. + (Setup instructions to be added) ## Pre-commit Hooks (Optional Developer Tools) diff --git a/docs/src/vacuum.md b/docs/src/vacuum.md index 493833db..bbd08fad 100644 --- a/docs/src/vacuum.md +++ b/docs/src/vacuum.md @@ -15,7 +15,7 @@ The module provides: ### VacuumInput Contains plasma boundary data and calculation parameters including: -- Plasma boundary coordinates (r, z) on DCON theta grid +- Plasma boundary coordinates (r, z) on JPEC theta grid - Free toroidal angle parameter (ν) where ϕ = 2πζ + ν(ψ, θ) - Poloidal mode numbers (mlow, mpert) - Toroidal mode number (n) @@ -56,8 +56,8 @@ using JPEC # Create VacuumInput struct with plasma boundary data # Note: ν is the free toroidal angle parameter where ϕ = 2πζ + ν(ψ, θ) inputs = JPEC.Vacuum.VacuumInput( - r = plasma_r_coords, # Plasma R coordinates on DCON theta grid - z = plasma_z_coords, # Plasma Z coordinates on DCON theta grid + r = plasma_r_coords, # Plasma R coordinates on JPEC theta grid + z = plasma_z_coords, # Plasma Z coordinates on JPEC theta grid ν = nu_array, # Toroidal angle parameter (formerly delta/qa) mlow = 1, # Lowest poloidal mode number mpert = 10, # Number of poloidal modes diff --git a/examples/DIIID-like_ideal_example/equil.toml b/examples/DIIID-like_ideal_example/equil.toml deleted file mode 100644 index e85c02a9..00000000 --- a/examples/DIIID-like_ideal_example/equil.toml +++ /dev/null @@ -1,30 +0,0 @@ -[EQUIL_CONTROL] -eq_type = "efit" # Type of the input 2D equilibrium file -eq_filename = "TKMKR_D3Dlike_default_Hmode.geqdsk" # path to equilibrium file - -jac_type = "hamada" # Coordinate system (hamada, pest, boozer, equal_arc) -power_bp = 0 # del.B ~ B_p^power_bp * B^power_b / R^power_r -power_b = 0 # del.B ~ B_p^power_bp * B^power_b / R^power_r -power_r = 0 # del.B ~ B_p^power_bp * B^power_b / R^power_r - -grid_type = "ldp" # Radial grid packing -psilow = 1e-4 # Min psi (normalized) -psihigh = 0.993 # Max psi (normalized) -mpsi = 128 # Radial grid intervals -mtheta = 256 # Poloidal grid intervals - -newq0 = 0 # Override q(0) -etol = 1e-7 # Reconstruction tolerance -use_classic_splines = false # Use classical spline (vs. tri-diagonal) - -input_only = false # Quit after input read - -[EQUIL_OUTPUT] -gse_flag = false # Output G-S equation accuracy diagnostics -out_eq_1d = false # ASCII output of 1D eq data -bin_eq_1d = false # Binary output of 1D eq data -out_eq_2d = false # ASCII output of 2D eq data -bin_eq_2d = true # Binary output of 2D eq data (used by GPEC) -out_2d = false # ASCII output of processed 2D data -bin_2d = false # Binary output of processed 2D data -dump_flag = false # Binary dump of equilibrium data diff --git a/examples/DIIID-like_ideal_example/forcing.dat b/examples/DIIID-like_ideal_example/forcing.dat new file mode 100644 index 00000000..a9f51f7f --- /dev/null +++ b/examples/DIIID-like_ideal_example/forcing.dat @@ -0,0 +1,4 @@ +# Forcing data for perturbed equilibrium calculations +# Format: n m real(amplitude) imag(amplitude) +# Single mode test case: n=1, m=6, amplitude=1e-4 +1 6 1e-4 0.0 diff --git a/examples/DIIID-like_ideal_example/dcon.toml b/examples/DIIID-like_ideal_example/jpec.toml similarity index 52% rename from examples/DIIID-like_ideal_example/dcon.toml rename to examples/DIIID-like_ideal_example/jpec.toml index be52a3d6..29475cf7 100644 --- a/examples/DIIID-like_ideal_example/dcon.toml +++ b/examples/DIIID-like_ideal_example/jpec.toml @@ -1,4 +1,30 @@ -[DCON_CONTROL] +[Equilibrium] +eq_type = "efit" # Type of the input 2D equilibrium file +eq_filename = "TKMKR_D3Dlike_default_Hmode.geqdsk" # Path to equilibrium file +jac_type = "hamada" # Coordinate system (hamada, pest, boozer, equal_arc) +power_bp = 0 # Poloidal field power exponent for Jacobian +power_b = 0 # Toroidal field power exponent for Jacobian +power_r = 0 # Major radius power exponent for Jacobian +grid_type = "ldp" # Radial grid packing type +psilow = 1e-4 # Lower limit of normalized flux coordinate +psihigh = 0.993 # Upper limit of normalized flux coordinate +mpsi = 128 # Number of radial grid points +mtheta = 256 # Number of poloidal grid points +newq0 = 0 # Override for on-axis safety factor (0 = use input value) +etol = 1e-7 # Error tolerance for equilibrium solver +force_termination = false # Terminate after equilibrium setup (skip stability calculations) + +[Wall] +shape = "nowall" # Wall shape (nowall, conformal, elliptical, dee, mod_dee, filepath) +a = 0.2415 # Distance from plasma (conformal) or shape parameter +aw = 0.05 # Half-thickness parameter for Dee-shaped walls +bw = 1.5 # Elongation parameter for wall shapes +cw = 0 # Offset of wall center from major radius +dw = 0.5 # Triangularity parameter for wall shapes +tw = 0.05 # Sharpness of wall corners (try 0.05 as initial value) +equal_arc_wall = true # Equal arc length distribution of nodes on wall + +[ForceFreeStates] bal_flag = false # Ideal MHD ballooning criterion for short wavelengths mat_flag = true # Construct coefficient matrices for diagnostic purposes ode_flag = true # Integrate ODE's for determining stability of internal long-wavelength mode (must be true for GPEC) @@ -38,12 +64,14 @@ crossover = 1e-2 # Fractional distance from rational q at which tol singfac_min = 1e-4 # Fractional distance from rational q at which ideal jump enforced ucrit = 1e4 # Maximum fraction of solutions allowed before re-normalized -[WALL] -shape = "nowall" -aw = 0.05 -bw = 1.5 -cw = 0 -dw = 0.5 -tw = 0.05 -equal_arc_wall = 0 -a = 0.2415 +[ForcingTerms] +forcing_data_file = "forcing.dat" # Path to forcing data file (n, m, complex amplitude) +forcing_data_format = "ascii" # Format of forcing data: "ascii" or "hdf5" + +[PerturbedEquilibrium] +fixed_boundary = false # Use fixed boundary conditions +output_eigenmodes = true # Output eigenmode fields as b-fields +compute_response = true # Compute plasma response to forcing +compute_singular_coupling = true # Compute singular layer coupling metrics +verbose = true # Enable verbose logging +write_outputs_to_HDF5 = true # Write perturbed equilibrium outputs to HDF5 diff --git a/examples/DIIID-like_ideal_example/run_and_analyze.ipynb b/examples/DIIID-like_ideal_example/run_and_analyze.ipynb index cb34d867..e70dce65 100644 --- a/examples/DIIID-like_ideal_example/run_and_analyze.ipynb +++ b/examples/DIIID-like_ideal_example/run_and_analyze.ipynb @@ -6,7 +6,7 @@ "metadata": {}, "source": [ "## Overview\n", - "In this notebook, we will run DCON on a Solovev ideal example equilibrium and plot the results" + "In this notebook, we will run JPEC on a Solovev ideal example equilibrium and plot the results" ] }, { @@ -30,7 +30,7 @@ "metadata": {}, "source": [ "## Run the code\n", - "We will run the main DCON code using the inputs specified in `dcon.toml`, `equil.toml`, and `vac.in`. We output the `euler.h5` file, which is a Julia version of the `euler.bin` file." + "We will run JPEC using the inputs specified in `jpec.toml`. We output the `jpec.h5` file." ] }, { @@ -40,30 +40,39 @@ "metadata": {}, "outputs": [], "source": [ - "# Run DCON in Julia\n", + "# Run\n", "Pkg.activate(\"../..\")\n", - "using JPEC\n", - "JPEC.DCON.Main(\"./\") # \"./\" tells us to obtain inputs and direct outputs to our current folder" + "using Revise, JPEC\n" ] }, { - "cell_type": "markdown", + "cell_type": "code", + "execution_count": null, "id": "4", "metadata": {}, + "outputs": [], + "source": [ + "results = JPEC.main([\"./\"]) # \"./\" tells us to obtain inputs and direct outputs to our current folder" + ] + }, + { + "cell_type": "markdown", + "id": "5", + "metadata": {}, "source": [ "## Analyze Outputs\n", - "We will now analyze the outputs of the run, the most important of which are located in the `euler.h5` output file" + "We will now analyze the outputs of the run, the most important of which are located in the `jpec.h5` output file" ] }, { "cell_type": "code", "execution_count": null, - "id": "5", + "id": "6", "metadata": {}, "outputs": [], "source": [ - "# Read in the euler.h5 data\n", - "eh5 = h5open(\"euler.h5\", \"r\")\n", + "# Read in the jpec.h5 data\n", + "eh5 = h5open(\"jpec.h5\", \"r\")\n", "mlow = read(eh5[\"info/mlow\"])\n", "xi_psi = read(eh5[\"integration/xi_psi\"])\n", "psifac = read(eh5[\"integration/psi\"])\n", @@ -76,12 +85,12 @@ "# scale energy eigenvector matrices\n", "chi1 = 2π*psio\n", "wt = wt*(chi1*1e-3)\n", - "println(\"Done reading euler.h5\")" + "println(\"Done reading jpec.h5\")" ] }, { "cell_type": "markdown", - "id": "6", + "id": "7", "metadata": {}, "source": [ "### Plot comparison of xi_psi for a few poloidal mode numbers" @@ -90,7 +99,7 @@ { "cell_type": "code", "execution_count": null, - "id": "7", + "id": "8", "metadata": {}, "outputs": [], "source": [ @@ -105,7 +114,7 @@ }, { "cell_type": "markdown", - "id": "8", + "id": "9", "metadata": {}, "source": [ "### Compare the eigenvectors and eigenvalues of each DCON energy matrix eigenmode\n", @@ -115,7 +124,7 @@ { "cell_type": "code", "execution_count": null, - "id": "9", + "id": "10", "metadata": {}, "outputs": [], "source": [ @@ -166,7 +175,7 @@ }, { "cell_type": "markdown", - "id": "10", + "id": "11", "metadata": {}, "source": [ "### Plot crit (the smallest eigenvalue of $W^{-1}$) versus $\\Psi$\n", @@ -176,7 +185,7 @@ { "cell_type": "code", "execution_count": null, - "id": "11", + "id": "12", "metadata": {}, "outputs": [], "source": [ @@ -189,10 +198,16 @@ } ], "metadata": { + "kernelspec": { + "display_name": "Julia 1.11.6", + "language": "julia", + "name": "julia-1.11" + }, "language_info": { "file_extension": ".jl", "mimetype": "application/julia", - "name": "julia" + "name": "julia", + "version": "1.11.6" } }, "nbformat": 4, diff --git a/examples/DIIID-like_ideal_example/vac.in b/examples/DIIID-like_ideal_example/vac.in deleted file mode 100644 index 68e91e03..00000000 --- a/examples/DIIID-like_ideal_example/vac.in +++ /dev/null @@ -1,99 +0,0 @@ -! **** Running DCON's input *** -&MODES - mth = 480 - xiin(1:9) = 0 0 0 0 0 0 0 1 0 - lsymz = .TRUE. - leqarcw = 1 - lzio = 0 - lgato = 0 - lrgato = 0 -/ -&DEBUGS - checkd = .FALSE. - check1 = .FALSE. - check2 = .FALSE. - checke = .FALSE. - checks = .FALSE. - wall = .FALSE. - lkplt = 0 - verbose_timer_output = f -/ -&VACDAT - ishape = 6 - aw = 0.05 - bw = 1.5 - cw = 0 - dw = 0.5 - tw = 0.05 - nsing = 500 - epsq = 1e-05 - noutv = 37 - idgt = 6 - idot = 0 - idsk = 0 - delg = 15.01 - delfac = 0.001 - cn0 = 1 -/ -&SHAPE - ipshp = 0 - xpl = 100 - apl = 1 - a = 20 - b = 170 - bpl = 1 - dpl = 0 - r = 1 - abulg = 0.932 - bbulg = 17.0 - tbulg = 0.02 - qain = 2.5 -/ -&DIAGNS - lkdis = .FALSE. - ieig = 0 - iloop = 0 - lpsub = 1 - nloop = 128 - nloopr = 0 - nphil = 3 - nphse = 1 - xofsl = 0 - ntloop = 32 - aloop = 0.01 - bloop = 1.6 - dloop = 0.5 - rloop = 1.0 - deloop = 0.001 - mx = 21 - mz = 21 - nph = 0 - nxlpin = 6 - nzlpin = 11 - epslp = 0.02 - xlpmin = 0.7 - xlpmax = 2.7 - zlpmin = -1.5 - zlpmax = 1.5 - linterior = 2 -/ -&SPRK - nminus = 0 - nplus = 0 - mphi = 16 - lwrt11 = 0 - civ = 0.0 - sp2sgn1 = 1 - sp2sgn2 = 1 - sp2sgn3 = 1 - sp2sgn4 = 1 - sp2sgn5 = 1 - sp3sgn1 = -1 - sp3sgn2 = -1 - sp3sgn3 = -1 - sp3sgn4 = 1 - sp3sgn5 = 1 - lff = 0 - ff = 1.6 - fv = 1.6 1.6 1.6 1.6 1.6 1.0 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 -/ diff --git a/examples/Solovev_ideal_example/equil.toml b/examples/Solovev_ideal_example/equil.toml deleted file mode 100644 index 7abe0a9d..00000000 --- a/examples/Solovev_ideal_example/equil.toml +++ /dev/null @@ -1,30 +0,0 @@ -[EQUIL_CONTROL] -eq_type = "sol" # Type of the input 2D equilibrium file -eq_filename = "sol.toml" # path to equilibrium file - -jac_type = "pest" # Coordinate system (hamada, pest, boozer, equal_arc) -power_bp = 0 # del.B ~ B_p^power_bp * B^power_b / R^power_r -power_b = 0 # del.B ~ B_p^power_bp * B^power_b / R^power_r -power_r = 0 # del.B ~ B_p^power_bp * B^power_b / R^power_r - -grid_type = "ldp" # Radial grid packing -psilow = 1e-4 # Min psi (normalized) -psihigh = 0.99999 # Max psi (normalized) -mpsi = 128 # Radial grid intervals -mtheta = 256 # Poloidal grid intervals - -newq0 = 0 # Override q(0) -etol = 1e-7 # Reconstruction tolerance -use_classic_splines = false # Use classical spline (vs. tri-diagonal) - -input_only = false # Quit after input read - -[EQUIL_OUTPUT] -gse_flag = false # Output G-S equation accuracy diagnostics -out_eq_1d = false # ASCII output of 1D eq data -bin_eq_1d = false # Binary output of 1D eq data -out_eq_2d = false # ASCII output of 2D eq data -bin_eq_2d = true # Binary output of 2D eq data (used by GPEC) -out_2d = false # ASCII output of processed 2D data -bin_2d = false # Binary output of processed 2D data -dump_flag = false # Binary dump of equilibrium data diff --git a/examples/Solovev_ideal_example/dcon.toml b/examples/Solovev_ideal_example/jpec.toml similarity index 59% rename from examples/Solovev_ideal_example/dcon.toml rename to examples/Solovev_ideal_example/jpec.toml index 9215f583..bbba9ced 100644 --- a/examples/Solovev_ideal_example/dcon.toml +++ b/examples/Solovev_ideal_example/jpec.toml @@ -1,4 +1,42 @@ -[DCON_CONTROL] +[Equilibrium] +eq_type = "sol" # Type of the input 2D equilibrium file +eq_filename = "sol.toml" # Path to equilibrium file +jac_type = "pest" # Coordinate system (hamada, pest, boozer, equal_arc) +power_bp = 0 # Poloidal field power exponent for Jacobian +power_b = 0 # Toroidal field power exponent for Jacobian +power_r = 0 # Major radius power exponent for Jacobian +grid_type = "ldp" # Radial grid packing type +psilow = 1e-4 # Lower limit of normalized flux coordinate +psihigh = 0.99999 # Upper limit of normalized flux coordinate +mpsi = 128 # Number of radial grid points +mtheta = 256 # Number of poloidal grid points +newq0 = 0 # Override for on-axis safety factor (0 = use input value) +etol = 1e-7 # Error tolerance for equilibrium solver +force_termination = false # Terminate after equilibrium setup (skip stability calculations) + + +[Wall] +shape = "conformal" # Wall shape (nowall, conformal, elliptical, dee, mod_dee, filepath) +a = 0.2415 # Distance from plasma (conformal) or shape parameter +aw = 0.05 # Half-thickness parameter for Dee-shaped walls +bw = 1.5 # Elongation parameter for wall shapes +cw = 0 # Offset of wall center from major radius +dw = 0.5 # Triangularity parameter for wall shapes +tw = 0.05 # Sharpness of wall corners (try 0.05 as initial value) +equal_arc_wall = true # Equal arc length distribution of nodes on wall + +# [PerturbedEquilibrium] +# # Uncomment this section to enable perturbed equilibrium calculations +# forcing_data_file = "forcing.dat" # Path to forcing data (n, m, real, imag) +# forcing_data_format = "ascii" # "ascii" or "hdf5" +# fixed_boundary = false # Fixed boundary flag +# output_eigenmodes = true # Output mode fields as b-fields +# compute_response = true # Compute plasma response +# compute_singular_coupling = true # Compute singular coupling metrics +# verbose = true # Enable verbose logging +# write_outputs_to_HDF5 = true # Write outputs to HDF5 + +[ForceFreeStates] bal_flag = false # Ideal MHD ballooning criterion for short wavelengths mat_flag = true # Construct coefficient matrices for diagnostic purposes ode_flag = true # Integrate ODE's for determining stability of internal long-wavelength mode (must be true for GPEC) diff --git a/examples/Solovev_ideal_example/run_and_analyze.ipynb b/examples/Solovev_ideal_example/run_and_analyze.ipynb index cb34d867..435c04e4 100644 --- a/examples/Solovev_ideal_example/run_and_analyze.ipynb +++ b/examples/Solovev_ideal_example/run_and_analyze.ipynb @@ -6,7 +6,7 @@ "metadata": {}, "source": [ "## Overview\n", - "In this notebook, we will run DCON on a Solovev ideal example equilibrium and plot the results" + "In this notebook, we will run JPEC on a Solovev ideal example equilibrium and plot the results" ] }, { @@ -30,7 +30,7 @@ "metadata": {}, "source": [ "## Run the code\n", - "We will run the main DCON code using the inputs specified in `dcon.toml`, `equil.toml`, and `vac.in`. We output the `euler.h5` file, which is a Julia version of the `euler.bin` file." + "We will run the JPEC code using the inputs specified in `jpec.toml`. We output the `jpec.h5` file." ] }, { @@ -40,10 +40,10 @@ "metadata": {}, "outputs": [], "source": [ - "# Run DCON in Julia\n", + "# Run\n", "Pkg.activate(\"../..\")\n", "using JPEC\n", - "JPEC.DCON.Main(\"./\") # \"./\" tells us to obtain inputs and direct outputs to our current folder" + "JPEC.main([\"./\"]) # \"./\" tells us to obtain inputs and direct outputs to our current folder" ] }, { @@ -52,7 +52,7 @@ "metadata": {}, "source": [ "## Analyze Outputs\n", - "We will now analyze the outputs of the run, the most important of which are located in the `euler.h5` output file" + "We will now analyze the outputs of the run, the most important of which are located in the `jpec.h5` output file" ] }, { @@ -62,8 +62,8 @@ "metadata": {}, "outputs": [], "source": [ - "# Read in the euler.h5 data\n", - "eh5 = h5open(\"euler.h5\", \"r\")\n", + "# Read in the jpec.h5 data\n", + "eh5 = h5open(\"jpec.h5\", \"r\")\n", "mlow = read(eh5[\"info/mlow\"])\n", "xi_psi = read(eh5[\"integration/xi_psi\"])\n", "psifac = read(eh5[\"integration/psi\"])\n", @@ -76,7 +76,7 @@ "# scale energy eigenvector matrices\n", "chi1 = 2π*psio\n", "wt = wt*(chi1*1e-3)\n", - "println(\"Done reading euler.h5\")" + "println(\"Done reading jpec.h5\")" ] }, { diff --git a/examples/Solovev_ideal_example/vac.in b/examples/Solovev_ideal_example/vac.in deleted file mode 100644 index 0f17d7c6..00000000 --- a/examples/Solovev_ideal_example/vac.in +++ /dev/null @@ -1,99 +0,0 @@ -! **** Running DCON's input *** - -&MODES - mth = 480 - xiin(1:9) = 0 0 0 0 0 0 0 1 0 - lsymz = .TRUE. - leqarcw = 1 - lzio = 0 - lgato = 0 - lrgato = 0 -/ -&DEBUGS - checkd = .FALSE. - check1 = .FALSE. - check2 = .FALSE. - checke = .FALSE. - checks = .FALSE. - wall = .FALSE. - lkplt = 0 -/ -&VACDAT - ishape = 6 - aw = 0.05 - bw = 1.5 - cw = 0 - dw = 0.5 - tw = 0.05 - nsing = 500 - epsq = 1e-05 - noutv = 37 - idgt = 6 - idot = 0 - idsk = 0 - delg = 15.01 - delfac = 0.001 - cn0 = 1 -/ -&SHAPE - ipshp = 0 - xpl = 100 - apl = 1 - a = 0.2415 - b = 170 - bpl = 1 - dpl = 0 - r = 1 - abulg = 0.932 - bbulg = 17.0 - tbulg = 0.02 - qain = 2.5 -/ -&DIAGNS - lkdis = .FALSE. - ieig = 0 - iloop = 0 - lpsub = 1 - nloop = 128 - nloopr = 0 - nphil = 3 - nphse = 1 - xofsl = 0 - ntloop = 32 - aloop = 0.01 - bloop = 1.6 - dloop = 0.5 - rloop = 1.0 - deloop = 0.001 - mx = 21 - mz = 21 - nph = 0 - nxlpin = 6 - nzlpin = 11 - epslp = 0.02 - xlpmin = 0.7 - xlpmax = 2.7 - zlpmin = -1.5 - zlpmax = 1.5 - linterior = 2 -/ -&SPRK - nminus = 0 - nplus = 0 - mphi = 16 - lwrt11 = 0 - civ = 0.0 - sp2sgn1 = 1 - sp2sgn2 = 1 - sp2sgn3 = 1 - sp2sgn4 = 1 - sp2sgn5 = 1 - sp3sgn1 = -1 - sp3sgn2 = -1 - sp3sgn3 = -1 - sp3sgn4 = 1 - sp3sgn5 = 1 - lff = 0 - ff = 1.6 - fv = 1.6 1.6 1.6 1.6 1.6 1.0 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 -/ diff --git a/examples/Solovev_ideal_example/vacuum_wall_debug.jl b/examples/Solovev_ideal_example/vacuum_wall_debug.jl index cfa2c7d6..5fd157f7 100644 --- a/examples/Solovev_ideal_example/vacuum_wall_debug.jl +++ b/examples/Solovev_ideal_example/vacuum_wall_debug.jl @@ -33,7 +33,7 @@ if !use_wall_arg end -JPEC.Vacuum.set_dcon_params(mtheta_eq, mlow, mhigh, +JPEC.Vacuum.set_surface_params(mtheta_eq, mlow, mhigh, n, vac_inputs.qa, vac_inputs.r, vac_inputs.z, vac_inputs.delta) @@ -41,7 +41,7 @@ wv_fortran = JPEC.Vacuum.mscvac(wv_block, mpert, mtheta_eq, mthvac, complex_flag, kernelsign, wall_flag, farwall_flag, grri, xzpts, ahg_file, "$(@__DIR__)") -JPEC.Vacuum.unset_dcon_params() +JPEC.Vacuum.unset_surface_params() # Now deepcopy the julia vac_inputs and set the new ms vac_inputs = deepcopy(vac_inputs) diff --git a/examples/Solovev_ideal_example_multi_n/equil.toml b/examples/Solovev_ideal_example_multi_n/equil.toml deleted file mode 100644 index 7abe0a9d..00000000 --- a/examples/Solovev_ideal_example_multi_n/equil.toml +++ /dev/null @@ -1,30 +0,0 @@ -[EQUIL_CONTROL] -eq_type = "sol" # Type of the input 2D equilibrium file -eq_filename = "sol.toml" # path to equilibrium file - -jac_type = "pest" # Coordinate system (hamada, pest, boozer, equal_arc) -power_bp = 0 # del.B ~ B_p^power_bp * B^power_b / R^power_r -power_b = 0 # del.B ~ B_p^power_bp * B^power_b / R^power_r -power_r = 0 # del.B ~ B_p^power_bp * B^power_b / R^power_r - -grid_type = "ldp" # Radial grid packing -psilow = 1e-4 # Min psi (normalized) -psihigh = 0.99999 # Max psi (normalized) -mpsi = 128 # Radial grid intervals -mtheta = 256 # Poloidal grid intervals - -newq0 = 0 # Override q(0) -etol = 1e-7 # Reconstruction tolerance -use_classic_splines = false # Use classical spline (vs. tri-diagonal) - -input_only = false # Quit after input read - -[EQUIL_OUTPUT] -gse_flag = false # Output G-S equation accuracy diagnostics -out_eq_1d = false # ASCII output of 1D eq data -bin_eq_1d = false # Binary output of 1D eq data -out_eq_2d = false # ASCII output of 2D eq data -bin_eq_2d = true # Binary output of 2D eq data (used by GPEC) -out_2d = false # ASCII output of processed 2D data -bin_2d = false # Binary output of processed 2D data -dump_flag = false # Binary dump of equilibrium data diff --git a/examples/Solovev_ideal_example_multi_n/dcon.toml b/examples/Solovev_ideal_example_multi_n/jpec.toml similarity index 59% rename from examples/Solovev_ideal_example_multi_n/dcon.toml rename to examples/Solovev_ideal_example_multi_n/jpec.toml index 4fcd99e4..309fb446 100644 --- a/examples/Solovev_ideal_example_multi_n/dcon.toml +++ b/examples/Solovev_ideal_example_multi_n/jpec.toml @@ -1,4 +1,30 @@ -[DCON_CONTROL] +[Equilibrium] +eq_type = "sol" # Type of the input 2D equilibrium file +eq_filename = "sol.toml" # Path to equilibrium file +jac_type = "pest" # Coordinate system (hamada, pest, boozer, equal_arc) +power_bp = 0 # Poloidal field power exponent for Jacobian +power_b = 0 # Toroidal field power exponent for Jacobian +power_r = 0 # Major radius power exponent for Jacobian +grid_type = "ldp" # Radial grid packing type +psilow = 1e-4 # Lower limit of normalized flux coordinate +psihigh = 0.99999 # Upper limit of normalized flux coordinate +mpsi = 128 # Number of radial grid points +mtheta = 256 # Number of poloidal grid points +newq0 = 0 # Override for on-axis safety factor (0 = use input value) +etol = 1e-7 # Error tolerance for equilibrium solver +force_termination = false # Terminate after equilibrium setup (skip stability calculations) + +[Wall] +shape = "conformal" # Wall shape (nowall, conformal, elliptical, dee, mod_dee, filepath) +a = 0.2415 # Distance from plasma (conformal) or shape parameter +aw = 0.05 # Half-thickness parameter for Dee-shaped walls +bw = 1.5 # Elongation parameter for wall shapes +cw = 0 # Offset of wall center from major radius +dw = 0.5 # Triangularity parameter for wall shapes +tw = 0.05 # Sharpness of wall corners (try 0.05 as initial value) +equal_arc_wall = true # Equal arc length distribution of nodes on wall + +[ForceFreeStates] bal_flag = false # Ideal MHD ballooning criterion for short wavelengths mat_flag = true # Construct coefficient matrices for diagnostic purposes ode_flag = true # Integrate ODE's for determining stability of internal long-wavelength mode (must be true for GPEC) diff --git a/examples/Solovev_ideal_example_multi_n/run_and_analyze.ipynb b/examples/Solovev_ideal_example_multi_n/run_and_analyze.ipynb index 941c1f07..48aba6ed 100644 --- a/examples/Solovev_ideal_example_multi_n/run_and_analyze.ipynb +++ b/examples/Solovev_ideal_example_multi_n/run_and_analyze.ipynb @@ -29,7 +29,7 @@ "metadata": {}, "source": [ "## Run the code\n", - "We will run the main DCON code using the inputs specified in `dcon.toml`, `equil.toml`, and `vac.in`. We run the code 3 times: once with both n=1 and n=2 at the same time using the files in this folder, and then for a single-n to compare the outputs with in their respective subfolders. Each run will output a `euler.h5` file (but we set the default file naming in the `dcon.toml` to contain an \"_n1\" and \"_n2\" for the single-n cases), which is a Julia version of the `euler.bin` file." + "We will run JPEC using the inputs specified in `jpec.toml`. We run the code 3 times: once with both n=1 and n=2 at the same time using the files in this folder, and then for a single-n to compare the outputs with in their respective subfolders. Each run will output a `jpec.h5` file (but we set the default file naming in the `dcon.toml` to contain an \"_n1\" and \"_n2\" for the single-n cases), which is a Julia version of the `euler.bin` file." ] }, { @@ -39,12 +39,12 @@ "metadata": {}, "outputs": [], "source": [ - "# Run DCON in Julia\n", + "# Run\n", "Pkg.activate(\"../..\")\n", "using JPEC\n", - "JPEC.DCON.Main(\"./\") # \"./\" tells us to obtain inputs and direct outputs to our current folder\n", - "JPEC.DCON.Main(\"single_n_1\")\n", - "JPEC.DCON.Main(\"single_n_2\")" + "JPEC.main([\"./\"]) # \"./\" tells us to obtain inputs and direct outputs to our current folder\n", + "JPEC.main([\"./single_n_1\"])\n", + "JPEC.main([\"./single_n_2\"])" ] }, { @@ -53,7 +53,7 @@ "metadata": {}, "source": [ "## Analyze Outputs\n", - "We will now analyze the outputs of the run, the most important of which are located in the `euler.h5` output file. We load in the data from both single-n runs and the multi-n run here." + "We will now analyze the outputs of the run, the most important of which are located in the `jpec.h5` output file. We load in the data from both single-n runs and the multi-n run here." ] }, { @@ -63,8 +63,8 @@ "metadata": {}, "outputs": [], "source": [ - "# Read in the multi-n run euler.h5 data\n", - "eh5 = h5open(\"euler.h5\", \"r\")\n", + "# Read in the multi-n run jpec.h5 data\n", + "eh5 = h5open(\"jpec.h5\", \"r\")\n", "mlow = read(eh5[\"info/mlow\"])\n", "mhigh = read(eh5[\"info/mhigh\"])\n", "nlow = read(eh5[\"info/nlow\"])\n", @@ -79,7 +79,7 @@ "# scale energy eigenvector matrices\n", "chi1 = 2π*psio\n", "wt .*= (chi1*1e-3)\n", - "println(\"Done reading euler.h5\")" + "println(\"Done reading jpec.h5\")" ] }, { @@ -89,7 +89,7 @@ "metadata": {}, "outputs": [], "source": [ - "# Read in the single-n run with n=1 euler.h5 data\n", + "# Read in the single-n run with n=1 jpec.h5 data\n", "eh5 = h5open(\"single_n_1/euler_n1.h5\", \"r\")\n", "mlow = read(eh5[\"info/mlow\"])\n", "psio = read(eh5[\"equil/psio\"])\n", @@ -107,7 +107,7 @@ "metadata": {}, "outputs": [], "source": [ - "# Read in the single-n run with n=2 euler.h5 data\n", + "# Read in the single-n run with n=2 jpec.h5 data\n", "eh5 = h5open(\"single_n_2/euler_n2.h5\", \"r\")\n", "mlow = read(eh5[\"info/mlow\"])\n", "psio = read(eh5[\"equil/psio\"])\n", diff --git a/examples/Solovev_ideal_example_multi_n/single_n_1/equil.toml b/examples/Solovev_ideal_example_multi_n/single_n_1/equil.toml deleted file mode 100644 index 7abe0a9d..00000000 --- a/examples/Solovev_ideal_example_multi_n/single_n_1/equil.toml +++ /dev/null @@ -1,30 +0,0 @@ -[EQUIL_CONTROL] -eq_type = "sol" # Type of the input 2D equilibrium file -eq_filename = "sol.toml" # path to equilibrium file - -jac_type = "pest" # Coordinate system (hamada, pest, boozer, equal_arc) -power_bp = 0 # del.B ~ B_p^power_bp * B^power_b / R^power_r -power_b = 0 # del.B ~ B_p^power_bp * B^power_b / R^power_r -power_r = 0 # del.B ~ B_p^power_bp * B^power_b / R^power_r - -grid_type = "ldp" # Radial grid packing -psilow = 1e-4 # Min psi (normalized) -psihigh = 0.99999 # Max psi (normalized) -mpsi = 128 # Radial grid intervals -mtheta = 256 # Poloidal grid intervals - -newq0 = 0 # Override q(0) -etol = 1e-7 # Reconstruction tolerance -use_classic_splines = false # Use classical spline (vs. tri-diagonal) - -input_only = false # Quit after input read - -[EQUIL_OUTPUT] -gse_flag = false # Output G-S equation accuracy diagnostics -out_eq_1d = false # ASCII output of 1D eq data -bin_eq_1d = false # Binary output of 1D eq data -out_eq_2d = false # ASCII output of 2D eq data -bin_eq_2d = true # Binary output of 2D eq data (used by GPEC) -out_2d = false # ASCII output of processed 2D data -bin_2d = false # Binary output of processed 2D data -dump_flag = false # Binary dump of equilibrium data diff --git a/examples/Solovev_ideal_example_multi_n/single_n_1/dcon.toml b/examples/Solovev_ideal_example_multi_n/single_n_1/jpec.toml similarity index 59% rename from examples/Solovev_ideal_example_multi_n/single_n_1/dcon.toml rename to examples/Solovev_ideal_example_multi_n/single_n_1/jpec.toml index 7c9725aa..771b1adb 100644 --- a/examples/Solovev_ideal_example_multi_n/single_n_1/dcon.toml +++ b/examples/Solovev_ideal_example_multi_n/single_n_1/jpec.toml @@ -1,4 +1,30 @@ -[DCON_CONTROL] +[Equilibrium] +eq_type = "sol" # Type of the input 2D equilibrium file +eq_filename = "sol.toml" # Path to equilibrium file +jac_type = "pest" # Coordinate system (hamada, pest, boozer, equal_arc) +power_bp = 0 # Poloidal field power exponent for Jacobian +power_b = 0 # Toroidal field power exponent for Jacobian +power_r = 0 # Major radius power exponent for Jacobian +grid_type = "ldp" # Radial grid packing type +psilow = 1e-4 # Lower limit of normalized flux coordinate +psihigh = 0.99999 # Upper limit of normalized flux coordinate +mpsi = 128 # Number of radial grid points +mtheta = 256 # Number of poloidal grid points +newq0 = 0 # Override for on-axis safety factor (0 = use input value) +etol = 1e-7 # Error tolerance for equilibrium solver +force_termination = false # Terminate after equilibrium setup (skip stability calculations) + +[Wall] +shape = "conformal" # Wall shape (nowall, conformal, elliptical, dee, mod_dee, filepath) +a = 0.2415 # Distance from plasma (conformal) or shape parameter +aw = 0.05 # Half-thickness parameter for Dee-shaped walls +bw = 1.5 # Elongation parameter for wall shapes +cw = 0 # Offset of wall center from major radius +dw = 0.5 # Triangularity parameter for wall shapes +tw = 0.05 # Sharpness of wall corners (try 0.05 as initial value) +equal_arc_wall = true # Equal arc length distribution of nodes on wall + +[ForceFreeStates] bal_flag = false # Ideal MHD ballooning criterion for short wavelengths mat_flag = true # Construct coefficient matrices for diagnostic purposes ode_flag = true # Integrate ODE's for determining stability of internal long-wavelength mode (must be true for GPEC) diff --git a/examples/Solovev_ideal_example_multi_n/single_n_1/vac.in b/examples/Solovev_ideal_example_multi_n/single_n_1/vac.in deleted file mode 100644 index 0f17d7c6..00000000 --- a/examples/Solovev_ideal_example_multi_n/single_n_1/vac.in +++ /dev/null @@ -1,99 +0,0 @@ -! **** Running DCON's input *** - -&MODES - mth = 480 - xiin(1:9) = 0 0 0 0 0 0 0 1 0 - lsymz = .TRUE. - leqarcw = 1 - lzio = 0 - lgato = 0 - lrgato = 0 -/ -&DEBUGS - checkd = .FALSE. - check1 = .FALSE. - check2 = .FALSE. - checke = .FALSE. - checks = .FALSE. - wall = .FALSE. - lkplt = 0 -/ -&VACDAT - ishape = 6 - aw = 0.05 - bw = 1.5 - cw = 0 - dw = 0.5 - tw = 0.05 - nsing = 500 - epsq = 1e-05 - noutv = 37 - idgt = 6 - idot = 0 - idsk = 0 - delg = 15.01 - delfac = 0.001 - cn0 = 1 -/ -&SHAPE - ipshp = 0 - xpl = 100 - apl = 1 - a = 0.2415 - b = 170 - bpl = 1 - dpl = 0 - r = 1 - abulg = 0.932 - bbulg = 17.0 - tbulg = 0.02 - qain = 2.5 -/ -&DIAGNS - lkdis = .FALSE. - ieig = 0 - iloop = 0 - lpsub = 1 - nloop = 128 - nloopr = 0 - nphil = 3 - nphse = 1 - xofsl = 0 - ntloop = 32 - aloop = 0.01 - bloop = 1.6 - dloop = 0.5 - rloop = 1.0 - deloop = 0.001 - mx = 21 - mz = 21 - nph = 0 - nxlpin = 6 - nzlpin = 11 - epslp = 0.02 - xlpmin = 0.7 - xlpmax = 2.7 - zlpmin = -1.5 - zlpmax = 1.5 - linterior = 2 -/ -&SPRK - nminus = 0 - nplus = 0 - mphi = 16 - lwrt11 = 0 - civ = 0.0 - sp2sgn1 = 1 - sp2sgn2 = 1 - sp2sgn3 = 1 - sp2sgn4 = 1 - sp2sgn5 = 1 - sp3sgn1 = -1 - sp3sgn2 = -1 - sp3sgn3 = -1 - sp3sgn4 = 1 - sp3sgn5 = 1 - lff = 0 - ff = 1.6 - fv = 1.6 1.6 1.6 1.6 1.6 1.0 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 -/ diff --git a/examples/Solovev_ideal_example_multi_n/single_n_2/equil.toml b/examples/Solovev_ideal_example_multi_n/single_n_2/equil.toml deleted file mode 100644 index 7abe0a9d..00000000 --- a/examples/Solovev_ideal_example_multi_n/single_n_2/equil.toml +++ /dev/null @@ -1,30 +0,0 @@ -[EQUIL_CONTROL] -eq_type = "sol" # Type of the input 2D equilibrium file -eq_filename = "sol.toml" # path to equilibrium file - -jac_type = "pest" # Coordinate system (hamada, pest, boozer, equal_arc) -power_bp = 0 # del.B ~ B_p^power_bp * B^power_b / R^power_r -power_b = 0 # del.B ~ B_p^power_bp * B^power_b / R^power_r -power_r = 0 # del.B ~ B_p^power_bp * B^power_b / R^power_r - -grid_type = "ldp" # Radial grid packing -psilow = 1e-4 # Min psi (normalized) -psihigh = 0.99999 # Max psi (normalized) -mpsi = 128 # Radial grid intervals -mtheta = 256 # Poloidal grid intervals - -newq0 = 0 # Override q(0) -etol = 1e-7 # Reconstruction tolerance -use_classic_splines = false # Use classical spline (vs. tri-diagonal) - -input_only = false # Quit after input read - -[EQUIL_OUTPUT] -gse_flag = false # Output G-S equation accuracy diagnostics -out_eq_1d = false # ASCII output of 1D eq data -bin_eq_1d = false # Binary output of 1D eq data -out_eq_2d = false # ASCII output of 2D eq data -bin_eq_2d = true # Binary output of 2D eq data (used by GPEC) -out_2d = false # ASCII output of processed 2D data -bin_2d = false # Binary output of processed 2D data -dump_flag = false # Binary dump of equilibrium data diff --git a/examples/Solovev_ideal_example_multi_n/single_n_2/dcon.toml b/examples/Solovev_ideal_example_multi_n/single_n_2/jpec.toml similarity index 59% rename from examples/Solovev_ideal_example_multi_n/single_n_2/dcon.toml rename to examples/Solovev_ideal_example_multi_n/single_n_2/jpec.toml index be15056d..d74d81b4 100644 --- a/examples/Solovev_ideal_example_multi_n/single_n_2/dcon.toml +++ b/examples/Solovev_ideal_example_multi_n/single_n_2/jpec.toml @@ -1,4 +1,30 @@ -[DCON_CONTROL] +[Equilibrium] +eq_type = "sol" # Type of the input 2D equilibrium file +eq_filename = "sol.toml" # Path to equilibrium file +jac_type = "pest" # Coordinate system (hamada, pest, boozer, equal_arc) +power_bp = 0 # Poloidal field power exponent for Jacobian +power_b = 0 # Toroidal field power exponent for Jacobian +power_r = 0 # Major radius power exponent for Jacobian +grid_type = "ldp" # Radial grid packing type +psilow = 1e-4 # Lower limit of normalized flux coordinate +psihigh = 0.99999 # Upper limit of normalized flux coordinate +mpsi = 128 # Number of radial grid points +mtheta = 256 # Number of poloidal grid points +newq0 = 0 # Override for on-axis safety factor (0 = use input value) +etol = 1e-7 # Error tolerance for equilibrium solver +force_termination = false # Terminate after equilibrium setup (skip stability calculations) + +[Wall] +shape = "conformal" # Wall shape (nowall, conformal, elliptical, dee, mod_dee, filepath) +a = 0.2415 # Distance from plasma (conformal) or shape parameter +aw = 0.05 # Half-thickness parameter for Dee-shaped walls +bw = 1.5 # Elongation parameter for wall shapes +cw = 0 # Offset of wall center from major radius +dw = 0.5 # Triangularity parameter for wall shapes +tw = 0.05 # Sharpness of wall corners (try 0.05 as initial value) +equal_arc_wall = true # Equal arc length distribution of nodes on wall + +[ForceFreeStates] bal_flag = false # Ideal MHD ballooning criterion for short wavelengths mat_flag = true # Construct coefficient matrices for diagnostic purposes ode_flag = true # Integrate ODE's for determining stability of internal long-wavelength mode (must be true for GPEC) diff --git a/examples/Solovev_ideal_example_multi_n/single_n_2/vac.in b/examples/Solovev_ideal_example_multi_n/single_n_2/vac.in deleted file mode 100644 index 0f17d7c6..00000000 --- a/examples/Solovev_ideal_example_multi_n/single_n_2/vac.in +++ /dev/null @@ -1,99 +0,0 @@ -! **** Running DCON's input *** - -&MODES - mth = 480 - xiin(1:9) = 0 0 0 0 0 0 0 1 0 - lsymz = .TRUE. - leqarcw = 1 - lzio = 0 - lgato = 0 - lrgato = 0 -/ -&DEBUGS - checkd = .FALSE. - check1 = .FALSE. - check2 = .FALSE. - checke = .FALSE. - checks = .FALSE. - wall = .FALSE. - lkplt = 0 -/ -&VACDAT - ishape = 6 - aw = 0.05 - bw = 1.5 - cw = 0 - dw = 0.5 - tw = 0.05 - nsing = 500 - epsq = 1e-05 - noutv = 37 - idgt = 6 - idot = 0 - idsk = 0 - delg = 15.01 - delfac = 0.001 - cn0 = 1 -/ -&SHAPE - ipshp = 0 - xpl = 100 - apl = 1 - a = 0.2415 - b = 170 - bpl = 1 - dpl = 0 - r = 1 - abulg = 0.932 - bbulg = 17.0 - tbulg = 0.02 - qain = 2.5 -/ -&DIAGNS - lkdis = .FALSE. - ieig = 0 - iloop = 0 - lpsub = 1 - nloop = 128 - nloopr = 0 - nphil = 3 - nphse = 1 - xofsl = 0 - ntloop = 32 - aloop = 0.01 - bloop = 1.6 - dloop = 0.5 - rloop = 1.0 - deloop = 0.001 - mx = 21 - mz = 21 - nph = 0 - nxlpin = 6 - nzlpin = 11 - epslp = 0.02 - xlpmin = 0.7 - xlpmax = 2.7 - zlpmin = -1.5 - zlpmax = 1.5 - linterior = 2 -/ -&SPRK - nminus = 0 - nplus = 0 - mphi = 16 - lwrt11 = 0 - civ = 0.0 - sp2sgn1 = 1 - sp2sgn2 = 1 - sp2sgn3 = 1 - sp2sgn4 = 1 - sp2sgn5 = 1 - sp3sgn1 = -1 - sp3sgn2 = -1 - sp3sgn3 = -1 - sp3sgn4 = 1 - sp3sgn5 = 1 - lff = 0 - ff = 1.6 - fv = 1.6 1.6 1.6 1.6 1.6 1.0 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 -/ diff --git a/examples/Solovev_ideal_example_multi_n/vac.in b/examples/Solovev_ideal_example_multi_n/vac.in deleted file mode 100644 index 0f17d7c6..00000000 --- a/examples/Solovev_ideal_example_multi_n/vac.in +++ /dev/null @@ -1,99 +0,0 @@ -! **** Running DCON's input *** - -&MODES - mth = 480 - xiin(1:9) = 0 0 0 0 0 0 0 1 0 - lsymz = .TRUE. - leqarcw = 1 - lzio = 0 - lgato = 0 - lrgato = 0 -/ -&DEBUGS - checkd = .FALSE. - check1 = .FALSE. - check2 = .FALSE. - checke = .FALSE. - checks = .FALSE. - wall = .FALSE. - lkplt = 0 -/ -&VACDAT - ishape = 6 - aw = 0.05 - bw = 1.5 - cw = 0 - dw = 0.5 - tw = 0.05 - nsing = 500 - epsq = 1e-05 - noutv = 37 - idgt = 6 - idot = 0 - idsk = 0 - delg = 15.01 - delfac = 0.001 - cn0 = 1 -/ -&SHAPE - ipshp = 0 - xpl = 100 - apl = 1 - a = 0.2415 - b = 170 - bpl = 1 - dpl = 0 - r = 1 - abulg = 0.932 - bbulg = 17.0 - tbulg = 0.02 - qain = 2.5 -/ -&DIAGNS - lkdis = .FALSE. - ieig = 0 - iloop = 0 - lpsub = 1 - nloop = 128 - nloopr = 0 - nphil = 3 - nphse = 1 - xofsl = 0 - ntloop = 32 - aloop = 0.01 - bloop = 1.6 - dloop = 0.5 - rloop = 1.0 - deloop = 0.001 - mx = 21 - mz = 21 - nph = 0 - nxlpin = 6 - nzlpin = 11 - epslp = 0.02 - xlpmin = 0.7 - xlpmax = 2.7 - zlpmin = -1.5 - zlpmax = 1.5 - linterior = 2 -/ -&SPRK - nminus = 0 - nplus = 0 - mphi = 16 - lwrt11 = 0 - civ = 0.0 - sp2sgn1 = 1 - sp2sgn2 = 1 - sp2sgn3 = 1 - sp2sgn4 = 1 - sp2sgn5 = 1 - sp3sgn1 = -1 - sp3sgn2 = -1 - sp3sgn3 = -1 - sp3sgn4 = 1 - sp3sgn5 = 1 - lff = 0 - ff = 1.6 - fv = 1.6 1.6 1.6 1.6 1.6 1.0 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 -/ diff --git a/examples/fourier_transform_usage.jl b/examples/fourier_transform_usage.jl new file mode 100644 index 00000000..54e18041 --- /dev/null +++ b/examples/fourier_transform_usage.jl @@ -0,0 +1,187 @@ +""" +Example: Using FourierTransforms utility for different grids + +This example demonstrates how to use the FourierTransforms utility module +to create pre-computed Fourier transform objects for different grids: + +1. PerturbedEquilibrium: Simple harmonic basis (no phase shift) +2. Vacuum: Basis with toroidal phase coupling (n*qa*delta) +""" + +using JPEC +using JPEC.Utilities.FourierTransforms + +# ============================================================================== +# Example 1: PerturbedEquilibrium Case (No Phase Shift) +# ============================================================================== +println("\n" * "="^70) +println("Example 1: PerturbedEquilibrium - Simple Harmonic Basis") +println("="^70) + +# Grid parameters for PerturbedEquilibrium +mtheta = 480 # Number of theta grid points +mpert = 41 # Number of Fourier modes +mlow = -20 # Lowest mode number + +# Create FourierTransform object (no phase shift, default arguments) +ft_pe = FourierTransform(mtheta, mpert, mlow) + +println("Created FourierTransform for PerturbedEquilibrium:") +println(" mtheta = $mtheta (theta grid points)") +println(" mpert = $mpert (number of modes)") +println(" mlow = $mlow (lowest mode: m ∈ [$mlow, $(mlow+mpert-1)])") +println("\nBasis functions: cos(m*θ), sin(m*θ) with m = $mlow:$(mlow+mpert-1)") + +# Create test data: a simple function in theta space +theta = range(0, 2π, length=mtheta+1)[1:end-1] +f_theta = sin.(5 .* theta) .+ 0.5 .* cos.(8 .* theta) + +# Forward transform: theta → modes +println("\n--- Forward Transform ---") +modes = ft_pe(f_theta) +println("Input: f(θ) at $mtheta theta points (real-valued)") +println("Output: $mpert complex Fourier modes") +println("Dominant modes (|amplitude| > 0.01):") +for l in 1:mpert + m = mlow + l - 1 + if abs(modes[l]) > 0.01 + println(" m=$m: amplitude = $(abs(modes[l])), phase = $(angle(modes[l]))") + end +end + +# Inverse transform: modes → theta +println("\n--- Inverse Transform ---") +f_reconstructed = inverse(ft_pe, modes) +println("Input: $mpert complex Fourier modes") +println("Output: $(length(f_reconstructed)) theta-space values") +println("Reconstruction error: $(maximum(abs.(real.(f_reconstructed) .- f_theta)))") + +# ============================================================================== +# Example 2: Vacuum Case (With Toroidal Phase) +# ============================================================================== +println("\n" * "="^70) +println("Example 2: Vacuum - Basis with Toroidal Coupling") +println("="^70) + +# Grid parameters for Vacuum +mthvac = 480 # Number of vacuum theta grid points +mpert_vac = 41 # Number of Fourier modes +mlow_vac = -20 # Lowest mode number +n = 1 # Toroidal mode number +qa = 2.5 # Safety factor at boundary +delta = zeros(mthvac) # Toroidal phase shift (zero for this example) + +# Create FourierTransform object with toroidal phase +ft_vac = FourierTransform(mthvac, mpert_vac, mlow_vac; n=n, qa=qa, delta=delta) + +println("Created FourierTransform for Vacuum:") +println(" mthvac = $mthvac (vacuum theta grid points)") +println(" mpert = $mpert_vac (number of modes)") +println(" mlow = $mlow_vac (lowest mode: m ∈ [$mlow_vac, $(mlow_vac+mpert_vac-1)])") +println(" n = $n (toroidal mode number)") +println(" qa = $qa (safety factor)") +println("\nBasis functions: cos(m*θ + n*qa*δ), sin(m*θ + n*qa*δ)") + +# Example: Transform Green's function data +g_theta = randn(mthvac) # Simulated Green's function in theta space +println("\n--- Forward Transform (Green's function) ---") +g_modes = ft_vac(g_theta) +println("Transformed $mthvac theta-space values to $mpert_vac complex modes") + +# Inverse transform back +g_reconstructed = inverse(ft_vac, g_modes) +println("\n--- Inverse Transform ---") +println("Reconstruction error: $(maximum(abs.(real.(g_reconstructed) .- g_theta)))") + +# ============================================================================== +# Example 3: Multiple Functions Simultaneously +# ============================================================================== +println("\n" * "="^70) +println("Example 3: Batch Transform (Multiple Functions)") +println("="^70) + +# Create matrix of multiple theta-space functions +n_funcs = 10 +data_matrix = randn(mtheta, n_funcs) + +println("Transforming $n_funcs functions simultaneously") +println("Input shape: ($(size(data_matrix, 1)), $(size(data_matrix, 2)))") + +# Forward transform all at once +modes_matrix = ft_pe(data_matrix) +println("Output shape: ($(size(modes_matrix, 1)), $(size(modes_matrix, 2)))") +println(" → Each column is one transformed function") + +# Inverse transform +reconstructed_matrix = inverse(ft_pe, modes_matrix) +max_error = maximum(abs.(real.(reconstructed_matrix) .- data_matrix)) +println("\nMax reconstruction error across all functions: $max_error") + +# ============================================================================== +# Example 4: Replacing FFTW in existing code +# ============================================================================== +println("\n" * "="^70) +println("Example 4: Migration from FFTW") +println("="^70) + +using FFTW + +# Old approach: using FFTW +function theta_to_modes_fftw(theta_values::Vector{Float64}, mpert::Int) + mtheta = length(theta_values) + fft_result = fft(theta_values) + modes = zeros(ComplexF64, mpert) + for i in 1:min(mpert, length(fft_result)) + modes[i] = fft_result[i] / mtheta + end + return modes +end + +# New approach: using FourierTransform +# (already shown above: modes = ft_pe(theta_values)) + +# Compare approaches +test_data = randn(mtheta) +modes_fft = theta_to_modes_fftw(test_data, mpert) +modes_custom = ft_pe(test_data) + +println("Comparison of FFTW vs FourierTransform:") +println(" Both methods transform $mtheta theta points to $mpert modes") +println(" Results differ because:") +println(" - FFTW uses exp(i*k*θ) basis (standard FFT)") +println(" - FourierTransform uses cos(m*θ) + i*sin(m*θ) basis") +println(" - FourierTransform allows custom mode range (m = $mlow:$(mlow+mpert-1))") +println(" - FourierTransform supports phase shifts for vacuum calculations") + +# ============================================================================== +# Summary +# ============================================================================== +println("\n" * "="^70) +println("Summary: When to Use FourierTransform") +println("="^70) +println(""" +1. Pre-compute transforms for a specific grid: + ft = FourierTransform(mtheta, mpert, mlow) + +2. For PerturbedEquilibrium (no phase shift): + ft = FourierTransform(mtheta, mpert, mlow) + modes = ft(theta_data) + +3. For Vacuum (with toroidal coupling): + ft = FourierTransform(mthvac, mpert, mlow; n=n, qa=qa, delta=delta) + modes = ft(green_function_data) + +4. Avoid FFTW when: + - You need custom mode ranges (not 0:N-1) + - You need phase-shifted basis functions + - You want to pre-compute and reuse coefficients + - You're matching Vacuum module conventions + +5. Benefits: + - Type-stable, efficient functor pattern + - Pre-computed basis functions (no repeated trig calculations) + - Direct complex number support + - Consistent interface across modules +""") + +println("="^70) diff --git a/jpec b/jpec new file mode 100755 index 00000000..b8d9b79a --- /dev/null +++ b/jpec @@ -0,0 +1,7 @@ +#!/usr/bin/env julia --project=@. +# JPEC executable script +# Usage: ./jpec [path/to/directory] +# If no path is provided, uses current directory + +using JPEC +JPEC.main(ARGS) diff --git a/notebooks/Chease_Binary_example/equil.toml b/notebooks/Chease_Binary_example/equil.toml deleted file mode 100644 index e75c90ec..00000000 --- a/notebooks/Chease_Binary_example/equil.toml +++ /dev/null @@ -1,30 +0,0 @@ -[EQUIL_CONTROL] -eq_type = "chease" # Type of the input 2D equilibrium file -eq_filename = "INP1" # path to equilibrium file - -jac_type = "hamada" # Coordinate system (hamada, pest, boozer, equal_arc) -power_bp = 0 # del.B ~ B_p^power_bp * B^power_b / R^power_r -power_b = 0 # del.B ~ B_p^power_bp * B^power_b / R^power_r -power_r = 0 # del.B ~ B_p^power_bp * B^power_b / R^power_r - -grid_type = "ldp" # Radial grid packing -psilow = 1e-4 # Min psi (normalized) -psihigh = 0.993 # Max psi (normalized) -mpsi = 128 # Radial grid intervals -mtheta = 256 # Poloidal grid intervals - -newq0 = 0 # Override q(0) -etol = 1e-7 # Reconstruction tolerance -use_classic_splines = false # Use classical spline (vs. tri-diagonal) - -input_only = false # Quit after input read - -[EQUIL_OUTPUT] -gse_flag = false # Output G-S equation accuracy diagnostics -out_eq_1d = false # ASCII output of 1D eq data -bin_eq_1d = false # Binary output of 1D eq data -out_eq_2d = false # ASCII output of 2D eq data -bin_eq_2d = true # Binary output of 2D eq data (used by GPEC) -out_2d = false # ASCII output of processed 2D data -bin_2d = false # Binary output of processed 2D data -dump_flag = false # Binary dump of equilibrium data diff --git a/notebooks/equil_gfile_example.ipynb b/notebooks/equil_gfile_example.ipynb index 5471e644..6d852756 100644 --- a/notebooks/equil_gfile_example.ipynb +++ b/notebooks/equil_gfile_example.ipynb @@ -21,31 +21,7 @@ "id": "1", "metadata": {}, "outputs": [], - "source": [ - "# 1. Define the input parameters for the equilibrium solver.\n", - "# - eq_filename: The name of the g-file we just created.\n", - "# - eq_type: \"efit\" for a standard g-file.\n", - "# - jac_type: \"boozer\" or \"hamada\" for the output coordinates.\n", - "# - mpsi, mtheta: Resolution of the output grid.\n", - "\n", - " equil_control = JPEC.Equilibrium.EquilibriumControl(;\n", - " eq_filename=\"beta_1.00\", # eq_filename\n", - " eq_type=\"efit\", # eq_type\n", - " jac_type=\"boozer\", # jac_type\n", - " grid_type=\"ldp\",\n", - " psilow=0.01, # psilow\n", - " psihigh=0.994) # psihigh\n", - "\n", - "equil_config = JPEC.Equilibrium.EquilibriumConfig(equil_control,JPEC.Equilibrium.EquilibriumOutput())\n", - "# 2. Run the main equilibrium setup function.\n", - "# This will read the file, solve the direct problem, and return the final object.\n", - "println(\"Starting equilibrium reconstruction...\")\n", - "\n", - "\n", - "plasma_eq = JPEC.Equilibrium.setup_equilibrium(equil_config)\n", - "#plasma_eq = JPEC.Equilibrium.setup_equilibrium(\"./DIIID_example/equil.toml\")\n", - "println(\"Equilibrium reconstruction complete.\")" - ] + "source": "# 1. Define the input parameters for the equilibrium solver.\n# - eq_filename: The name of the g-file we just created.\n# - eq_type: \"efit\" for a standard g-file.\n# - jac_type: \"boozer\" or \"hamada\" for the output coordinates.\n# - mpsi, mtheta: Resolution of the output grid.\n\nequil_config = JPEC.Equilibrium.EquilibriumConfig(;\n eq_filename=\"beta_1.00\", # eq_filename\n eq_type=\"efit\", # eq_type\n jac_type=\"boozer\", # jac_type\n grid_type=\"ldp\",\n psilow=0.01, # psilow\n psihigh=0.994) # psihigh\n\n# 2. Run the main equilibrium setup function.\n# This will read the file, solve the direct problem, and return the final object.\nprintln(\"Starting equilibrium reconstruction...\")\n\n\nplasma_eq = JPEC.Equilibrium.setup_equilibrium(equil_config)\n#plasma_eq = JPEC.Equilibrium.setup_equilibrium(\"./DIIID_example/equil.toml\")\nprintln(\"Equilibrium reconstruction complete.\")" }, { "cell_type": "code", @@ -53,54 +29,7 @@ "id": "2", "metadata": {}, "outputs": [], - "source": [ - "# --- Final Code to Generate 2D Color Plots of R(ψ,θ) and Z(ψ,θ) ---\n", - "println(\"\\n--- Generating data for 2D color plots ---\")\n", - "# 1. Define the grid in flux coordinates (ψ_norm, θ_new)\n", - "# This part is identical to the previous steps.\n", - "equil_control = plasma_eq.config.control\n", - "psi_norm_grid = collect(range(equil_control.psilow, equil_control.psihigh, length=equil_control.mpsi + 1))\n", - "theta_new_grid = collect(range(0.0, 1.0, length=equil_control.mtheta + 1))\n", - "# 2. Evaluate the `rzphi` spline to get the R and Z values.\n", - "println(\"Evaluating the 'rzphi' mapping spline...\")\n", - "fs_grid = JPEC.Spl.bicube_eval(plasma_eq.rzphi, psi_norm_grid, theta_new_grid)\n", - "println(\"Evaluation complete.\")\n", - "# 3. Transform the spline output to physical (R, Z) coordinates.\n", - "# This calculates R_grid[i,j] = R(ψ[i], θ[j]) and Z_grid[i,j] = Z(ψ[i], θ[j])\n", - "rfac_sq = fs_grid[:, :, 1]\n", - "eta_term = fs_grid[:, :, 2]\n", - "theta_new_mesh = ones(length(psi_norm_grid)) * theta_new_grid'\n", - "eta_grid = 2.0 * pi .* (theta_new_mesh .+ eta_term)\n", - "rfac_grid = sqrt.(max.(0.0, rfac_sq))\n", - "R_grid = plasma_eq.ro .+ rfac_grid .* cos.(eta_grid)\n", - "Z_grid = plasma_eq.zo .+ rfac_grid .* sin.(eta_grid)\n", - "println(\"Calculated R and Z grids.\")\n", - "# 4. Create the 2D color plot for R(ψ,θ)\n", - "println(\"--- Plotting heatmap for R(ψ,θ) ---\")\n", - "p_r_heatmap = heatmap(\n", - " psi_norm_grid,\n", - " theta_new_grid,\n", - " R_grid', # Note the transpose ' to match axis dimensions\n", - " title=\"Major Radius R(ψ, θ)\",\n", - " xlabel=\"Normalized Psi (ψₙ)\",\n", - " ylabel=\"Poloidal Angle (θ_new)\",\n", - " colorbar_title=\"R [m]\"\n", - ")\n", - "display(p_r_heatmap)\n", - "# 5. Create the 2D color plot for Z(ψ,θ)\n", - "println(\"--- Plotting heatmap for Z(ψ,θ) ---\")\n", - "p_z_heatmap = heatmap(\n", - " psi_norm_grid,\n", - " theta_new_grid,\n", - " Z_grid', # Note the transpose ' to match axis dimensions\n", - " title=\"Vertical Position Z(ψ, θ)\",\n", - " xlabel=\"Normalized Psi (ψₙ)\",\n", - " ylabel=\"Poloidal Angle (θ_new)\",\n", - " colorbar_title=\"Z [m]\"\n", - ")\n", - "display(p_z_heatmap)\n", - "println(\"\\n2D color plots for R and Z have been saved.\")" - ] + "source": "# --- Final Code to Generate 2D Color Plots of R(ψ,θ) and Z(ψ,θ) ---\nprintln(\"\\n--- Generating data for 2D color plots ---\")\n# 1. Define the grid in flux coordinates (ψ_norm, θ_new)\n# This part is identical to the previous steps.\nequil_config = plasma_eq.config\npsi_norm_grid = collect(range(equil_config.psilow, equil_config.psihigh, length=equil_config.mpsi + 1))\ntheta_new_grid = collect(range(0.0, 1.0, length=equil_config.mtheta + 1))\n# 2. Evaluate the `rzphi` spline to get the R and Z values.\nprintln(\"Evaluating the 'rzphi' mapping spline...\")\nfs_grid = JPEC.Spl.bicube_eval(plasma_eq.rzphi, psi_norm_grid, theta_new_grid)\nprintln(\"Evaluation complete.\")\n# 3. Transform the spline output to physical (R, Z) coordinates.\n# This calculates R_grid[i,j] = R(ψ[i], θ[j]) and Z_grid[i,j] = Z(ψ[i], θ[j])\nrfac_sq = fs_grid[:, :, 1]\neta_term = fs_grid[:, :, 2]\ntheta_new_mesh = ones(length(psi_norm_grid)) * theta_new_grid'\neta_grid = 2.0 * pi .* (theta_new_mesh .+ eta_term)\nrfac_grid = sqrt.(max.(0.0, rfac_sq))\nR_grid = plasma_eq.ro .+ rfac_grid .* cos.(eta_grid)\nZ_grid = plasma_eq.zo .+ rfac_grid .* sin.(eta_grid)\nprintln(\"Calculated R and Z grids.\")\n# 4. Create the 2D color plot for R(ψ,θ)\nprintln(\"--- Plotting heatmap for R(ψ,θ) ---\")\np_r_heatmap = heatmap(\n psi_norm_grid,\n theta_new_grid,\n R_grid', # Note the transpose ' to match axis dimensions\n title=\"Major Radius R(ψ, θ)\",\n xlabel=\"Normalized Psi (ψₙ)\",\n ylabel=\"Poloidal Angle (θ_new)\",\n colorbar_title=\"R [m]\"\n)\ndisplay(p_r_heatmap)\n# 5. Create the 2D color plot for Z(ψ,θ)\nprintln(\"--- Plotting heatmap for Z(ψ,θ) ---\")\np_z_heatmap = heatmap(\n psi_norm_grid,\n theta_new_grid,\n Z_grid', # Note the transpose ' to match axis dimensions\n title=\"Vertical Position Z(ψ, θ)\",\n xlabel=\"Normalized Psi (ψₙ)\",\n ylabel=\"Poloidal Angle (θ_new)\",\n colorbar_title=\"Z [m]\"\n)\ndisplay(p_z_heatmap)\nprintln(\"\\n2D color plots for R and Z have been saved.\")" }, { "cell_type": "code", @@ -258,4 +187,4 @@ }, "nbformat": 4, "nbformat_minor": 5 -} +} \ No newline at end of file diff --git a/notebooks/equil_inverse_test.ipynb b/notebooks/equil_inverse_test.ipynb index 939c53b1..c84eb247 100644 --- a/notebooks/equil_inverse_test.ipynb +++ b/notebooks/equil_inverse_test.ipynb @@ -46,54 +46,7 @@ "id": "2", "metadata": {}, "outputs": [], - "source": [ - "# --- Final Code to Generate 2D Color Plots of R(ψ,θ) and Z(ψ,θ) ---\n", - "println(\"\\n--- Generating data for 2D color plots ---\")\n", - "# 1. Define the grid in flux coordinates (ψ_norm, θ_new)\n", - "# This part is identical to the previous steps.\n", - "equil_control = plasma_eq.config.control\n", - "psi_norm_grid = collect(range(equil_control.psilow, equil_control.psihigh, length=equil_control.mpsi + 1))\n", - "theta_new_grid = collect(range(0.0, 1.0, length=equil_control.mtheta + 1))\n", - "# 2. Evaluate the `rzphi` spline to get the R and Z values.\n", - "println(\"Evaluating the 'rzphi' mapping spline...\")\n", - "fs_grid = JPEC.Spl.bicube_eval(plasma_eq.rzphi, psi_norm_grid, theta_new_grid)\n", - "println(\"Evaluation complete.\")\n", - "# 3. Transform the spline output to physical (R, Z) coordinates.\n", - "# This calculates R_grid[i,j] = R(ψ[i], θ[j]) and Z_grid[i,j] = Z(ψ[i], θ[j])\n", - "rfac_sq = fs_grid[:, :, 1]\n", - "eta_term = fs_grid[:, :, 2]\n", - "theta_new_mesh = ones(length(psi_norm_grid)) * theta_new_grid'\n", - "eta_grid = 2.0 * pi .* (theta_new_mesh .+ eta_term)\n", - "rfac_grid = sqrt.(max.(0.0, rfac_sq))\n", - "R_grid = plasma_eq.ro .+ rfac_grid .* cos.(eta_grid)\n", - "Z_grid = plasma_eq.zo .+ rfac_grid .* sin.(eta_grid)\n", - "println(\"Calculated R and Z grids.\")\n", - "# 4. Create the 2D color plot for R(ψ,θ)\n", - "println(\"--- Plotting heatmap for R(ψ,θ) ---\")\n", - "p_r_heatmap = heatmap(\n", - " psi_norm_grid,\n", - " theta_new_grid,\n", - " R_grid', # Note the transpose ' to match axis dimensions\n", - " title=\"Major Radius R(ψ, θ)\",\n", - " xlabel=\"Normalized Psi (ψₙ)\",\n", - " ylabel=\"Poloidal Angle (θ_new)\",\n", - " colorbar_title=\"R [m]\"\n", - ")\n", - "display(p_r_heatmap)\n", - "# 5. Create the 2D color plot for Z(ψ,θ)\n", - "println(\"--- Plotting heatmap for Z(ψ,θ) ---\")\n", - "p_z_heatmap = heatmap(\n", - " psi_norm_grid,\n", - " theta_new_grid,\n", - " Z_grid', # Note the transpose ' to match axis dimensions\n", - " title=\"Vertical Position Z(ψ, θ)\",\n", - " xlabel=\"Normalized Psi (ψₙ)\",\n", - " ylabel=\"Poloidal Angle (θ_new)\",\n", - " colorbar_title=\"Z [m]\"\n", - ")\n", - "display(p_z_heatmap)\n", - "println(\"\\n2D color plots for R and Z have been saved.\")\n" - ] + "source": "# --- Final Code to Generate 2D Color Plots of R(ψ,θ) and Z(ψ,θ) ---\nprintln(\"\\n--- Generating data for 2D color plots ---\")\n# 1. Define the grid in flux coordinates (ψ_norm, θ_new)\n# This part is identical to the previous steps.\nequil_config = plasma_eq.config\npsi_norm_grid = collect(range(equil_config.psilow, equil_config.psihigh, length=equil_config.mpsi + 1))\ntheta_new_grid = collect(range(0.0, 1.0, length=equil_config.mtheta + 1))\n# 2. Evaluate the `rzphi` spline to get the R and Z values.\nprintln(\"Evaluating the 'rzphi' mapping spline...\")\nfs_grid = JPEC.Spl.bicube_eval(plasma_eq.rzphi, psi_norm_grid, theta_new_grid)\nprintln(\"Evaluation complete.\")\n# 3. Transform the spline output to physical (R, Z) coordinates.\n# This calculates R_grid[i,j] = R(ψ[i], θ[j]) and Z_grid[i,j] = Z(ψ[i], θ[j])\nrfac_sq = fs_grid[:, :, 1]\neta_term = fs_grid[:, :, 2]\ntheta_new_mesh = ones(length(psi_norm_grid)) * theta_new_grid'\neta_grid = 2.0 * pi .* (theta_new_mesh .+ eta_term)\nrfac_grid = sqrt.(max.(0.0, rfac_sq))\nR_grid = plasma_eq.ro .+ rfac_grid .* cos.(eta_grid)\nZ_grid = plasma_eq.zo .+ rfac_grid .* sin.(eta_grid)\nprintln(\"Calculated R and Z grids.\")\n# 4. Create the 2D color plot for R(ψ,θ)\nprintln(\"--- Plotting heatmap for R(ψ,θ) ---\")\np_r_heatmap = heatmap(\n psi_norm_grid,\n theta_new_grid,\n R_grid', # Note the transpose ' to match axis dimensions\n title=\"Major Radius R(ψ, θ)\",\n xlabel=\"Normalized Psi (ψₙ)\",\n ylabel=\"Poloidal Angle (θ_new)\",\n colorbar_title=\"R [m]\"\n)\ndisplay(p_r_heatmap)\n# 5. Create the 2D color plot for Z(ψ,θ)\nprintln(\"--- Plotting heatmap for Z(ψ,θ) ---\")\np_z_heatmap = heatmap(\n psi_norm_grid,\n theta_new_grid,\n Z_grid', # Note the transpose ' to match axis dimensions\n title=\"Vertical Position Z(ψ, θ)\",\n xlabel=\"Normalized Psi (ψₙ)\",\n ylabel=\"Poloidal Angle (θ_new)\",\n colorbar_title=\"Z [m]\"\n)\ndisplay(p_z_heatmap)\nprintln(\"\\n2D color plots for R and Z have been saved.\")" }, { "cell_type": "code", @@ -251,4 +204,4 @@ }, "nbformat": 4, "nbformat_minor": 5 -} +} \ No newline at end of file diff --git a/notebooks/equil_lar_example.ipynb b/notebooks/equil_lar_example.ipynb index d5d62da1..57864052 100644 --- a/notebooks/equil_lar_example.ipynb +++ b/notebooks/equil_lar_example.ipynb @@ -20,10 +20,7 @@ "id": "1", "metadata": {}, "outputs": [], - "source": [ - "equil_input = JPEC.Equilibrium.EquilibriumConfig(; control=JPEC.Equilibrium.EquilibriumControl(eq_type=\"lar\", eq_filename=\"lar.toml\"), output=JPEC.Equilibrium.EquilibriumOutput())\n", - "lar_input = JPEC.Equilibrium.LargeAspectRatioConfig(; lar_r0=10, lar_a=1, beta0=0.001, q0=1.5, p_pres=2, p_sig=1)" - ] + "source": "equil_input = JPEC.Equilibrium.EquilibriumConfig(; eq_type=\"lar\", eq_filename=\"lar.toml\")\nlar_input = JPEC.Equilibrium.LargeAspectRatioConfig(; lar_r0=10, lar_a=1, beta0=0.001, q0=1.5, p_pres=2, p_sig=1)" }, { "cell_type": "code", @@ -41,54 +38,7 @@ "id": "3", "metadata": {}, "outputs": [], - "source": [ - "# --- Final Code to Generate 2D Color Plots of R(ψ,θ) and Z(ψ,θ) ---\n", - "println(\"\\n--- Generating data for 2D color plots ---\")\n", - "# 1. Define the grid in flux coordinates (ψ_norm, θ_new)\n", - "# This part is identical to the previous steps.\n", - "equil_control = plasma_eq.config.control\n", - "psi_norm_grid = collect(range(equil_control.psilow, equil_control.psihigh, length=equil_control.mpsi + 1))\n", - "theta_new_grid = collect(range(0.0, 1.0, length=equil_control.mtheta + 1))\n", - "# 2. Evaluate the `rzphi` spline to get the R and Z values.\n", - "println(\"Evaluating the 'rzphi' mapping spline...\")\n", - "fs_grid = JPEC.Spl.bicube_eval(plasma_eq.rzphi, psi_norm_grid, theta_new_grid)\n", - "println(\"Evaluation complete.\")\n", - "# 3. Transform the spline output to physical (R, Z) coordinates.\n", - "# This calculates R_grid[i,j] = R(ψ[i], θ[j]) and Z_grid[i,j] = Z(ψ[i], θ[j])\n", - "rfac_sq = fs_grid[:, :, 1]\n", - "eta_term = fs_grid[:, :, 2]\n", - "theta_new_mesh = ones(length(psi_norm_grid)) * theta_new_grid'\n", - "eta_grid = 2.0 * pi .* (theta_new_mesh .+ eta_term)\n", - "rfac_grid = sqrt.(max.(0.0, rfac_sq))\n", - "R_grid = plasma_eq.ro .+ rfac_grid .* cos.(eta_grid)\n", - "Z_grid = plasma_eq.zo .+ rfac_grid .* sin.(eta_grid)\n", - "println(\"Calculated R and Z grids.\")\n", - "# 4. Create the 2D color plot for R(ψ,θ)\n", - "println(\"--- Plotting heatmap for R(ψ,θ) ---\")\n", - "p_r_heatmap = heatmap(\n", - " psi_norm_grid,\n", - " theta_new_grid,\n", - " R_grid', # Note the transpose ' to match axis dimensions\n", - " title=\"Major Radius R(ψ, θ)\",\n", - " xlabel=\"Normalized Psi (ψₙ)\",\n", - " ylabel=\"Poloidal Angle (θ_new)\",\n", - " colorbar_title=\"R [m]\"\n", - ")\n", - "display(p_r_heatmap)\n", - "# 5. Create the 2D color plot for Z(ψ,θ)\n", - "println(\"--- Plotting heatmap for Z(ψ,θ) ---\")\n", - "p_z_heatmap = heatmap(\n", - " psi_norm_grid,\n", - " theta_new_grid,\n", - " Z_grid', # Note the transpose ' to match axis dimensions\n", - " title=\"Vertical Position Z(ψ, θ)\",\n", - " xlabel=\"Normalized Psi (ψₙ)\",\n", - " ylabel=\"Poloidal Angle (θ_new)\",\n", - " colorbar_title=\"Z [m]\"\n", - ")\n", - "display(p_z_heatmap)\n", - "println(\"\\n2D color plots for R and Z have been saved.\")" - ] + "source": "# --- Final Code to Generate 2D Color Plots of R(ψ,θ) and Z(ψ,θ) ---\nprintln(\"\\n--- Generating data for 2D color plots ---\")\n# 1. Define the grid in flux coordinates (ψ_norm, θ_new)\n# This part is identical to the previous steps.\nequil_config = plasma_eq.config\npsi_norm_grid = collect(range(equil_config.psilow, equil_config.psihigh, length=equil_config.mpsi + 1))\ntheta_new_grid = collect(range(0.0, 1.0, length=equil_config.mtheta + 1))\n# 2. Evaluate the `rzphi` spline to get the R and Z values.\nprintln(\"Evaluating the 'rzphi' mapping spline...\")\nfs_grid = JPEC.Spl.bicube_eval(plasma_eq.rzphi, psi_norm_grid, theta_new_grid)\nprintln(\"Evaluation complete.\")\n# 3. Transform the spline output to physical (R, Z) coordinates.\n# This calculates R_grid[i,j] = R(ψ[i], θ[j]) and Z_grid[i,j] = Z(ψ[i], θ[j])\nrfac_sq = fs_grid[:, :, 1]\neta_term = fs_grid[:, :, 2]\ntheta_new_mesh = ones(length(psi_norm_grid)) * theta_new_grid'\neta_grid = 2.0 * pi .* (theta_new_mesh .+ eta_term)\nrfac_grid = sqrt.(max.(0.0, rfac_sq))\nR_grid = plasma_eq.ro .+ rfac_grid .* cos.(eta_grid)\nZ_grid = plasma_eq.zo .+ rfac_grid .* sin.(eta_grid)\nprintln(\"Calculated R and Z grids.\")\n# 4. Create the 2D color plot for R(ψ,θ)\nprintln(\"--- Plotting heatmap for R(ψ,θ) ---\")\np_r_heatmap = heatmap(\n psi_norm_grid,\n theta_new_grid,\n R_grid', # Note the transpose ' to match axis dimensions\n title=\"Major Radius R(ψ, θ)\",\n xlabel=\"Normalized Psi (ψₙ)\",\n ylabel=\"Poloidal Angle (θ_new)\",\n colorbar_title=\"R [m]\"\n)\ndisplay(p_r_heatmap)\n# 5. Create the 2D color plot for Z(ψ,θ)\nprintln(\"--- Plotting heatmap for Z(ψ,θ) ---\")\np_z_heatmap = heatmap(\n psi_norm_grid,\n theta_new_grid,\n Z_grid', # Note the transpose ' to match axis dimensions\n title=\"Vertical Position Z(ψ, θ)\",\n xlabel=\"Normalized Psi (ψₙ)\",\n ylabel=\"Poloidal Angle (θ_new)\",\n colorbar_title=\"Z [m]\"\n)\ndisplay(p_z_heatmap)\nprintln(\"\\n2D color plots for R and Z have been saved.\")" }, { "cell_type": "code", @@ -246,4 +196,4 @@ }, "nbformat": 4, "nbformat_minor": 5 -} +} \ No newline at end of file diff --git a/notebooks/equil_soloviev_example.ipynb b/notebooks/equil_soloviev_example.ipynb index da8588ec..0c4e7812 100644 --- a/notebooks/equil_soloviev_example.ipynb +++ b/notebooks/equil_soloviev_example.ipynb @@ -94,54 +94,7 @@ "id": "5", "metadata": {}, "outputs": [], - "source": [ - "# --- Final Code to Generate 2D Color Plots of R(ψ,θ) and Z(ψ,θ) ---\n", - "println(\"\\n--- Generating data for 2D color plots ---\")\n", - "# 1. Define the grid in flux coordinates (ψ_norm, θ_new)\n", - "# This part is identical to the previous steps.\n", - "equil_control = plasma_eq.config.control\n", - "psi_norm_grid = collect(range(equil_control.psilow, equil_control.psihigh, length=equil_control.mpsi + 1))\n", - "theta_new_grid = collect(range(0.0, 1.0, length=equil_control.mtheta + 1))\n", - "# 2. Evaluate the `rzphi` spline to get the R and Z values.\n", - "println(\"Evaluating the 'rzphi' mapping spline...\")\n", - "fs_grid = JPEC.Spl.bicube_eval(plasma_eq.rzphi, psi_norm_grid, theta_new_grid)\n", - "println(\"Evaluation complete.\")\n", - "# 3. Transform the spline output to physical (R, Z) coordinates.\n", - "# This calculates R_grid[i,j] = R(ψ[i], θ[j]) and Z_grid[i,j] = Z(ψ[i], θ[j])\n", - "rfac_sq = fs_grid[:, :, 1]\n", - "eta_term = fs_grid[:, :, 2]\n", - "theta_new_mesh = ones(length(psi_norm_grid)) * theta_new_grid'\n", - "eta_grid = 2.0 * pi .* (theta_new_mesh .+ eta_term)\n", - "rfac_grid = sqrt.(max.(0.0, rfac_sq))\n", - "R_grid = plasma_eq.ro .+ rfac_grid .* cos.(eta_grid)\n", - "Z_grid = plasma_eq.zo .+ rfac_grid .* sin.(eta_grid)\n", - "println(\"Calculated R and Z grids.\")\n", - "# 4. Create the 2D color plot for R(ψ,θ)\n", - "println(\"--- Plotting heatmap for R(ψ,θ) ---\")\n", - "p_r_heatmap = heatmap(\n", - " psi_norm_grid,\n", - " theta_new_grid,\n", - " R_grid', # Note the transpose ' to match axis dimensions\n", - " title=\"Major Radius R(ψ, θ)\",\n", - " xlabel=\"Normalized Psi (ψₙ)\",\n", - " ylabel=\"Poloidal Angle (θ_new)\",\n", - " colorbar_title=\"R [m]\"\n", - ")\n", - "display(p_r_heatmap)\n", - "# 5. Create the 2D color plot for Z(ψ,θ)\n", - "println(\"--- Plotting heatmap for Z(ψ,θ) ---\")\n", - "p_z_heatmap = heatmap(\n", - " psi_norm_grid,\n", - " theta_new_grid,\n", - " Z_grid', # Note the transpose ' to match axis dimensions\n", - " title=\"Vertical Position Z(ψ, θ)\",\n", - " xlabel=\"Normalized Psi (ψₙ)\",\n", - " ylabel=\"Poloidal Angle (θ_new)\",\n", - " colorbar_title=\"Z [m]\"\n", - ")\n", - "display(p_z_heatmap)\n", - "println(\"\\n2D color plots for R and Z have been saved.\")" - ] + "source": "# --- Final Code to Generate 2D Color Plots of R(ψ,θ) and Z(ψ,θ) ---\nprintln(\"\\n--- Generating data for 2D color plots ---\")\n# 1. Define the grid in flux coordinates (ψ_norm, θ_new)\n# This part is identical to the previous steps.\nequil_config = plasma_eq.config\npsi_norm_grid = collect(range(equil_config.psilow, equil_config.psihigh, length=equil_config.mpsi + 1))\ntheta_new_grid = collect(range(0.0, 1.0, length=equil_config.mtheta + 1))\n# 2. Evaluate the `rzphi` spline to get the R and Z values.\nprintln(\"Evaluating the 'rzphi' mapping spline...\")\nfs_grid = JPEC.Spl.bicube_eval(plasma_eq.rzphi, psi_norm_grid, theta_new_grid)\nprintln(\"Evaluation complete.\")\n# 3. Transform the spline output to physical (R, Z) coordinates.\n# This calculates R_grid[i,j] = R(ψ[i], θ[j]) and Z_grid[i,j] = Z(ψ[i], θ[j])\nrfac_sq = fs_grid[:, :, 1]\neta_term = fs_grid[:, :, 2]\ntheta_new_mesh = ones(length(psi_norm_grid)) * theta_new_grid'\neta_grid = 2.0 * pi .* (theta_new_mesh .+ eta_term)\nrfac_grid = sqrt.(max.(0.0, rfac_sq))\nR_grid = plasma_eq.ro .+ rfac_grid .* cos.(eta_grid)\nZ_grid = plasma_eq.zo .+ rfac_grid .* sin.(eta_grid)\nprintln(\"Calculated R and Z grids.\")\n# 4. Create the 2D color plot for R(ψ,θ)\nprintln(\"--- Plotting heatmap for R(ψ,θ) ---\")\np_r_heatmap = heatmap(\n psi_norm_grid,\n theta_new_grid,\n R_grid', # Note the transpose ' to match axis dimensions\n title=\"Major Radius R(ψ, θ)\",\n xlabel=\"Normalized Psi (ψₙ)\",\n ylabel=\"Poloidal Angle (θ_new)\",\n colorbar_title=\"R [m]\"\n)\ndisplay(p_r_heatmap)\n# 5. Create the 2D color plot for Z(ψ,θ)\nprintln(\"--- Plotting heatmap for Z(ψ,θ) ---\")\np_z_heatmap = heatmap(\n psi_norm_grid,\n theta_new_grid,\n Z_grid', # Note the transpose ' to match axis dimensions\n title=\"Vertical Position Z(ψ, θ)\",\n xlabel=\"Normalized Psi (ψₙ)\",\n ylabel=\"Poloidal Angle (θ_new)\",\n colorbar_title=\"Z [m]\"\n)\ndisplay(p_z_heatmap)\nprintln(\"\\n2D color plots for R and Z have been saved.\")" }, { "cell_type": "code", @@ -299,4 +252,4 @@ }, "nbformat": 4, "nbformat_minor": 5 -} +} \ No newline at end of file diff --git a/notebooks/vac.in b/notebooks/vac.in deleted file mode 100644 index 68e91e03..00000000 --- a/notebooks/vac.in +++ /dev/null @@ -1,99 +0,0 @@ -! **** Running DCON's input *** -&MODES - mth = 480 - xiin(1:9) = 0 0 0 0 0 0 0 1 0 - lsymz = .TRUE. - leqarcw = 1 - lzio = 0 - lgato = 0 - lrgato = 0 -/ -&DEBUGS - checkd = .FALSE. - check1 = .FALSE. - check2 = .FALSE. - checke = .FALSE. - checks = .FALSE. - wall = .FALSE. - lkplt = 0 - verbose_timer_output = f -/ -&VACDAT - ishape = 6 - aw = 0.05 - bw = 1.5 - cw = 0 - dw = 0.5 - tw = 0.05 - nsing = 500 - epsq = 1e-05 - noutv = 37 - idgt = 6 - idot = 0 - idsk = 0 - delg = 15.01 - delfac = 0.001 - cn0 = 1 -/ -&SHAPE - ipshp = 0 - xpl = 100 - apl = 1 - a = 20 - b = 170 - bpl = 1 - dpl = 0 - r = 1 - abulg = 0.932 - bbulg = 17.0 - tbulg = 0.02 - qain = 2.5 -/ -&DIAGNS - lkdis = .FALSE. - ieig = 0 - iloop = 0 - lpsub = 1 - nloop = 128 - nloopr = 0 - nphil = 3 - nphse = 1 - xofsl = 0 - ntloop = 32 - aloop = 0.01 - bloop = 1.6 - dloop = 0.5 - rloop = 1.0 - deloop = 0.001 - mx = 21 - mz = 21 - nph = 0 - nxlpin = 6 - nzlpin = 11 - epslp = 0.02 - xlpmin = 0.7 - xlpmax = 2.7 - zlpmin = -1.5 - zlpmax = 1.5 - linterior = 2 -/ -&SPRK - nminus = 0 - nplus = 0 - mphi = 16 - lwrt11 = 0 - civ = 0.0 - sp2sgn1 = 1 - sp2sgn2 = 1 - sp2sgn3 = 1 - sp2sgn4 = 1 - sp2sgn5 = 1 - sp3sgn1 = -1 - sp3sgn2 = -1 - sp3sgn3 = -1 - sp3sgn4 = 1 - sp3sgn5 = 1 - lff = 0 - ff = 1.6 - fv = 1.6 1.6 1.6 1.6 1.6 1.0 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 -/ diff --git a/notebooks/vacuum_fortran_example.ipynb b/notebooks/vacuum_fortran_example.ipynb index 8b3a8280..663cd832 100644 --- a/notebooks/vacuum_fortran_example.ipynb +++ b/notebooks/vacuum_fortran_example.ipynb @@ -32,9 +32,9 @@ "zin = rand(Float64, lmax - lmin + 1)\n", "deltain = rand(Float64, lmax - lmin + 1)\n", "\n", - "println(\"Testing set_dcon_params…\")\n", - "JPEC.Vacuum.set_dcon_params(mthin, lmin, lmax, nnin, qa1in, xin, zin, deltain)\n", - "println(\"set_dcon_params OK!\")" + "println(\"Testing set_surface_params…\")\n", + "JPEC.Vacuum.set_surface_params(mthin, lmin, lmax, nnin, qa1in, xin, zin, deltain)\n", + "println(\"set_surface_params OK!\")" ] }, { diff --git a/src/DCON/Main.jl b/src/DCON/Main.jl deleted file mode 100644 index 4b1513ad..00000000 --- a/src/DCON/Main.jl +++ /dev/null @@ -1,309 +0,0 @@ -function Main(path::String="./") - - println("DCON START") - println("----------------------------------") - start_time = time() - - # Read input data and set up data structures - intr = DconInternal(; dir_path=path) - # TODO: leaving DCON_CONTROL as a part of the toml file, eventually can combine equil, gpec, etc. into one input file? - inputs = TOML.parsefile(joinpath(intr.dir_path, "dcon.toml")) - ctrl = DconControl(; (Symbol(k) => v for (k, v) in inputs["DCON_CONTROL"])...) - equil = Equilibrium.setup_equilibrium(joinpath(intr.dir_path, "equil.toml")) - if "WALL" in keys(inputs) - intr.wall_settings = Vacuum.WallShapeSettings(; (Symbol(k) => v for (k, v) in inputs["WALL"])...) - else - intr.wall_settings = Vacuum.WallShapeSettings() - end - if "DEBUG" in keys(inputs) - intr.debug_settings = DebugSettings(; (Symbol(k) => v for (k, v) in inputs["DEBUG"])...) - else - intr.debug_settings = DebugSettings() - end - # Set up variables - # TODO: dcon_kin_threads logic? - ctrl.delta_mhigh *= 2 # for consistency with Fortran DCON TODO: why is this present in the Fortran? - - # Determine psilim and qlim (where we will integrate to) - sing_lim!(intr, ctrl, equil) - if ctrl.set_psilim_via_dmlim && ctrl.psiedge < intr.psilim - @warn "Only one of set_psilim_via_dmlim and psiedge < psilim can be used at a time. - Setting psiedge = 1.0 and determining dW from psilim = $(intr.psilim) determined from dmlim = $(ctrl.dmlim)." - ctrl.psiedge = 1.0 - end - - # If truncating before psihigh, reform equilibrium if desired - if intr.psilim != equil.config.control.psihigh && ctrl.reform_eq_with_psilim - @warn "Reforming equilibrium splines from psihigh to psilim not implemented yet. Proceeding with psihigh = $(equil.config.control.psihigh)." - # JMH - Nik please put the logic we discussed here - # something like ? - # equil.config.control.psihigh = intr.psilim - # equil = set_up_equilibrium(equil.config) - end - - # Compute Mercier and Ballooning stability (if desired) - # This holds di, dr, h (calculated in mercier_scan), ca1, and ca2 (calculated in ballooning scan) - profiles_xs = equil.profiles.xs - locstab_fs = zeros(Float64, length(profiles_xs), 5) - if ctrl.mer_flag - if ctrl.verbose - println("Evaluating Mercier criterion") - end - mercier_scan!(locstab_fs, equil) - end - if ctrl.bal_flag - compute_ballooning_stability!(ctrl, locstab_fs, equil) - end - - # Fit data to splines - intr.locstab = cubic_interp(profiles_xs, locstab_fs; bc=CubicFit(), extrap=:extension) - - # Determine toroidal mode numbers - if ctrl.nn_low == 0 && ctrl.nn_high == 0 - error("Either nn_low or nn_high must be set in DCON_CONTROL (both are 0)") - elseif ctrl.nn_low == 0 - ctrl.nn_low = ctrl.nn_high - elseif ctrl.nn_high == 0 - ctrl.nn_high = ctrl.nn_low - end - if ctrl.nn_low > ctrl.nn_high - error("nn_low cannot be greater than nn_high") - end - intr.nlow = ctrl.nn_low - intr.nhigh = ctrl.nn_high - intr.npert = intr.nhigh - intr.nlow + 1 - nstring = intr.npert == 1 ? "$(intr.nlow)" : "$(intr.nlow):$(intr.nhigh)" - - # Find all singular surfaces in the equilibrium - sing_find!(intr, equil) - - # Determine poloidal mode numbers - if ctrl.delta_mlow < 0 || ctrl.delta_mhigh < 0 - error("Negative delta_mlow or delta_mhigh not allowed") - end - if ctrl.cyl_flag - intr.mlow = ctrl.delta_mlow - intr.mhigh = ctrl.delta_mhigh - elseif ctrl.sing_start == 0 - intr.mlow = min(intr.nlow * equil.params.qmin, 0) - 4 - ctrl.delta_mlow - intr.mhigh = trunc(Int, intr.nhigh * equil.params.qmax) + ctrl.delta_mhigh - else - intr.mmin = Inf # HUGE in Fortran - for ising in Int(ctrl.sing_start):intr.msing - intr.mmin = min(intr.mmin, sing[ising].m) - end - intr.mlow = intr.mmin - ctrl.delta_mlow - intr.mhigh = trunc(Int, intr.nhigh * equil.params.qmax) + ctrl.delta_mhigh - end - intr.mpert = intr.mhigh - intr.mlow + 1 - if ctrl.delta_mband >= intr.mpert - @warn "Banded matrices not implemented yet, setting delta_mband to 0" - ctrl.delta_mband = 0 - end - intr.mband = intr.mpert - 1 - ctrl.delta_mband - intr.mband = min(max(intr.mband, 0), intr.mpert - 1) - intr.numpert_total = intr.mpert * intr.npert - - # Fit equilibrium quantities to Fourier-spline functions. - if ctrl.mat_flag || ctrl.ode_flag - if ctrl.verbose - println("Run parameters:") - println( - " q0 = $(@sprintf("%.3f", equil.params.q0)), qmin = $(@sprintf("%.3f", equil.params.qmin)), qmax = $(@sprintf("%.3f", equil.params.qmax)), q95 = $(@sprintf("%.3f", equil.params.q95))" - ) - println(" qlim = $(@sprintf("%.5f", intr.qlim)), psilim = $(@sprintf("%.9f", intr.psilim))") - println(" betat = $(@sprintf("%.3f", equil.params.betat)), betan = $(@sprintf("%.3f", equil.params.betan)), betap1 = $(@sprintf("%.3f", equil.params.betap1))") - println( - " mlow = $(@sprintf("%4i", intr.mlow)), mhigh = $(@sprintf("%4i", intr.mhigh)), mpert = $(@sprintf("%4i", intr.mpert)), mband = $(@sprintf("%4i", intr.mband))" - ) - println(" nlow = $(@sprintf("%4i", intr.nlow)), nhigh = $(@sprintf("%4i", intr.nhigh)), npert = $(@sprintf("%4i", intr.npert))") - end - - # Compute metric tensor - metric = make_metric(equil; mband=intr.mband, fft_flag=ctrl.fft_flag) - - if ctrl.verbose - println(" Computing F, G, and K Matrices") - end - - # Compute matrices and populate FourFitVars struct - ffit = make_matrix(equil, intr, metric) - - if ctrl.kin_flag - error("kin_flag not implemented yet") - end - - # NOTE: Asymptotic calculations for ideal DCON are now computed on-demand during - # singular surface crossings in cross_ideal_singular_surf!. This makes it clear that - # asymptotics are only needed for ideal DCON and are not inherent properties of - # the singular surface. - - if ctrl.kin_flag - # ksing_find() - end - end - - # Integrate Euler-Lagrange Equation - if ctrl.ode_flag - if ctrl.verbose - println("Integrating Euler-Lagrange equation") - end - odet = eulerlagrange_integration(ctrl, equil, ffit, intr) - if odet.nzero > 0 && ctrl.verbose - println("Fixed-boundary mode unstable for n = $nstring.") - end - end - - # Compute free boundary energies - if ctrl.vac_flag && !(ctrl.ksing > 0 && ctrl.ksing <= intr.msing + 1) - if ctrl.verbose - println("Computing free boundary energies") - end - vac_data = free_run!(odet, ctrl, equil, ffit, intr) - if real(vac_data.et[1]) < 0 - if ctrl.verbose - println("Free-boundary mode unstable for n = $nstring.") - end - else - if ctrl.verbose - println("All free-boundary modes stable for n = $nstring.") - end - end - end - - if ctrl.write_outputs_to_HDF5 - if ctrl.verbose - println("Writing saved data to $(ctrl.HDF5_filename)") - end - write_outputs_to_HDF5(ctrl, equil, intr, odet, ctrl.vac_flag ? vac_data : nothing) - end - - end_time = time() - start_time - println("----------------------------------") - println("Run time: $(@sprintf("%.3e", end_time)) seconds") - println("Normal termination.") - - # TODO: Do not allow perturbed equilibrium calculations if zero crossings are found - - return (ctrl=ctrl, equil=equil, intr=intr, ffit=ffit, odet=odet, vac_data=ctrl.vac_flag ? vac_data : nothing) -end - -""" - write_outputs_to_HDF5(ctrl::DconControl, equil::Equilibrium.PlasmaEquilibrium, intr::DconInternal, odet::OdeState) - -Helper function to write the HDF5 output file with relevant run and equilibrium parameters. -This combines the functionality of several pieces of the Fortran code in `ode_output.f`, -primarily `ode_output_open` and the various `bin_euler` writes that occur throughout the -integration. Some parameters are only dumped in their respective flags are true, e.g. -vacuum data if `vac_flag` is true. - -### TODOs - -Combine spline unpacking if possible, too many extra lines -""" -function write_outputs_to_HDF5(ctrl::DconControl, equil::Equilibrium.PlasmaEquilibrium, intr::DconInternal, odet::OdeState, vac::Union{VacuumData,Nothing}) - - h5open(joinpath(intr.dir_path, ctrl.HDF5_filename), "w") do out_h5 - - # Store input parameters - for (key, val) in zip(fieldnames(DconControl), getfield.(Ref(ctrl), fieldnames(DconControl))) - out_h5["input/DCON_CONTROL/$key"] = val - end - for (key, val) in zip(fieldnames(Equilibrium.EquilibriumControl), getfield.(Ref(equil.config.control), fieldnames(Equilibrium.EquilibriumControl))) - out_h5["input/EQUIL_CONTROL/$key"] = val - end - # TODO: assuming EQUIL_OUTPUT is going to be deprecated - # TODO: should we store the equilibrium? difficult since it could be a gfile, sol.in, etc. - # TODO: if we do one input file, can just pass that in instead and loop easily since its parsed - # as a dict already (for (k, v) in inputs["DCON_CONTROL"]...). We have to do this since custom structs - # don't inherently have an iterator by default - - # Write derived run parameters - out_h5["info/mpert"] = intr.mpert - out_h5["info/mband"] = intr.mband - out_h5["info/mlow"] = intr.mlow - out_h5["info/mhigh"] = intr.mhigh - out_h5["info/npert"] = intr.npert - out_h5["info/nlow"] = intr.nlow - out_h5["info/nhigh"] = intr.nhigh - m = [(i - 1) % intr.mpert + intr.mlow for i in 1:(intr.numpert_total)] - n = [(i - 1) ÷ intr.mpert + intr.nlow for i in 1:(intr.numpert_total)] - out_h5["info/mn_index"] = hcat(m, n) # (N, 2) matrix - out_h5["info/psilim"] = intr.psilim - out_h5["info/qlim"] = intr.qlim - out_h5["info/q1lim"] = intr.q1lim - - # Write derived equilibrium parameters - for (key, val) in zip(fieldnames(Equilibrium.EquilibriumParameters), getfield.(Ref(equil.params), fieldnames(Equilibrium.EquilibriumParameters))) - if val !== nothing # TODO: looks like ro, zo, psio, and b_norm are not set, so skipping those for now but should fix eventually - out_h5["equil/$key"] = val - end - end - out_h5["equil/psio"] = equil.psio - out_h5["equil/ro"] = equil.ro - out_h5["equil/zo"] = equil.zo - - # Write spline arrays (using profiles with named splines) - profiles = equil.profiles - out_h5["splines/sq/xs"] = profiles.xs - out_h5["splines/sq/fs/2piF"] = profiles.F_spline.y - out_h5["splines/sq/fs/mu0p"] = profiles.P_spline.y - out_h5["splines/sq/fs/dVdpsi"] = profiles.dVdpsi_spline.y - out_h5["splines/sq/fs/q"] = profiles.q_spline.y - out_h5["splines/sq/fs1/2piF"] = profiles.F_deriv.(profiles.xs) - out_h5["splines/sq/fs1/mu0p"] = profiles.P_deriv.(profiles.xs) - out_h5["splines/sq/fs1/dVdpsi"] = profiles.dVdpsi_deriv.(profiles.xs) - out_h5["splines/sq/fs1/q"] = profiles.q_deriv.(profiles.xs) - out_h5["splines/sq/xpower"] = 0 - out_h5["splines/rzphi/xs"] = equil.rzphi_xs - out_h5["splines/rzphi/ys"] = equil.rzphi_ys - # Extract grid point values from interpolants for HDF5 output - out_h5["splines/rzphi/fs/rcoords"] = equil.rzphi_rsquared.nodal_derivs.partials[1, :, :] - out_h5["splines/rzphi/fs/offset"] = equil.rzphi_offset.nodal_derivs.partials[1, :, :] - out_h5["splines/rzphi/fs/nu"] = equil.rzphi_nu.nodal_derivs.partials[1, :, :] - out_h5["splines/rzphi/fs/jac"] = equil.rzphi_jac.nodal_derivs.partials[1, :, :] - - # Write local stability data - if ctrl.mer_flag - locstab_xs = intr.locstab.cache.x - out_h5["locstab/di"] = intr.locstab.y[:, 1] ./ locstab_xs - out_h5["locstab/dr"] = intr.locstab.y[:, 2] ./ locstab_xs - out_h5["singular/di0"] = [intr.locstab(sing.psifac)[1] / sing.psifac for sing in intr.sing] - end - if ctrl.bal_flag - out_h5["locstab/ca1"] = intr.locstab.y[:, 4] - end - - # Write integration data - # TODO: technically this should only be written if ode_flag is true, but that's going to get deprecated eventually - out_h5["integration/nstep"] = odet.step - out_h5["integration/psi"] = odet.psi_store - out_h5["integration/q"] = odet.q_store - out_h5["integration/xi_psi"] = odet.u_store[:, :, 1, :] - out_h5["integration/u2"] = odet.u_store[:, :, 2, :] # TODO: what to name this? These are the "conjugate momenta" of u1 - out_h5["integration/dxi_psi"] = odet.ud_store[:, :, 1, :] - out_h5["integration/xi_s"] = odet.ud_store[:, :, 2, :] - out_h5["integration/crit"] = odet.crit_store - - # Write singular surface data - out_h5["singular/msing"] = intr.msing - out_h5["singular/psi"] = [sing.psifac for sing in intr.sing] - out_h5["singular/q"] = [sing.q for sing in intr.sing] - out_h5["singular/q1"] = [sing.q1 for sing in intr.sing] - out_h5["singular/ca_left"] = odet.ca_l - out_h5["singular/ca_right"] = odet.ca_r - - # Write vacuum Data - if ctrl.vac_flag - out_h5["vacuum/wt"] = vac.wt - out_h5["vacuum/wt0"] = vac.wt0 - out_h5["vacuum/ep"] = vac.ep - out_h5["vacuum/ev"] = vac.ev - out_h5["vacuum/et"] = vac.et - out_h5["vacuum/x_plasma"] = vac.xzpts[:, 1] - out_h5["vacuum/z_plasma"] = vac.xzpts[:, 2] - out_h5["vacuum/x_wall"] = vac.xzpts[:, 3] - out_h5["vacuum/z_wall"] = vac.xzpts[:, 4] - end - end -end diff --git a/src/Equilibrium/DirectEquilibrium.jl b/src/Equilibrium/DirectEquilibrium.jl index 440083c0..8393c89f 100644 --- a/src/Equilibrium/DirectEquilibrium.jl +++ b/src/Equilibrium/DirectEquilibrium.jl @@ -286,9 +286,9 @@ function direct_fieldline_int(psifac::Float64, raw_profile::DirectRunInput, ro:: u0[2] = sqrt((r - ro)^2 + (z - zo)^2) bfield = DirectBField() - equil_input = raw_profile.config.control + equil_config = raw_profile.config params = FieldLineDerivParams(ro, zo, raw_profile.psi_in, raw_profile.sq_in, sq_in_deriv, raw_profile.psio, - equil_input.power_bp, equil_input.power_b, equil_input.power_r, bfield) + equil_config.power_bp, equil_config.power_b, equil_config.power_r, bfield) # Use a callback to refine the solution at each step to stay on the flux surface function refine_affect!(integrator) @@ -421,7 +421,7 @@ robustness. function equilibrium_solver(raw_profile::DirectRunInput) # Shorthand - equil_params = raw_profile.config.control + equil_params = raw_profile.config psio = raw_profile.psio mtheta = equil_params.mtheta mpsi = equil_params.mpsi diff --git a/src/Equilibrium/Equilibrium.jl b/src/Equilibrium/Equilibrium.jl index 4376be25..40b5789b 100644 --- a/src/Equilibrium/Equilibrium.jl +++ b/src/Equilibrium/Equilibrium.jl @@ -17,7 +17,7 @@ include("InverseEquilibrium.jl") include("AnalyticEquilibrium.jl") # --- Expose types and functions to the user --- -export setup_equilibrium, EquilibriumConfig, EquilibriumControl, EquilibriumOutput, PlasmaEquilibrium, EquilibriumParameters, ProfileSplines +export setup_equilibrium, EquilibriumConfig, PlasmaEquilibrium, EquilibriumParameters, ProfileSplines # --- Constants --- const mu0 = 4π * 1e-7 @@ -42,9 +42,9 @@ function setup_equilibrium(path::String="equil.toml") end function setup_equilibrium(eq_config::EquilibriumConfig, additional_input=nothing) - @printf "Equilibrium file: %s\n" eq_config.control.eq_filename + @printf "Equilibrium file: %s\n" eq_config.eq_filename - eq_type = eq_config.control.eq_type + eq_type = eq_config.eq_type # Parse file and prepare initial data structures and splines if eq_type == "efit" eq_input = read_efit(eq_config) @@ -55,14 +55,14 @@ function setup_equilibrium(eq_config::EquilibriumConfig, additional_input=nothin elseif eq_type == "lar" if additional_input === nothing - additional_input = LargeAspectRatioConfig(eq_config.control.eq_filename) + additional_input = LargeAspectRatioConfig(eq_config.eq_filename) end eq_input = lar_run(eq_config, additional_input) elseif eq_type == "sol" if additional_input === nothing - additional_input = SolovevConfig(eq_config.control.eq_filename) + additional_input = SolovevConfig(eq_config.eq_filename) end eq_input = sol_run(eq_config, additional_input) @@ -506,7 +506,7 @@ function equilibrium_gse!(equil::PlasmaEquilibrium) end # Write contour data - h5open(joinpath(dirname(equil.config.control.eq_filename), "gsec.h5"), "w") do file + h5open(joinpath(dirname(equil.config.eq_filename), "gsec.h5"), "w") do file file["mpsi"] = mpsi file["mtheta"] = mtheta file["r"] = Float32.(r) @@ -520,7 +520,7 @@ function equilibrium_gse!(equil::PlasmaEquilibrium) end # Write xy plot data - h5open(joinpath(dirname(equil.config.control.eq_filename), "gse.h5"), "w") do file + h5open(joinpath(dirname(equil.config.eq_filename), "gse.h5"), "w") do file gse_data = Array{Float32,3}(undef, mpsi + 1, mtheta + 1, 7) for ipsi in 0:mpsi for itheta in 0:mtheta @@ -537,7 +537,7 @@ function equilibrium_gse!(equil::PlasmaEquilibrium) end # Write integrated error criterion - h5open(joinpath(dirname(equil.config.control.eq_filename), "gsei.h5"), "w") do file + h5open(joinpath(dirname(equil.config.eq_filename), "gsei.h5"), "w") do file file["xs"] = Float32.(flux.xs) file["term"] = Float32.(term) file["totali"] = Float32.(totali) diff --git a/src/Equilibrium/EquilibriumTypes.jl b/src/Equilibrium/EquilibriumTypes.jl index 66cd86c8..cf5dcdeb 100644 --- a/src/Equilibrium/EquilibriumTypes.jl +++ b/src/Equilibrium/EquilibriumTypes.jl @@ -3,9 +3,10 @@ function symbolize_keys(dict::Dict{String,Any}) end """ - EquilibriumControl + EquilibriumConfig(...) -A mutable struct containing control parameters for equilibrium reconstruction. +A mutable struct containing configuration parameters for equilibrium reconstruction. +Bundles all necessary settings originally specified in the equil fortran namelists. ## Fields @@ -24,11 +25,10 @@ A mutable struct containing control parameters for equilibrium reconstruction. - `mtheta::Int` - Number of poloidal grid points - `newq0::Int` - Override for on-axis safety factor (0 = use input value) - `etol::Float64` - Error tolerance for equilibrium solver - - `use_classic_splines::Bool` - Use classic spline interpolation method - - `input_only::Bool` - Only process input without full reconstruction + - `force_termination::Bool` - Terminate after equilibrium setup (skip stability calculations) - `use_galgrid::Bool` - Use the same grid as galerkin method """ -@kwdef mutable struct EquilibriumControl +@kwdef mutable struct EquilibriumConfig eq_type::String = "efit" eq_filename::String = "mypath" r0exp::Float64 = 1.0 @@ -47,17 +47,16 @@ A mutable struct containing control parameters for equilibrium reconstruction. newq0::Int = 0 etol::Float64 = 1e-7 - use_classic_splines::Bool = false - input_only::Bool = false + force_termination::Bool = false use_galgrid::Bool = true """ Modified internal constructor that enforces self consistency within the inputs """ - function EquilibriumControl(eq_type, eq_filename, r0exp, b0exp, jac_type, power_bp, power_b, power_r, - grid_type, psilow, psihigh, mpsi, mtheta, newq0, etol, use_classic_splines, - input_only, use_galgrid) + function EquilibriumConfig(eq_type, eq_filename, r0exp, b0exp, jac_type, power_bp, power_b, power_r, + grid_type, psilow, psihigh, mpsi, mtheta, newq0, etol, + force_termination, use_galgrid) if jac_type == "hamada" @info "Forcing hamada coordinate jacobian exponents: power_*" power_b = 0 @@ -89,87 +88,71 @@ A mutable struct containing control parameters for equilibrium reconstruction. error("Cannot recognize jac_type = $(jac_type)") end return new(eq_type, eq_filename, r0exp, b0exp, jac_type, power_bp, power_b, power_r, - grid_type, psilow, psihigh, mpsi, mtheta, newq0, etol, use_classic_splines, - input_only, use_galgrid) + grid_type, psilow, psihigh, mpsi, mtheta, newq0, etol, + force_termination, use_galgrid) end end """ - EquilibriumOutput - -A mutable struct containing flags for equilibrium output options. +Outer constructor for EquilibriumConfig from a parsed TOML dictionary +""" +function EquilibriumConfig(equil_dict::Dict{String,Any}, base_path::String="./") + # Check for required fields + required_keys = ("eq_filename", "eq_type") + missingkeys = filter(k -> !haskey(equil_dict, k), required_keys) -## Fields + if !isempty(missingkeys) + error("Missing required key(s) in [Equilibrium]: $(join(missingkeys, ", "))") + end - - `gse_flag::Bool` - Output GSE (Grad-Shafranov Equation) data - - `out_eq_1d::Bool` - Output 1D equilibrium profiles in text format - - `bin_eq_1d::Bool` - Output 1D equilibrium profiles in binary format - - `out_eq_2d::Bool` - Output 2D equilibrium data in text format - - `bin_eq_2d::Bool` - Output 2D equilibrium data in binary format - - `out_2d::Bool` - Output 2D flux surface data in text format - - `bin_2d::Bool` - Output 2D flux surface data in binary format - - `dump_flag::Bool` - Output diagnostic dump files -""" -@kwdef mutable struct EquilibriumOutput - gse_flag::Bool = false - out_eq_1d::Bool = false - bin_eq_1d::Bool = false - out_eq_2d::Bool = false - bin_eq_2d::Bool = true - out_2d::Bool = false - bin_2d::Bool = false - dump_flag::Bool = false -end + # Filter to only known parameters + config_fields = Set(String.(fieldnames(EquilibriumConfig))) + config_data = Dict{String,Any}() -""" - EquilibriumConfig(...) + for (k, v) in equil_dict + if k in config_fields + config_data[k] = v + else + @warn "Unknown equilibrium parameter: $k" + end + end -A container struct that bundles all necessary configuration settings originally specified in the equil -fortran namelists. -""" -@kwdef mutable struct EquilibriumConfig - control::EquilibriumControl = EquilibriumControl() - output::EquilibriumOutput = EquilibriumOutput() -end + # Construct validated struct + config = EquilibriumConfig(; symbolize_keys(config_data)...) + if !isabspath(config.eq_filename) + config.eq_filename = normpath(joinpath(base_path, config.eq_filename)) + end -""" -Constructor that allows users to form a EquilibriumConfig struct from dictionaries -for convenience when most of the defaults are fine. -""" -function EquilibriumConfig(control::Dict, output::Dict) - construct = EquilibriumControl(; control...) - outstruct = EquilibriumOutput(; output...) - return EquilibriumConfig(; control=construct, output=outstruct) + return config end """ Outer constructor for EquilibriumConfig that enables a toml file interface for specifying the configuration settings + +DEPRECATED: Use [Equilibrium] section in jpec.toml instead """ -# if this also have default, then conflicts with @kwdef mutable struct EquilibriumConfig. function EquilibriumConfig(path::String) raw = TOML.parsefile(path) # Extract EQUIL_CONTROL with default fallback - control_data = get(raw, "EQUIL_CONTROL", Dict()) - output_data = get(raw, "EQUIL_OUTPUT", Dict()) + config_data = get(raw, "EQUIL_CONTROL", Dict()) - # Check for required fields in control_data + # Check for required fields required_keys = ("eq_filename", "eq_type") - missingkeys = filter(k -> !haskey(control_data, k), required_keys) + missingkeys = filter(k -> !haskey(config_data, k), required_keys) if !isempty(missingkeys) - error("Missing required key(s) in [EQUIL_CONTROL]: $(join(missing, ", "))") + error("Missing required key(s) in [EQUIL_CONTROL]: $(join(missingkeys, ", "))") end - # Construct validated structs - control = EquilibriumControl(; symbolize_keys(control_data)...) - if !isabspath(control.eq_filename) - control.eq_filename = normpath(joinpath(dirname(path), control.eq_filename)) + # Construct validated struct + config = EquilibriumConfig(; symbolize_keys(config_data)...) + if !isabspath(config.eq_filename) + config.eq_filename = normpath(joinpath(dirname(path), config.eq_filename)) end - output = EquilibriumOutput(; symbolize_keys(output_data)...) - return EquilibriumConfig(; control=control, output=output) + return config end """ diff --git a/src/Equilibrium/InverseEquilibrium.jl b/src/Equilibrium/InverseEquilibrium.jl index 30b374a0..e589fd8d 100644 --- a/src/Equilibrium/InverseEquilibrium.jl +++ b/src/Equilibrium/InverseEquilibrium.jl @@ -55,12 +55,12 @@ function equilibrium_solver(input::InverseRunInput) zo = input.zo psio = input.psio - grid_type = config.control.grid_type - mpsi = config.control.mpsi - mtheta = config.control.mtheta - psilow = config.control.psilow - psihigh = config.control.psihigh - newq0 = config.control.newq0 + grid_type = config.grid_type + mpsi = config.mpsi + mtheta = config.mtheta + psilow = config.psilow + psihigh = config.psihigh + newq0 = config.newq0 me = 3 @@ -195,7 +195,7 @@ function equilibrium_solver(input::InverseRunInput) spl_fs[itheta+1, 2] = f_deta spl_fs[itheta+1, 3] = r * jacfac spl_fs[itheta+1, 4] = spl_fs[itheta+1, 3] / (r * r) - spl_fs[itheta+1, 5] = spl_fs[itheta+1, 3] * bp^config.control.power_bp * b^config.control.power_b / r^config.control.power_r + spl_fs[itheta+1, 5] = spl_fs[itheta+1, 3] * bp^config.power_bp * b^config.power_b / r^config.power_r end # c----------------------------------------------------------------------- diff --git a/src/Equilibrium/ReadEquilibrium.jl b/src/Equilibrium/ReadEquilibrium.jl index 275430c4..1ce8a974 100644 --- a/src/Equilibrium/ReadEquilibrium.jl +++ b/src/Equilibrium/ReadEquilibrium.jl @@ -55,8 +55,8 @@ them into a `DirectRunInput` object. - A `DirectRunInput` object ready for the direct solver. """ function read_efit(config::EquilibriumConfig) - println("--> Processing EFIT g-file: $(config.control.eq_filename)") - lines = readlines(config.control.eq_filename) + println("--> Processing EFIT g-file: $(config.eq_filename)") + lines = readlines(config.eq_filename) # --- Parse Header --- header1_parts = split(lines[1]) @@ -130,12 +130,12 @@ Parses a binary CHEASE file, creates initial 1D and 2D splines with proper normalization (R0, B0 scaling), and bundles them into a `InverseRunInput` object. """ function read_chease_binary(config::EquilibriumConfig) - println("--> Reading CHEASE file (Binary): $(config.control.eq_filename)") + println("--> Reading CHEASE file (Binary): $(config.eq_filename)") - R0EXP = config.control.r0exp - B0EXP = config.control.b0exp + R0EXP = config.r0exp + B0EXP = config.b0exp - open(config.control.eq_filename, "r") do io + open(config.eq_filename, "r") do io seekstart(io) read(io, UInt32) ntnova = read(io, Int32) @@ -235,10 +235,10 @@ them into a `InverseRunInput` object. - A `InverseRunInput` object ready for the inverse solver. """ function read_chease_ascii(config::EquilibriumConfig) - println("--> Reading CHEASE file: $(config.control.eq_filename)") - lines = readlines(config.control.eq_filename) - R0EXP = config.control.r0exp - B0EXP = config.control.b0exp + println("--> Reading CHEASE file: $(config.eq_filename)") + lines = readlines(config.eq_filename) + R0EXP = config.r0exp + B0EXP = config.b0exp # --- Parse Header (FORMAT 10: 3I5) --- header_parts = split(lines[1]) @@ -379,4 +379,4 @@ function read_chease_ascii(config::EquilibriumConfig) println("--> Finished reading CHEASE equilibrium.") println(" Magnetic axis at (ro=$ro, zo=$zo), psio=$psio") return InverseRunInput(config, sq_in, rz_in_xs, rz_in_ys, rz_in_R, rz_in_Z, ro, zo, psio) -end +end \ No newline at end of file diff --git a/src/DCON/Bal.jl b/src/ForceFreeStates/Bal.jl similarity index 98% rename from src/DCON/Bal.jl rename to src/ForceFreeStates/Bal.jl index f30f04fe..1f858319 100644 --- a/src/DCON/Bal.jl +++ b/src/ForceFreeStates/Bal.jl @@ -114,7 +114,7 @@ function integrate_ballooning_ode(ipsi::Int, growth_parameter::Float64, ode_coeff_interp::CubicSeriesInterpolant, asymptotic_interp::CubicSeriesInterpolant, eigenfunctions::Matrix{Float64}, reference_angle::Float64, - control::DconControl) + control::ForceFreeStatesControl) TOLERANCE = 1e-5 MINIMUM_STEP = 1e-10 @@ -448,7 +448,7 @@ equation if the surface is Mercier unstable. ## Arguments - - `ctrl::DconControl`: Control parameters for the analysis. + - `ctrl::ForceFreeStatesControl`: Control parameters for the analysis. - `locstab_fs::Matrix{Float64}`: Local stability matrix to store results (modified in place). - `plasma_eq::Equilibrium.PlasmaEquilibrium`: Plasma equilibrium data. @@ -457,7 +457,7 @@ This function modifies `locstab_fs` in place with: - Column 1: Mercier criterion × ψ - Columns 4-5: Asymptotic coefficients ca₁ and ca₂ """ -function compute_ballooning_stability!(ctrl::DconControl, locstab_fs::Matrix{Float64}, plasma_eq::Equilibrium.PlasmaEquilibrium) +function compute_ballooning_stability!(ctrl::ForceFreeStatesControl, locstab_fs::Matrix{Float64}, plasma_eq::Equilibrium.PlasmaEquilibrium) if ctrl.verbose println("Evaluating high-n ballooning criterion...") diff --git a/src/DCON/EulerLagrange.jl b/src/ForceFreeStates/EulerLagrange.jl similarity index 90% rename from src/DCON/EulerLagrange.jl rename to src/ForceFreeStates/EulerLagrange.jl index 42feddc9..db60b19e 100644 --- a/src/DCON/EulerLagrange.jl +++ b/src/ForceFreeStates/EulerLagrange.jl @@ -1,5 +1,5 @@ """ - eulerlagrange_integration(ctrl::DconControl, equil::Equilibrium.PlasmaEquilibrium, ffit::FourFitVars, intr::DconInternal) + eulerlagrange_integration(ctrl::ForceFreeStatesControl, equil::Equilibrium.PlasmaEquilibrium, ffit::FourFitVars, intr::ForceFreeStatesInternal) Main driver for integrating the Euler-Lagrange equations across the plasma and detecting singular surfaces. Formerly `ode_run`. Has the same functionality as `ode_run` in the Fortran code, with the addition of @@ -20,7 +20,7 @@ restype functionality if we decide to do this An OdeState struct containing the final state of the ODE solver after integration is complete. """ -function eulerlagrange_integration(ctrl::DconControl, equil::Equilibrium.PlasmaEquilibrium, ffit::FourFitVars, intr::DconInternal) +function eulerlagrange_integration(ctrl::ForceFreeStatesControl, equil::Equilibrium.PlasmaEquilibrium, ffit::FourFitVars, intr::ForceFreeStatesInternal) # Initialization odet = OdeState(intr.numpert_total, ctrl.numsteps_init, ctrl.numunorms_init, intr.msing) @@ -90,7 +90,7 @@ function eulerlagrange_integration(ctrl::DconControl, equil::Equilibrium.PlasmaE end """ - initialize_el_at_axis!(odet::OdeState, ctrl::DconControl, profiles::Equilibrium.ProfileSplines, intr::DconInternal) + initialize_el_at_axis!(odet::OdeState, ctrl::ForceFreeStatesControl, profiles::Equilibrium.ProfileSplines, intr::ForceFreeStatesInternal) Initialize the OdeState struct for the case of sing_start = 0 (axis initialization). Formerly `ode_axis_init!`. This now only initializes `psifac`, `ising_start`, and `u`. @@ -100,7 +100,7 @@ Formerly `ode_axis_init!`. This now only initializes `psifac`, `ising_start`, an Support for `kin_flag` Move ising_start logic to chunk_el_integration_bounds? """ -function initialize_el_at_axis!(odet::OdeState, ctrl::DconControl, profiles::Equilibrium.ProfileSplines, intr::DconInternal) +function initialize_el_at_axis!(odet::OdeState, ctrl::ForceFreeStatesControl, profiles::Equilibrium.ProfileSplines, intr::ForceFreeStatesInternal) # Default psifac to minimum equilibrium psi value odet.psifac = profiles.xs[1] @@ -144,13 +144,13 @@ function initialize_el_at_axis!(odet::OdeState, ctrl::DconControl, profiles::Equ end end -# TODO: NOT IMPLEMENTED YET! (low priority, just make sure sing_start = 0 in dcon.toml) +# TODO: NOT IMPLEMENTED YET! (low priority, just make sure sing_start = 0 in toml) function initialize_el_at_singular_surf() return end """ - chunk_el_integration_bounds(odet::OdeState, ctrl::DconControl, intr::DconInternal) + chunk_el_integration_bounds(odet::OdeState, ctrl::ForceFreeStatesControl, intr::ForceFreeStatesInternal) Pre-compute all integration chunks from the current position to the edge. Returns a vector of `IntegrationChunk` objects, each representing a region to integrate @@ -162,8 +162,8 @@ making the integration flow more predictable and easier to parallelize (e.g., fo ### Arguments - `odet::OdeState` - ODE state struct (starting position and singular surface index) - - `ctrl::DconControl` - Control parameters - - `intr::DconInternal` - Internal data (singular surfaces, limits) + - `ctrl::ForceFreeStatesControl` - Control parameters + - `intr::ForceFreeStatesInternal` - Internal data (singular surfaces, limits) ### Returns @@ -173,7 +173,7 @@ making the integration flow more predictable and easier to parallelize (e.g., fo Support for `kin_flag` """ -function chunk_el_integration_bounds(odet::OdeState, ctrl::DconControl, intr::DconInternal) +function chunk_el_integration_bounds(odet::OdeState, ctrl::ForceFreeStatesControl, intr::ForceFreeStatesInternal) chunks = IntegrationChunk[] # Start from current position @@ -181,7 +181,7 @@ function chunk_el_integration_bounds(odet::OdeState, ctrl::DconControl, intr::Dc ising_current = odet.ising_start # Wrapper to find next singular surface to integrate toward that is resonant within integration limits - function find_next_resonant_surface!(ising::Int, intr::DconInternal) + function find_next_resonant_surface!(ising::Int, intr::ForceFreeStatesInternal) ising += 1 while ising <= intr.msing if intr.psilim < intr.sing[ising].psifac || @@ -239,7 +239,7 @@ function chunk_el_integration_bounds(odet::OdeState, ctrl::DconControl, intr::Dc end """ - cross_ideal_singular_surf!(odet::OdeState, ctrl::DconControl, equil::Equilibrium.PlasmaEquilibrium, ffit::FourFitVars, intr::DconInternal) + cross_ideal_singular_surf!(odet::OdeState, ctrl::ForceFreeStatesControl, equil::Equilibrium.PlasmaEquilibrium, ffit::FourFitVars, intr::ForceFreeStatesInternal) Handle the crossing of a rational surface during integration if `kin_flag` is false. Formerly `ode_ideal_cross!`. Performs the same function as `ode_ideal_cross` in the Fortran code. @@ -247,13 +247,13 @@ Differences mainly in integration data storage logic, but otherwise identical. I reinitializes the solution vector at the singularity, and updates relevant state variables. Asymptotics are now computed on-demand here instead of being pre-computed, making it clear -that asymptotic calculations are specific to ideal DCON and not inherent to the singular surface. +that asymptotic calculations are specific to ideal ForceFreeStates and not inherent to the singular surface. ### Arguments - `ising::Int` - Index of the singular surface being crossed """ -function cross_ideal_singular_surf!(odet::OdeState, ctrl::DconControl, equil::Equilibrium.PlasmaEquilibrium, ffit::FourFitVars, intr::DconInternal, ising::Int) +function cross_ideal_singular_surf!(odet::OdeState, ctrl::ForceFreeStatesControl, equil::Equilibrium.PlasmaEquilibrium, ffit::FourFitVars, intr::ForceFreeStatesInternal, ising::Int) # Fixup solution at singular surface compute_solution_norms!(odet.u, odet, ctrl, intr, true) @@ -319,7 +319,7 @@ function cross_kinetic_singular_surf() end """ - integrate_el_region!(odet::OdeState, ctrl::DconControl, equil::Equilibrium.PlasmaEquilibrium, ffit::FourFitVars, intr::DconInternal, chunk::IntegrationChunk) + integrate_el_region!(odet::OdeState, ctrl::ForceFreeStatesControl, equil::Equilibrium.PlasmaEquilibrium, ffit::FourFitVars, intr::ForceFreeStatesInternal, chunk::IntegrationChunk) Integrate the Euler-Lagrange equations from `psi_start` to `psi_end`. Formerly `ode_step!`. Performs the same function as `ode_step` in the Fortran code, with the addition of @@ -333,10 +333,10 @@ making it clear what region is being integrated. ### Arguments - `odet::OdeState` - ODE state struct (modified in-place) - - `ctrl::DconControl` - Control parameters + - `ctrl::ForceFreeStatesControl` - Control parameters - `equil::Equilibrium.PlasmaEquilibrium` - Plasma equilibrium - `ffit::FourFitVars` - Fourier fit variables - - `intr::DconInternal` - Internal data + - `intr::ForceFreeStatesInternal` - Internal data - `chunk::IntegrationChunk` - Integration chunk containing start and end ψ for integration ### TODOs @@ -344,7 +344,7 @@ making it clear what region is being integrated. Check sensitivity of results to tolerances, currently using same logic as Fortran Check absolute tolerances, currently only relative tolerances are updated """ -function integrate_el_region!(odet::OdeState, ctrl::DconControl, equil::Equilibrium.PlasmaEquilibrium, ffit::FourFitVars, intr::DconInternal, chunk::IntegrationChunk) +function integrate_el_region!(odet::OdeState, ctrl::ForceFreeStatesControl, equil::Equilibrium.PlasmaEquilibrium, ffit::FourFitVars, intr::ForceFreeStatesInternal, chunk::IntegrationChunk) # Callback to be run at every step, handles fixups, tolerances, and data storage cb = DiscreteCallback((u, t, integrator) -> true, integrator_callback!) @@ -418,7 +418,7 @@ function integrator_callback!(integrator) end """ - compute_tols(ctrl::DconControl, intr::DconInternal, odet::OdeState) + compute_tols(ctrl::ForceFreeStatesControl, intr::ForceFreeStatesInternal, odet::OdeState) Compute relative and absolute tolerances for the ODE solver based on proximity to singular surfaces and magnitude of the solution vectors. In Fortran, this was @@ -440,7 +440,7 @@ Add back absolute tolerance calculation if needed - rtol: Relative tolerance """ -function compute_tols(ctrl::DconControl, intr::DconInternal, odet::OdeState, ising::Int) +function compute_tols(ctrl::ForceFreeStatesControl, intr::ForceFreeStatesInternal, odet::OdeState, ising::Int) singfac_local = Inf # Relative tolerance if false # kin_flag (not implemented) @@ -468,7 +468,7 @@ function compute_tols(ctrl::DconControl, intr::DconInternal, odet::OdeState, isi end """ - compute_solution_norms!(u::Array{ComplexF64,3}, odet::OdeState, ctrl::DconControl, intr::DconInternal, sing_flag::Bool) + compute_solution_norms!(u::Array{ComplexF64,3}, odet::OdeState, ctrl::ForceFreeStatesControl, intr::ForceFreeStatesInternal, sing_flag::Bool) Computes norms of the solution vectors of the array `u` and normalizes them if this is not the first call after a fixup. Formerly `ode_unorm!`. @@ -492,7 +492,7 @@ operate on `u` directly without extra copies. Add resizing logic for unorm arrays when ifix exceeds allocated size """ -function compute_solution_norms!(u::Array{ComplexF64,3}, odet::OdeState, ctrl::DconControl, intr::DconInternal, sing_flag::Bool) +function compute_solution_norms!(u::Array{ComplexF64,3}, odet::OdeState, ctrl::ForceFreeStatesControl, intr::ForceFreeStatesInternal, sing_flag::Bool) # Compute norms of first solution vectors, abort if any are zero odet.unorm .= norm.(eachcol(u[:, :, 1])) @@ -514,7 +514,7 @@ function compute_solution_norms!(u::Array{ComplexF64,3}, odet::OdeState, ctrl::D odet.ifix += 1 else @warn "unorm storage reached, no longer saving fixfac data. Stability outputs and unorming will be correct, but cannot reconstruct `u`. \n - Increase `numunorms_init` in dcon.toml if needed. Automatic resizing will be added in a future version." + Increase `numunorms_init` if needed. Automatic resizing will be added in a future version." end apply_gaussian_reduction!(u, odet, intr, sing_flag) odet.new = true @@ -523,7 +523,7 @@ function compute_solution_norms!(u::Array{ComplexF64,3}, odet::OdeState, ctrl::D end """ - apply_gaussian_reduction!(u::Array{ComplexF64,3}, odet::OdeState, intr::DconInternal, sing_flag::Bool) + apply_gaussian_reduction!(u::Array{ComplexF64,3}, odet::OdeState, intr::ForceFreeStatesInternal, sing_flag::Bool) Applies Gaussian reduction to orthogonalize solution vectors in `u`. Formerly `ode_fixup!`. Performs the same function as `ode_fixup` in the Fortran code, @@ -533,7 +533,7 @@ This will update both `u` and relevant fields in `odet` in-place. See the description of `compute_solution_norms!` for more details on the benefits of in-place `u` updates. """ -function apply_gaussian_reduction!(u::Array{ComplexF64,3}, odet::OdeState, intr::DconInternal, sing_flag::Bool) +function apply_gaussian_reduction!(u::Array{ComplexF64,3}, odet::OdeState, intr::ForceFreeStatesInternal, sing_flag::Bool) # Store data for the current fixup ifix = odet.ifix @@ -569,7 +569,7 @@ function apply_gaussian_reduction!(u::Array{ComplexF64,3}, odet::OdeState, intr: end """ - findmax_dW_edge!(odet::OdeState, ctrl::DconControl, equil::Equilibrium.PlasmaEquilibrium, ffit::FourFitVars, intr::DconInternal) + findmax_dW_edge!(odet::OdeState, ctrl::ForceFreeStatesControl, equil::Equilibrium.PlasmaEquilibrium, ffit::FourFitVars, intr::ForceFreeStatesInternal) Records the total dW in the integration region between `ctrl.psiedge` and `ctrl.psilim`. This performs the same function as `ode_record_edge` in the @@ -585,7 +585,7 @@ We have also separated the computation of the wv matrix spline and the total dW calculation into `free_compute_wv_spline` and `free_compute_total` respectively for clarity. We create the wv matrix spline once prior to the loop. """ -function findmax_dW_edge!(odet::OdeState, ctrl::DconControl, equil::Equilibrium.PlasmaEquilibrium, ffit::FourFitVars, intr::DconInternal) +function findmax_dW_edge!(odet::OdeState, ctrl::ForceFreeStatesControl, equil::Equilibrium.PlasmaEquilibrium, ffit::FourFitVars, intr::ForceFreeStatesInternal) # Since we search for the maximum dW, initialize to -Infinity fill!(odet.dW_edge, -Inf * (1 + im)) @@ -607,7 +607,7 @@ function findmax_dW_edge!(odet::OdeState, ctrl::DconControl, equil::Equilibrium. end """ - transform_u!(odet::OdeState, intr::DconInternal) + transform_u!(odet::OdeState, intr::ForceFreeStatesInternal) Constructs the transformation matrices to form the true solution vectors. Effectively "undoes" the Gaussian reduction applied during fixups throughout the integration, such that @@ -616,7 +616,7 @@ Performs a similar function as `idcon_transform` + `idcon_build` in the Fortran except we separate the building of the transformation matrices and determining the coefficients for a chosen force-free solution, which can be done in postprocessing. """ -function transform_u!(odet::OdeState, intr::DconInternal) +function transform_u!(odet::OdeState, intr::ForceFreeStatesInternal) # Gaussian reduction matrices for each fixup gauss = Array{ComplexF64,3}(undef, intr.numpert_total, intr.numpert_total, odet.ifix) diff --git a/src/DCON/FixedBoundaryStability.jl b/src/ForceFreeStates/FixedBoundaryStability.jl similarity index 100% rename from src/DCON/FixedBoundaryStability.jl rename to src/ForceFreeStates/FixedBoundaryStability.jl diff --git a/src/DCON/DCON.jl b/src/ForceFreeStates/ForceFreeStates.jl similarity index 77% rename from src/DCON/DCON.jl rename to src/ForceFreeStates/ForceFreeStates.jl index d32b4e19..5d5adfb6 100644 --- a/src/DCON/DCON.jl +++ b/src/ForceFreeStates/ForceFreeStates.jl @@ -1,6 +1,6 @@ -module DCON +module ForceFreeStates -# All imports and includes for the DCON module +# All imports and includes for the ForceFreeStates module using LinearAlgebra using LinearAlgebra.LAPACK using TOML @@ -12,14 +12,13 @@ using FastInterpolations import ..Equilibrium import ..Spl -import ..Util +import ..Utilities import ..Vacuum using Printf import StaticArrays: @MVector, @MMatrix # Include all necessary files -include("DconStructs.jl") -include("Main.jl") +include("ForceFreeStatesStructs.jl") include("Mercier.jl") include("Bal.jl") include("EulerLagrange.jl") @@ -29,7 +28,7 @@ include("FixedBoundaryStability.jl") include("Utils.jl") include("Free.jl") -# These are used for various small tolerances and root finders throughout DCON +# These are used for various small tolerances and root finders throughout ForceFreeStates global eps = 1e-10 global itmax = 50 diff --git a/src/DCON/DconStructs.jl b/src/ForceFreeStates/ForceFreeStatesStructs.jl similarity index 96% rename from src/DCON/DconStructs.jl rename to src/ForceFreeStates/ForceFreeStatesStructs.jl index 483961a6..a86b0059 100644 --- a/src/DCON/DconStructs.jl +++ b/src/ForceFreeStates/ForceFreeStatesStructs.jl @@ -11,6 +11,8 @@ A mutable struct holding data related to the singular surfaces in the equilibriu - `n::Vector{Int}` - Toroidal mode number(s) - `q::Float64` - Safety factor (= m/n) - `q1::Float64` - Derivative of safety factor with respect to ψ + - `grri::Array{Float64,2}` - Interior Green's function at this surface [2*mthvac, 2*mpert] + - `grre::Array{Float64,2}` - Exterior Green's function at this surface [2*mthvac, 2*mpert] """ @kwdef mutable struct SingType psifac::Float64 = 0.0 @@ -19,12 +21,14 @@ A mutable struct holding data related to the singular surfaces in the equilibriu n::Vector{Int} = Int[] q::Float64 = 0.0 q1::Float64 = 0.0 + grri::Array{Float64,2} = Array{Float64}(undef, 0, 0) + grre::Array{Float64,2} = Array{Float64}(undef, 0, 0) end """ SingAsymptotics -A struct containing asymptotic expansion data for ideal DCON calculations at a singular surface. +A struct containing asymptotic expansion data for ideal ForceFreeStates calculations at a singular surface. This data is computed on-demand during singular surface crossings in `cross_ideal_singular_surf!`. ## Fields @@ -85,7 +89,7 @@ A mutable struct containing settings for debugging and benchmarking output. end """ - DconInternal + ForceFreeStatesInternal A mutable struct holding internal state variables for stability calculations. @@ -115,7 +119,7 @@ A mutable struct holding internal state variables for stability calculations. - `locstab::CubicSeriesInterpolant` - Spline for local stability analysis - `wall_settings::Vacuum.WallShapeSettings` - Wall shape settings for vacuum calculations """ -@kwdef mutable struct DconInternal +@kwdef mutable struct ForceFreeStatesInternal dir_path::String = "" mlow::Int = 0 mhigh::Int = 0 @@ -143,9 +147,9 @@ A mutable struct holding internal state variables for stability calculations. end """ - DconControl + ForceFreeStatesControl -A mutable struct containing control parameters for stability analysis, set by the user in dcon.toml. +A mutable struct containing control parameters for stability analysis, set by the user in jpec.toml. ## Fields @@ -193,7 +197,6 @@ A mutable struct containing control parameters for stability analysis, set by th - `qlow::Float64` - Integration terminated at q limit determined by minimum of qlow and q0 from equil - `reform_eq_with_psilim::Bool` - Reform equilibrium with computed psilim (not yet implemented) - `psiedge::Float64` - If less then psilim, calculates dW(psi) between psiedge and psilim, then runs with truncation at max(dW) - - `dcon_kin_threads::Int` - Number of threads for kinetic calculations (not yet implemented) - `parallel_threads::Int` - Number of parallel threads (not yet implemented) - `diagnose::Bool` - Enable diagnostic output (not yet implemented) - `diagnose_ca::Bool` - Enable asymptotic coefficient diagnostics (not yet implemented) @@ -201,8 +204,9 @@ A mutable struct containing control parameters for stability analysis, set by th - `HDF5_filename::String` - Name of HDF5 output file - `force_wv_symmetry::Bool` - Boolean flag to enforce symmetry in the vacuum response matrix - `save_interval::Int` - Save every Nth ODE step (1=all, 10=every 10th). Always saves near rational surfaces. (Same as `euler_step` in the Fortran) + - `force_termination::Bool` - Terminate after force-free states (skip perturbed equilibrium calculations) """ -@kwdef mutable struct DconControl +@kwdef mutable struct ForceFreeStatesControl verbose::Bool = true bal_flag::Bool = false mat_flag::Bool = false @@ -247,14 +251,14 @@ A mutable struct containing control parameters for stability analysis, set by th qlow::Float64 = 0.0 reform_eq_with_psilim::Bool = false psiedge::Float64 = 1.0 - dcon_kin_threads::Int = 1 parallel_threads::Int = 1 diagnose::Bool = false diagnose_ca::Bool = false write_outputs_to_HDF5::Bool = true - HDF5_filename::String = "euler.h5" + HDF5_filename::String = "jpec.h5" force_wv_symmetry::Bool = true save_interval::Int = 10 + force_termination::Bool = false end @kwdef mutable struct FourFitVars @@ -320,6 +324,7 @@ Populated in `Free.jl`. - `ev::Vector{ComplexF64}` - Vacuum eigenvalues - `et::Vector{ComplexF64}` - Total eigenvalues of plasma + vacuum - `grri::Array{Float64, 2}` - Green's function radial integrals (2×mthvac × 2×mpert) + - `grre::Array{Float64, 2}` - Green's function radial integrals (2×mthvac × 2×mpert) - `xzpts::Array{Float64, 2}` - Coordinate points [R_plasma, Z_plasma, R_wall, Z_wall] (mthvac × 4) """ @kwdef mutable struct VacuumData @@ -337,6 +342,7 @@ Populated in `Free.jl`. # VACUUM can't handle 3D yet, so these are temporary mpert arrays # TODO: Matt separated grri into a few arrays for IPEC, will need to do that later grri::Array{Float64,2} = Array{Float64}(undef, 2 * mthvac, 2 * mpert) + grre::Array{Float64,2} = Array{Float64}(undef, 2 * mthvac, 2 * mpert) xzpts::Array{Float64,2} = Array{Float64}(undef, mthvac, 4) end @@ -345,7 +351,7 @@ VacuumData(mthvac::Int, mpert::Int, numpert_total::Int) = VacuumData(; mthvac, m """ OdeState -A mutable struct to hold the state of the ODE solver used by the DCON integration routines. +A mutable struct to hold the state of the ODE solver used by the ForceFreeStates integration routines. This struct stores configuration parameters used to allocate arrays, the evolving stored solution during integration, diagnostic arrays used for normalization / Gaussian reduction, and a small set of temporary matrices and factors used to compute singular-layer corrections. diff --git a/src/DCON/Fourfit.jl b/src/ForceFreeStates/Fourfit.jl similarity index 93% rename from src/DCON/Fourfit.jl rename to src/ForceFreeStates/Fourfit.jl index 3a976d46..65a7a24c 100644 --- a/src/DCON/Fourfit.jl +++ b/src/ForceFreeStates/Fourfit.jl @@ -13,7 +13,7 @@ named `metric` in the Fortran `fourfit_make_metric` subroutine. - `ys::Vector{Float64}`: Poloidal angle coordinates `θ` in radians (0 to 2π). - `fs::Array{Float64, 3}`: The raw metric data on the grid, size `(mpsi, mtheta, 8)`. The 8 quantities are: `g¹¹`, `g²²`, `g³³`, `g²³`, `g³¹`, `g¹²`, `J`, `∂J/∂ψ`. - - `fourier_coeffs::Util.FourierCoefficients`: The FFT coefficients (no spline interpolation needed). + - `fourier_coeffs::Utilities.FourierCoefficients`: The FFT coefficients (no spline interpolation needed). """ @kwdef mutable struct MetricData mpsi::Int @@ -21,7 +21,7 @@ named `metric` in the Fortran `fourfit_make_metric` subroutine. xs::Vector{Float64} = zeros(mpsi) ys::Vector{Float64} = zeros(mtheta) fs::Array{Float64,3} = zeros(mpsi, mtheta, 8) - fourier_coeffs::Util.FourierCoefficients = Util.empty_FourierCoefficients() + fourier_coeffs::Utilities.FourierCoefficients = Utilities.empty_FourierCoefficients() end MetricData(mpsi::Int, mtheta::Int) = MetricData(; mpsi, mtheta) @@ -122,14 +122,14 @@ function make_metric(equil::Equilibrium.PlasmaEquilibrium; mband::Int, fft_flag: end # --- Compute Fourier coefficients (no spline overhead since we only access at grid points) --- - metric.fourier_coeffs = Util.FourierCoefficients(metric.xs, metric.ys, metric.fs, mband) + metric.fourier_coeffs = Utilities.FourierCoefficients(metric.xs, metric.ys, metric.fs, mband) return metric end """ - make_matrix(metric::MetricData, equil::Equilibrium.PlasmaEquilibrium, intr::DconInternal) -> FourFitVars + make_matrix(metric::MetricData, equil::Equilibrium.PlasmaEquilibrium, intr::ForceFreeStatesInternal) -> FourFitVars -Constructs main DCON matrices for a given toroidal mode number and returns +Constructs main ForceFreeStates matrices for a given toroidal mode number and returns them as a new `FourFitVars` object. See the appendix of the 2016 Glasser DCON paper for details on the matrix definitions. Performs the same function as `fourfit_make_matrix` in the Fortran code, except F, G, and K are now @@ -159,7 +159,7 @@ and does not affect the actual matrix sizes, they are all dense. Add kinetic metric tensor components for kin_flag = true Set powers if necessary """ -function make_matrix(equil::Equilibrium.PlasmaEquilibrium, intr::DconInternal, metric::MetricData) +function make_matrix(equil::Equilibrium.PlasmaEquilibrium, intr::ForceFreeStatesInternal, metric::MetricData) # --- Extract inputs --- profiles = equil.profiles @@ -217,14 +217,14 @@ function make_matrix(equil::Equilibrium.PlasmaEquilibrium, intr::DconInternal, m # The 8 quantities are: g11, g22, g33, g23, g31, g12, jmat, jmat1 fc = metric.fourier_coeffs for m in 0:intr.mband - g11[mid-m] = Util.get_complex_coeff(fc, ipsi, m, 1) - g22[mid-m] = Util.get_complex_coeff(fc, ipsi, m, 2) - g33[mid-m] = Util.get_complex_coeff(fc, ipsi, m, 3) - g23[mid-m] = Util.get_complex_coeff(fc, ipsi, m, 4) - g31[mid-m] = Util.get_complex_coeff(fc, ipsi, m, 5) - g12[mid-m] = Util.get_complex_coeff(fc, ipsi, m, 6) - jmat[mid-m] = Util.get_complex_coeff(fc, ipsi, m, 7) - jmat1[mid-m] = Util.get_complex_coeff(fc, ipsi, m, 8) + g11[mid-m] = Utilities.get_complex_coeff(fc, ipsi, m, 1) + g22[mid-m] = Utilities.get_complex_coeff(fc, ipsi, m, 2) + g33[mid-m] = Utilities.get_complex_coeff(fc, ipsi, m, 3) + g23[mid-m] = Utilities.get_complex_coeff(fc, ipsi, m, 4) + g31[mid-m] = Utilities.get_complex_coeff(fc, ipsi, m, 5) + g12[mid-m] = Utilities.get_complex_coeff(fc, ipsi, m, 6) + jmat[mid-m] = Utilities.get_complex_coeff(fc, ipsi, m, 7) + jmat1[mid-m] = Utilities.get_complex_coeff(fc, ipsi, m, 8) end # Fill upper half (+1:mband) with conjugate symmetry @@ -255,7 +255,7 @@ function make_matrix(equil::Equilibrium.PlasmaEquilibrium, intr::DconInternal, m # Some complex indexing here... we flatten the 2D (mpert x npert) x (mpert x npert) matrix, # so we need a "ipert" for m, m', n, and n' (in full 3D). In 2D, just m, m', and n ipert = ipert_m + (ipert_n - 1) * intr.mpert - jpert = jpert_m + (ipert_n - 1) * intr.mpert # TODO: this will be jpert_n in 3D-DCON + jpert = jpert_m + (ipert_n - 1) * intr.mpert # TODO: this will be jpert_n in 3D-ForceFreeStates ipert_flat = ipert + (jpert - 1) * intr.numpert_total # Compute matrix values at the current m, m', n (and eventually n') diff --git a/src/DCON/Free.jl b/src/ForceFreeStates/Free.jl similarity index 88% rename from src/DCON/Free.jl rename to src/ForceFreeStates/Free.jl index 7152953c..9dd14a14 100644 --- a/src/DCON/Free.jl +++ b/src/ForceFreeStates/Free.jl @@ -1,5 +1,5 @@ """ - free_run!(odet::OdeState, ctrl::DconControl, equil::Equilibrium.PlasmaEquilibrium, ffit::FourFitVars, intr::DconInternal) -> VacuumData + free_run!(odet::OdeState, ctrl::ForceFreeStatesControl, equil::Equilibrium.PlasmaEquilibrium, ffit::FourFitVars, intr::ForceFreeStatesInternal) -> VacuumData Compute the free boundary energies using the Julia port of the VACUUM code. Performs the same function as `free_run` in the Fortran code, except now all data is passed in memory instead of via files. This @@ -7,7 +7,7 @@ modifies `odet` in place to normalize the eigenfunctions stored in `u_store` and and returns a `VacuumData` struct containing the data needed for perturbed equilibrium calculations and data dumping. """ -function free_run!(odet::OdeState, ctrl::DconControl, equil::Equilibrium.PlasmaEquilibrium, ffit::FourFitVars, intr::DconInternal) +function free_run!(odet::OdeState, ctrl::ForceFreeStatesControl, equil::Equilibrium.PlasmaEquilibrium, ffit::FourFitVars, intr::ForceFreeStatesInternal) # Initializations and allocations vac = VacuumData(ctrl.mthvac, intr.mpert, intr.numpert_total) @@ -28,19 +28,20 @@ function free_run!(odet::OdeState, ctrl::DconControl, equil::Equilibrium.PlasmaE for ipert_n in 1:intr.npert # Set VACUUM run parameters and boundary shape n = ipert_n - 1 + intr.nlow - vac_inputs = compute_vacuum_inputs(intr.psilim, n, ctrl, equil, intr) + vac_inputs = compute_vacuum_inputs(intr.psilim, n, equil, intr, ctrl) fill!(vac.grri, 0.0) + fill!(vac.grre, 0.0) fill!(vac.xzpts, 0.0) - # Compute block of vacuum energy matrix for one toroidal mode number - wv_block, vac.grri, vac.xzpts = Vacuum.compute_vacuum_response(vac_inputs, intr.wall_settings) + # Compute vacuum energy matrix and both Green's functions + wv_block, vac.grri, vac.grre, vac.xzpts = Vacuum.compute_vacuum_response(vac_inputs, intr.wall_settings) # Output data for unit testing and benchmarking if intr.debug_settings.output_benchmark_data @info "Outputting top level vacuum debug data for n = $n" farwall_flag = intr.wall_settings.shape == "nowall" ? true : false benchmark_inputs = VacuumBenchmarkInputs( - wv_block, intr.mpert, equil.config.control.mtheta, ctrl.mthvac, + wv_block, intr.mpert, equil.control.mtheta, ctrl.mthvac, true, vac_inputs.kernelsign, false, farwall_flag, vac.grri, vac.xzpts, "ahg2msc_dcon.out", intr.dir_path, vac_inputs, intr.wall_settings, @@ -129,7 +130,7 @@ function free_run!(odet::OdeState, ctrl::DconControl, equil::Equilibrium.PlasmaE end """ - compute_vacuum_inputs(psifac::Float64, n::Int, ctrl::DconControl, equil::Equilibrium.PlasmaEquilibrium, intr::DconInternal) + compute_vacuum_inputs(psifac::Float64, n::Int, equil::Equilibrium.PlasmaEquilibrium, intr::ForceFreeStatesInternal, ctrl::ForceFreeStatesControl) Compute the necessary input paramters to the VACUUM code for computing the vacuum response matrix. Performs the same function as `free_write_msc` in the Fortran code, except we create a data struct @@ -140,15 +141,15 @@ the r, z, and ν values at the plasma boundary, as well as mode numbers and numb - `psifac`: Flux surface value at the plasma boundary (Float64) - `n`: Toroidal mode number (Int) - - `ctrl`: DCON control parameters (DconControl) - `equil`: Plasma equilibrium data (Equilibrium.PlasmaEquilibrium) - - `intr`: Internal DCON parameters (DconInternal) + - `intr`: Internal ForceFreeStates parameters + - `ctrl`: ForceFreeStates control parameters """ -function compute_vacuum_inputs(psifac::Float64, n::Int, ctrl::DconControl, equil::Equilibrium.PlasmaEquilibrium, intr::DconInternal) +function compute_vacuum_inputs(psifac::Float64, n::Int, equil::Equilibrium.PlasmaEquilibrium, intr::ForceFreeStatesInternal, ctrl::ForceFreeStatesControl) # Allocations theta_norm = equil.rzphi_ys - mtheta = equil.config.control.mtheta + 1 + mtheta = equil.config.mtheta + 1 angle = zeros(Float64, mtheta) r = zeros(Float64, mtheta) z = zeros(Float64, mtheta) @@ -189,14 +190,14 @@ function compute_vacuum_inputs(psifac::Float64, n::Int, ctrl::DconControl, equil end """ - free_compute_wv_spline(ctrl::DconControl, equil::Equilibrium.PlasmaEquilibrium, intr::DconInternal) + free_compute_wv_spline(ctrl::ForceFreeStatesControl, equil::Equilibrium.PlasmaEquilibrium, intr::ForceFreeStatesInternal) Compute a spline of vacuum response matrices over the range of psi from 'ctrl.psi_edge' to `intr.qlim`. This is used for fast evaluation of wt during `ode_record_edge`. Performs the same function as `free_wvmats` in the Fortran code. Currently defaults to 4 spline points per q-window minimum. """ -function free_compute_wv_spline(ctrl::DconControl, equil::Equilibrium.PlasmaEquilibrium, intr::DconInternal) +function free_compute_wv_spline(ctrl::ForceFreeStatesControl, equil::Equilibrium.PlasmaEquilibrium, intr::ForceFreeStatesInternal) profiles = equil.profiles @@ -256,7 +257,7 @@ function free_compute_wv_spline(ctrl::DconControl, equil::Equilibrium.PlasmaEqui end """ - free_compute_total(equil::Equilibrium.PlasmaEquilibrium, ffit::FourFitVars, intr::DconInternal, odet::OdeState) -> ComplexF64 + free_compute_total(equil::Equilibrium.PlasmaEquilibrium, ffit::FourFitVars, intr::ForceFreeStatesInternal, odet::OdeState) -> ComplexF64 Compute total complex energy eigenvalue (total1). This is a trimmed down version of `free_run` that only computes the total energy eigenvalue for the mode unstable mode, used in `findmax_dW_edge!` @@ -264,7 +265,7 @@ which calls this function at each step in the psiedge -> psilim region of integr the same function as `free_test` in the Fortran code, except we have moved the creation of the wv matrix spline to `free_compute_wv_spline` and pass it in `odet.wvmat` (a complex-valued spline). """ -function free_compute_total(equil::Equilibrium.PlasmaEquilibrium, ffit::FourFitVars, intr::DconInternal, odet::OdeState) +function free_compute_total(equil::Equilibrium.PlasmaEquilibrium, ffit::FourFitVars, intr::ForceFreeStatesInternal, odet::OdeState) # Allocations wp = zeros(ComplexF64, intr.numpert_total, intr.numpert_total) diff --git a/src/DCON/Mercier.jl b/src/ForceFreeStates/Mercier.jl similarity index 100% rename from src/DCON/Mercier.jl rename to src/ForceFreeStates/Mercier.jl diff --git a/src/DCON/Sing.jl b/src/ForceFreeStates/Sing.jl similarity index 94% rename from src/DCON/Sing.jl rename to src/ForceFreeStates/Sing.jl index 7214e1cc..0bc8241c 100644 --- a/src/DCON/Sing.jl +++ b/src/ForceFreeStates/Sing.jl @@ -1,11 +1,11 @@ """ - sing_find!(intr::DconInternal, equil::Equilibrium.PlasmaEquilibrium) + sing_find!(intr::ForceFreeStatesInternal, equil::Equilibrium.PlasmaEquilibrium) Locate singular rational q-surfaces (q = m/nn) using a bisection method between extrema of the q-profile, and store their properties in `intr.sing`. Performs the same function as `sing_find` in the Fortran code. """ -function sing_find!(intr::DconInternal, equil::Equilibrium.PlasmaEquilibrium) +function sing_find!(intr::ForceFreeStatesInternal, equil::Equilibrium.PlasmaEquilibrium) profiles = equil.profiles @@ -63,7 +63,7 @@ function sing_find!(intr::DconInternal, equil::Equilibrium.PlasmaEquilibrium) end """ - sing_lim!(ctrl::DconControl, equil::Equilibrium.PlasmaEquilibrium, intr::DconInternal) + sing_lim!(ctrl::ForceFreeStatesControl, equil::Equilibrium.PlasmaEquilibrium, intr::ForceFreeStatesInternal) Compute and set integration ψ, q, and q' limits by handling cases where user truncates before the last singular surface. Performs a similar function to `sing_lim` @@ -81,14 +81,14 @@ If `qlim < qmax`, a Newton iteration is performed to find the corresponding Note that the Newton iteration will be triggered if either `set_psilim_via_dmlim` is true or `ctrl.qhigh < equil.params.qmax`. Otherwise, the equilibrium edge values are used. """ -function sing_lim!(intr::DconInternal, ctrl::DconControl, equil::Equilibrium.PlasmaEquilibrium) +function sing_lim!(intr::ForceFreeStatesInternal, ctrl::ForceFreeStatesControl, equil::Equilibrium.PlasmaEquilibrium) profiles = equil.profiles # Initial guesses based on equilibrium intr.qlim = min(equil.params.qmax, ctrl.qhigh) # equilibrium solve only goes up to qmax, so we're capped there intr.q1lim = profiles.q_deriv(profiles.xs[end]; hint=Ref(profiles.npts_minus_1)) - intr.psilim = equil.config.control.psihigh + intr.psilim = equil.config.psihigh # Optionally override qlim based on dmlim if ctrl.set_psilim_via_dmlim @@ -110,7 +110,7 @@ function sing_lim!(intr::DconInternal, ctrl::DconControl, equil::Equilibrium.Pla if intr.qlim < equil.params.qmax # Find nearest ψ index where q ≈ qlim _, jpsi = findmin(abs.(profiles.q_spline.y .- intr.qlim)) - jpsi = min(jpsi, equil.config.control.mpsi - 1) + jpsi = min(jpsi, equil.config.mpsi - 1) intr.psilim = profiles.xs[jpsi] converged = false @@ -131,25 +131,24 @@ function sing_lim!(intr::DconInternal, ctrl::DconControl, equil::Equilibrium.Pla end """ - compute_sing_asymptotics(singp::SingType, ctrl::DconControl, equil::Equilibrium.PlasmaEquilibrium, ffit::FourFitVars, intr::DconInternal) + compute_sing_asymptotics(singp::SingType, ctrl::ForceFreeStatesControl, equil::Equilibrium.PlasmaEquilibrium, ffit::FourFitVars, intr::ForceFreeStatesInternal) Calculate asymptotic vmat and mmat matrices for a singular surface. Formerly `sing_vmat!`. Returns a `SingAsymptotics` struct with the computed data instead of mutating the `SingType` struct. This makes it clear that asymptotics are computed on-demand -for ideal DCON and are not inherent properties of the singular surface. +for ideal ForceFreeStates and are not inherent properties of the singular surface. See equations 41-48 in the 2016 Glasser DCON paper for the mathematical details. ### Arguments - `singp::SingType`: Singular surface parameters - - Other standard DCON parameters ### Returns - `SingAsymptotics`: Struct containing all asymptotic expansion data """ -function compute_sing_asymptotics(singp::SingType, ctrl::DconControl, equil::Equilibrium.PlasmaEquilibrium, ffit::FourFitVars, intr::DconInternal) +function compute_sing_asymptotics(singp::SingType, ctrl::ForceFreeStatesControl, equil::Equilibrium.PlasmaEquilibrium, ffit::FourFitVars, intr::ForceFreeStatesInternal) # Allocations vmat = zeros(ComplexF64, intr.numpert_total, 2 * intr.numpert_total, 2, 2 * ctrl.sing_order + 1) @@ -217,7 +216,7 @@ function compute_sing_asymptotics(singp::SingType, ctrl::DconControl, equil::Equ end """ - compute_sing_mmat!(mmat::Array{ComplexF64,4}, singp::SingType, ctrl::DconControl, profiles::Equilibrium.ProfileSplines, ffit::FourFitVars, intr::DconInternal) + compute_sing_mmat!(mmat::Array{ComplexF64,4}, singp::SingType, ctrl::ForceFreeStatesControl, profiles::Equilibrium.ProfileSplines, ffit::FourFitVars, intr::ForceFreeStatesInternal) Calculate asymptotic mmat matrix for a singular surface. Formerly `sing_mmat!`. Performs the same function as `sing_mmat` in the Fortran code. Main differences are 1-indexing for @@ -247,7 +246,7 @@ Better way to unpack the cubic splines Rename variables to be more intuitive? I don't like ff - maybe f and f_fact instead of f_lower Add a spline for F directly instead of the lower triangular factorization to avoid complexity? """ -function compute_sing_mmat!(mmat::Array{ComplexF64,4}, singp::SingType, ctrl::DconControl, profiles::Equilibrium.ProfileSplines, ffit::FourFitVars, intr::DconInternal) +function compute_sing_mmat!(mmat::Array{ComplexF64,4}, singp::SingType, ctrl::ForceFreeStatesControl, profiles::Equilibrium.ProfileSplines, ffit::FourFitVars, intr::ForceFreeStatesInternal) q_spline = profiles.q_spline q_d1 = profiles.q_deriv @@ -508,7 +507,7 @@ function compute_sing_mmat!(mmat::Array{ComplexF64,4}, singp::SingType, ctrl::Dc end """ - solve_higher_order_vmat!(vmat::Array{ComplexF64,4}, mmat::Array{ComplexF64,4}, m0mat::Matrix{ComplexF64}, alpha::Vector{ComplexF64}, r1::Vector{Int}, r2::Vector{Int}, n1::Vector{Int}, n2::Vector{Int}, power::Vector{ComplexF64}, intr::DconInternal, k::Int) + solve_higher_order_vmat!(vmat::Array{ComplexF64,4}, mmat::Array{ComplexF64,4}, m0mat::Matrix{ComplexF64}, alpha::Vector{ComplexF64}, r1::Vector{Int}, r2::Vector{Int}, n1::Vector{Int}, n2::Vector{Int}, power::Vector{ComplexF64}, intr::ForceFreeStatesInternal, k::Int) Solves iteratively for the next order in the power series `vmat`. See equation 47 in the Glasser 2016 DCON paper. Identical to the Fortran @@ -534,7 +533,7 @@ function solve_higher_order_vmat!( n1::Vector{Int}, n2::Vector{Int}, power::Vector{ComplexF64}, - intr::DconInternal, + intr::ForceFreeStatesInternal, k::Int ) @@ -647,7 +646,7 @@ function sing_get_ua(sing_asymp::SingAsymptotics, z::Float64) end """ - sing_get_ca(u::Array{ComplexF64,3}, ua::Array{ComplexF64,3}, intr::DconInternal) + sing_get_ca(u::Array{ComplexF64,3}, ua::Array{ComplexF64,3}, intr::ForceFreeStatesInternal) Compute the asymptotic expansion coefficients according to equation 50 in Glasser 2016 DCON paper. Performs the same function as @@ -657,9 +656,9 @@ Compute the asymptotic expansion coefficients according to equation - `u::Array{ComplexF64,3}`: Current solution matrix, shape (numpert_total, numpert_total, 2) - `ua::Array{ComplexF64,3}`: Asymptotic solution matrix, shape (numpert_total, numpert_total, 2) - - `intr::DconInternal`: Internal DCON data containing perturbation dimensions + - `intr::ForceFreeStatesInternal`: Internal ForceFreeStates data containing perturbation dimensions """ -function sing_get_ca(u::Array{ComplexF64,3}, ua::Array{ComplexF64,3}, intr::DconInternal) +function sing_get_ca(u::Array{ComplexF64,3}, ua::Array{ComplexF64,3}, intr::ForceFreeStatesInternal) # Build temp1 temp1 = zeros(ComplexF64, 2 * intr.numpert_total, 2 * intr.numpert_total) @@ -686,7 +685,7 @@ end sing_der!( du::Array{ComplexF64,3}, u::Array{ComplexF64,3}, - params::Tuple{DconControl, Equilibrium.PlasmaEquilibrium, FourFitVars, DconInternal, OdeState}, + params::Tuple{ForceFreeStatesControl, Equilibrium.PlasmaEquilibrium, FourFitVars, ForceFreeStatesInternal, OdeState}, psieval::Float64 ) @@ -713,7 +712,7 @@ more simplistic code with similar performance. - `du::Array{ComplexF64,3}`: Pre-allocated array to hold the derivative result, shape (mpert, mpert, 2), updated in-place - `u::Array{ComplexF64,3}`: Current state array, shape (mpert, mpert, 2) - - `params::Tuple{DconControl, Equilibrium.PlasmaEquilibrium, FourFitVars, DconInternal, OdeState}`: Tuple of relevant structs + - `params::Tuple{ForceFreeStatesControl, Equilibrium.PlasmaEquilibrium, FourFitVars, ForceFreeStatesInternal, OdeState}`: Tuple of relevant structs - `psieval::Float64`: Current psi value at which to evaluate the derivative ### TODOs @@ -721,8 +720,8 @@ more simplistic code with similar performance. Implement kin_flag functionality """ function sing_der!(du::Array{ComplexF64,3}, u::Array{ComplexF64,3}, - params::Tuple{DconControl,Equilibrium.PlasmaEquilibrium, - FourFitVars,DconInternal,OdeState,IntegrationChunk}, + params::Tuple{ForceFreeStatesControl,Equilibrium.PlasmaEquilibrium, + FourFitVars,ForceFreeStatesInternal,OdeState,IntegrationChunk}, psieval::Float64) # Unpack structs and initialize diff --git a/src/DCON/Utils.jl b/src/ForceFreeStates/Utils.jl similarity index 100% rename from src/DCON/Utils.jl rename to src/ForceFreeStates/Utils.jl diff --git a/src/ForcingTerms/ForcingTerms.jl b/src/ForcingTerms/ForcingTerms.jl new file mode 100644 index 00000000..afe38287 --- /dev/null +++ b/src/ForcingTerms/ForcingTerms.jl @@ -0,0 +1,173 @@ +module ForcingTerms +# Module for external field specification and forcing term calculations + +using DelimitedFiles +using HDF5 + +""" + ForcingTermsControl + +User-facing control parameters from TOML [ForcingTerms] section. + +## Fields + +Forcing Data: + - `forcing_data_file::String` - Path to forcing data file (n, m, complex amplitude) + - `forcing_data_format::String` - Format of forcing data: "ascii" or "hdf5" (default: "ascii") + +Future: Will include Fortran coil.in parameters for external coil configurations +""" +Base.@kwdef mutable struct ForcingTermsControl + # Forcing data file settings + forcing_data_file::String = "forcing.dat" + forcing_data_format::String = "ascii" + + # Future: Fortran coil.in parameters will go here +end + +""" + ForcingMode + +Data structure for a single forcing mode. + +## Fields + + - `n::Int` - Toroidal mode number + - `m::Int` - Poloidal mode number + - `amplitude::ComplexF64` - Complex amplitude of the forcing field +""" +Base.@kwdef mutable struct ForcingMode + n::Int = 0 + m::Int = 0 + amplitude::ComplexF64 = 0.0 + 0.0im +end + +""" + load_forcing_data!(forcing_modes::Vector{ForcingMode}, dir_path::String, forcing_data_file::String, forcing_data_format::String, verbose::Bool=false) + +Load forcing data from ASCII or HDF5 file. + +ASCII format: Three or four columns (n, m, real amplitude, [optional] imag amplitude) +- Column 1: Toroidal mode number (n) +- Column 2: Poloidal mode number (m) +- Column 3: Complex amplitude (real part) +- Column 4: Complex amplitude (imaginary part) [optional, assumes 0 if not present] + +Example ASCII file: +``` +1 2 0.5 0.1 +1 3 0.3 -0.2 +2 4 0.8 0.0 +``` + +HDF5 format: +- Dataset "n": Integer array of toroidal mode numbers +- Dataset "m": Integer array of poloidal mode numbers +- Dataset "amplitude_real": Real parts of amplitudes +- Dataset "amplitude_imag": Imaginary parts of amplitudes +""" +function load_forcing_data!( + forcing_modes::Vector{ForcingMode}, + dir_path::String, + forcing_data_file::String, + forcing_data_format::String, + verbose::Bool=false +) + filepath = joinpath(dir_path, forcing_data_file) + + if verbose + println("Loading forcing data from $filepath") + end + + if forcing_data_format == "ascii" + load_forcing_ascii!(forcing_modes, filepath, verbose) + elseif forcing_data_format == "hdf5" + load_forcing_hdf5!(forcing_modes, filepath, verbose) + else + error("Unknown forcing data format: $(forcing_data_format). Use 'ascii' or 'hdf5'.") + end + + if verbose + println(" Loaded $(length(forcing_modes)) forcing modes") + end +end + +""" + load_forcing_ascii!(forcing_modes::Vector{ForcingMode}, filepath::String, verbose::Bool) + +Load forcing data from ASCII file with columns: n, m, real(amplitude), imag(amplitude) +""" +function load_forcing_ascii!( + forcing_modes::Vector{ForcingMode}, + filepath::String, + verbose::Bool +) + if !isfile(filepath) + error("Forcing data file not found: $filepath") + end + + data = readdlm(filepath, comments=true, comment_char='#') + nrows = size(data, 1) + ncols = size(data, 2) + + if ncols < 3 + error("ASCII forcing file must have at least 3 columns (n, m, real_amplitude)") + end + + # Clear existing forcing modes + empty!(forcing_modes) + + for i in 1:nrows + n = Int(data[i, 1]) + m = Int(data[i, 2]) + real_part = Float64(data[i, 3]) + imag_part = ncols >= 4 ? Float64(data[i, 4]) : 0.0 + + push!(forcing_modes, ForcingMode( + n=n, + m=m, + amplitude=complex(real_part, imag_part) + )) + end +end + +""" + load_forcing_hdf5!(forcing_modes::Vector{ForcingMode}, filepath::String, verbose::Bool) + +Load forcing data from HDF5 file with datasets: n, m, amplitude_real, amplitude_imag +""" +function load_forcing_hdf5!( + forcing_modes::Vector{ForcingMode}, + filepath::String, + verbose::Bool +) + if !isfile(filepath) + error("Forcing data file not found: $filepath") + end + + h5open(filepath, "r") do file + n_array = read(file, "n") + m_array = read(file, "m") + amp_real = read(file, "amplitude_real") + amp_imag = haskey(file, "amplitude_imag") ? read(file, "amplitude_imag") : zeros(length(n_array)) + + if length(n_array) != length(m_array) || length(n_array) != length(amp_real) + error("Inconsistent array lengths in HDF5 forcing file") + end + + # Clear existing forcing modes + empty!(forcing_modes) + + for i in eachindex(n_array) + push!(forcing_modes, ForcingMode( + n=Int(n_array[i]), + m=Int(m_array[i]), + amplitude=complex(amp_real[i], amp_imag[i]) + )) + end + end +end + +export ForcingTermsControl, ForcingMode, load_forcing_data! + +end # module ForcingTerms \ No newline at end of file diff --git a/src/JPEC.jl b/src/JPEC.jl old mode 100644 new mode 100755 index 94ca8e40..26a94f0f --- a/src/JPEC.jl +++ b/src/JPEC.jl @@ -1,14 +1,14 @@ # JPEC.jl module JPEC -include("Utilities/Utilities.jl") -import .UtilitiesMod as Util -export UtilitiesMod, Util - include("Splines/Splines.jl") import .SplinesMod as Spl export SplinesMod, Spl +include("Utilities/Utilities.jl") +import .Utilities as Utilities +export Utilities + include("Equilibrium/Equilibrium.jl") import .Equilibrium as Equilibrium export Equilibrium @@ -17,11 +17,407 @@ include("Vacuum/Vacuum.jl") import .Vacuum as Vacuum export Vacuum -include("DCON/DCON.jl") -import .DCON as DCON -export DCON +include("ForceFreeStates/ForceFreeStates.jl") +import .ForceFreeStates as ForceFreeStates +export ForceFreeStates + +include("ForcingTerms/ForcingTerms.jl") +import .ForcingTerms as ForcingTerms +export ForcingTerms + +include("PerturbedEquilibrium/PerturbedEquilibrium.jl") +import .PerturbedEquilibrium as PerturbedEquilibrium +export PerturbedEquilibrium include(joinpath(@__DIR__, "..", "deps", "build_helpers.jl")) export build_fortran, build_spline_fortran, build_vacuum_fortran +# Additional imports for main function +using TOML +using Printf +using HDF5 + +# Import FastInterpolations functions and types needed in main +import FastInterpolations: cubic_interp, CubicFit + +# Import ForceFreeStates types and functions needed for main +using .ForceFreeStates: ForceFreeStatesInternal, ForceFreeStatesControl, DebugSettings, VacuumData, OdeState +using .ForceFreeStates: sing_lim!, sing_find! +using .ForceFreeStates: mercier_scan!, compute_ballooning_stability! +using .ForceFreeStates: make_metric, make_matrix +using .ForceFreeStates: eulerlagrange_integration, free_run! + +function main(args::Vector{String}=String[]) + # Parse command line arguments + path = length(args) >= 1 ? args[1] : "./" + + println("\n" * "="^60) + println(" JPEC - Julia Perturbed Equilibrium Code") + println("="^60 * "\n") + + start_time = time() + + # Read input data and set up data structures + intr = ForceFreeStatesInternal(; dir_path=path) + inputs = TOML.parsefile(joinpath(intr.dir_path, "jpec.toml")) + ctrl = ForceFreeStatesControl(; (Symbol(k) => v for (k, v) in inputs["ForceFreeStates"])...) + + # Set up equilibrium from jpec.toml or fallback to equil.toml if it exists + if "Equilibrium" in keys(inputs) + eq_config = Equilibrium.EquilibriumConfig(inputs["Equilibrium"], intr.dir_path) + equil = Equilibrium.setup_equilibrium(eq_config) + elseif isfile(joinpath(intr.dir_path, "equil.toml")) + @warn "Reading from equil.toml is deprecated. Please move [EQUIL_CONTROL] and [EQUIL_OUTPUT] sections to [Equilibrium] in jpec.toml" + equil = Equilibrium.setup_equilibrium(joinpath(intr.dir_path, "equil.toml")) + else + error("No equilibrium configuration found. Add [Equilibrium] section to jpec.toml") + end + # Early exit if user only requested equilibrium setup + if equil.config.force_termination + end_time = time() - start_time + println("\n" * "="^60) + println("Equilibrium setup complete (force_termination = true).") + println("Run time: $(@sprintf("%.3e", end_time)) seconds") + println("Normal termination.") + println("="^60) + return + end + + + if "Wall" in keys(inputs) + intr.wall_settings = Vacuum.WallShapeSettings(; (Symbol(k) => v for (k, v) in inputs["Wall"])...) + else + intr.wall_settings = Vacuum.WallShapeSettings() + end + + if "DEBUG" in keys(inputs) + intr.debug_settings = DebugSettings(; (Symbol(k) => v for (k, v) in inputs["DEBUG"])...) + else + intr.debug_settings = DebugSettings() + end + + # Set up variables + # TODO: parallel threads logic + ctrl.delta_mhigh *= 2 # for consistency with Fortran DCON TODO: why is this present in the Fortran? + + # Determine psilim and qlim (where we will integrate to) + sing_lim!(intr, ctrl, equil) + if ctrl.set_psilim_via_dmlim && ctrl.psiedge < intr.psilim + @warn "Only one of set_psilim_via_dmlim and psiedge < psilim can be used at a time. + Setting psiedge = 1.0 and determining dW from psilim = $(intr.psilim) determined from dmlim = $(ctrl.dmlim)." + ctrl.psiedge = 1.0 + end + + # If truncating before psihigh, reform equilibrium if desired + if intr.psilim != equil.config.psihigh && ctrl.reform_eq_with_psilim + @warn "Reforming equilibrium splines from psihigh to psilim not implemented yet. Proceeding with psihigh = $(equil.config.psihigh)." + # JMH - Nik please put the logic we discussed here + # something like ? + # equil.config.psihigh = intr.psilim + # equil = set_up_equilibrium(equil.config) + end + + # Compute Mercier and Ballooning stability (if desired) + # This holds di, dr, h (calculated in mercier_scan), ca1, and ca2 (calculated in ballooning scan) + # Compute Mercier and Ballooning stability (if desired) + # This holds di, dr, h (calculated in mercier_scan), ca1, and ca2 (calculated in ballooning scan) + profiles_xs = equil.profiles.xs + locstab_fs = zeros(Float64, length(profiles_xs), 5) + if ctrl.mer_flag + if ctrl.verbose + println("Evaluating Mercier criterion") + end + mercier_scan!(locstab_fs, equil) + end + if ctrl.bal_flag + compute_ballooning_stability!(ctrl, locstab_fs, equil) + end + # Fit data to splines + intr.locstab = cubic_interp(profiles_xs, locstab_fs; bc=CubicFit(), extrap=:extension) + + # Determine toroidal mode numbers + if ctrl.nn_low == 0 && ctrl.nn_high == 0 + error("Either nn_low or nn_high must be set in ForceFreeStates (both are 0)") + elseif ctrl.nn_low == 0 + ctrl.nn_low = ctrl.nn_high + elseif ctrl.nn_high == 0 + ctrl.nn_high = ctrl.nn_low + end + if ctrl.nn_low > ctrl.nn_high + error("nn_low cannot be greater than nn_high") + end + intr.nlow = ctrl.nn_low + intr.nhigh = ctrl.nn_high + intr.npert = intr.nhigh - intr.nlow + 1 + nstring = intr.npert == 1 ? "$(intr.nlow)" : "$(intr.nlow):$(intr.nhigh)" + + # Find all singular surfaces in the equilibrium + sing_find!(intr, equil) + + # Determine poloidal mode numbers + if ctrl.delta_mlow < 0 || ctrl.delta_mhigh < 0 + error("Negative delta_mlow or delta_mhigh not allowed") + end + if ctrl.cyl_flag + intr.mlow = ctrl.delta_mlow + intr.mhigh = ctrl.delta_mhigh + elseif ctrl.sing_start == 0 + intr.mlow = min(intr.nlow * equil.params.qmin, 0) - 4 - ctrl.delta_mlow + intr.mhigh = trunc(Int, intr.nhigh * equil.params.qmax) + ctrl.delta_mhigh + else + intr.mmin = Inf # HUGE in Fortran + for ising in Int(ctrl.sing_start):intr.msing + intr.mmin = min(intr.mmin, sing[ising].m) + end + intr.mlow = intr.mmin - ctrl.delta_mlow + intr.mhigh = trunc(Int, intr.nhigh * equil.params.qmax) + ctrl.delta_mhigh + end + intr.mpert = intr.mhigh - intr.mlow + 1 + if ctrl.delta_mband >= intr.mpert + @warn "Banded matrices not implemented yet, setting delta_mband to 0" + ctrl.delta_mband = 0 + end + intr.mband = intr.mpert - 1 - ctrl.delta_mband + intr.mband = min(max(intr.mband, 0), intr.mpert - 1) + intr.numpert_total = intr.mpert * intr.npert + + # Fit equilibrium quantities to Fourier-spline functions. + if ctrl.mat_flag || ctrl.ode_flag + if ctrl.verbose + println("Run parameters:") + println(" q0 = $(@sprintf("%.3f", equil.params.q0)), qmin = $(@sprintf("%.3f", equil.params.qmin)), qmax = $(@sprintf("%.3f", equil.params.qmax)), q95 = $(@sprintf("%.3f", equil.params.q95))") + println(" qlim = $(@sprintf("%.5f", intr.qlim)), psilim = $(@sprintf("%.9f", intr.psilim))") + println(" betat = $(@sprintf("%.3f", equil.params.betat)), betan = $(@sprintf("%.3f", equil.params.betan)), betap1 = $(@sprintf("%.3f", equil.params.betap1))") + println(" mlow = $(@sprintf("%4i", intr.mlow)), mhigh = $(@sprintf("%4i", intr.mhigh)), mpert = $(@sprintf("%4i", intr.mpert)), mband = $(@sprintf("%4i", intr.mband))") + println(" nlow = $(@sprintf("%4i", intr.nlow)), nhigh = $(@sprintf("%4i", intr.nhigh)), npert = $(@sprintf("%4i", intr.npert))") + end + + # Compute metric tensor + metric = make_metric(equil; mband=intr.mband, fft_flag=ctrl.fft_flag) + + if ctrl.verbose + println(" Computing F, G, and K Matrices") + end + + # Compute matrices and populate FourFitVars struct + ffit = make_matrix(equil, intr, metric) + + if ctrl.kin_flag + error("kin_flag not implemented yet") + end + + # NOTE: Asymptotic calculations for ideal ForceFreeStates are now computed on-demand during + # singular surface crossings in cross_ideal_singular_surf!. This makes it clear that + # asymptotics are only needed for ideal ForceFreeStates and are not inherent properties of + # the singular surface. + + end + + # Integrate Euler-Lagrange Equation + if ctrl.ode_flag + if ctrl.verbose + println("Integrating Euler-Lagrange equation") + end + odet = eulerlagrange_integration(ctrl, equil, ffit, intr) + if odet.nzero > 0 && ctrl.verbose + println("Fixed-boundary mode unstable for n = $nstring.") + end + end + + # Compute free boundary energies + if ctrl.vac_flag && !(ctrl.ksing > 0 && ctrl.ksing <= intr.msing + 1) + if ctrl.verbose + println("Computing free boundary energies") + end + vac_data = free_run!(odet, ctrl, equil, ffit, intr) + if real(vac_data.et[1]) < 0 + if ctrl.verbose + println("Free-boundary mode unstable for n = $nstring.") + end + else + if ctrl.verbose + println("All free-boundary modes stable for n = $nstring.") + end + end + end + + if ctrl.write_outputs_to_HDF5 + if ctrl.verbose + println("Writing saved data to $(ctrl.HDF5_filename)") + end + write_outputs_to_HDF5(ctrl, equil, intr, odet, ctrl.vac_flag ? vac_data : nothing) + end + + # Early exit if user only requested force-free states + if ctrl.force_termination + end_time = time() - start_time + println("\n" * "="^60) + println("Force-free states complete (force_termination = true).") + println("Run time: $(@sprintf("%.3e", end_time)) seconds") + println("Normal termination.") + println("="^60) + return + end + + # Check for PerturbedEquilibrium section and run if present + if "PerturbedEquilibrium" in keys(inputs) + # Read ForcingTerms control parameters + if "ForcingTerms" in keys(inputs) + ft_ctrl = ForcingTerms.ForcingTermsControl(; + (Symbol(k) => v for (k, v) in inputs["ForcingTerms"])... + ) + else + ft_ctrl = ForcingTerms.ForcingTermsControl() # Use defaults + end + + pe_ctrl = PerturbedEquilibrium.PerturbedEquilibriumControl(; + (Symbol(k) => v for (k, v) in inputs["PerturbedEquilibrium"])... + ) + pe_intr = PerturbedEquilibrium.PerturbedEquilibriumInternal(; dir_path=intr.dir_path) + + # Run perturbed equilibrium calculations + # Pass vac_data and intr for response matrix calculations + pe_state = PerturbedEquilibrium.compute_perturbed_equilibrium( + equil, odet, ctrl.vac_flag ? vac_data : nothing, intr, ft_ctrl, pe_ctrl, pe_intr + ) + + # Write perturbed equilibrium outputs to same HDF5 file + if pe_ctrl.write_outputs_to_HDF5 + output_file = isempty(pe_ctrl.output_filename) ? ctrl.HDF5_filename : pe_ctrl.output_filename + PerturbedEquilibrium.write_outputs_to_HDF5( + pe_state, pe_intr, pe_ctrl, joinpath(intr.dir_path, output_file) + ) + end + end + + end_time = time() - start_time + println("\n" * "="^60) + println("Run time: $(@sprintf("%.3e", end_time)) seconds") + println("Normal termination.") + println("="^60) + + # TODO: Do not allow perturbed equilibrium calculations if zero crossings are found + + return (ctrl=ctrl, equil=equil, intr=intr, ffit=ffit, odet=odet, vac_data=ctrl.vac_flag ? vac_data : nothing) + +end + +""" + write_outputs_to_HDF5(ctrl::ForceFreeStatesControl, equil::Equilibrium.PlasmaEquilibrium, intr::ForceFreeStatesInternal, odet::OdeState) + +Helper function to write the HDF5 output file with relevant run and equilibrium parameters. +This combines the functionality of several pieces of the Fortran code in `ode_output.f`, +primarily `ode_output_open` and the various `bin_euler` writes that occur throughout the +integration. Some parameters are only dumped in their respective flags are true, e.g. +vacuum data if `vac_flag` is true. + +### TODOs + +Combine spline unpacking if possible, too many extra lines + +""" +function write_outputs_to_HDF5(ctrl::ForceFreeStatesControl, equil::Equilibrium.PlasmaEquilibrium, intr::ForceFreeStatesInternal, odet::OdeState, vac::Union{VacuumData, Nothing}) + + h5open(joinpath(intr.dir_path, ctrl.HDF5_filename), "w") do out_h5 + + # Store input parameters + for (key, val) in zip(fieldnames(ForceFreeStatesControl), getfield.(Ref(ctrl), fieldnames(ForceFreeStatesControl))) + out_h5["input/ForceFreeStates/$key"] = val + end + for (key, val) in zip(fieldnames(Equilibrium.EquilibriumConfig), getfield.(Ref(equil.config), fieldnames(Equilibrium.EquilibriumConfig))) + out_h5["input/EQUIL_CONTROL/$key"] = val + end + # TODO: assuming EQUIL_OUTPUT is going to be deprecated + # TODO: should we store the equilibrium? difficult since it could be a gfile, sol.in, etc. + # TODO: if we do one input file, can just pass that in instead and loop easily since its parsed + # as a dict already (for (k, v) in inputs["ForceFreeStates"]...). We have to do this since custom structs + # don't inherently have an iterator by default + + # Write derived run parameters + out_h5["info/mpert"] = intr.mpert + out_h5["info/mband"] = intr.mband + out_h5["info/mlow"] = intr.mlow + out_h5["info/mhigh"] = intr.mhigh + out_h5["info/npert"] = intr.npert + out_h5["info/nlow"] = intr.nlow + out_h5["info/nhigh"] = intr.nhigh + m = [(i - 1) % intr.mpert + intr.mlow for i in 1:(intr.numpert_total)] + n = [(i - 1) ÷ intr.mpert + intr.nlow for i in 1:(intr.numpert_total)] + out_h5["info/mn_index"] = hcat(m, n) # (N, 2) matrix + out_h5["info/psilim"] = intr.psilim + out_h5["info/qlim"] = intr.qlim + out_h5["info/q1lim"] = intr.q1lim + + # Write derived equilibrium parameters + for (key, val) in zip(fieldnames(Equilibrium.EquilibriumParameters), getfield.(Ref(equil.params), fieldnames(Equilibrium.EquilibriumParameters))) + if val !== nothing # TODO: looks like ro, zo, psio, and b_norm are not set, so skipping those for now but should fix eventually + out_h5["equil/$key"] = val + end + end + out_h5["equil/psio"] = equil.psio + out_h5["equil/ro"] = equil.ro + out_h5["equil/zo"] = equil.zo + + # Write spline arrays (using profiles with named splines) + profiles = equil.profiles + out_h5["splines/profiles/xs"] = profiles.xs + out_h5["splines/profiles/2piF"] = profiles.F_spline.y + out_h5["splines/profiles/mu0p"] = profiles.P_spline.y + out_h5["splines/profiles/dVdpsi"] = profiles.dVdpsi_spline.y + out_h5["splines/profiles/q"] = profiles.q_spline.y + out_h5["splines/rzphi/xs"] = equil.rzphi_xs + out_h5["splines/rzphi/ys"] = equil.rzphi_ys + # Extract grid point values from interpolants for HDF5 output + out_h5["splines/rzphi/rcoords"] = equil.rzphi_rsquared.nodal_derivs.partials[1, :, :] + out_h5["splines/rzphi/offset"] = equil.rzphi_offset.nodal_derivs.partials[1, :, :] + out_h5["splines/rzphi/nu"] = equil.rzphi_nu.nodal_derivs.partials[1, :, :] + out_h5["splines/rzphi/jac"] = equil.rzphi_jac.nodal_derivs.partials[1, :, :] + + # Write local stability data + if ctrl.mer_flag + locstab_xs = intr.locstab.cache.x + out_h5["locstab/di"] = intr.locstab.y[:, 1] ./ locstab_xs + out_h5["locstab/dr"] = intr.locstab.y[:, 2] ./ locstab_xs + out_h5["singular/di0"] = [intr.locstab(sing.psifac)[1] / sing.psifac for sing in intr.sing] + end + if ctrl.bal_flag + out_h5["locstab/ca1"] = intr.locstab.y[:, 4] + end + + # Write integration data + # TODO: technically this should only be written if ode_flag is true, but that's going to get deprecated eventually + out_h5["integration/nstep"] = odet.step + out_h5["integration/psi"] = odet.psi_store + out_h5["integration/q"] = odet.q_store + out_h5["integration/xi_psi"] = odet.u_store[:, :, 1, :] + out_h5["integration/u2"] = odet.u_store[:, :, 2, :] # TODO: what to name this? These are the "conjugate momenta" of u1 + out_h5["integration/dxi_psi"] = odet.ud_store[:, :, 1, :] + out_h5["integration/xi_s"] = odet.ud_store[:, :, 2, :] + out_h5["integration/crit"] = odet.crit_store + + # Write singular surface data + out_h5["singular/msing"] = intr.msing + out_h5["singular/psi"] = [sing.psifac for sing in intr.sing] + out_h5["singular/q"] = [sing.q for sing in intr.sing] + out_h5["singular/q1"] = [sing.q1 for sing in intr.sing] + out_h5["singular/ca_left"] = odet.ca_l + out_h5["singular/ca_right"] = odet.ca_r + + # Write vacuum Data + if ctrl.vac_flag + out_h5["vacuum/wt"] = vac.wt + out_h5["vacuum/wt0"] = vac.wt0 + out_h5["vacuum/ep"] = vac.ep + out_h5["vacuum/ev"] = vac.ev + out_h5["vacuum/et"] = vac.et + out_h5["vacuum/x_plasma"] = vac.xzpts[:, 1] + out_h5["vacuum/z_plasma"] = vac.xzpts[:, 2] + out_h5["vacuum/x_wall"] = vac.xzpts[:, 3] + out_h5["vacuum/z_wall"] = vac.xzpts[:, 4] + end + end +end + +export main + end # module JPEC diff --git a/src/PerturbedEquilibrium/FieldReconstruction.jl b/src/PerturbedEquilibrium/FieldReconstruction.jl new file mode 100644 index 00000000..fd053e5b --- /dev/null +++ b/src/PerturbedEquilibrium/FieldReconstruction.jl @@ -0,0 +1,314 @@ +""" +Field reconstruction from eigenmode response. + +Converts eigenmode response coefficients to physical displacement and magnetic +field perturbations in mode space, following the GPEC gpeq module approach. + +This module mimics the GPEC Fortran subroutines: +- gpeq_sol: Get equilibrium solution at each radial point +- gpeq_contra: Compute contravariant field from covariant displacement +- gpeq_cova: Compute covariant components using metric tensors +- gpeq_normal: Compute normal (flux surface) components +- gpeq_tangent: Compute tangential components + +Fields are computed using ideal MHD relations in flux coordinates: + b^ψ = i * χ₁ * (m - n*q) * ξ_ψ + b^θ = -i * (χ₁ * ∂ξ_ψ/∂ψ + n * ξ_ζ) + b^ζ = -i * (χ₁ * (q'*ξ_ψ + q*∂ξ_ψ/∂ψ) + m * ξ_ζ) + +where χ₁ = 2π * Ψ₀ (total poloidal flux normalization). + +Reference: GPEC gpeq.f lines 100-102 +""" + +""" + reconstruct_physical_fields( + response_vector::Vector{ComplexF64}, + ForceFreeStates_results::OdeState, + equil::Equilibrium.PlasmaEquilibrium, + ffs_intr::ForceFreeStatesInternal, + intr::PerturbedEquilibriumInternal + ) -> (xi_modes, b_modes) + +Reconstruct displacement and magnetic field from eigenmode response in mode space. + +This function mimics GPEC's gpeq module, computing fields using ideal MHD +algebraic relations in flux coordinates. All fields are returned in mode space +[npsi, mpert] rather than real space. + +# Process (following GPEC) + +1. Sum weighted eigenmode contributions → ξ_ψ(ψ, m) +2. Compute contravariant field from ideal MHD → b^ψ, b^θ, b^ζ +3. Return mode-space fields (can convert to real space later if needed) + +# Arguments + +- `response_vector::Vector{ComplexF64}`: Response coefficients [numpert_total] +- `ForceFreeStates_results::OdeState`: ForceFreeStates results with u_store eigenmodes +- `equil::Equilibrium.PlasmaEquilibrium`: Equilibrium data +- `ffs_intr::ForceFreeStatesInternal`: Mode information (m, n ranges) +- `intr::PerturbedEquilibriumInternal`: Internal state + +# Returns + +Tuple of (xi_modes, b_modes) where each is a NamedTuple: +- `xi_modes.psi`: ξ_ψ component [npsi, mpert] (covariant) +- `b_modes.psi`: b^ψ component [npsi, mpert] (contravariant) +- `b_modes.theta`: b^θ component [npsi, mpert] (contravariant) +- `b_modes.zeta`: b^ζ component [npsi, mpert] (contravariant) + +All in mode space, matching GPEC output format. + +# Notes + +- Uses ForceFreeStates radial grid (not equilibrium grid) +- Works entirely in mode space - no Fourier transforms +- Follows GPEC gpeq_sol, gpeq_contra formulation +- Field from ideal MHD: b^ψ = i*χ₁*(m-n*q)*ξ_ψ +""" +function reconstruct_physical_fields( + response_vector::Vector{ComplexF64}, + ForceFreeStates_results::OdeState, + equil::Equilibrium.PlasmaEquilibrium, + ffs_intr::ForceFreeStatesInternal, + intr::PerturbedEquilibriumInternal +) + # Get dimensions + npsi = size(ForceFreeStates_results.u_store, 4) + mpert = ffs_intr.mpert + + # Step 1: Sum weighted eigenmode contributions to get covariant ξ_ψ in mode space + xi_psi_modes = sum_eigenmode_contributions( + response_vector, + ForceFreeStates_results, + ffs_intr + ) + + # Step 2: Compute perturbed field in mode space using ideal MHD relations + # This mimics GPEC's gpeq_sol and gpeq_contra + b_psi_modes, b_theta_modes, b_zeta_modes = compute_perturbed_field_modes( + xi_psi_modes, + ForceFreeStates_results, + equil, + ffs_intr + ) + + # Package outputs in NamedTuples for clarity + xi_modes = ( + psi = xi_psi_modes, # [npsi, mpert] - covariant radial displacement + theta = zeros(ComplexF64, npsi, mpert), # Placeholder - not computed from ForceFreeStates + zeta = zeros(ComplexF64, npsi, mpert) # Placeholder - not computed from ForceFreeStates + ) + + b_modes = ( + psi = b_psi_modes, # [npsi, mpert] - contravariant radial field + theta = b_theta_modes, # [npsi, mpert] - contravariant poloidal field + zeta = b_zeta_modes # [npsi, mpert] - contravariant toroidal field + ) + + return xi_modes, b_modes +end + +""" + sum_eigenmode_contributions( + response_vector::Vector{ComplexF64}, + ForceFreeStates_results::OdeState, + ffs_intr::ForceFreeStatesInternal + ) -> xi_psi_modes + +Sum eigenmode contributions weighted by response coefficients. + +The response vector contains one coefficient for each (i,j) eigenmode pair. +This function weights each eigenmode by its coefficient and sums them to +get the total covariant radial displacement ξ_ψ in mode space. + +Mimics GPEC's approach where the DCON eigenmodes are weighted by the +plasma response to external forcing. + +# Arguments + +- `response_vector::Vector{ComplexF64}`: Response coefficient for each eigenmode [numpert_total] +- `ForceFreeStates_results::OdeState`: ForceFreeStates results with u_store containing eigenmodes +- `ffs_intr::ForceFreeStatesInternal`: Mode information (mpert, etc.) + +# Returns + +- `xi_psi_modes::Matrix{ComplexF64}`: Covariant radial displacement ξ_ψ(ψ, m) [npsi, mpert] + +# Notes + +- In ForceFreeStates formulation, u_store[:, :, 1, :] contains ξ_ψ (covariant radial displacement) +- The response vector is ordered as [mode1_mode1, mode1_mode2, ..., mode2_mode1, ...] +- We sum over all (i,j) eigenmode pairs to get the total response for each mode i +- This corresponds to xsp_mn in GPEC notation +""" +function sum_eigenmode_contributions( + response_vector::Vector{ComplexF64}, + ForceFreeStates_results::OdeState, + ffs_intr::ForceFreeStatesInternal +) + # Extract dimensions + numpert_total = length(response_vector) + npsi = size(ForceFreeStates_results.u_store, 4) + mpert = ffs_intr.mpert + + # Initialize output array (npsi × mpert) + # xsp_mn in GPEC notation (covariant radial displacement) + xi_psi_modes = zeros(ComplexF64, npsi, mpert) + + # Sum weighted eigenmode contributions + # For each eigenmode pair (i, j) in the response matrix + idx = 1 + for i in 1:mpert + for j in 1:mpert + if idx > numpert_total + break + end + + # Weight this eigenmode by its response coefficient + coeff = response_vector[idx] + + # Add this eigenmode's contribution to each radial point + # Accumulate into mode i (diagonal contribution) + for ipsi in 1:npsi + # u_store[i, j, component, radial_index] + # Component 1 = ξ_ψ (covariant radial displacement) + xi_psi_modes[ipsi, i] += coeff * ForceFreeStates_results.u_store[i, j, 1, ipsi] + end + + idx += 1 + end + end + + return xi_psi_modes +end + +""" + compute_perturbed_field_modes( + xi_psi_modes::Matrix{ComplexF64}, + ForceFreeStates_results::OdeState, + equil::Equilibrium.PlasmaEquilibrium, + ffs_intr::ForceFreeStatesInternal + ) -> (b_psi_modes, b_theta_modes, b_zeta_modes) + +Compute perturbed magnetic field from displacement using ideal MHD relations in mode space. + +This function mimics GPEC's gpeq_sol subroutine (gpeq.f lines 100-102), computing +contravariant field components from covariant displacement using the algebraic +ideal MHD relations in flux coordinates. + +# Ideal MHD Relations (from GPEC gpeq.f) + +```fortran +bwp_mn = (chi1*singfac*twopi*ifac*xsp_mn) ! b^ψ +bwt_mn = -(chi1*xsp1_mn + twopi*ifac*nn*xss_mn) ! b^θ +bwz_mn = -(chi1*(q1*xsp_mn + sq%f(4)*xsp1_mn) + twopi*ifac*mfac*xss_mn) ! b^ζ +``` + +where: +- xsp_mn = ξ_ψ (covariant radial displacement) +- xsp1_mn = ∂ξ_ψ/∂ψ (radial derivative) +- xss_mn = ξ_ζ (covariant toroidal displacement) +- singfac = m - n*q (resonance factor) +- chi1 = 2π*Ψ₀ (flux normalization) +- ifac = i (imaginary unit) + +# Arguments + +- `xi_psi_modes::Matrix{ComplexF64}`: Covariant radial displacement ξ_ψ(ψ,m) [npsi, mpert] +- `ForceFreeStates_results::OdeState`: ForceFreeStates results with psi_store for radial grid +- `equil::Equilibrium.PlasmaEquilibrium`: Equilibrium with q(ψ), q'(ψ), Ψ₀ +- `ffs_intr::ForceFreeStatesInternal`: Mode numbers (mlow, mhigh, n) + +# Returns + +Tuple of three matrices, all [npsi, mpert]: +- `b_psi_modes::Matrix{ComplexF64}`: Contravariant radial field b^ψ(ψ,m) +- `b_theta_modes::Matrix{ComplexF64}`: Contravariant poloidal field b^θ(ψ,m) +- `b_zeta_modes::Matrix{ComplexF64}`: Contravariant toroidal field b^ζ(ψ,m) + +# Notes + +- This is a simplified version assuming ξ_ζ = 0 (no toroidal displacement) +- Radial derivatives ∂ξ_ψ/∂ψ are computed using finite differences +- All calculations done in mode space (no Fourier transforms) +- Matches GPEC formulation exactly for consistency +""" +function compute_perturbed_field_modes( + xi_psi_modes::Matrix{ComplexF64}, + ForceFreeStates_results::OdeState, + equil::Equilibrium.PlasmaEquilibrium, + ffs_intr::ForceFreeStatesInternal +) + npsi, mpert = size(xi_psi_modes) + + # Initialize output arrays + b_psi_modes = zeros(ComplexF64, npsi, mpert) + b_theta_modes = zeros(ComplexF64, npsi, mpert) + b_zeta_modes = zeros(ComplexF64, npsi, mpert) + + # Get mode numbers + mlow = ffs_intr.mlow + nn = ffs_intr.nlow # Toroidal mode number + + # Normalization constant: chi1 = 2π * Ψ₀ + chi1 = 2π * equil.psio + twopi = 2π + ifac = im # Imaginary unit + + # Compute radial derivative of displacement using finite differences + # ∂ξ_ψ/∂ψ (xsp1_mn in GPEC) + xi_psi1_modes = zeros(ComplexF64, npsi, mpert) + for ipert in 1:mpert + for ipsi in 2:npsi-1 + # Centered difference + dpsi = ForceFreeStates_results.psi_store[ipsi+1] - ForceFreeStates_results.psi_store[ipsi-1] + xi_psi1_modes[ipsi, ipert] = (xi_psi_modes[ipsi+1, ipert] - xi_psi_modes[ipsi-1, ipert]) / dpsi + end + # Forward difference at axis + if npsi > 1 + dpsi = ForceFreeStates_results.psi_store[2] - ForceFreeStates_results.psi_store[1] + xi_psi1_modes[1, ipert] = (xi_psi_modes[2, ipert] - xi_psi_modes[1, ipert]) / dpsi + end + # Backward difference at edge + if npsi > 1 + dpsi = ForceFreeStates_results.psi_store[npsi] - ForceFreeStates_results.psi_store[npsi-1] + xi_psi1_modes[npsi, ipert] = (xi_psi_modes[npsi, ipert] - xi_psi_modes[npsi-1, ipert]) / dpsi + end + end + + # Compute field for each radial point and mode + for ipsi in 1:npsi + psi_norm = ForceFreeStates_results.psi_store[ipsi] + + # Get equilibrium quantities at this surface + q = equil.profiles.q_spline(psi_norm) # Safety factor q(ψ) + q1 = equil.profiles.q_deriv(psi_norm) # Derivative q'(ψ) = dq/dψ + + # Compute field for each poloidal mode + for ipert in 1:mpert + m = mlow + ipert - 1 # Poloidal mode number + + # Resonance factor: singfac = m - n*q + singfac = m - nn * q + + # Get displacement and derivative at this point + xsp = xi_psi_modes[ipsi, ipert] # ξ_ψ + xsp1 = xi_psi1_modes[ipsi, ipert] # ∂ξ_ψ/∂ψ + xss = 0.0 + 0.0im # ξ_ζ = 0 (not computed from ForceFreeStates) + + # GPEC gpeq.f line 100-102: Compute contravariant field + # b^ψ = i * χ₁ * (m - n*q) * ξ_ψ + b_psi_modes[ipsi, ipert] = chi1 * singfac * twopi * ifac * xsp + + # b^θ = -i * (χ₁ * ∂ξ_ψ/∂ψ + n * ξ_ζ) + b_theta_modes[ipsi, ipert] = -(chi1 * xsp1 + twopi * ifac * nn * xss) + + # b^ζ = -i * (χ₁ * (q'*ξ_ψ + q*∂ξ_ψ/∂ψ) + m * ξ_ζ) + b_zeta_modes[ipsi, ipert] = -(chi1 * (q1 * xsp + q * xsp1) + twopi * ifac * m * xss) + end + end + + return b_psi_modes, b_theta_modes, b_zeta_modes +end diff --git a/src/PerturbedEquilibrium/PerturbedEquilibrium.jl b/src/PerturbedEquilibrium/PerturbedEquilibrium.jl new file mode 100644 index 00000000..e38f1cfa --- /dev/null +++ b/src/PerturbedEquilibrium/PerturbedEquilibrium.jl @@ -0,0 +1,128 @@ +module PerturbedEquilibrium + +# Imports +using HDF5 +using Printf +using LinearAlgebra +using Statistics + +# Import parent modules +import ..Spl +import ..Equilibrium +import ..ForceFreeStates +import ..ForceFreeStates: OdeState, VacuumData, ForceFreeStatesInternal +import ..Vacuum +import ..ForcingTerms +import ..ForcingTerms: ForcingMode, load_forcing_data! +import ..Utilities +import ..Utilities.FourierTransforms +import DelimitedFiles: readdlm + +# Include module files +include("PerturbedEquilibriumStructs.jl") +include("ResponseMatrices.jl") +include("FieldReconstruction.jl") +include("Response.jl") +include("SingularCoupling.jl") +include("Utils.jl") + +# Export main types +export PerturbedEquilibriumControl +export PerturbedEquilibriumInternal +export PerturbedEquilibriumState +export ForcingMode + +# Export main functions +export compute_perturbed_equilibrium +export write_outputs_to_HDF5 + +""" + compute_perturbed_equilibrium( + equil::Equilibrium.PlasmaEquilibrium, + ForceFreeStates_results::OdeState, + vac_data::Union{VacuumData, Nothing}, + ffs_intr::ForceFreeStatesInternal, + ft_ctrl::ForcingTerms.ForcingTermsControl, + ctrl::PerturbedEquilibriumControl, + intr::PerturbedEquilibriumInternal + )::PerturbedEquilibriumState + +Main entry point for perturbed equilibrium calculations. + +Computes plasma response to external forcing and calculates singular layer +coupling metrics. + +## Arguments + - `equil`: Equilibrium solution from Equilibrium module + - `ForceFreeStates_results`: Stability calculation results from ForceFreeStates module + - `vac_data`: Vacuum response data from ForceFreeStates free boundary calculation + - `ffs_intr`: ForceFreeStates internal state with mode information + - `ft_ctrl`: Forcing terms control parameters from [ForcingTerms] section + - `ctrl`: Control parameters from [PerturbedEquilibrium] section + - `intr`: Internal state variables + +## Returns + - `PerturbedEquilibriumState`: Calculation results + +## Workflow +1. Load forcing data from file +2. Compute plasma response (if enabled) +3. Calculate singular coupling metrics (if enabled) +4. Output eigenmode fields (if enabled) +""" +function compute_perturbed_equilibrium( + equil::Equilibrium.PlasmaEquilibrium, + ForceFreeStates_results::OdeState, + vac_data::Union{VacuumData, Nothing}, + ffs_intr::ForceFreeStates.ForceFreeStatesInternal, + ft_ctrl::ForcingTerms.ForcingTermsControl, + ctrl::PerturbedEquilibriumControl, + intr::PerturbedEquilibriumInternal +)::PerturbedEquilibriumState + + if ctrl.verbose + println("\nPERTURBED EQUILIBRIUM START") + println("----------------------------------") + end + start_time = time() + + state = PerturbedEquilibriumState() + + # Step 0: Initialize mode arrays for convenient indexing + initialize_mode_arrays!(intr, ffs_intr) + + # Step 1: Load forcing data + load_forcing_data!(intr.forcing_modes, intr.dir_path, ft_ctrl.forcing_data_file, ft_ctrl.forcing_data_format, ctrl.verbose) + + # Step 2: Compute plasma response + if ctrl.compute_response + if vac_data === nothing + @warn "Vacuum data not available. Skipping plasma response calculation. Set vac_flag=true in [ForceFreeStates] section." + else + compute_plasma_response!(state, equil, ForceFreeStates_results, vac_data, ffs_intr, intr, ctrl) + end + end + + # Step 3: Compute singular coupling metrics + if ctrl.compute_singular_coupling + if vac_data === nothing + @warn "Vacuum data not available. Skipping singular coupling calculation. Set vac_flag=true in [ForceFreeStates] section." + else + compute_singular_coupling_metrics!(state, equil, ForceFreeStates_results, vac_data, ffs_intr, intr, ctrl) + end + end + + # Step 4: Output eigenmode fields (integrated into HDF5 output) + # This is handled by write_outputs_to_HDF5 in main() + + end_time = time() - start_time + if ctrl.verbose + println("----------------------------------") + println("Run time: $(@sprintf("%.3e", end_time)) seconds") + println("PERTURBED EQUILIBRIUM COMPLETE") + end + + return state +end + +end # module PerturbedEquilibrium diff --git a/src/PerturbedEquilibrium/PerturbedEquilibriumStructs.jl b/src/PerturbedEquilibrium/PerturbedEquilibriumStructs.jl new file mode 100644 index 00000000..f3ebc202 --- /dev/null +++ b/src/PerturbedEquilibrium/PerturbedEquilibriumStructs.jl @@ -0,0 +1,123 @@ +""" + PerturbedEquilibriumControl + +User-facing control parameters from TOML [PerturbedEquilibrium] section. + +## Fields + +Note: Forcing data file settings are now in [ForcingTerms] section. + +High Priority (MWE): + - `fixed_boundary::Bool` - Fixed boundary flag (default: false) + - `output_eigenmodes::Bool` - Output mode fields as b-fields (default: true) + - `compute_response::Bool` - Compute plasma response (default: true) + - `compute_singular_coupling::Bool` - Compute singular coupling metrics (default: true) + - `verbose::Bool` - Enable verbose logging (default: true) + +Output Settings: + - `output_filename::String` - Combined output file with ForceFreeStates results (default: uses ForceFreeStates HDF5_filename) + - `write_outputs_to_HDF5::Bool` - Write perturbed equilibrium outputs to HDF5 (default: true) + +Medium Priority (defer for MWE): + - `filter_modes::Bool` - Enable mode filtering (default: false) + - `singular_point_method::String` - Method for singular point treatment (default: "standard") +""" +@kwdef mutable struct PerturbedEquilibriumControl + # High Priority (MWE) + fixed_boundary::Bool = false + output_eigenmodes::Bool = true + compute_response::Bool = true + compute_singular_coupling::Bool = true + verbose::Bool = true + + # Output settings + output_filename::String = "" # Empty means use ForceFreeStates filename + write_outputs_to_HDF5::Bool = true + + # Medium Priority (include but simple for MWE) + filter_modes::Bool = false + singular_point_method::String = "standard" +end + +""" + PerturbedEquilibriumInternal + +Internal state variables for perturbed equilibrium calculations. + +## Fields + + - `dir_path::String` - Working directory path + - `forcing_modes::Vector{ForcingMode}` - Loaded forcing mode data + - `plasma_response::Matrix{ComplexF64}` - Plasma response matrix + - `singular_coupling_metrics::Dict{String,Float64}` - Coupling metrics at singular surfaces + - `m_modes::Vector{Int}` - Poloidal mode numbers for each index i in 1:numpert_total + - `n_modes::Vector{Int}` - Toroidal mode numbers for each index i in 1:numpert_total +""" +@kwdef mutable struct PerturbedEquilibriumInternal + dir_path::String = "./" + forcing_modes::Vector{ForcingMode} = ForcingMode[] + plasma_response::Matrix{ComplexF64} = zeros(ComplexF64, 0, 0) + singular_coupling_metrics::Dict{String,Float64} = Dict{String,Float64}() + m_modes::Vector{Int} = Int[] + n_modes::Vector{Int} = Int[] +end + +""" + PerturbedEquilibriumState + +Results from perturbed equilibrium calculations. + +## Fields + +Response fields (mode space): + - `xi_modes::Union{Nothing, NamedTuple}` - Displacement (psi, theta, zeta) [npsi, mpert] + - `b_modes::Union{Nothing, NamedTuple}` - Magnetic field (psi, theta, zeta) [npsi, mpert] + +Singular coupling matrices [msing, numpert_total]: + - `resonant_flux::Matrix{ComplexF64}` - Resonant flux Φ_r/A (singcoup(1,:,:)) + - `resonant_current::Matrix{ComplexF64}` - Resonant current (singcoup(2,:,:)) + - `island_width_sq::Matrix{ComplexF64}` - (w/2)² in ψ_n (singcoup(3,:,:)) + - `penetrated_field::Matrix{ComplexF64}` - Resonant field (singcoup(4,:,:)) + - `delta_prime::Matrix{ComplexF64}` - Tearing stability Δ' (singcoup(5,:,:)) + +Diagnostic quantities [msing]: + - `island_half_width::Vector{Float64}` - Actual w/2 (meters or ψ_n) + - `chirikov_parameter::Vector{Float64}` - Island overlap metric + +Note: numpert_total = mpert × npert handles all (m,n) mode combinations + +Legacy fields (deprecated): + - `coupling_coefficient::ComplexF64` - Use singular coupling matrices instead + - `resonant_amplitude::Float64` - Use island diagnostics instead + +Energies: + - `plasma_energy::Float64` - Plasma perturbation energy + - `vacuum_energy::Float64` - Vacuum perturbation energy + - `total_energy::Float64` - Total perturbation energy +""" +@kwdef mutable struct PerturbedEquilibriumState + # Response fields in mode space [npsi, mpert] following GPEC notation + # NamedTuples contain (psi, theta, zeta) components in flux coordinates + xi_modes::Union{Nothing, NamedTuple} = nothing + b_modes::Union{Nothing, NamedTuple} = nothing + + # Singular coupling matrices [msing × mpert] - GPEC singcoup(1:5,:,:) + resonant_flux::Matrix{ComplexF64} = zeros(ComplexF64, 0, 0) # singcoup(1,:,:) + resonant_current::Matrix{ComplexF64} = zeros(ComplexF64, 0, 0) # singcoup(2,:,:) + island_width_sq::Matrix{ComplexF64} = zeros(ComplexF64, 0, 0) # singcoup(3,:,:) + penetrated_field::Matrix{ComplexF64} = zeros(ComplexF64, 0, 0) # singcoup(4,:,:) + delta_prime::Matrix{ComplexF64} = zeros(ComplexF64, 0, 0) # singcoup(5,:,:) + + # Diagnostic quantities [msing] + island_half_width::Vector{Float64} = Float64[] # Actual w/2 (meters or ψ_n) + chirikov_parameter::Vector{Float64} = Float64[] # Island overlap + + # Legacy singular coupling results (deprecated - use matrices above) + coupling_coefficient::ComplexF64 = 0.0 + 0.0im + resonant_amplitude::Float64 = 0.0 + + # Energies + plasma_energy::Float64 = 0.0 + vacuum_energy::Float64 = 0.0 + total_energy::Float64 = 0.0 +end diff --git a/src/PerturbedEquilibrium/Response.jl b/src/PerturbedEquilibrium/Response.jl new file mode 100644 index 00000000..1aea79c1 --- /dev/null +++ b/src/PerturbedEquilibrium/Response.jl @@ -0,0 +1,94 @@ +""" + compute_plasma_response!( + state::PerturbedEquilibriumState, + equil::Equilibrium.PlasmaEquilibrium, + ForceFreeStates_results::OdeState, + vac_data::VacuumData, + ffs_intr::ForceFreeStatesInternal, + intr::PerturbedEquilibriumInternal, + ctrl::PerturbedEquilibriumControl + ) + +Compute plasma response to external forcing using ForceFreeStates eigenmode solutions. + +Implements resp_index=0 calculation from gpresp.f: +1. Build flux matrix from eigenmodes +2. Calculate plasma inductance (energy-based) +3. Calculate surface inductance from Green's function +4. Compute permeability matrix +5. Apply forcing to get response +""" +function compute_plasma_response!( + state::PerturbedEquilibriumState, + equil::Equilibrium.PlasmaEquilibrium, + ForceFreeStates_results::OdeState, + vac_data::VacuumData, + ffs_intr::ForceFreeStatesInternal, + intr::PerturbedEquilibriumInternal, + ctrl::PerturbedEquilibriumControl +) + if ctrl.verbose + println("Computing plasma response using resp_index=0 (energy-based inductance)") + end + + # Build flux matrix from ForceFreeStates eigenmodes + if ctrl.verbose + println(" Building flux matrix from eigenmodes") + end + flux_matrix = build_flux_matrix(equil, ForceFreeStates_results, vac_data, ffs_intr) + + # Calculate plasma inductance matrix + if ctrl.verbose + println(" Calculating plasma inductance matrix") + end + plasma_inductance = calc_plasma_inductance(flux_matrix, vac_data.et) + + # Calculate surface inductance from Green's functions + if ctrl.verbose + println(" Calculating surface inductance from Green's functions") + end + surface_inductance = calc_surface_inductance(vac_data.grri, vac_data.grre, flux_matrix, ffs_intr) + + # Calculate permeability matrix + if ctrl.verbose + println(" Calculating permeability matrix") + end + permeability = calc_permeability(plasma_inductance, surface_inductance) + + # Store permeability in internal state for later use + intr.plasma_response = permeability + + # Map forcing modes to eigenmode basis + if ctrl.verbose + println(" Mapping forcing modes to eigenmode basis") + println(" Number of forcing modes: $(length(intr.forcing_modes))") + end + forcing_vector = map_forcing_to_eigenmodes(intr.forcing_modes, ffs_intr) + + # Compute plasma response + if ctrl.verbose + println(" Computing response = permeability * forcing") + end + response_vector = compute_plasma_response_vector(permeability, forcing_vector) + + # Reconstruct physical fields from response in mode space + if ctrl.verbose + println(" Reconstructing physical fields from response (mode space)") + end + + xi_modes, b_modes = reconstruct_physical_fields( + response_vector, ForceFreeStates_results, equil, ffs_intr, intr + ) + + state.xi_modes = xi_modes + state.b_modes = b_modes + + if ctrl.verbose + println(" Response calculation complete") + println(" Response vector size: $(length(response_vector))") + println(" Max response amplitude: $(maximum(abs.(response_vector)))") + println(" Mode-space field dimensions: $(size(xi_modes.psi))") + println(" ξ_ψ max amplitude: $(maximum(abs.(xi_modes.psi)))") + println(" b^ψ max amplitude: $(maximum(abs.(b_modes.psi)))") + end +end diff --git a/src/PerturbedEquilibrium/ResponseMatrices.jl b/src/PerturbedEquilibrium/ResponseMatrices.jl new file mode 100644 index 00000000..abc5d569 --- /dev/null +++ b/src/PerturbedEquilibrium/ResponseMatrices.jl @@ -0,0 +1,564 @@ +""" +Response matrix construction for perturbed equilibrium calculations. + +Based on gpresp.f from GPEC, implementing resp_index=0 (energy-based inductance). +Uses ForceFreeStates eigenmode solutions and vacuum response data. +""" + +# Use FourierTransform utility instead of FFTW for theta ↔ mode transforms +using ..Utilities.FourierTransforms + +""" + extract_boundary_displacements( + equil::Equilibrium.PlasmaEquilibrium, + ForceFreeStates_results::OdeState, + intr::ForceFreeStatesInternal + )::NamedTuple + +Extract eigenmode displacements and equilibrium quantities at the plasma boundary. + +This function extracts the data needed to compute the normal magnetic field at the +plasma surface from ForceFreeStates eigenmode solutions. + +## What's extracted: + +1. **Boundary displacement**: ξ_ψ from `u_store[:, :, 1, end]` + - This is the radial (normal) component of the eigenmode displacement + - At the last radial integration point (plasma edge) + - Dimensions: [numpert_total, numpert_total] + +2. **Flux surface spacing**: dΨ/dρ at boundary + - From equilibrium bicubic spline evaluation + - Needed to convert displacement to magnetic field + +3. **Safety factor**: q at boundary + - Used to compute singular factors (m - n*q) + - Identifies resonant surfaces + +## Arguments +- `equil`: Equilibrium solution containing flux surfaces and q-profile +- `ForceFreeStates_results`: ODE integration results containing u_store with eigenmodes +- `intr`: ForceFreeStates internal state with boundary location (psilim) + +## Returns +Named tuple with: +- `ξ_psi_boundary`: Boundary displacement [numpert_total, numpert_total] +- `dPsi_drho`: Flux surface spacing at boundary (scalar) +- `q_boundary`: Safety factor at boundary (scalar) +- `psi_boundary`: Normalized flux at boundary (scalar) +""" +function extract_boundary_displacements( + equil::Equilibrium.PlasmaEquilibrium, + ForceFreeStates_results::OdeState, + intr::ForceFreeStatesInternal +) + # Extract boundary displacement (normal component) + # u_store dimensions: [numpert_total, numpert_total, 2, numsteps] + # Index 1 in 3rd dimension is ξ_ψ (radial displacement) + # Last index in 4th dimension is the boundary + ξ_psi_boundary = ForceFreeStates_results.u_store[:, :, 1, ForceFreeStates_results.step] + + # Get boundary location in normalized flux coordinates + psi_boundary = ForceFreeStates_results.psi_store[ForceFreeStates_results.step] + + # Evaluate equilibrium quantities at boundary + # Safety factor at boundary + q_boundary = ForceFreeStates_results.q_store[ForceFreeStates_results.step] + + # Flux surface spacing dΨ/dρ + # In ForceFreeStates, ρ = √ψ where ψ is normalized poloidal flux + # The actual poloidal flux is Ψ = ψ * psio + # Therefore: + # dΨ/dψ = psio + # dψ/dρ = 2ρ = 2√ψ + # dΨ/dρ = (dΨ/dψ) * (dψ/dρ) = psio * 2√ψ + dPsi_drho = equil.psio * 2.0 * sqrt(psi_boundary) + + return ( + ξ_psi_boundary = ξ_psi_boundary, + dPsi_drho = dPsi_drho, + q_boundary = q_boundary, + psi_boundary = psi_boundary + ) +end + +""" + compute_normal_magnetic_field( + boundary_data::NamedTuple, + intr::ForceFreeStatesInternal + )::Matrix{ComplexF64} + +Compute normal magnetic field at plasma boundary from eigenmode displacements. + +This is the key step that converts eigenmode displacements to magnetic field perturbations +at the plasma surface. Formula from GPEC: + + bwp_mn[i,j] = i * (dΨ/dρ) * (m[i] - n*q_boundary) * ξ_ψ[i,j] + +## Physical Interpretation: +- ξ_ψ[i,j]: Displacement of mode i due to eigenmode j +- singfac[i] = m[i] - n*q: Measures distance from rational surface +- dΨ/dρ: Converts displacement to flux perturbation +- Factor of i: Phase relationship for oscillating fields + +## Arguments +- `boundary_data`: Output from extract_boundary_displacements() + - ξ_psi_boundary: Boundary displacement [numpert_total, numpert_total] + - dPsi_drho: Flux surface spacing at boundary (scalar) + - q_boundary: Safety factor at boundary (scalar) + - psi_boundary: Normalized flux at boundary (scalar) +- `intr`: ForceFreeStates internal state with mode arrays (mlow, mhigh, nlow, etc.) + +## Returns +- `bwp_mn[numpert_total, numpert_total]`: Normal magnetic field matrix where bwp_mn[i,j] + is the normal field of Fourier mode i in response to eigenmode j +""" +function compute_normal_magnetic_field( + boundary_data::NamedTuple, + intr::ForceFreeStatesInternal +)::Matrix{ComplexF64} + + numpert_total = intr.numpert_total + bwp_mn = zeros(ComplexF64, numpert_total, numpert_total) + + # Extract boundary data + ξ_psi = boundary_data.ξ_psi_boundary + dPsi_drho = boundary_data.dPsi_drho + q_boundary = boundary_data.q_boundary + + # Compute singular factor for each Fourier mode: singfac[i] = m[i] - n*q_boundary + # Mode indexing: modes are ordered as (m, n) pairs + # Linear index i corresponds to: m = (i-1) % mpert + mlow, n = (i-1) ÷ mpert + nlow + singfac = zeros(Float64, numpert_total) + for i in 1:numpert_total + m_mode = (i - 1) % intr.mpert + intr.mlow + n_mode = (i - 1) ÷ intr.mpert + intr.nlow + singfac[i] = m_mode - n_mode * q_boundary + end + + # Compute normal magnetic field: bwp_mn[i,j] = i * (dΨ/dρ) * singfac[i] * ξ_ψ[i,j] + for i in 1:numpert_total + for j in 1:numpert_total + bwp_mn[i, j] = 1im * dPsi_drho * singfac[i] * ξ_psi[i, j] + end + end + + return bwp_mn +end + +""" + build_flux_matrix( + equil::Equilibrium.PlasmaEquilibrium, + ForceFreeStates_results::OdeState, + vac_data::VacuumData, + intr::ForceFreeStatesInternal + )::Matrix{ComplexF64} + +Build vacuum poloidal flux matrix from ForceFreeStates eigenmode solutions. + +This extracts the vacuum flux response for each eigenmode at the plasma boundary. +In GPEC, this comes from `bwp_mn` (boundary normal field) computed from eigenmode +displacements. + +The flux matrix relates eigenmode displacements to vacuum poloidal flux: +1. Extract eigenmode displacement at plasma boundary from u_store +2. Compute normal magnetic field: B_ψ = i×(dΨ/dρ)×(m - n×q)×ξ_ψ +3. Result is flux[mode_i, eigenmode_j] = bwp_mn[i,j] + +## Arguments +- `equil`: Equilibrium solution containing flux surfaces and q-profile +- `ForceFreeStates_results`: ForceFreeStates ODE integration results containing eigenmodes +- `vac_data`: Vacuum response data from free boundary calculation +- `intr`: ForceFreeStates internal state with mode information + +## Returns +- `flxmats[numpert_total, numpert_total]`: Complex flux matrix where flxmats[i,j] is the + vacuum flux of mode i in response to eigenmode j +""" +function build_flux_matrix( + equil::Equilibrium.PlasmaEquilibrium, + ForceFreeStates_results::OdeState, + vac_data::VacuumData, + intr::ForceFreeStatesInternal +)::Matrix{ComplexF64} + + # Step 1: Extract boundary displacements and equilibrium quantities + boundary_data = extract_boundary_displacements(equil, ForceFreeStates_results, intr) + + # Step 2: Compute normal magnetic field at plasma boundary + # This is the actual implementation of GPEC's bwp_mn calculation + # bwp_mn[i,j] = i * (dΨ/dρ) * (m[i] - n*q_boundary) * ξ_ψ[i,j] + flxmats = compute_normal_magnetic_field(boundary_data, intr) + + return flxmats +end + +""" + calc_plasma_inductance( + flux_matrix::Matrix{ComplexF64}, + energy_vector::Vector{ComplexF64} + )::Matrix{ComplexF64} + +Calculate plasma inductance matrix using energy-based formula (resp_index=0). + +Formula from gpresp.f lines 214-226: +``` +L[i,j] = Σ_k flux[i,k] * conj(flux[j,k]) / (et[k] * 2) +``` + +## Arguments +- `flux_matrix`: Vacuum flux matrix from build_flux_matrix +- `energy_vector`: Total energy for each eigenmode (vac_data.et) + +## Returns +- Plasma inductance matrix [mpert, mpert] +""" +function calc_plasma_inductance( + flux_matrix::Matrix{ComplexF64}, + energy_vector::Vector{ComplexF64} +)::Matrix{ComplexF64} + + mpert = size(flux_matrix, 1) + L = zeros(ComplexF64, mpert, mpert) + + for i in 1:mpert + for j in 1:mpert + for k in 1:mpert + if abs(energy_vector[k]) > 1e-10 # Avoid division by zero + L[i,j] += flux_matrix[i,k] * conj(flux_matrix[j,k]) / (energy_vector[k] * 2.0) + end + end + end + end + + return L +end + +""" + pack_complex_to_realimag(modes::Vector{ComplexF64})::Vector{Float64} + +Pack complex mode coefficients into real/imaginary pairs for Green's function application. + +Converts [a+bi, c+di, ...] to [a, b, c, d, ...] + +## Arguments +- `modes`: Complex Fourier mode coefficients [mpert] + +## Returns +- Packed real/imaginary array [2*mpert] +""" +function pack_complex_to_realimag(modes::Vector{ComplexF64})::Vector{Float64} + mpert = length(modes) + packed = zeros(Float64, 2 * mpert) + for i in 1:mpert + packed[2*i - 1] = real(modes[i]) + packed[2*i] = imag(modes[i]) + end + return packed +end + +""" + unpack_realimag_to_complex(packed::Vector{Float64})::Vector{ComplexF64} + +Unpack real/imaginary pairs back to complex coefficients. + +Converts [a, b, c, d, ...] to [a+bi, c+di, ...] + +## Arguments +- `packed`: Real/imaginary pairs [2*mpert] + +## Returns +- Complex mode coefficients [mpert] +""" +function unpack_realimag_to_complex(packed::Vector{Float64})::Vector{ComplexF64} + mpert = length(packed) ÷ 2 + modes = zeros(ComplexF64, mpert) + for i in 1:mpert + modes[i] = packed[2*i - 1] + 1im * packed[2*i] + end + return modes +end + +""" + apply_green_function( + green::Matrix{Float64}, + mode_coeffs::Vector{ComplexF64} + )::Vector{Float64} + +Apply Green's function to Fourier mode coefficients to get potential in theta space. + +The Green's function matrix maps Fourier coefficients to theta-space values: + χ(θ) = G * b_fourier + +## Green's Function Structure + +From ForceFreeStates vacuum calculation, `green` is a real matrix [2*mtheta, 2*mpert] where: +- Rows 1:mtheta correspond to plasma surface theta points +- Rows mtheta+1:2*mtheta correspond to wall surface theta points (if wall present) +- Columns are packed as [Re(mode_1), Im(mode_1), Re(mode_2), Im(mode_2), ...] + +## Arguments +- `green`: Green's function matrix [2*mtheta, 2*mpert] +- `mode_coeffs`: Complex Fourier mode coefficients [mpert] + +## Returns +- Potential values in theta space [mtheta] at plasma surface (real-valued) +""" +function apply_green_function( + green::Matrix{Float64}, + mode_coeffs::Vector{ComplexF64} +)::Vector{Float64} + # Pack complex coefficients to real/imag format for Green's function + # Format: [Re(mode_1), Im(mode_1), Re(mode_2), Im(mode_2), ...] + packed_coeffs = pack_complex_to_realimag(mode_coeffs) + + # Apply Green's function: chi_theta = green * b_fourier + # Extract only plasma surface rows (first mtheta rows) + # Result is real-valued potential at theta points + mtheta = size(green, 1) ÷ 2 + chi_theta = green[1:mtheta, :] * packed_coeffs + + return chi_theta +end + + +""" + calc_surface_inductance( + grri::Matrix{Float64}, + grre::Matrix{Float64}, + flux_matrix::Matrix{ComplexF64}, + intr::ForceFreeStatesInternal +)::Matrix{ComplexF64} + +Calculate surface/vacuum inductance matrix from Green's functions using GPEC algorithm. + +Uses BOTH Green's function matrices computed during ForceFreeStates vacuum calculation: +- grri: Interior potential (kernelsign=-1) +- grre: Exterior potential (kernelsign=+1) + +## GPEC Algorithm: +1. For each eigenmode, apply Green's functions to flux matrix (bwp_mn) +2. Compute interior potential: chi_mn = grri * bwp_mn +3. Compute exterior potential: che_mn = grre * bwp_mn +4. Calculate surface current: kax_mn = (chi_mn - che_mn) / μ₀ +5. Transform back to mode space using Fourier transform +6. Solve: surf_indmats = hermitianize(flxmats * inv(kaxmats)) + +Green's function structure from ForceFreeStates: +- Dimensions: [2*mtheta, 2*mpert] +- Complex numbers stored as adjacent real/imaginary pairs +- Maps Fourier coefficients to theta-space potential values + +## Arguments +- `grri`: Interior Green's function matrix (kernelsign=-1) +- `grre`: Exterior Green's function matrix (kernelsign=+1) +- `flux_matrix`: Normal magnetic field matrix (bwp_mn) [numpert_total, numpert_total] +- `intr`: ForceFreeStates internal state with mode information + +## Returns +- Surface inductance matrix [numpert_total, numpert_total] +""" +function calc_surface_inductance( + grri::Matrix{Float64}, + grre::Matrix{Float64}, + flux_matrix::Matrix{ComplexF64}, + intr::ForceFreeStatesInternal +)::Matrix{ComplexF64} + + numpert_total = intr.numpert_total + mpert = intr.mpert + npert = intr.npert + mtheta = size(grri, 1) ÷ 2 + + # Physical constant + μ₀ = 4π * 1e-7 + + # Create Fourier transform object for theta ↔ mode conversions + # This pre-computes basis functions cos(m*θ), sin(m*θ) for efficient reuse + # No phase shift needed since we're working in magnetic coordinates + ft = FourierTransform(mtheta, mpert, intr.mlow) + + # Initialize matrices for surface current in mode space + kax_matrix = zeros(ComplexF64, numpert_total, numpert_total) + + # Initialize surface inductance matrix (declare outside try-catch for scoping) + surf_ind = zeros(ComplexF64, numpert_total, numpert_total) + + # For each eigenmode j, compute surface current in all modes + for j in 1:numpert_total + # Extract normal field for eigenmode j (only poloidal modes for this toroidal n) + # Assuming single toroidal mode for now (npert == 1) + if npert > 1 + @warn "calc_surface_inductance: Multiple toroidal modes not yet supported, using n=1 only" maxlog=1 + end + + # Extract poloidal mode coefficients for this eigenmode + # For n=1: indices 1:mpert correspond to the poloidal modes + mode_start = 1 + mode_end = min(mpert, numpert_total) + bwp_modes = flux_matrix[mode_start:mode_end, j] + + # Apply interior Green's function to get χ(θ) at plasma surface + chi_theta = apply_green_function(grri, bwp_modes) + + # Apply exterior Green's function to get χ_e(θ) at plasma surface + che_theta = apply_green_function(grre, bwp_modes) + + # Compute surface current in theta space: kax(θ) = (χ - χ_e) / μ₀ + # This is the jump in potential across the plasma surface + kax_theta = (chi_theta .- che_theta) ./ μ₀ + + # Transform back to Fourier mode space using pre-computed FourierTransform + # ft(data) performs forward transform: theta → modes + kax_modes = ft(kax_theta) + + # Store in kax_matrix for this eigenmode + kax_matrix[mode_start:mode_end, j] = kax_modes + end + + # Compute surface inductance: L_surf = flxmats * inv(kaxmats) + # Use pseudo-inverse for numerical stability + try + # Regularization: add small diagonal term to improve conditioning + regularization = 1e-10 * maximum(abs.(kax_matrix)) + kax_reg = kax_matrix + regularization * I + + surf_ind = flux_matrix * inv(kax_reg) + + # Hermitianize to ensure physical inductance matrix + surf_ind = 0.5 * (surf_ind + surf_ind') + catch e + @warn "Surface inductance inversion failed: $e. Using fallback method." + # Fallback: use correlation method + surf_ind = zeros(ComplexF64, numpert_total, numpert_total) + for i in 1:mpert + for j in 1:mpert + correlation = 0.0 + 0.0im + for k in 1:mtheta + # Simple correlation of Green's function differences + chi_i = grri[k, 2*i-1] + 1im * grri[k, 2*i] + che_i = grre[k, 2*i-1] + 1im * grre[k, 2*i] + chi_j = grri[k, 2*j-1] + 1im * grri[k, 2*j] + che_j = grre[k, 2*j-1] + 1im * grre[k, 2*j] + jump_i = chi_i - che_i + jump_j = chi_j - che_j + correlation += jump_i * conj(jump_j) + end + surf_ind[i, j] = μ₀ * correlation / mtheta + end + end + surf_ind = 0.5 * (surf_ind + surf_ind') + end + + return surf_ind +end + +""" + calc_permeability( + plasma_inductance::Matrix{ComplexF64}, + surface_inductance::Matrix{ComplexF64} + )::Matrix{ComplexF64} + +Calculate permeability matrix relating applied forcing to plasma response. + +Formula: μ = L_plasma^(-1) * L_surface + +This gives the linear response of the plasma to external perturbations. + +## Arguments +- `plasma_inductance`: Plasma inductance matrix +- `surface_inductance`: Surface inductance matrix + +## Returns +- Permeability matrix [mpert, mpert] +""" +function calc_permeability( + plasma_inductance::Matrix{ComplexF64}, + surface_inductance::Matrix{ComplexF64} +)::Matrix{ComplexF64} + + # Invert plasma inductance and multiply by surface inductance + # μ = L_plasma^(-1) * L_surface + + try + plas_inv = inv(plasma_inductance) + permeability = plas_inv * surface_inductance + return permeability + catch e + @warn "Failed to invert plasma inductance matrix: $e" + # Return identity as fallback + return Matrix{ComplexF64}(I, size(plasma_inductance)) + end +end + +""" + map_forcing_to_eigenmodes( + forcing_modes::Vector{ForcingMode}, + intr::ForceFreeStatesInternal + )::Vector{ComplexF64} + +Map external forcing modes to eigenmode basis. + +Matches forcing mode numbers (n,m) to the eigenmode basis used in ForceFreeStates +and creates a forcing vector in that basis. + +## Arguments +- `forcing_modes`: External forcing modes from input file +- `intr`: ForceFreeStates internal state with mode arrays + +## Returns +- Forcing vector in eigenmode basis [mpert] +""" +function map_forcing_to_eigenmodes( + forcing_modes::Vector{ForcingMode}, + intr::ForceFreeStatesInternal +)::Vector{ComplexF64} + + numpert_total = intr.mpert * intr.npert + forcing_vector = zeros(ComplexF64, numpert_total) + + # Create mode index map: (m,n) -> linear index + for forcing_mode in forcing_modes + # Find matching mode in eigenmode basis + for i in 1:numpert_total + # Calculate m and n for this index + # Using 0-based indexing converted to 1-based: + # m = (i-1) % mpert + mlow + # n = (i-1) ÷ mpert + nlow + m_mode = (i - 1) % intr.mpert + intr.mlow + n_mode = (i - 1) ÷ intr.mpert + intr.nlow + + if m_mode == forcing_mode.m && n_mode == forcing_mode.n + forcing_vector[i] = forcing_mode.amplitude + break + end + end + end + + return forcing_vector +end + +""" + compute_plasma_response_vector( + permeability::Matrix{ComplexF64}, + forcing_vector::Vector{ComplexF64} + )::Vector{ComplexF64} + +Compute plasma response to external forcing. + +Response = Permeability * Forcing + +## Arguments +- `permeability`: Permeability matrix +- `forcing_vector`: External forcing in eigenmode basis + +## Returns +- Plasma response vector in eigenmode basis +""" +function compute_plasma_response_vector( + permeability::Matrix{ComplexF64}, + forcing_vector::Vector{ComplexF64} +)::Vector{ComplexF64} + + return permeability * forcing_vector +end diff --git a/src/PerturbedEquilibrium/SingularCoupling.jl b/src/PerturbedEquilibrium/SingularCoupling.jl new file mode 100644 index 00000000..99d25c70 --- /dev/null +++ b/src/PerturbedEquilibrium/SingularCoupling.jl @@ -0,0 +1,867 @@ +""" +Singular surface coupling and field calculations. + +Implements GPEC's singular surface analysis (gpout.f lines 585-665, 1700-1810): +- Delta' (tearing stability parameter) +- Resonant currents +- Island half-widths +- Chirikov parameters (island overlap) +- Coupling matrices + +Reference: ~/Code/gpec/gpec/gpout.f +""" + +""" + compute_singular_coupling_metrics!( + state::PerturbedEquilibriumState, + equil::Equilibrium.PlasmaEquilibrium, + ForceFreeStates_results::OdeState, + vac_data::VacuumData, + ffs_intr::ForceFreeStatesInternal, + intr::PerturbedEquilibriumInternal, + ctrl::PerturbedEquilibriumControl + ) + +Compute singular layer coupling metrics and island diagnostics. + +Calculates the coupling between forcing modes and resonant surfaces, which determines +the effectiveness of external perturbations at rational surfaces where q = m/n. + +Implements GPEC algorithm from gpout.f: +1. For each forcing mode, apply unit field and compute plasma response +2. For each singular surface, evaluate field jump and compute coupling quantities +3. Calculate diagnostic island widths and overlap parameters + +## Arguments +- `state`: Output state structure to store results +- `equil`: Equilibrium solution with q-profile and flux surfaces +- `ForceFreeStates_results`: ForceFreeStates stability calculation results +- `vac_data`: Vacuum response data including Green's functions +- `ffs_intr`: ForceFreeStates internal state with singular surface data +- `intr`: PerturbedEquilibrium internal state with permeability matrix +- `ctrl`: Control parameters + +## Modifies +Populates the following fields in `state`: +- `resonant_flux[msing, numpert_total]`: Normalized resonant flux Φ_r/A +- `resonant_current[msing, numpert_total]`: Resonant current density +- `island_width_sq[msing, numpert_total]`: Square of island half-width (w/2)² +- `penetrated_field[msing, numpert_total]`: Normal field at resonant surface +- `delta_prime[msing, numpert_total]`: Tearing stability parameter Δ' +- `island_half_width[msing]`: Dimensional island half-width +- `chirikov_parameter[msing]`: Island overlap metric + +Note: numpert_total = mpert × npert handles all (m,n) mode combinations +""" +function compute_singular_coupling_metrics!( + state::PerturbedEquilibriumState, + equil::Equilibrium.PlasmaEquilibrium, + ForceFreeStates_results::OdeState, + vac_data::VacuumData, + ffs_intr::ForceFreeStatesInternal, + intr::PerturbedEquilibriumInternal, + ctrl::PerturbedEquilibriumControl +) + if ctrl.verbose + println("Computing singular coupling metrics (GPEC method)") + end + + # Extract dimensions + msing = ffs_intr.msing + mpert = ffs_intr.mpert + npert = ffs_intr.npert + numpert_total = ffs_intr.numpert_total + + if msing == 0 + if ctrl.verbose + println(" No singular surfaces found. Skipping singular coupling calculation.") + end + return + end + + # Initialize output arrays [npert, msing] + # For single n: these collapse to [msing] vectors + # For multiple n: [npert, msing] matrices with zeros for non-resonant combinations + state.resonant_flux = zeros(ComplexF64, npert, msing) + state.resonant_current = zeros(ComplexF64, npert, msing) + state.island_width_sq = zeros(ComplexF64, npert, msing) + state.penetrated_field = zeros(ComplexF64, npert, msing) + state.delta_prime = zeros(ComplexF64, npert, msing) + state.island_half_width = zeros(Float64, msing) + state.chirikov_parameter = zeros(Float64, msing) + + # Physical constants + chi1 = 2π * equil.psio # Flux normalization + twopi = 2π + μ₀ = 4π * 1e-7 + + # Get permeability matrix from internal state + permeability = intr.plasma_response + if size(permeability, 1) == 0 + @warn "Permeability matrix not computed. Skipping singular coupling." + return + end + + if ctrl.verbose + println(" Number of singular surfaces: $msing") + println(" Number of toroidal modes (n): $npert") + if npert > 1 + println(" Metric arrays: [$npert × $msing]") + else + println(" Metric arrays: [$msing] (single n)") + end + end + + # Get vacuum calculation parameters + mtheta_eq = length(equil.rzphi_ys) # Equilibrium poloidal grid size + mtheta = vac_data.mthvac # Vacuum poloidal grid size + mlow = ffs_intr.mlow + nlow = ffs_intr.nlow + nhigh = ffs_intr.nhigh + + # Perturbed equilibrium calculations always use nowall + wall_settings = Vacuum.WallShapeSettings(shape="nowall") + + if ctrl.verbose + println(" Processing toroidal modes n = $nlow:$nhigh") + end + + # Main loop: Process each toroidal mode number separately + for nn in nlow:nhigh + n_idx = nn - nlow + 1 # Index for storing in [npert, msing] arrays + + if ctrl.verbose + println(" Computing metrics for n = $nn...") + end + + # For each singular surface, compute Green's functions for this n + # and calculate metrics if surface is resonant + for s in 1:msing + sing_surf = ffs_intr.sing[s] + + # Check if this surface is resonant for this n + # m_res = round(q * n) must be an integer and within our m range + m_res_float = sing_surf.q * nn + m_res = round(Int, m_res_float) + + # Check if m/n is truly integer (within tolerance) + if abs(m_res_float - m_res) > 1e-6 + # Not resonant for this n, leave metrics as zero + continue + end + + # Check if resonant mode is within our m range + if m_res < ffs_intr.mlow || m_res > ffs_intr.mhigh + continue + end + + # Compute Green's functions at this surface for this n + vac_input = Vacuum.create_vacuum_input_at_psi( + equil, + sing_surf.psifac, + mtheta_eq, + mtheta, + mpert, + mlow, + nn + ) + grri, grre = Vacuum.compute_greens_functions_only(vac_input, wall_settings) + + # Store in singular surface struct (overwrites for each n) + ffs_intr.sing[s].grri = grri + ffs_intr.sing[s].grre = grre + + # Find the linear index for the resonant mode (m_res, nn) + resnum = findfirst(j -> intr.m_modes[j] == m_res && intr.n_modes[j] == nn, 1:numpert_total) + if resnum === nothing + @warn "Could not find index for resonant mode (m=$m_res, n=$nn)" maxlog=1 + continue + end + + # Evaluate field derivative jump across surface + respsi = sing_surf.psifac + if size(ForceFreeStates_results.ca_l, 4) >= s && size(ForceFreeStates_results.ca_r, 4) >= s + # Use asymptotic coefficients from ideal MHD solution + # resnum is the resonant mode index, and we evaluate at forcing mode resnum + lbwp1 = ForceFreeStates_results.ca_l[resnum, resnum, 2, s] + rbwp1 = ForceFreeStates_results.ca_r[resnum, resnum, 2, s] + else + # Fallback: finite difference + spot = 1e-6 + lpsi = respsi - spot / (abs(nn) * abs(sing_surf.q1)) + rpsi = respsi + spot / (abs(nn) * abs(sing_surf.q1)) + lbwp1 = interpolate_field_derivative(ForceFreeStates_results, lpsi, resnum, resnum) + rbwp1 = interpolate_field_derivative(ForceFreeStates_results, rpsi, resnum, resnum) + end + + # Compute Delta' (tearing stability parameter) + delta_prime_val = (rbwp1 - lbwp1) / (twopi * chi1) + state.delta_prime[n_idx, s] = delta_prime_val + + # Compute resonant current + j_c = compute_current_density(equil, sing_surf.psifac) + delcurs = (rbwp1 - lbwp1) * j_c * im / (twopi * m_res) + resonant_current_val = -delcurs / im + state.resonant_current[n_idx, s] = resonant_current_val + + # Compute singular flux from current using surface inductance + singflx_mn = compute_singular_flux( + resonant_current_val, + vac_data, + ffs_intr, + intr, + sing_surf, + resnum, + nn + ) + + # Compute island half-width squared + shear = sing_surf.q1 * sing_surf.psifac / sing_surf.q + if abs(shear) > 1e-10 + state.island_width_sq[n_idx, s] = 4.0 * singflx_mn / (twopi * shear * sing_surf.q * chi1) + else + state.island_width_sq[n_idx, s] = 0.0 + 0.0im + end + + # Get interpolated field at resonant surface + interpbwn = interpolate_field_at_surface(ForceFreeStates_results, respsi, resnum, resnum, equil, m_res, nn) + + # Normalize by surface area + area = compute_surface_area(equil, sing_surf.psifac) + state.penetrated_field[n_idx, s] = interpbwn / area + state.resonant_flux[n_idx, s] = singflx_mn / area + + if ctrl.verbose && n_idx == 1 && s == 1 + println(" Surface 1: q = $(sing_surf.q), ψ = $(sing_surf.psifac)") + println(" Resonant mode: m = $m_res, n = $nn") + println(" Delta': $(real(delta_prime_val))") + end + end + end + + # Post-processing: Compute diagnostic quantities + compute_island_diagnostics!(state, ffs_intr, equil, chi1, twopi) + + if ctrl.verbose + println(" Singular coupling calculation complete") + max_island_width = maximum(abs.(state.island_half_width)) + max_delta_prime = maximum(abs.(real.(state.delta_prime))) + println(" Max island half-width: $(@sprintf("%.3e", max_island_width))") + println(" Max Delta': $(@sprintf("%.3e", max_delta_prime))") + end +end + +""" + interpolate_field_derivative( + ForceFreeStates_results::OdeState, + psi::Float64, + mode_idx::Int, + forcing_idx::Int + )::ComplexF64 + +Interpolate field derivative ∂b/∂ψ at arbitrary flux coordinate. + +Uses linear interpolation of stored ForceFreeStates solution. +""" +function interpolate_field_derivative( + ForceFreeStates_results::OdeState, + psi::Float64, + mode_idx::Int, + forcing_idx::Int +)::ComplexF64 + # Find bracketing points in psi_store + psi_store = ForceFreeStates_results.psi_store[1:ForceFreeStates_results.step] + + # Find indices that bracket psi + idx_right = findfirst(p -> p >= psi, psi_store) + + if idx_right === nothing + # psi is beyond stored range, use last value + return ForceFreeStates_results.ud_store[mode_idx, forcing_idx, 1, ForceFreeStates_results.step] + elseif idx_right == 1 + # psi is before stored range, use first value + return ForceFreeStates_results.ud_store[mode_idx, forcing_idx, 1, 1] + else + # Interpolate between idx_left and idx_right + idx_left = idx_right - 1 + + psi_left = psi_store[idx_left] + psi_right = psi_store[idx_right] + weight = (psi - psi_left) / (psi_right - psi_left) + + val_left = ForceFreeStates_results.ud_store[mode_idx, forcing_idx, 1, idx_left] + val_right = ForceFreeStates_results.ud_store[mode_idx, forcing_idx, 1, idx_right] + + return val_left * (1.0 - weight) + val_right * weight + end +end + +""" + interpolate_field_at_surface( + ForceFreeStates_results::OdeState, + psi::Float64, + mode_idx::Int, + forcing_idx::Int, + equil::Equilibrium.PlasmaEquilibrium, + m_mode::Int, + n_mode::Int + )::ComplexF64 + +Interpolate magnetic field at arbitrary flux surface. + +Interpolates displacement from ForceFreeStates solution and converts to field using +ideal MHD relation from flux coordinates: + b^ψ = i * χ₁ * (m - n*q) * ξ_ψ + +This matches the field reconstruction in FieldReconstruction.jl. +""" +function interpolate_field_at_surface( + ForceFreeStates_results::OdeState, + psi::Float64, + mode_idx::Int, + forcing_idx::Int, + equil::Equilibrium.PlasmaEquilibrium, + m_mode::Int, + n_mode::Int +)::ComplexF64 + # Get displacement at this surface via interpolation + psi_store = ForceFreeStates_results.psi_store[1:ForceFreeStates_results.step] + + idx_right = findfirst(p -> p >= psi, psi_store) + + if idx_right === nothing + xi_psi = ForceFreeStates_results.u_store[mode_idx, forcing_idx, 1, ForceFreeStates_results.step] + elseif idx_right == 1 + xi_psi = ForceFreeStates_results.u_store[mode_idx, forcing_idx, 1, 1] + else + idx_left = idx_right - 1 + psi_left = psi_store[idx_left] + psi_right = psi_store[idx_right] + weight = (psi - psi_left) / (psi_right - psi_left) + + val_left = ForceFreeStates_results.u_store[mode_idx, forcing_idx, 1, idx_left] + val_right = ForceFreeStates_results.u_store[mode_idx, forcing_idx, 1, idx_right] + + xi_psi = val_left * (1.0 - weight) + val_right * weight + end + + # Get safety factor at this surface + q = equil.profiles.q_spline(psi) + + # Convert displacement to field using ideal MHD relation + # b^ψ = i * χ₁ * (m - n*q) * ξ_ψ (from FieldReconstruction.jl line 304) + chi1 = 2π * equil.psio + twopi = 2π + singfac = m_mode - n_mode * q + b_psi = chi1 * singfac * twopi * im * xi_psi + + return b_psi +end + +""" + compute_current_density( + equil::Equilibrium.PlasmaEquilibrium, + psi::Float64 + )::Float64 + +Compute effective current density coefficient at given flux surface. + +Implements GPEC's j_c calculation (gpout.f line 560-578): + j_c = χ₁² * q / (μ₀ * integral) + +where the integral is computed via flux surface integration: + integral = ∫ (jac * |∇ψ| * sqreqb / |∇ψ|³) dθ + +## GPEC Formula + +```fortran +DO itheta=0,mthsurf + CALL bicube_eval(rzphi,respsi,theta(itheta),1) + rfac=SQRT(rzphi%f(1)) + jac=rzphi%f(4) + w(1,1)=(1+rzphi%fy(2))*twopi**2*rfac*r(itheta)/jac + w(1,2)=-rzphi%fy(1)*pi*r(itheta)/(rfac*jac) + delpsi(itheta)=SQRT(w(1,1)**2+w(1,2)**2) + sqreqb(itheta)=(sq%f(1)**2+chi1**2*delpsi(itheta)**2)/(twopi*r(itheta))**2 + jcfun(itheta)=sqreqb(itheta)/(delpsi(itheta)**3) + j_c(ising)=j_c(ising)+jac*delpsi(itheta)*jcfun(itheta)/mthsurf +ENDDO +j_c(ising)=j_c(ising)-jac*delpsi(mthsurf)*jcfun(mthsurf)/mthsurf ! trapezoidal rule +j_c(ising)=1.0/j_c(ising)*chi1**2*sq%f(4)/mu0 +``` + +## Implementation + +Uses trapezoidal rule integration around the flux surface with metric quantities +from the equilibrium bicubic spline (rzphi). The integrand includes: +- jac: Jacobian of flux coordinates +- |∇ψ|: Flux gradient magnitude (delpsi) +- sqreqb: Magnetic field quantity (F² + χ₁²|∇ψ|²)/(2πR)² +""" +function compute_current_density( + equil::Equilibrium.PlasmaEquilibrium, + psi::Float64 +)::Float64 + # Physical constants + μ₀ = 4π * 1e-7 + chi1 = 2π * equil.psio + twopi = 2π + + # Get equilibrium quantities at this surface + F_tor = equil.profiles.F_spline(psi) # Toroidal field function F = R*B_tor times 2π + q = equil.profiles.q_spline(psi) # Safety factor + + # Magnetic axis location + ro = equil.ro + zo = equil.zo + + # Number of theta points for integration + # Match GPEC's mthsurf (typically 101 points from theta=0 to theta=1) + mthsurf = length(equil.rzphi_xs) - 1 + + # Integrate around flux surface using trapezoidal rule + integral = 0.0 + + # Storage for last point (needed for trapezoidal rule correction) + last_jac = 0.0 + last_delpsi = 0.0 + last_jcfun = 0.0 + + for itheta in 0:mthsurf + # Theta coordinate normalized to [0, 1] + theta = itheta / mthsurf + + # Evaluate bicubic splines with derivatives at (psi, theta) + # New API uses separate interpolants for each component + r2 = equil.rzphi_rsquared((psi, theta)) # rfac² + deta = equil.rzphi_offset((psi, theta)) # angle offset + jac = equil.rzphi_jac((psi, theta)) # Jacobian + r2_y = equil.rzphi_rsquared((psi, theta); deriv=(0,1)) # ∂(rfac²)/∂theta + deta_y = equil.rzphi_offset((psi, theta); deriv=(0,1)) # ∂(deta)/∂theta + + rfac = sqrt(abs(r2)) + fy_rfac2 = r2_y + fy_deta = deta_y + + # Compute R coordinate (Z not needed for this calculation) + eta = twopi * (theta + deta) + r = ro + rfac * cos(eta) + + # Compute metric components w(1,1) and w(1,2) for |∇ψ| + # These come from the metric tensor in flux coordinates + w11 = (1.0 + fy_deta) * twopi^2 * rfac * r / jac + w12 = -fy_rfac2 * π * r / (rfac * jac) + + # Flux gradient magnitude |∇ψ| + delpsi = sqrt(w11^2 + w12^2) + + # Magnetic field quantity: sqreqb = (F² + χ₁²|∇ψ|²) / (2πR)² + # F is toroidal field function (already includes factor of 2π from sq) + sqreqb = (F_tor^2 + chi1^2 * delpsi^2) / (twopi * r)^2 + + # Integrand function + jcfun = sqreqb / (delpsi^3) + + # Accumulate integral (trapezoidal rule) + integral += jac * delpsi * jcfun / mthsurf + + # Store last point for correction + if itheta == mthsurf + last_jac = jac + last_delpsi = delpsi + last_jcfun = jcfun + end + end + + # Trapezoidal rule end correction (subtract half of last point contribution) + integral -= last_jac * last_delpsi * last_jcfun / mthsurf + + # Final normalization: j_c = (1/integral) * χ₁² * q / μ₀ + j_c = (1.0 / integral) * chi1^2 * q / μ₀ + + return j_c +end + +""" + apply_green_function_simple( + green::Matrix{Float64}, + mode_coeffs::Vector{ComplexF64} + )::Vector{Float64} + +Apply Green's function to Fourier mode coefficients. + +Simplified version that extracts only plasma surface rows (first mtheta rows). + +## Arguments +- `green`: Green's function matrix [2*mtheta, 2*mpert] +- `mode_coeffs`: Complex Fourier coefficients [mpert] + +## Returns +- Potential at plasma surface theta points [mtheta] +""" +function apply_green_function_simple( + green::Matrix{Float64}, + mode_coeffs::Vector{ComplexF64} +)::Vector{Float64} + mtheta = size(green, 1) ÷ 2 + mpert = length(mode_coeffs) + + # Pack complex to real/imag format + packed = zeros(Float64, 2 * mpert) + for i in 1:mpert + packed[2*i - 1] = real(mode_coeffs[i]) + packed[2*i] = imag(mode_coeffs[i]) + end + + # Apply Green's function (only plasma surface rows) + chi_theta = green[1:mtheta, :] * packed + + return chi_theta +end + +""" + compute_surface_inductance_from_greens( + grri::Matrix{Float64}, + grre::Matrix{Float64}, + ffs_intr::ForceFreeStatesInternal, + intr::PerturbedEquilibriumInternal + )::Matrix{ComplexF64} + +Compute surface inductance matrix from Green's functions at flux surface. + +This implements the full GPEC gpvacuum_flxsurf algorithm (gpvacuum.f line 299-331): +1. For each mode, apply Green's functions to unit flux +2. Compute surface current from potential jump: kax = (chi - che) / μ₀ +3. Fourier transform to mode space +4. Return inductance: L_surf = flux * inv(current) + +## Arguments +- `grri`: Interior Green's function [2*mtheta, 2*mpert] (stored in sing_surf) +- `grre`: Exterior Green's function [2*mtheta, 2*mpert] (stored in sing_surf) +- `ffs_intr`: ForceFreeStates internal state +- `intr`: PerturbedEquilibrium internal state with mode arrays + +## Returns +Surface inductance matrix [numpert_total, numpert_total] + +## GPEC Reference +```fortran +DO i=1,mpert + vbwp_mn=0; vbwp_mn(i)=1.0 + chi_fun = grri * vbwp_mn + che_fun = grre * vbwp_mn + kax_fun = (chi_fun - che_fun) / mu0 + CALL iscdftf(mfac, mpert, kax_fun, mthsurf, fkaxmats(:,i)) +ENDDO +fsurf_indmats = fflxmats * inv(fkaxmats) +``` +""" +function compute_surface_inductance_from_greens( + grri::Matrix{Float64}, + grre::Matrix{Float64}, + ffs_intr::ForceFreeStatesInternal, + intr::PerturbedEquilibriumInternal +)::Matrix{ComplexF64} + mpert = ffs_intr.mpert + mtheta = size(grri, 1) ÷ 2 + + # Physical constant + μ₀ = 4π * 1e-7 + + # Create Fourier transform object + ft = FourierTransforms.FourierTransform(mtheta, mpert, ffs_intr.mlow) + + # Initialize matrices (mpert x mpert for single toroidal mode number) + # Green's functions are computed for a specific n, so inductance is only over poloidal modes + flux_matrix = zeros(ComplexF64, mpert, mpert) + current_matrix = zeros(ComplexF64, mpert, mpert) + + # For each poloidal mode, compute surface current from Green's functions + for i in 1:mpert + # Unit flux for mode i + vbwp_mn = zeros(ComplexF64, mpert) + vbwp_mn[i] = 1.0 + + # Apply Green's functions using same approach as ResponseMatrices.jl + chi_theta = apply_green_function(grri, vbwp_mn) + che_theta = apply_green_function(grre, vbwp_mn) + + # Surface current from potential jump + kax_theta = (chi_theta .- che_theta) ./ μ₀ + + # Transform back to Fourier mode space + kax_modes = ft(kax_theta) + + # Store in matrices + flux_matrix[:, i] = vbwp_mn + current_matrix[:, i] = kax_modes + end + + # Compute surface inductance: L_surf = flux * inv(current) + # Initialize L_surf outside try-catch for scoping + L_surf = zeros(ComplexF64, mpert, mpert) + + # Check matrix condition before inversion + current_mag = maximum(abs.(current_matrix)) + current_min = minimum(abs.(current_matrix[abs.(current_matrix) .> 1e-15])) + + if current_mag < 1e-15 + @warn "Current matrix is all zeros! Cannot compute surface inductance." maxlog=1 + @warn " This indicates grri and grre are identical or Green's functions failed." maxlog=1 + # Fallback: diagonal approximation + for i in 1:mpert + L_surf[i, i] = μ₀ * 1e-6 + end + else + try + # Regularization for numerical stability + regularization = 1e-12 * current_mag + current_reg = current_matrix + regularization * I + + L_surf = flux_matrix * inv(current_reg) + + # Hermitianize + L_surf = 0.5 * (L_surf + L_surf') + catch e + @warn "Surface inductance inversion failed: $e" maxlog=1 + @warn " Current matrix magnitude: $current_mag, min non-zero: $current_min" maxlog=1 + @warn " Check if Green's functions are computed correctly." maxlog=1 + # Fallback: diagonal approximation + for i in 1:mpert + L_surf[i, i] = μ₀ * 1e-6 + end + end + end + + return L_surf +end + +""" + compute_singular_flux( + current::ComplexF64, + vac_data::VacuumData, + ffs_intr::ForceFreeStatesInternal, + intr::PerturbedEquilibriumInternal, + sing_surf::ForceFreeStates.SingType, + mode_idx::Int, + nn::Int + )::ComplexF64 + +Compute singular flux from resonant current using surface inductance. + +Uses surface inductance matrix at the singular surface: + Φ = L_surf * I + +where L_surf is computed from Green's functions stored in sing_surf. + +## GPEC Formula + +```fortran +fkaxmn(resnum) = singcurs(ising,i) / (twopi*nn) +singflx_mn = MATMUL(fsurfindmats(ising,:,:), fkaxmn) +``` + +## Implementation + +Uses surface inductance matrix computed from pre-stored Green's functions. +The Green's functions (grri, grre) are calculated at each singular surface +during initialization and stored in the sing_surf struct for efficiency. +""" +function compute_singular_flux( + current::ComplexF64, + vac_data::VacuumData, + ffs_intr::ForceFreeStatesInternal, + intr::PerturbedEquilibriumInternal, + sing_surf::ForceFreeStates.SingType, + mode_idx::Int, + nn::Int +)::ComplexF64 + if abs(nn) == 0 + return 0.0 + 0.0im + end + + # Get mode numbers for this index + mpert = ffs_intr.mpert + mlow = ffs_intr.mlow + m_mode = intr.m_modes[mode_idx] + + # Poloidal mode index within mpert range + m_idx = m_mode - mlow + 1 + + # Build current vector (size mpert for single toroidal mode) + # L_surf is mpert x mpert since Green's functions are for single n + current_vector = zeros(ComplexF64, mpert) + current_vector[m_idx] = current / (2π * nn) + + # Compute surface inductance from stored Green's functions + # These were pre-computed at the singular surface location + L_surf = compute_surface_inductance_from_greens( + sing_surf.grri, + sing_surf.grre, + ffs_intr, + intr + ) + + # Compute flux: Φ = L * I + flux_vector = L_surf * current_vector + + # Return flux at resonant mode + return flux_vector[m_idx] +end + +""" + compute_surface_area( + equil::Equilibrium.PlasmaEquilibrium, + psi::Float64 + )::Float64 + +Compute flux surface area at given ψ. + +Implements GPEC's area calculation (gpout.f line 568): + area = ∫ jac * |∇ψ| dθ + +where the integral is computed around the flux surface. + +## GPEC Formula + +```fortran +DO itheta=0,mthsurf + CALL bicube_eval(rzphi,respsi,theta(itheta),1) + rfac=SQRT(rzphi%f(1)) + jac=rzphi%f(4) + w(1,1)=(1+rzphi%fy(2))*twopi**2*rfac*r(itheta)/jac + w(1,2)=-rzphi%fy(1)*pi*r(itheta)/(rfac*jac) + delpsi(itheta)=SQRT(w(1,1)**2+w(1,2)**2) + area(ising)=area(ising)+jac*delpsi(itheta)/mthsurf +ENDDO +area(ising)=area(ising)-jac*delpsi(mthsurf)/mthsurf ! trapezoidal rule +``` + +## Implementation + +Uses trapezoidal rule integration around the flux surface with: +- jac: Jacobian of flux coordinates from rzphi +- |∇ψ|: Flux gradient magnitude (delpsi) from metric tensor +""" +function compute_surface_area( + equil::Equilibrium.PlasmaEquilibrium, + psi::Float64 +)::Float64 + # Physical constants + twopi = 2π + + # Magnetic axis location + ro = equil.ro + zo = equil.zo + + # Number of theta points for integration + mthsurf = length(equil.rzphi_xs) - 1 + + # Integrate around flux surface using trapezoidal rule + area = 0.0 + + # Storage for last point (needed for trapezoidal rule correction) + last_jac = 0.0 + last_delpsi = 0.0 + + for itheta in 0:mthsurf + # Theta coordinate normalized to [0, 1] + theta = itheta / mthsurf + + # Evaluate bicubic splines with derivatives at (psi, theta) + r2 = equil.rzphi_rsquared((psi, theta)) + jac = equil.rzphi_jac((psi, theta)) + deta = equil.rzphi_offset((psi, theta)) + r2_y = equil.rzphi_rsquared((psi, theta); deriv=(0,1)) + deta_y = equil.rzphi_offset((psi, theta); deriv=(0,1)) + + # Compute rfac + rfac = sqrt(abs(r2)) + fy_rfac2 = r2_y + fy_deta = deta_y + + # Compute R coordinate + eta = twopi * (theta + deta) + r = ro + rfac * cos(eta) + + # Compute metric components for |∇ψ| + w11 = (1.0 + fy_deta) * twopi^2 * rfac * r / jac + w12 = -fy_rfac2 * π * r / (rfac * jac) + + # Flux gradient magnitude + delpsi = sqrt(w11^2 + w12^2) + + # Accumulate area integral (trapezoidal rule) + area += jac * delpsi / mthsurf + + # Store last point for correction + if itheta == mthsurf + last_jac = jac + last_delpsi = delpsi + end + end + + # Trapezoidal rule end correction + area -= last_jac * last_delpsi / mthsurf + + return area +end + +""" + compute_island_diagnostics!( + state::PerturbedEquilibriumState, + ffs_intr::ForceFreeStatesInternal, + equil::Equilibrium.PlasmaEquilibrium, + chi1::Float64, + twopi::Float64 + ) + +Compute island half-width and Chirikov parameter for each singular surface. + +Uses the resonant flux and island_width_sq from singular coupling calculation. +""" +function compute_island_diagnostics!( + state::PerturbedEquilibriumState, + ffs_intr::ForceFreeStatesInternal, + equil::Equilibrium.PlasmaEquilibrium, + chi1::Float64, + twopi::Float64 +) + msing = ffs_intr.msing + + # Compute island half-width for each singular surface + # Take maximum over all toroidal modes n + for s in 1:msing + sing_surf = ffs_intr.sing[s] + + # Island half-width: w/2 = √|island_width_sq| + # Array is [npert, msing], so index with [:, s] to get all n for this surface + max_island_sq = maximum(abs.(state.island_width_sq[:, s])) + state.island_half_width[s] = sqrt(abs(max_island_sq)) + + # Compute Chirikov parameter: overlap = w/2 / distance_to_nearest_surface + # Find distance to nearest singular surface + if msing > 1 + distances = Float64[] + for s2 in 1:msing + if s2 != s + dist = abs(ffs_intr.sing[s].psifac - ffs_intr.sing[s2].psifac) + push!(distances, dist) + end + end + + if !isempty(distances) + hdist = minimum(distances) / 2.0 # Half-distance + if hdist > 1e-10 + state.chirikov_parameter[s] = state.island_half_width[s] / hdist + else + state.chirikov_parameter[s] = Inf + end + else + state.chirikov_parameter[s] = 0.0 + end + else + state.chirikov_parameter[s] = 0.0 + end + end +end diff --git a/src/PerturbedEquilibrium/Utils.jl b/src/PerturbedEquilibrium/Utils.jl new file mode 100644 index 00000000..2a01ffcd --- /dev/null +++ b/src/PerturbedEquilibrium/Utils.jl @@ -0,0 +1,138 @@ +""" + initialize_mode_arrays!( + intr::PerturbedEquilibriumInternal, + ffs_intr::ForceFreeStatesInternal + ) + +Initialize mode number arrays for convenient indexing. + +Pre-computes m_modes[i] and n_modes[i] for each linear index i in 1:numpert_total, +avoiding repeated index arithmetic throughout the code. + +## Mode Indexing Convention + +For linear index i ∈ [1, numpert_total]: +- m_modes[i] = (i-1) % mpert + mlow +- n_modes[i] = (i-1) ÷ mpert + nlow + +This matches the convention used in ForceFreeStates where modes are ordered as: +(m1,n1), (m2,n1), ..., (mpert,n1), (m1,n2), (m2,n2), ..., (mpert,npert) +""" +function initialize_mode_arrays!( + intr::PerturbedEquilibriumInternal, + ffs_intr::ForceFreeStatesInternal +) + numpert_total = ffs_intr.numpert_total + mpert = ffs_intr.mpert + mlow = ffs_intr.mlow + nlow = ffs_intr.nlow + + # Allocate arrays + intr.m_modes = zeros(Int, numpert_total) + intr.n_modes = zeros(Int, numpert_total) + + # Fill arrays using indexing convention + for i in 1:numpert_total + m_idx = (i - 1) % mpert + 1 # 1-based index into mpert range + n_idx = (i - 1) ÷ mpert + 1 # 1-based index into npert range + + intr.m_modes[i] = m_idx - 1 + mlow + intr.n_modes[i] = n_idx - 1 + nlow + end +end + +""" + write_outputs_to_HDF5( + state::PerturbedEquilibriumState, + intr::PerturbedEquilibriumInternal, + ctrl::PerturbedEquilibriumControl, + filename::String + ) + +Write perturbed equilibrium results to HDF5 file (appends to existing ForceFreeStates output). + +## Output Structure + +``` +perturbed_equilibrium/ +├── forcing_modes/ +│ ├── n # Toroidal mode numbers +│ ├── m # Poloidal mode numbers +│ ├── amplitude_real # Real parts of forcing amplitudes +│ └── amplitude_imag # Imaginary parts of forcing amplitudes +├── response/ +│ ├── xi_perturbed # Displacement field +│ └── b_perturbed # Magnetic field perturbation +├── singular_coupling/ +│ ├── coupling_coefficient_real +│ ├── coupling_coefficient_imag +│ └── resonant_amplitude +└── energies/ + ├── plasma_energy + ├── vacuum_energy + └── total_energy +``` +""" +function write_outputs_to_HDF5( + state::PerturbedEquilibriumState, + intr::PerturbedEquilibriumInternal, + ctrl::PerturbedEquilibriumControl, + filename::String +) + if ctrl.verbose + println("Writing perturbed equilibrium data to $filename") + end + + h5open(filename, "cw") do file # "cw" = create or read/write + # Create perturbed_equilibrium group + pe_group = haskey(file, "perturbed_equilibrium") ? file["perturbed_equilibrium"] : create_group(file, "perturbed_equilibrium") + + # Write forcing modes + forcing_group = haskey(pe_group, "forcing_modes") ? pe_group["forcing_modes"] : create_group(pe_group, "forcing_modes") + n_modes = [mode.n for mode in intr.forcing_modes] + m_modes = [mode.m for mode in intr.forcing_modes] + amp_real = [real(mode.amplitude) for mode in intr.forcing_modes] + amp_imag = [imag(mode.amplitude) for mode in intr.forcing_modes] + + forcing_group["n"] = n_modes + forcing_group["m"] = m_modes + forcing_group["amplitude_real"] = amp_real + forcing_group["amplitude_imag"] = amp_imag + + # Write response fields (if computed) + if !isnothing(state.xi_modes) + response_group = haskey(pe_group, "response") ? pe_group["response"] : create_group(pe_group, "response") + response_group["xi_psi_real"] = real.(state.xi_modes.psi) + response_group["xi_psi_imag"] = imag.(state.xi_modes.psi) + if !isnothing(state.b_modes) + response_group["b_psi_real"] = real.(state.b_modes.psi) + response_group["b_psi_imag"] = imag.(state.b_modes.psi) + response_group["b_theta_real"] = real.(state.b_modes.theta) + response_group["b_theta_imag"] = imag.(state.b_modes.theta) + response_group["b_zeta_real"] = real.(state.b_modes.zeta) + response_group["b_zeta_imag"] = imag.(state.b_modes.zeta) + end + end + + # Write singular coupling metrics + coupling_group = haskey(pe_group, "singular_coupling") ? pe_group["singular_coupling"] : create_group(pe_group, "singular_coupling") + coupling_group["coupling_coefficient_real"] = real(state.coupling_coefficient) + coupling_group["coupling_coefficient_imag"] = imag(state.coupling_coefficient) + coupling_group["resonant_amplitude"] = state.resonant_amplitude + + # Write additional metrics + for (key, val) in intr.singular_coupling_metrics + coupling_group[key] = val + end + + # Write energies + energy_group = haskey(pe_group, "energies") ? pe_group["energies"] : create_group(pe_group, "energies") + energy_group["plasma_energy"] = state.plasma_energy + energy_group["vacuum_energy"] = state.vacuum_energy + energy_group["total_energy"] = state.total_energy + end + + if ctrl.verbose + println(" Perturbed equilibrium output complete") + end +end diff --git a/src/Utilities/FourierTransforms.jl b/src/Utilities/FourierTransforms.jl new file mode 100644 index 00000000..920261bd --- /dev/null +++ b/src/Utilities/FourierTransforms.jl @@ -0,0 +1,784 @@ +""" + FourierTransforms + +Utility module for efficient Fourier transforms using pre-computed basis functions. + +Provides a functor-based interface for Fourier transforms between theta-space and mode-space +representations. Supports both simple harmonic transforms (cos(m*θ), sin(m*θ)) and phase-shifted +transforms (cos(m*θ + n*qa*δ), sin(m*θ + n*qa*δ)) used in vacuum field calculations. + +# Features + +- Pre-computes trigonometric basis functions for efficiency +- Direct complex number support (no manual real/imaginary splitting) +- Type-stable functor pattern for high performance +- Supports different grids (mthvac, mtheta) with different mode ranges + +# Example + +```julia +using JPEC.Utilities.FourierTransforms + +# For PerturbedEquilibrium (no phase shift) +ft = FourierTransform(mtheta, mpert, mlow) + +# For Vacuum (with n*qa*delta phase) +ft_vac = FourierTransform(mthvac, mpert, mlow; n=1, qa=2.5, delta=delta_array) + +# Forward transform: theta-space → Fourier modes +theta_data = randn(mtheta, 10) # 10 different theta-space functions +modes = ft(theta_data) # Complex modes [mpert, 10] + +# Inverse transform: Fourier modes → theta-space +reconstructed = inverse(ft, modes) +``` +""" +module FourierTransforms + +using LinearAlgebra + +export FourierTransform, inverse +export compute_fourier_coefficients +export transform!, inverse_transform! +export fourier_transform!, fourier_inverse_transform! + +""" + compute_fourier_coefficients(mtheta, mpert, mlow; n=0, qa=0.0, delta=zeros(mtheta)) + +Compute Fourier basis function coefficients for transforms between theta-space and mode-space. + +# Arguments + +- `mtheta::Int`: Number of poloidal grid points (theta resolution) +- `mpert::Int`: Number of Fourier modes (spectral resolution) +- `mlow::Int`: Lowest mode number (mode numbering starts here) + +# Keyword Arguments + +- `n::Int=0`: Toroidal mode number (default: 0, no toroidal coupling) +- `qa::Float64=0.0`: Safety factor at boundary (default: 0.0) +- `delta::Vector{Float64}=zeros(mtheta)`: Toroidal phase shift array (default: no shift) + +# Returns + +- `cslth::Matrix{Float64}`: Cosine coefficients [mtheta, mpert] where `cslth[i,l] = cos(m*θᵢ + n*qa*δᵢ)` +- `snlth::Matrix{Float64}`: Sine coefficients [mtheta, mpert] where `snlth[i,l] = sin(m*θᵢ + n*qa*δᵢ)` + +# Notes + +The theta grid is uniform: `θᵢ = 2π*(i-1)/mtheta` for `i = 1:mtheta` + +Mode numbers are: `m = mlow, mlow+1, ..., mlow+mpert-1` + +When `n=0, qa=0, delta=0` (default), this reduces to simple harmonic basis: +- `cslth[i,l] = cos(m*θᵢ)` +- `snlth[i,l] = sin(m*θᵢ)` + +For vacuum calculations with toroidal coupling, the phase includes `n*qa*delta`: +- `cslth[i,l] = cos(m*θᵢ + n*qa*δᵢ)` +- `snlth[i,l] = sin(m*θᵢ + n*qa*δᵢ)` +""" +function compute_fourier_coefficients( + mtheta::Int, + mpert::Int, + mlow::Int; + n::Int=0, + qa::Float64=0.0, + delta::Vector{Float64}=zeros(Float64, mtheta) +) + # Validate inputs + @assert mtheta > 0 "mtheta must be positive" + @assert mpert >= 0 "mpert must be non-negative" + @assert length(delta) == mtheta "delta must have length mtheta" + + # Handle edge case: mpert = 0 returns empty arrays + if mpert == 0 + return zeros(Float64, mtheta, 0), zeros(Float64, mtheta, 0) + end + + # Uniform theta grid: [0, 2π) + theta_grid = range(0, 2π, length=mtheta+1)[1:end-1] + + # Compute toroidal phase shift (zero if n=0 or qa=0) + nqdelta = n * qa .* delta + + # Allocate coefficient matrices + cslth = zeros(Float64, mtheta, mpert) + snlth = zeros(Float64, mtheta, mpert) + + # Compute basis functions for each mode and theta point + for l in 1:mpert + m = mlow + l - 1 # Mode number + for i in 1:mtheta + # Total argument: m*θ + n*qa*δ + arg = theta_grid[i] * m + nqdelta[i] + cslth[i, l] = cos(arg) + snlth[i, l] = sin(arg) + end + end + + return cslth, snlth +end + +""" + FourierTransform + +Callable struct for efficient Fourier transforms with pre-computed basis functions. + +# Fields + +- `mtheta::Int`: Number of theta grid points +- `mpert::Int`: Number of Fourier modes +- `mlow::Int`: Lowest mode number +- `cslth::Matrix{Float64}`: Cosine basis functions [mtheta, mpert] +- `snlth::Matrix{Float64}`: Sine basis functions [mtheta, mpert] + +# Usage + +Create once with appropriate grid parameters, then use repeatedly: + +```julia +# Create transform (coefficients computed once) +ft = FourierTransform(mtheta, mpert, mlow) + +# Forward transform (callable): theta → modes +modes = ft(theta_data) + +# Inverse transform: modes → theta +theta_reconstructed = inverse(ft, modes) +``` + +See also: [`compute_fourier_coefficients`](@ref), [`inverse`](@ref) +""" +struct FourierTransform + mtheta::Int + mpert::Int + mlow::Int + cslth::Matrix{Float64} + snlth::Matrix{Float64} +end + +""" + FourierTransform(mtheta, mpert, mlow; n=0, qa=0.0, delta=zeros(mtheta)) + +Construct a FourierTransform object with pre-computed basis functions. + +# Arguments + +- `mtheta::Int`: Number of poloidal grid points +- `mpert::Int`: Number of Fourier modes +- `mlow::Int`: Lowest mode number + +# Keyword Arguments + +- `n::Int=0`: Toroidal mode number +- `qa::Float64=0.0`: Safety factor at boundary +- `delta::Vector{Float64}=zeros(mtheta)`: Toroidal phase shift array + +# Returns + +`FourierTransform` object ready for forward and inverse transforms. + +# Examples + +```julia +# Simple case: no phase shift (for PerturbedEquilibrium) +ft = FourierTransform(480, 40, -20) + +# With toroidal phase (for Vacuum calculations) +ft_vac = FourierTransform(480, 40, -20; n=1, qa=2.5, delta=delta_array) +``` +""" +function FourierTransform( + mtheta::Int, + mpert::Int, + mlow::Int; + n::Int=0, + qa::Float64=0.0, + delta::Vector{Float64}=zeros(Float64, mtheta) +) + cslth, snlth = compute_fourier_coefficients(mtheta, mpert, mlow; n, qa, delta) + return FourierTransform(mtheta, mpert, mlow, cslth, snlth) +end + +""" + (ft::FourierTransform)(data::AbstractVecOrMat{<:Real}) + +Forward Fourier transform: theta-space → mode-space (callable functor). + +Transforms real-valued data at theta grid points into complex Fourier mode coefficients. + +# Arguments + +- `data::AbstractVecOrMat{Float64}`: Data at theta points + - If `Vector{Float64}` with length `mtheta`: single function to transform + - If `Matrix{Float64}` with size `(mtheta, n)`: n functions to transform simultaneously + +# Returns + +- Complex Fourier coefficients + - If input is `Vector`: returns `Vector{ComplexF64}` of length `mpert` + - If input is `Matrix`: returns `Matrix{ComplexF64}` of size `(mpert, n)` + +# Formula + +For each mode `l` (corresponding to mode number `m = mlow + l - 1`): + +``` +mode[l] = Σᵢ data[i] * (cos(m*θᵢ + phase) + im*sin(m*θᵢ + phase)) +``` + +This is equivalent to computing: +``` +real_part[l] = Σᵢ data[i] * cos(m*θᵢ + phase) +imag_part[l] = Σᵢ data[i] * sin(m*θᵢ + phase) +mode[l] = complex(real_part[l], imag_part[l]) +``` + +# Examples + +```julia +ft = FourierTransform(480, 40, -20) + +# Transform a single function +f_theta = sin.(theta_grid) +f_modes = ft(f_theta) # Vector{ComplexF64} of length 40 + +# Transform multiple functions at once +data = randn(480, 10) # 10 different functions +modes = ft(data) # Matrix{ComplexF64} of size (40, 10) +``` +""" +function (ft::FourierTransform)(data::AbstractVecOrMat{<:Real}) + # Forward transform using matrix multiplication + # real_part = data^T * cslth (or cslth^T * data for vectors) + # imag_part = data^T * snlth (or snlth^T * data for vectors) + # Return as complex + + if data isa AbstractVector + # For vector input: [mtheta] → [mpert] + @assert length(data) == ft.mtheta "Input vector must have length mtheta=$(ft.mtheta)" + real_part = ft.cslth' * data + imag_part = ft.snlth' * data + return complex.(real_part, imag_part) + else + # For matrix input: [mtheta, n] → [mpert, n] + @assert size(data, 1) == ft.mtheta "Input matrix first dimension must be mtheta=$(ft.mtheta)" + real_part = ft.cslth' * data + imag_part = ft.snlth' * data + return complex.(real_part, imag_part) + end +end + +""" + (ft::FourierTransform)(data::AbstractVecOrMat{<:Complex}) + +Forward Fourier transform for complex-valued theta-space data. + +Transforms complex data at theta grid points into complex Fourier mode coefficients. + +# Arguments + +- `data::AbstractVecOrMat{ComplexF64}`: Complex data at theta points + +# Returns + +- Complex Fourier coefficients with same shape as real input case + +# Formula + +For complex input `data = data_real + im*data_imag`, computes: + +``` +Re{mode[l]} = Σᵢ (data_real[i]*cos - data_imag[i]*sin) +Im{mode[l]} = Σᵢ (data_real[i]*sin + data_imag[i]*cos) +``` + +where cos and sin are the basis functions at theta point i for mode l. +""" +function (ft::FourierTransform)(data::AbstractVecOrMat{<:Complex}) + # For complex input, need to handle real and imaginary parts carefully + # Forward transform: exp(-i*m*θ) = cos(m*θ) - i*sin(m*θ) + + if data isa AbstractVector + @assert length(data) == ft.mtheta "Input vector must have length mtheta=$(ft.mtheta)" + real_part = ft.cslth' * real.(data) .- ft.snlth' * imag.(data) + imag_part = ft.cslth' * imag.(data) .+ ft.snlth' * real.(data) + return complex.(real_part, imag_part) + else + @assert size(data, 1) == ft.mtheta "Input matrix first dimension must be mtheta=$(ft.mtheta)" + real_part = ft.cslth' * real.(data) .- ft.snlth' * imag.(data) + imag_part = ft.cslth' * imag.(data) .+ ft.snlth' * real.(data) + return complex.(real_part, imag_part) + end +end + +""" + inverse(ft::FourierTransform, modes::AbstractVecOrMat{<:Complex}) + +Inverse Fourier transform: mode-space → theta-space. + +Reconstructs theta-space data from complex Fourier mode coefficients. + +# Arguments + +- `modes::AbstractVecOrMat{ComplexF64}`: Fourier mode coefficients + - If `Vector{ComplexF64}` with length `mpert`: single mode expansion to reconstruct + - If `Matrix{ComplexF64}` with size `(mpert, n)`: n mode expansions to reconstruct + +# Returns + +- Reconstructed data at theta points + - If input is `Vector`: returns `Vector{ComplexF64}` of length `mtheta` + - If input is `Matrix`: returns `Matrix{ComplexF64}` of size `(mtheta, n)` + +# Formula + +For each theta point `i`: + +``` +data[i] = (2π/mtheta) * Σₗ modes[l] * (cos(m*θᵢ + phase) + im*sin(m*θᵢ + phase)) +``` + +This is equivalent to: +``` +data[i] = (2π/mtheta) * Σₗ [Re{modes[l]}*cos - Im{modes[l]}*sin + + im*(Re{modes[l]}*sin + Im{modes[l]}*cos)] +``` + +# Notes + +The normalization factor `2π/mtheta` ensures proper Fourier series reconstruction. + +For real-valued theta data, the result will have negligible imaginary parts (machine precision). +Use `real.(inverse(ft, modes))` to extract the real part if needed. + +# Examples + +```julia +ft = FourierTransform(480, 40, -20) + +# Reconstruct from modes +modes = randn(ComplexF64, 40) +theta_data = inverse(ft, modes) # Vector{ComplexF64} of length 480 + +# For real reconstruction +theta_real = real.(inverse(ft, modes)) +``` +""" +function inverse(ft::FourierTransform, modes::AbstractVecOrMat{<:Complex}) + # Inverse transform with normalization + # For mode expansion: f(θ) = Σₗ cₗ * exp(i*m*θ) = Σₗ [Re{cₗ}*cos - Im{cₗ}*sin + i*(Re{cₗ}*sin + Im{cₗ}*cos)] + + dth = 2π / ft.mtheta + + if modes isa AbstractVector + @assert length(modes) == ft.mpert "Input vector must have length mpert=$(ft.mpert)" + # Real and imaginary parts of reconstructed function + real_part = ft.cslth * real.(modes) .- ft.snlth * imag.(modes) + imag_part = ft.cslth * imag.(modes) .+ ft.snlth * real.(modes) + return complex.(real_part, imag_part) .* dth + else + @assert size(modes, 1) == ft.mpert "Input matrix first dimension must be mpert=$(ft.mpert)" + real_part = ft.cslth * real.(modes) .- ft.snlth * imag.(modes) + imag_part = ft.cslth * imag.(modes) .+ ft.snlth * real.(modes) + return complex.(real_part, imag_part) .* dth + end +end + +""" + inverse(ft::FourierTransform, modes::AbstractVecOrMat{<:Real}) + +Inverse Fourier transform for real mode coefficients (pure cosine expansion). + +Special case where all modes are real-valued, corresponding to a cosine-only Fourier series. + +# Arguments + +- `modes::AbstractVecOrMat{Float64}`: Real Fourier coefficients + +# Returns + +- Real-valued data at theta points with normalization factor `2π/mtheta` + +# Formula + +``` +data[i] = (2π/mtheta) * Σₗ modes[l] * cos(m*θᵢ + phase) +``` + +# Notes + +This is a special case and less commonly used. Most applications use complex modes. +""" +function inverse(ft::FourierTransform, modes::AbstractVecOrMat{<:Real}) + dth = 2π / ft.mtheta + + if modes isa AbstractVector + @assert length(modes) == ft.mpert "Input vector must have length mpert=$(ft.mpert)" + return (ft.cslth * modes) .* dth + else + @assert size(modes, 1) == ft.mpert "Input matrix first dimension must be mpert=$(ft.mpert)" + return (ft.cslth * modes) .* dth + end +end + +# ============================================================================== +# In-place transform functions for performance-critical applications +# ============================================================================== + +""" + transform!(output::AbstractVecOrMat{ComplexF64}, ft::FourierTransform, data::AbstractVecOrMat{<:Real}) + +In-place forward Fourier transform for real-valued theta-space data. + +More efficient than the allocating version `ft(data)` when called repeatedly, +as it reuses the pre-allocated output array. + +# Arguments + +- `output::AbstractVecOrMat{ComplexF64}`: Pre-allocated output array for Fourier modes + - For vector input: must have length `mpert` + - For matrix input: must have size `(mpert, n)` where `n = size(data, 2)` +- `ft::FourierTransform`: Fourier transform object with pre-computed coefficients +- `data::AbstractVecOrMat{Float64}`: Real-valued data at theta points + - For vector: length must be `mtheta` + - For matrix: first dimension must be `mtheta` + +# Returns + +- `output`: The modified output array (for chaining) + +# Performance + +This function uses in-place matrix multiplication (`mul!`) to avoid allocations, +making it suitable for tight loops and performance-critical code. + +# Example + +```julia +ft = FourierTransform(480, 40, -20) + +# Pre-allocate output buffer +modes = zeros(ComplexF64, 40) + +# Reuse buffer in loop +for i in 1:1000 + data = get_theta_data(i) + transform!(modes, ft, data) # No allocations + process_modes(modes) +end +``` +""" +function transform!(output::AbstractVecOrMat{ComplexF64}, ft::FourierTransform, data::AbstractVecOrMat{<:Real}) + if data isa AbstractVector + @assert length(data) == ft.mtheta "Input vector must have length mtheta=$(ft.mtheta)" + @assert length(output) == ft.mpert "Output vector must have length mpert=$(ft.mpert)" + + # Extract real and imaginary views + real_part = reinterpret(Float64, output) + real_view = @view real_part[1:2:end] # Real components + imag_view = @view real_part[2:2:end] # Imaginary components + + # In-place computation: real = cslth' * data, imag = snlth' * data + mul!(real_view, ft.cslth', data) + mul!(imag_view, ft.snlth', data) + else + @assert size(data, 1) == ft.mtheta "Input matrix first dimension must be mtheta=$(ft.mtheta)" + @assert size(output, 1) == ft.mpert "Output matrix first dimension must be mpert=$(ft.mpert)" + @assert size(output, 2) == size(data, 2) "Output and input must have same number of columns" + + # For matrices, we need temporary storage for real/imag parts + n_cols = size(data, 2) + real_part = similar(output, Float64, ft.mpert, n_cols) + imag_part = similar(output, Float64, ft.mpert, n_cols) + + # In-place computation + mul!(real_part, ft.cslth', data) + mul!(imag_part, ft.snlth', data) + + # Combine into complex output + output .= complex.(real_part, imag_part) + end + + return output +end + +""" + transform!(output::AbstractVecOrMat{ComplexF64}, ft::FourierTransform, data::AbstractVecOrMat{<:Complex}) + +In-place forward Fourier transform for complex-valued theta-space data. + +# Arguments + +- `output::AbstractVecOrMat{ComplexF64}`: Pre-allocated output array for Fourier modes +- `ft::FourierTransform`: Fourier transform object +- `data::AbstractVecOrMat{ComplexF64}`: Complex-valued data at theta points + +# Returns + +- `output`: The modified output array + +# Formula + +For complex input `data = data_real + im*data_imag`: +``` +Re{mode[l]} = Σᵢ (data_real[i]*cos - data_imag[i]*sin) +Im{mode[l]} = Σᵢ (data_real[i]*sin + data_imag[i]*cos) +``` +""" +function transform!(output::AbstractVecOrMat{ComplexF64}, ft::FourierTransform, data::AbstractVecOrMat{<:Complex}) + if data isa AbstractVector + @assert length(data) == ft.mtheta "Input vector must have length mtheta=$(ft.mtheta)" + @assert length(output) == ft.mpert "Output vector must have length mpert=$(ft.mpert)" + + # Temporary storage for intermediate results + real_part = similar(output, Float64) + imag_part = similar(output, Float64) + temp1 = similar(output, Float64) + temp2 = similar(output, Float64) + + # Re{mode} = cslth' * real(data) - snlth' * imag(data) + mul!(temp1, ft.cslth', real.(data)) + mul!(temp2, ft.snlth', imag.(data)) + real_part .= temp1 .- temp2 + + # Im{mode} = cslth' * imag(data) + snlth' * real(data) + mul!(temp1, ft.cslth', imag.(data)) + mul!(temp2, ft.snlth', real.(data)) + imag_part .= temp1 .+ temp2 + + output .= complex.(real_part, imag_part) + else + @assert size(data, 1) == ft.mtheta "Input matrix first dimension must be mtheta=$(ft.mtheta)" + @assert size(output, 1) == ft.mpert "Output matrix first dimension must be mpert=$(ft.mpert)" + @assert size(output, 2) == size(data, 2) "Output and input must have same number of columns" + + n_cols = size(data, 2) + real_part = similar(output, Float64, ft.mpert, n_cols) + imag_part = similar(output, Float64, ft.mpert, n_cols) + temp1 = similar(real_part) + temp2 = similar(real_part) + + # Re{mode} = cslth' * real(data) - snlth' * imag(data) + mul!(temp1, ft.cslth', real.(data)) + mul!(temp2, ft.snlth', imag.(data)) + real_part .= temp1 .- temp2 + + # Im{mode} = cslth' * imag(data) + snlth' * real(data) + mul!(temp1, ft.cslth', imag.(data)) + mul!(temp2, ft.snlth', real.(data)) + imag_part .= temp1 .+ temp2 + + output .= complex.(real_part, imag_part) + end + + return output +end + +""" + inverse_transform!(output::AbstractVecOrMat{ComplexF64}, ft::FourierTransform, modes::AbstractVecOrMat{<:Complex}) + +In-place inverse Fourier transform: mode-space → theta-space. + +More efficient than the allocating version `inverse(ft, modes)` when called repeatedly. + +# Arguments + +- `output::AbstractVecOrMat{ComplexF64}`: Pre-allocated output array for theta-space data + - For vector: must have length `mtheta` + - For matrix: must have size `(mtheta, n)` where `n = size(modes, 2)` +- `ft::FourierTransform`: Fourier transform object +- `modes::AbstractVecOrMat{ComplexF64}`: Complex Fourier mode coefficients + - For vector: length must be `mpert` + - For matrix: first dimension must be `mpert` + +# Returns + +- `output`: The modified output array (for chaining) + +# Formula + +``` +data[i] = (2π/mtheta) * Σₗ [Re{modes[l]}*cos - Im{modes[l]}*sin + + im*(Re{modes[l]}*sin + Im{modes[l]}*cos)] +``` + +# Example + +```julia +ft = FourierTransform(480, 40, -20) + +# Pre-allocate output buffer +theta_data = zeros(ComplexF64, 480) + +# Reuse buffer in loop +for i in 1:1000 + modes = get_modes(i) + inverse_transform!(theta_data, ft, modes) # No allocations + process_theta_data(theta_data) +end +``` +""" +function inverse_transform!(output::AbstractVecOrMat{ComplexF64}, ft::FourierTransform, modes::AbstractVecOrMat{<:Complex}) + dth = 2π / ft.mtheta + + if modes isa AbstractVector + @assert length(modes) == ft.mpert "Input vector must have length mpert=$(ft.mpert)" + @assert length(output) == ft.mtheta "Output vector must have length mtheta=$(ft.mtheta)" + + # Temporary storage + real_part = similar(output, Float64) + imag_part = similar(output, Float64) + temp1 = similar(output, Float64) + temp2 = similar(output, Float64) + + # Re{data} = cslth * real(modes) - snlth * imag(modes) + mul!(temp1, ft.cslth, real.(modes)) + mul!(temp2, ft.snlth, imag.(modes)) + real_part .= (temp1 .- temp2) .* dth + + # Im{data} = cslth * imag(modes) + snlth * real(modes) + mul!(temp1, ft.cslth, imag.(modes)) + mul!(temp2, ft.snlth, real.(modes)) + imag_part .= (temp1 .+ temp2) .* dth + + output .= complex.(real_part, imag_part) + else + @assert size(modes, 1) == ft.mpert "Input matrix first dimension must be mpert=$(ft.mpert)" + @assert size(output, 1) == ft.mtheta "Output matrix first dimension must be mtheta=$(ft.mtheta)" + @assert size(output, 2) == size(modes, 2) "Output and input must have same number of columns" + + n_cols = size(modes, 2) + real_part = similar(output, Float64, ft.mtheta, n_cols) + imag_part = similar(output, Float64, ft.mtheta, n_cols) + temp1 = similar(real_part) + temp2 = similar(real_part) + + # Re{data} = cslth * real(modes) - snlth * imag(modes) + mul!(temp1, ft.cslth, real.(modes)) + mul!(temp2, ft.snlth, imag.(modes)) + real_part .= (temp1 .- temp2) .* dth + + # Im{data} = cslth * imag(modes) + snlth * real(modes) + mul!(temp1, ft.cslth, imag.(modes)) + mul!(temp2, ft.snlth, real.(modes)) + imag_part .= (temp1 .+ temp2) .* dth + + output .= complex.(real_part, imag_part) + end + + return output +end + +# ============================================================================== +# Low-level matrix transforms with offsets (for Vacuum module compatibility) +# ============================================================================== + +""" + fourier_transform!(gil::Matrix{Float64}, gij::Matrix{Float64}, cs::Matrix{Float64}, m00::Int, l00::Int) + +Low-level Fourier transform with offset support for Vacuum module. + +Performs a truncated Fourier transform of `gij` onto `gil` using pre-computed +Fourier coefficients `cs`, with support for block offsets in the output matrix. + +This is used by the Vacuum module for transforming Green's function matrices that +have specific block structure (e.g., plasma and wall contributions packed together). + +# Arguments + +- `gil::Matrix{Float64}`: Output matrix, updated in-place at offset block +- `gij::Matrix{Float64}`: Input matrix (mtheta × mtheta) containing theta-space data +- `cs::Matrix{Float64}`: Fourier coefficient matrix (mtheta × mpert), either `cslth` or `snlth` +- `m00::Int`: Row offset in `gil` matrix (0-based indexing convention) +- `l00::Int`: Column offset in `gil` matrix (0-based indexing convention) + +# Operation + +Computes: `gil[m00+i, l00+l] = Σⱼ gij[i, j] * cs[j, l]` for i ∈ 1:mtheta, l ∈ 1:mpert + +The block `gil[m00+1:m00+mtheta, l00+1:l00+mpert]` is zeroed and then filled. + +# Notes + +- This function uses 0-based offset convention (add 1 for Julia indexing) +- The `cs` matrix should be either `cslth` or `snlth` from a `FourierTransform` object +- Used for packing real and imaginary parts in separate blocks of `gil` + +# Example + +```julia +ft = FourierTransform(mtheta, mpert, mlow; n=n, qa=qa, delta=delta) +gil = zeros(2*mtheta, 2*mpert) # Packed real/imag structure +gij = ... # Some theta-space data + +# Transform using cosine coefficients, real part block (offset 0, 0) +fourier_transform!(gil, gij, ft.cslth, 0, 0) + +# Transform using sine coefficients, imaginary part block (offset 0, mpert) +fourier_transform!(gil, gij, ft.snlth, 0, mpert) +``` +""" +function fourier_transform!(gil::Matrix{Float64}, gij::Matrix{Float64}, cs::Matrix{Float64}, m00::Int, l00::Int) + # Zero out relevant gil block + mtheta, mpert = size(cs) + fill!(view(gil, m00+1:m00+mtheta, l00+1:l00+mpert), 0.0) + + # Fourier transform via matrix multiply: gil[i, l] = Σ_j gij[i, j] * cs[j, l] + mul!(view(gil, m00+1:m00+mtheta, l00+1:l00+mpert), gij, cs) +end + +""" + fourier_inverse_transform!(gll::Matrix{Float64}, gil::Matrix{Float64}, cs::Matrix{Float64}, m00::Int, l00::Int) + +Low-level inverse Fourier transform with offset support for Vacuum module. + +Performs the inverse Fourier transform of `gil` onto `gll` using pre-computed +Fourier coefficients `cs`, with support for block offsets in the input matrix. + +This is used by the Vacuum module for inverse transforming Green's function matrices +back to mode space from theta space. + +# Arguments + +- `gll::Matrix{Float64}`: Output matrix (mpert × mpert), updated in-place +- `gil::Matrix{Float64}`: Input matrix containing Fourier-transformed data +- `cs::Matrix{Float64}`: Fourier coefficient matrix (mtheta × mpert), either `cslth` or `snlth` +- `m00::Int`: Row offset in `gil` matrix (0-based indexing convention) +- `l00::Int`: Column offset in `gil` matrix (0-based indexing convention) + +# Operation + +Computes: `gll[l2, l1] = (2π/mtheta) * Σᵢ cs[i, l2] * gil[m00+i, l00+l1]` + +# Notes + +- The normalization factor `2π/mtheta` ensures proper Fourier series reconstruction +- This function uses 0-based offset convention (add 1 for Julia indexing) +- The `cs` matrix should be either `cslth` or `snlth` from a `FourierTransform` object +- Output `gll` is completely zeroed before computation + +# Example + +```julia +ft = FourierTransform(mtheta, mpert, mlow; n=n, qa=qa, delta=delta) +gil = ... # Transformed Green's function data +arr = zeros(mpert, mpert) + +# Inverse transform from real part block using cosine coefficients +fourier_inverse_transform!(arr, gil, ft.cslth, 0, 0) +``` +""" +function fourier_inverse_transform!(gll::Matrix{Float64}, gil::Matrix{Float64}, cs::Matrix{Float64}, m00::Int, l00::Int) + # Zero out gll block + mtheta, mpert = size(cs) + fill!(view(gll, 1:mpert, 1:mpert), 0.0) + + # Inverse Fourier transform via matrix multiply: gll = cs^T * gil * (2π * dth) + # This computes: gll[l2, l1] = (2π * dth) * Σ_i cs[i, l2] * gil[i, l1] + dth = 2π / mtheta + mul!(gll, cs', view(gil, m00+1:m00+mtheta, l00+1:l00+mpert), 2π * dth, 0.0) +end + +end # module FourierTransforms diff --git a/src/Utilities/Utilities.jl b/src/Utilities/Utilities.jl index fe6bb1cd..6e5f2c0f 100644 --- a/src/Utilities/Utilities.jl +++ b/src/Utilities/Utilities.jl @@ -1,7 +1,26 @@ -module UtilitiesMod +""" + Utilities +Shared mathematical and computational utilities for JPEC modules. + +This module provides common functionality used across multiple JPEC modules, +including efficient Fourier transforms, numerical integration, and other +mathematical utilities. + +# Submodules + +- `FourierTransforms`: Efficient Fourier transforms with pre-computed basis functions +""" +module Utilities + +include("FourierTransforms.jl") include("FourierCoefficients.jl") +using .FourierTransforms +export FourierTransform, inverse, compute_fourier_coefficients +export transform!, inverse_transform! +export fourier_transform!, fourier_inverse_transform! + export FourierCoefficients, empty_FourierCoefficients, get_complex_coeff, get_complex_coeffs! -end +end # module Utilities \ No newline at end of file diff --git a/src/Vacuum/DataTypes.jl b/src/Vacuum/DataTypes.jl index 2ebbb0bc..cfcde7bf 100644 --- a/src/Vacuum/DataTypes.jl +++ b/src/Vacuum/DataTypes.jl @@ -1,30 +1,39 @@ +""" +Vacuum structures and initialization functions. +""" + """ VacuumInput -Struct holding plasma boundary and mode data as provided from DCON namelist and computed quantities. +Struct holding plasma boundary and mode data as provided from ForceFreeStates namelist and computed quantities. # Fields - - `r::Vector{Float64}`: Plasma boundary R-coordinate on DCON theta grid - - `z::Vector{Float64}`: Plasma boundary Z-coordinate on DCON theta grid - - `ν::Vector{Float64}`: Free parameter in specifying toroidal angle, ϕ = 2πζ + ν(ψ, θ), on DCON theta grid - - `mlow::Int`: Lower poloidal mode number - - `mpert::Int`: Number of poloidal modes - - `n::Int`: Toroidal mode number - - `mtheta::Int`: Number of poloidal grid points for vacuum calculations - - `kernelsign::Float64`: Sign for kernel; +1 or -1, only ≠ 1 for mutual inductance calculations - - `force_wv_symmetry::Bool`: Boolean flag to enforce symmetry in the vacuum response matrix (set in dcon.toml) +- `r::Vector{Float64}`: Plasma boundary R-coordinate as a function of poloidal angle +- `z::Vector{Float64}`: Plasma boundary Z-coordinate as a function of poloidal angle +- `ν::Vector{Float64}`: Free parameter in specifying toroidal angle, ϕ = 2πζ + ν(ψ, θ), on theta grid +- `mlow::Int`: Lower poloidal mode number for spectral representation +- `mhigh::Int`: Upper poloidal mode number (mhigh = mlow + mpert - 1) +- `mpert::Int`: Number of poloidal modes (mhigh - mlow + 1) +- `n::Int`: Toroidal mode number +- `qa::Float64`: Safety factor at plasma boundary +- `mtheta_eq::Int`: Number of equilibrium poloidal grid points (input grid resolution) +- `mtheta::Int`: Number of vacuum calculation poloidal grid points +- `force_wv_symmetry::Bool`: Boolean flag to enforce symmetry in the vacuum response matrix """ @kwdef struct VacuumInput r::Vector{Float64} = Float64[] z::Vector{Float64} = Float64[] ν::Vector{Float64} = Float64[] mlow::Int = 0 + mhigh::Int = 0 mpert::Int = 0 n::Int = 0 + qa::Float64 = 1.0 + mtheta_eq::Int = 1 mtheta::Int = 1 - kernelsign::Float64 = 1.0 force_wv_symmetry::Bool = true + # NOTE: kernelsign parameter deprecated - compute_vacuum_response now computes both grri and grre end """ @@ -32,25 +41,21 @@ end Struct holding plasma geometry data on the mtheta grid for vacuum calculations. Arrays are of length `mtheta`, where `mtheta` is the number of poloidal grid points and θ ∈ [0, 1). -It also precomputes trigonometric basis functions needed for Fourier calculations into matrices -of size (mtheta, mpert), where `mpert` is the number of poloidal modes. # Fields - `x::Vector{Float64}`: Plasma surface R-coordinate on VACUUM theta grid - `z::Vector{Float64}`: Plasma surface Z-coordinate on VACUUM theta grid + - `delta::Vector{Float64}`: Toroidal angle offset δ = -ν/(n*qa) for vacuum phase factor - `dx_dtheta::Vector{Float64}`: Derivative dR/dθ at plasma surface - `dz_dtheta::Vector{Float64}`: Derivative dZ/dθ at plasma surface - - `sin_ln_basis::Matrix{Float64}`: sin(lθ - nν) basis functions for poloidal modes at plasma surface - - `cos_ln_basis::Matrix{Float64}`: cos(lθ - nν) basis functions for poloidal modes at plasma surface """ struct PlasmaGeometry x::Vector{Float64} z::Vector{Float64} + delta::Vector{Float64} dx_dtheta::Vector{Float64} dz_dtheta::Vector{Float64} - sin_ln_basis::Matrix{Float64} - cos_ln_basis::Matrix{Float64} end """ @@ -61,20 +66,14 @@ Struct holding wall geometry data for vacuum calculations. Arrays are of length # Fields - - `nowall::Bool`: Boolean flag indicating if there is no wall - - `is_closed_toroidal::Bool`: Boolean flag indicating if the wall is a closed toroidal surface - - `x::Vector{Float64}`: Wall R-coordinates - - `z::Vector{Float64}`: Wall Z-coordinates - - `dx_dtheta::Vector{Float64}`: Derivative dR/dθ at wall - - `dz_dtheta::Vector{Float64}`: Derivative dZ/dθ at wall + - `nowall::Bool`: Boolean flag indicating if there is no wall + - `x::Vector{Float64}`: Wall R-coordinates + - `z::Vector{Float64}`: Wall Z-coordinates """ -struct WallGeometry - nowall::Bool - is_closed_toroidal::Bool - x::Vector{Float64} - z::Vector{Float64} - dx_dtheta::Vector{Float64} - dz_dtheta::Vector{Float64} +@kwdef struct WallGeometry + nowall::Bool = true + x::Vector{Float64} = Float64[] + z::Vector{Float64} = Float64[] end """ @@ -84,7 +83,7 @@ Struct containing input settings for vacuum wall geometry. # Fields - - `shape::String`: String selecting wall shape. Options are: +- `shape::String`: String selecting wall shape. Options are: + `"nowall"`: No wall + `"conformal"`: Wall conformal to plasma surface at distance `a` @@ -93,20 +92,20 @@ Struct containing input settings for vacuum wall geometry. + `"mod_dee"`: Modified Dee-shaped wall + `"filepath"`: Custom wall shape from the file you specify - - `a::Float64`: Distance of wall from plasma in units of major radius (conformal), or shape parameter (others) - - `aw::Float64`: Half-thickness parameter for Dee-shaped walls - - `bw::Float64`: Elongation parameter for wall shapes - - `cw::Float64`: Offset of the center of the wall from the major radius - - `dw::Float64`: Triangularity parameter for wall shapes - - `tw::Float64`: Sharpness of the corners of the wall (try 0.05 as initial value) - - `equal_arc_wall::Bool`: Flag to enforce equal arc length distribution of nodes on the wall - (recommended unless wall is very close to plasma) +- `a::Float64`: Distance of wall from plasma in units of major radius (conformal), or shape parameter (others) +- `aw::Float64`: Half-thickness parameter for Dee-shaped walls +- `bw::Float64`: Elongation parameter for wall shapes +- `cw::Float64`: Offset of the center of the wall from the major radius +- `dw::Float64`: Triangularity parameter for wall shapes +- `tw::Float64`: Sharpness of the corners of the wall (try 0.05 as initial value) +- `equal_arc_wall::Bool`: Flag to enforce equal arc length distribution of nodes on the wall + (recommended unless wall is very close to plasma) """ -@kwdef struct WallShapeSettings +@kwdef struct WallShapeSettings # Core shape selection shape::String = "nowall" - + # Standard geometric parameters for Dee/Mod-Dee a::Float64 = 0.3 aw::Float64 = 0.05 @@ -114,7 +113,7 @@ Struct containing input settings for vacuum wall geometry. cw::Float64 = 0.0 dw::Float64 = 0.5 tw::Float64 = 0.05 - + # Algorithmic options equal_arc_wall::Bool = true end @@ -122,94 +121,85 @@ end """ initialize_plasma_surface(inputs::VacuumInput) -> PlasmaGeometry -Initialize the plasma surface geometry based on the provided vacuum inputs. +Initialize the plasma surface geometry based on the provided vacuum inputs. -This function performs functionality from `readahg`, `arrays`, and `funint` in the +This function performs functionality from `readahg`, `arrays`, and `funint` in the original Fortran VACUUM code. It returns a `PlasmaGeometry` struct containing the necessary plasma surface data for vacuum calculations. # Process - 1. Interpolate the input plasma boundary arrays onto the mtheta grid - 2. Compute derivatives of the plasma boundary with respect to poloidal angle θ - using periodic cubic spline differentiation - 3. Compute trigonometric basis functions needed for Fourier calculations +1. Interpolate the input plasma boundary arrays onto the mtheta grid +2. Compute derivatives of the plasma boundary with respect to poloidal angle θ + using periodic cubic spline differentiation +3. Compute trigonometric basis functions needed for Fourier calculations # Arguments - - `inputs::VacuumInput`: Struct containing plasma boundary data and calculation parameters +- `inputs::VacuumInput`: Struct containing plasma boundary data and calculation parameters # Returns - - `PlasmaGeometry`: Struct containing plasma surface coordinates, derivatives, and basis functions +- `PlasmaGeometry`: Struct containing plasma surface coordinates, derivatives, and basis functions """ function initialize_plasma_surface(inputs::VacuumInput) - (; mtheta, mpert, mlow, ν, r, z, n) = inputs # Interpolate arrays from input onto mtheta grid - x_plasma = interp_to_new_grid(r, mtheta) - z_plasma = interp_to_new_grid(z, mtheta) - ν = interp_to_new_grid(ν, mtheta) + mtheta = inputs.mtheta + x_plasma = interp_to_new_grid(inputs.r, mtheta) + z_plasma = interp_to_new_grid(inputs.z, mtheta) + ν = interp_to_new_grid(inputs.ν, mtheta) + + # Compute delta from ν for vacuum phase factor + # delta = -ν/qa for use in phase: cos(m*θ + n*qa*δ) = cos(m*θ - n*ν) + delta = -ν ./ inputs.qa # Plasma boundary theta derivative: length mth with θ = [0, 1) θ_grid = range(; start=0, length=mtheta, step=2π/mtheta) dx_dtheta = periodic_cubic_deriv(θ_grid, x_plasma) dz_dtheta = periodic_cubic_deriv(θ_grid, z_plasma) - # Precompute Fourier transform terms, sin(lθ - nν) and cos(lθ - nν) - sin_ln_basis = zeros(Float64, mtheta, mpert) - cos_ln_basis = zeros(Float64, mtheta, mpert) - for j in 1:mpert - for i in 1:mtheta - l = mlow + j - 1 - cos_ln_basis[i, j] = cos(l * θ_grid[i] - n * ν[i]) - sin_ln_basis[i, j] = sin(l * θ_grid[i] - n * ν[i]) - end - end - return PlasmaGeometry( x_plasma, z_plasma, + delta, dx_dtheta, - dz_dtheta, - sin_ln_basis, - cos_ln_basis + dz_dtheta ) end """ initialize_wall(inputs::VacuumInput, plasma_surf::PlasmaGeometry, wall_settings::WallShapeSettings) -> WallGeometry -Initialize the wall geometry based on the provided vacuum inputs and wall shape settings. +Initialize the wall geometry based on the provided vacuum inputs and wall shape settings. -This performs functionality similar to portions of the `arrays` function in the original -Fortran VACUUM code. It returns a `WallGeometry` struct containing the necessary wall +This performs functionality similar to portions of the `arrays` function in the original +Fortran VACUUM code. It returns a `WallGeometry` struct containing the necessary wall surface data for vacuum calculations. # Arguments - - `inputs::VacuumInput`: Struct containing vacuum calculation parameters - - `plasma_surf::PlasmaGeometry`: Struct with plasma surface geometry (used for reference) - - `wall_settings::WallShapeSettings`: Struct specifying wall shape and parameters +- `inputs::VacuumInput`: Struct containing vacuum calculation parameters +- `plasma_surf::PlasmaGeometry`: Struct with plasma surface geometry (used for reference) +- `wall_settings::WallShapeSettings`: Struct specifying wall shape and parameters # Returns - - `WallGeometry`: Struct containing wall surface coordinates and derivatives +- `WallGeometry`: Struct containing wall surface coordinates and derivatives # Notes - - Supports multiple wall shapes: nowall, conformal, elliptical, dee, mod_dee, from_file - - Optionally redistributes wall points to equal arc length spacing if `equal_arc_wall=true` +- Supports multiple wall shapes: nowall, conformal, elliptical, dee, mod_dee, from_file +- Optionally redistributes wall points to equal arc length spacing if `equal_arc_wall=true` """ function initialize_wall(inputs::VacuumInput, plasma_surf::PlasmaGeometry, wall_settings::WallShapeSettings) - + # Basic wall flags nowall = wall_settings.shape == "nowall" - is_closed_toroidal = true - # All of these arrays are of length mtheta with θ = [0, 1) + # All of these arrays are of length mtheta with θ = [0, 1) mtheta = inputs.mtheta - + # Get wall shape from form_wall # Plasma surface coordinates x_plasma = plasma_surf.x @@ -217,7 +207,7 @@ function initialize_wall(inputs::VacuumInput, plasma_surf::PlasmaGeometry, wall_ # Output wall coordinate arrays x_wall = zeros(Float64, mtheta) - z_wall = zeros(Float64, mtheta) + z_wall = zeros(Float64, mtheta) # Common geometric parameters xmin = minimum(x_plasma) @@ -227,7 +217,7 @@ function initialize_wall(inputs::VacuumInput, plasma_surf::PlasmaGeometry, wall_ r_minor = 0.5 * (xmax - xmin) r_major = 0.5 * (xmax + xmin) - + # Destructuring settings for readability (; aw, bw, cw, dw, tw, a) = wall_settings wcentr = 0.0 # Initialize @@ -236,7 +226,7 @@ function initialize_wall(inputs::VacuumInput, plasma_surf::PlasmaGeometry, wall_ @info "Using no wall" elseif wall_settings.shape == "conformal" dx = a * r_minor - @info "Calculating conformal wall shape $((@sprintf "%.2e" dx)) m from plasma surface." + @info "Calculating conformal wall shape $((@sprintf "%.2e" dx)) m from plasma surface." wcentr = r_major centerstack_min = min(0.1, 0.1 * minimum(x_plasma)) # Avoid wall crossing R=0 axis for i in 1:mtheta @@ -258,9 +248,9 @@ function initialize_wall(inputs::VacuumInput, plasma_surf::PlasmaGeometry, wall_ zrad = 0.5 * (zmax - zmin) zh = sqrt(abs(zrad^2 - r_minor^2)) - zmuw = log((a/zh) + sqrt((a/zh)^2 + 1)) - bw_eff = (zh * cosh(zmuw)) / a - + zmuw = log((a/zh) + sqrt((a/zh)^2 + 1)) + bw_eff = (zh * cosh(zmuw)) / a + for i in 1:mtheta the = (i - 1) * (2π / mtheta) x_wall[i] = r_major + a * cos(the) @@ -309,28 +299,8 @@ function initialize_wall(inputs::VacuumInput, plasma_surf::PlasmaGeometry, wall_ # Optional: Re-parameterization if wall_settings.equal_arc_wall && (wall_settings.shape != "nowall") - @info "Re-distributing wall points to equal arc length spacing" - if !is_closed_toroidal - @error "Wall is not closed toroidally; equal arc length distribution assumes periodicity as cannot be safely used." - end - x_wall, z_wall, _, theta_grid, _ = distribute_to_equal_arc_grid(x_wall, z_wall, mtheta) - theta_grid .= theta_grid .* (2π) # Scale to [0, 2π) - irregular spacing - # Close the loop for periodic BC by appending first point at the end - theta_closed = vcat(collect(theta_grid), 2π) - x_closed = vcat(x_wall, x_wall[1]) - z_closed = vcat(z_wall, z_wall[1]) - spline_x = cubic_interp(theta_closed, x_closed; bc=PeriodicBC()) - spline_z = cubic_interp(theta_closed, z_closed; bc=PeriodicBC()) - d1_spline_x = deriv1(spline_x) - d1_spline_z = deriv1(spline_z) - theta_vec = collect(theta_grid) - dx_dtheta = d1_spline_x.(theta_vec) - dz_dtheta = d1_spline_z.(theta_vec) - else - # used regular theta grid spacing to build wall - theta_grid = range(0; stop=2π, length=mtheta + 1)[1:(end-1)] # length mtheta without endpoint - dx_dtheta = periodic_cubic_deriv(theta_grid, x_wall) - dz_dtheta = periodic_cubic_deriv(theta_grid, z_wall) + @info "Re-distributing wall points to equal arc length spacing (assumes closed, toroidal wall)." + x_wall, z_wall, _, _, _ = distribute_to_equal_arc_grid(x_wall, z_wall, mtheta) end if any(x_wall .<= 0.0) && !nowall @@ -339,11 +309,8 @@ function initialize_wall(inputs::VacuumInput, plasma_surf::PlasmaGeometry, wall_ end return WallGeometry( - nowall, - is_closed_toroidal, - x_wall, - z_wall, - dx_dtheta, - dz_dtheta + nowall=nowall, + x=x_wall, + z=z_wall ) end diff --git a/src/Vacuum/MathUtils.jl b/src/Vacuum/MathUtils.jl index 985449c7..0df35dae 100644 --- a/src/Vacuum/MathUtils.jl +++ b/src/Vacuum/MathUtils.jl @@ -1,63 +1,5 @@ -""" - fourier_inverse_transform!(gll, gil, cs, m00, l00) - -Perform the inverse Fourier transform of `gil` onto `gll` using Fourier coefficients stored in `cs`. - -# Arguments - - - `gll`: Output matrix (mpert × mpert) updated in-place - - `gil`: Input matrix (mtheta × mpert) containing Fourier-space data - - `cs`: Fourier coefficient matrix (mtheta × mpert) - - `m00`: Integer offset in the gil matrix (row offset) - - `l00`: Integer offset in the gil matrix (column offset) - -# Notes - - - Computes: `gll[l2, l1] = (2π * dth) * Σ_i cs[i, l2] * gil[i, l1]` - - Performs the same function as fouranv in the Fortran code. - -# Returns - - - gll(l2,l1) : output matrix updated in-place (mpert × mpert) -""" -function fourier_inverse_transform!(gll::Matrix{Float64}, gil::Matrix{Float64}, cs::Matrix{Float64}, m00::Int, l00::Int) - - # Zero out gll block - mtheta, mpert = size(cs) - fill!(view(gll, 1:mpert, 1:mpert), 0.0) - - # Inverse Fourier transform via matrix multiply: gll = cs^T * gil * (2π * dth) - # This computes: gll[l2, l1] = (2π * dth) * Σ_i cs[i, l2] * gil[i, l1] - dth = 2π / mtheta - mul!(gll, cs', view(gil, (m00+1):(m00+mtheta), (l00+1):(l00+mpert)), 2π * dth, 0.0) -end - -""" - fourier_transform!(gil, gij, cs, m00, l00, mth, mpert) - - Purpose: - This routine performs a truncated Fourier transform of gij onto gil - using Fourier coefficients stored in cs. - - Inputs: - gij(i,j) : input matrix of size (mth × mth), the "physical-space" data - cs(j,l) : Fourier coefficient matrix (mth × mpert) - m00, l00 : integer offsets in the gil matrix - mth : number of θ-grid points (dimension of gij along i, j) - mpert : number of Fourier modes - - Output: - gil(i', l') : output matrix updated in-place (mth × mpert), where i' = m00 + i and l' = l00 + l -""" -function fourier_transform!(gil::Matrix{Float64}, gij::Matrix{Float64}, cs::Matrix{Float64}, m00::Int, l00::Int) - - # Zero out relevant gil block - mtheta, mpert = size(cs) - fill!(view(gil, (m00+1):(m00+mtheta), (l00+1):(l00+mpert)), 0.0) - - # Fourier transform via matrix multiply: gil[i, l] = Σ_j gij[i, j] * cs[j, l] - mul!(view(gil, (m00+1):(m00+mtheta), (l00+1):(l00+mpert)), gij, cs) -end +# NOTE: fourier_transform! and fourier_inverse_transform! have been moved to +# src/Utilities/FourierTransforms.jl and are imported at the module level in Vacuum.jl # Returns the array of derivatives at all x points, I think this acts like difspl # in the Fortran but need to check/consolidate spline routines later @@ -196,9 +138,14 @@ function interp_to_new_grid(vecin::Vector{Float64}, mtheta::Int; dx0=0.0, dx1=0. return vecin end - # Input grids are from [0, 1] inclusive, since no interpolants will fall outside of this, we don't need periodic extrapolation + # Enforce periodicity: FastInterpolations requires y[1] ≈ y[end] for PeriodicBC + # For truly periodic data (poloidal angle), force endpoints to match exactly + vecin_periodic = copy(vecin) + vecin_periodic[end] = vecin_periodic[1] + + # Input grids are from [0, 1] inclusive, while no interpolants will fall outside of this, the bc still impacts the end behavior θin = collect(range(0.0, 1.0; length=mtheta_in)) - spline = cubic_interp(θin, vecin) + spline = cubic_interp(θin, vecin_periodic; bc=PeriodicBC()) # Interpolate to new grid with optional offsets vecout = zeros(mtheta) diff --git a/src/Vacuum/Vacuum.jl b/src/Vacuum/Vacuum.jl index c57f88a4..a2696d88 100644 --- a/src/Vacuum/Vacuum.jl +++ b/src/Vacuum/Vacuum.jl @@ -3,15 +3,27 @@ module Vacuum using TOML, SpecialFunctions, LinearAlgebra, Printf using FastInterpolations: cubic_interp, deriv1, PeriodicBC, NaturalBC -export mscvac, set_dcon_params, VacuumInput, compute_vacuum_response -export compute_vacuum_field -export kernel! -export WallShapeSettings +# Import parent modules +import ..Spl +import ..Equilibrium + +# Import FourierTransforms utility for coefficient calculation and transforms +using ..Utilities.FourierTransforms: compute_fourier_coefficients, fourier_transform!, fourier_inverse_transform! +# Include core data structures and functions first include("DataTypes.jl") include("Kernel2D.jl") include("MathUtils.jl") +# Include VacuumFromEquilibrium after DataTypes so VacuumInput is defined +include("VacuumFromEquilibrium.jl") + +export mscvac, set_surface_params, VacuumInput, compute_vacuum_response +export compute_vacuum_field +export kernel! +export WallShapeSettings +export extract_plasma_surface_at_psi, create_vacuum_input_at_psi, compute_greens_functions_only + # ====================================================================== # Legacy fortran vacuum module interface # ====================================================================== @@ -20,9 +32,9 @@ const libdir = joinpath(@__DIR__, "..", "..", "deps") const libvac = joinpath(libdir, "libvac") """ - set_dcon_params(mtheta, lmin, lmax, nnin, qa1in, xin, zin, deltain) + set_surface_params(mtheta, lmin, lmax, nnin, qa1in, xin, zin, deltain) -Initialize DCON parameters for vacuum field calculations. +Initialize ForceFreeStates parameters for vacuum field calculations. # Arguments @@ -50,14 +62,14 @@ xin = rand(Float64, n_modes) zin = rand(Float64, n_modes) deltain = rand(Float64, n_modes) -set_dcon_params(mtheta, lmin, lmax, nnin, qa1in, xin, zin, deltain) +set_surface_params(mtheta, lmin, lmax, nnin, qa1in, xin, zin, deltain) ``` """ -function set_dcon_params(mtheta::Integer, lmin::Integer, lmax::Integer, nnin::Integer, +function set_surface_params(mtheta::Integer, lmin::Integer, lmax::Integer, nnin::Integer, qa1in::Float64, xin::Vector{Float64}, zin::Vector{Float64}, deltain::Vector{Float64}) - return ccall((:set_dcon_params_, libvac), + return ccall((:set_surface_params_, libvac), Nothing, (Ref{Cint}, Ref{Cint}, Ref{Cint}, Ref{Cint}, Ref{Cdouble}, @@ -68,30 +80,30 @@ function set_dcon_params(mtheta::Integer, lmin::Integer, lmax::Integer, nnin::In end """ - unset_dcon_params() + unset_surface_params() -Unset DCON parameters previously set by `set_dcon_params`. +Unset ForceFreeStates parameters previously set by `set_surface_params`. -This subroutine deallocates in-memory arrays (`x_dcon`, `z_dcon`, and `delta_dcon`) -and resets the internal DCON state for future vacuum calculations. +This subroutine deallocates in-memory arrays (`x`, `z`, and `delta`) +and resets the internal ForceFreeStates state for future vacuum calculations. # Notes - - Must be called after `set_dcon_params` if you want to reset the DCON memory. + - Must be called after `set_surface_params` if you want to reset the surface data memory. - No arguments are required. # Example ```julia # Set parameters -set_dcon_params(mtheta, lmin, lmax, nnin, qa1in, xin, zin, deltain) +set_surface_params(mtheta, lmin, lmax, nnin, qa1in, xin, zin, deltain) -# Reset DCON parameters -unset_dcon_params() +# Reset surface parameters +unset_surface_params() ``` """ -function unset_dcon_params() - ccall((:unset_dcon_params_, libvac), Nothing, ()) +function unset_surface_params() + ccall((:unset_surface_params_, libvac), Nothing, ()) end """ @@ -111,7 +123,7 @@ Compute the vacuum response matrix for magnetostatic perturbations. - `farwall_flag`: Whether to use far-wall approximation (Bool) - `grrio`: Green's function data (Array{Float64,2}) - `xzptso`: Source point coordinates (Array{Float64,2}) - - `op_ahgfile`: Optional communication file for when set_dcon_params is not called (String or Nothing) + - `op_ahgfile`: Optional communication file for when set_surface_params is not called (String or Nothing) # Returns @@ -120,13 +132,13 @@ Compute the vacuum response matrix for magnetostatic perturbations. # Note -Requires prior initialization with `set_dcon_params()` before calling this function. +Requires prior initialization with `set_surface_params()` before calling this function. # Examples ```julia # Initialize parameters first -set_dcon_params(mtheta, lmin, lmax, nnin, qa1in, xin, zin, deltain) +set_surface_params(mtheta, lmin, lmax, nnin, qa1in, xin, zin, deltain) # Set up vacuum calculation mpert = 5 @@ -200,13 +212,30 @@ function mscvac( return wv end +""" + apply_kernelsign!(grad_greenfunction_mat, kernelsign, mtheta) + +Apply kernelsign transformation to Green's function matrix. +For kernelsign < 0 (interior potential), multiply by -1 and add 2 to diagonal. +""" +function apply_kernelsign!(grad_greenfunction_mat::Matrix{Float64}, kernelsign::Float64, mtheta::Int) + if kernelsign < 0 + grad_greenfunction_mat .*= kernelsign + # Account for factor of 2 in diagonal terms in eq. 90 of Chance + for i in 1:(2*mtheta) + grad_greenfunction_mat[i, i] += 2.0 + end + end + return nothing +end + """ compute_vacuum_response(inputs::VacuumInput, wall_settings::WallShapeSettings) -Compute the vacuum response matrix using provided vacuum inputs. +Compute the vacuum response matrix and both Green's functions using provided vacuum inputs. This is the pure Julia implementation that replaces the Fortran `mscvac` function. -It returns the relevant arrays: `wv`, `grri`, and `xzpts`. +It computes both interior (grri) and exterior (grre) Green's functions for GPEC response calculations. # Arguments @@ -217,22 +246,31 @@ It returns the relevant arrays: `wv`, `grri`, and `xzpts`. # Returns - `wv`: Complex vacuum response matrix (mpert × mpert) relating plasma perturbations to vacuum response - - `grri`: Green's function response matrix (2*mtheta × 2*mpert) in Fourier space + - `grri`: Interior Green's function matrix (2*mtheta × 2*mpert) with kernelsign=-1 + - `grre`: Exterior Green's function matrix (2*mtheta × 2*mpert) with kernelsign=+1 - `xzpts`: Coordinate array (mtheta × 4) containing [R_plasma, Z_plasma, R_wall, Z_wall] # Notes - - This function initializes the plasma surface and wall geometry internally + - This function computes both Green's functions to enable proper surface inductance calculations + - Uses FourierTransforms utility internally for consistent coefficient calculation - The vacuum response includes plasma-plasma and plasma-wall coupling effects - For n=0 modes with closed walls, a regularization factor is added to prevent singularities """ function compute_vacuum_response(inputs::VacuumInput, wall_settings::WallShapeSettings) # Initialization and allocations - (; mtheta, mpert, n, kernelsign, force_wv_symmetry) = inputs + (; mtheta, mpert, mlow, n, qa, force_wv_symmetry) = inputs plasma_surf = initialize_plasma_surface(inputs) wall = initialize_wall(inputs, plasma_surf, wall_settings) - grri = zeros(2 * mtheta, 2 * mpert) + + # Compute Fourier basis coefficients using FourierTransforms utility + # We only need the coefficient arrays for the existing fourier_transform! functions + cos_ln_basis, sin_ln_basis = compute_fourier_coefficients(mtheta, mpert, mlow; n=n, qa=qa, delta=plasma_surf.delta) + + # Allocate arrays for both Green's functions + grri = zeros(2 * mtheta, 2 * mpert) # Interior (kernelsign=-1) + grre = zeros(2 * mtheta, 2 * mpert) # Exterior (kernelsign=+1) grad_greenfunction_mat = zeros(2 * mtheta, 2 * mtheta) greenfunction_temp = zeros(mtheta, mtheta) @@ -241,8 +279,11 @@ function compute_vacuum_response(inputs::VacuumInput, wall_settings::WallShapeSe kernel!(grad_greenfunction_mat, greenfunction_temp, plasma_surf.x, plasma_surf.z, plasma_surf.x, plasma_surf.z, j1, j2, n) # Fourier transform plasma-plasma block - fourier_transform!(grri, greenfunction_temp, plasma_surf.cos_ln_basis, 0, 0) - fourier_transform!(grri, greenfunction_temp, plasma_surf.sin_ln_basis, 0, mpert) + # Populate both grri and grre with the same right-hand side + fourier_transform!(grri, greenfunction_temp, cos_ln_basis, 0, 0) + fourier_transform!(grri, greenfunction_temp, sin_ln_basis, 0, mpert) + fourier_transform!(grre, greenfunction_temp, cos_ln_basis, 0, 0) + fourier_transform!(grre, greenfunction_temp, sin_ln_basis, 0, mpert) !wall.nowall && begin # Plasma–Wall block @@ -257,9 +298,11 @@ function compute_vacuum_response(inputs::VacuumInput, wall_settings::WallShapeSe j1, j2 = 2, 1 kernel!(grad_greenfunction_mat, greenfunction_temp, wall.x, wall.z, plasma_surf.x, plasma_surf.z, j1, j2, n) - # Fourier transform wall blocks into grri - fourier_transform!(grri, greenfunction_temp, plasma_surf.cos_ln_basis, mtheta, 0) - fourier_transform!(grri, greenfunction_temp, plasma_surf.sin_ln_basis, mtheta, mpert) + # Fourier transform wall blocks into both grri and grre + fourier_transform!(grri, greenfunction_temp, cos_ln_basis, mtheta, 0) + fourier_transform!(grri, greenfunction_temp, sin_ln_basis, mtheta, mpert) + fourier_transform!(grre, greenfunction_temp, cos_ln_basis, mtheta, 0) + fourier_transform!(grre, greenfunction_temp, sin_ln_basis, mtheta, mpert) end # Add cn0 to make grdgre nonsingular for n=0 modes @@ -272,21 +315,26 @@ function compute_vacuum_response(inputs::VacuumInput, wall_settings::WallShapeSe end end - # Only needed for mutual inductance with the wall calculations - (kernelsign < 0) && begin - grad_greenfunction_mat .*= kernelsign - # Account for factor of 2 in diagonal terms in eq. 90 of Chance - for i in 1:(2*mtheta) - grad_greenfunction_mat[i, i] += 2.0 - end - end + # Compute both Green's functions with different kernel signs + # grri: interior potential (kernelsign=-1) + # grre: exterior potential (kernelsign=+1) - # Invert the vacuum response system of equations, eqs. 92-94ish of Chance 1997 (gelimb in Fortran) + # Make copies for each kernelsign + grad_greenfunction_mat_interior = copy(grad_greenfunction_mat) + grad_greenfunction_mat_exterior = copy(grad_greenfunction_mat) + + # Apply kernelsign transformations + apply_kernelsign!(grad_greenfunction_mat_interior, -1.0, mtheta) # Interior + apply_kernelsign!(grad_greenfunction_mat_exterior, +1.0, mtheta) # Exterior (no-op) + + # Invert the vacuum response system for both cases # If plasma only, lower blocks will be empty if wall.nowall - @views grri[1:mtheta, :] .= grad_greenfunction_mat[1:mtheta, 1:mtheta] \ grri[1:mtheta, :] + @views grri[1:mtheta, :] .= grad_greenfunction_mat_interior[1:mtheta, 1:mtheta] \ grri[1:mtheta, :] + @views grre[1:mtheta, :] .= grad_greenfunction_mat_exterior[1:mtheta, 1:mtheta] \ grre[1:mtheta, :] else - grri .= grad_greenfunction_mat \ grri + grri .= grad_greenfunction_mat_interior \ grri + grre .= grad_greenfunction_mat_exterior \ grre end # There's some logic that computes xpass/zpass and chiwc/chiws here, might eventually be needed? @@ -296,10 +344,10 @@ function compute_vacuum_response(inputs::VacuumInput, wall_settings::WallShapeSe aii = zeros(mpert, mpert) ari = zeros(mpert, mpert) air = zeros(mpert, mpert) - fourier_inverse_transform!(arr, grri, plasma_surf.cos_ln_basis, 0, 0) - fourier_inverse_transform!(aii, grri, plasma_surf.sin_ln_basis, 0, mpert) - fourier_inverse_transform!(ari, grri, plasma_surf.sin_ln_basis, 0, 0) - fourier_inverse_transform!(air, grri, plasma_surf.cos_ln_basis, 0, mpert) + fourier_inverse_transform!(arr, grre, cos_ln_basis, 0, 0) + fourier_inverse_transform!(aii, grre, sin_ln_basis, 0, mpert) + fourier_inverse_transform!(ari, grre, sin_ln_basis, 0, 0) + fourier_inverse_transform!(air, grre, cos_ln_basis, 0, mpert) # Final form of vacuum response matrix (eq. 114 of Chance 2007) wv = complex.(arr .+ aii, air .- ari) @@ -313,7 +361,8 @@ function compute_vacuum_response(inputs::VacuumInput, wall_settings::WallShapeSe @views xzpts[:, 2] .= plasma_surf.z @views xzpts[:, 3] .= wall.x @views xzpts[:, 4] .= wall.z - return wv, grri, xzpts + + return wv, grri, grre, xzpts end """ diff --git a/src/Vacuum/VacuumFromEquilibrium.jl b/src/Vacuum/VacuumFromEquilibrium.jl new file mode 100644 index 00000000..296621a6 --- /dev/null +++ b/src/Vacuum/VacuumFromEquilibrium.jl @@ -0,0 +1,272 @@ +""" +Functions to extract vacuum calculation inputs from equilibrium at arbitrary flux surfaces. + +This allows computing Green's functions at interior flux surfaces (e.g., singular surfaces) +by extracting the plasma geometry from the equilibrium bicubic spline. +""" + +# Note: Equilibrium module is imported in parent scope via ..Equilibrium +# Import FourierTransforms for coefficient calculation +using ..Utilities.FourierTransforms: compute_fourier_coefficients, fourier_transform! + +""" + extract_plasma_surface_at_psi( + equil::Equilibrium.PlasmaEquilibrium, + psi::Float64, + mtheta_eq::Int + ) -> (r, z, delta, qa) + +Extract plasma surface geometry from equilibrium at specified flux coordinate. + +Evaluates equilibrium bicubic spline around the flux surface to get R, Z coordinates +and computes the toroidal angle offset delta for vacuum calculations. + +## Arguments +- `equil`: Equilibrium solution with rzphi bicubic spline +- `psi`: Normalized flux coordinate (0 at axis, 1 at edge) +- `mtheta_eq`: Number of poloidal grid points for surface extraction + +## Returns +Tuple of: +- `r::Vector{Float64}`: R-coordinates around flux surface [mtheta_eq] +- `z::Vector{Float64}`: Z-coordinates around flux surface [mtheta_eq] +- `delta::Vector{Float64}`: Toroidal angle offset δ = -ν/qa [mtheta_eq] +- `qa::Float64`: Safety factor at this surface + +## Implementation + +The equilibrium bicubic spline `rzphi` stores: +- rzphi.f[1] = r² (or rfac²) +- rzphi.f[2] = deta (angle offset) +- rzphi.f[3] = dphi (toroidal angle) +- rzphi.f[4] = jac (Jacobian) + +From these we compute: +- rfac = √(rzphi.f[1]) +- eta = 2π*(θ + rzphi.f[2]) +- R = R₀ + rfac*cos(eta) +- Z = Z₀ + rfac*sin(eta) +- delta = rzphi.f[3] / qa + +## Reference +Matches GPEC's ahg_write and gpvacuum_flxsurf approach (gpvacuum.f line 291-296) +""" +function extract_plasma_surface_at_psi( + equil::Equilibrium.PlasmaEquilibrium, + psi::Float64, + mtheta_eq::Int +) + # Get magnetic axis location + ro = equil.ro + zo = equil.zo + + # Get safety factor at this surface + qa = equil.profiles.q_spline(psi) + + # Allocate output arrays + r = zeros(Float64, mtheta_eq) + z = zeros(Float64, mtheta_eq) + delta = zeros(Float64, mtheta_eq) + + # Evaluate equilibrium around the flux surface + twopi = 2π + for itheta in 0:mtheta_eq-1 + # Theta coordinate normalized to [0, 1) + theta = itheta / mtheta_eq + + # Evaluate bicubic splines at (psi, theta) + # New API uses separate interpolants for each component + r2 = equil.rzphi_rsquared((psi, theta)) # r² or rfac² + deta = equil.rzphi_offset((psi, theta)) # angle offset + dphi = equil.rzphi_nu((psi, theta)) # toroidal angle offset (nu) + + rfac = sqrt(abs(r2)) + + # Compute R, Z coordinates + eta = twopi * (theta + deta) + r[itheta+1] = ro + rfac * cos(eta) + z[itheta+1] = zo + rfac * sin(eta) + + # Toroidal angle offset: delta = -ν/qa where ϕ = 2πζ + ν + # In GPEC: delta = dphi (already normalized) + delta[itheta+1] = dphi + end + + return r, z, delta, qa +end + +""" + create_vacuum_input_at_psi( + equil::Equilibrium.PlasmaEquilibrium, + psi::Float64, + mtheta_eq::Int, + mtheta::Int, + mpert::Int, + mlow::Int, + n::Int + ) -> VacuumInput + +Create VacuumInput structure for computing Green's functions at arbitrary flux surface. + +Extracts plasma geometry from equilibrium and packages it into VacuumInput format. + +## Arguments +- `equil`: Equilibrium solution +- `psi`: Normalized flux coordinate +- `mtheta_eq`: Number of equilibrium poloidal points +- `mtheta`: Number of vacuum calculation poloidal points +- `mpert`: Number of perturbing poloidal modes +- `mlow`: Lowest poloidal mode number +- `n`: Toroidal mode number + +## Returns +VacuumInput structure ready for compute_vacuum_response() + +## Usage +This is used to compute Green's functions at singular surfaces: +```julia +vac_input = create_vacuum_input_at_psi(equil, sing_surf.psifac, ...) +grri, grre = compute_greens_functions_only(vac_input, wall_settings) +``` +""" +function create_vacuum_input_at_psi( + equil::Equilibrium.PlasmaEquilibrium, + psi::Float64, + mtheta_eq::Int, + mtheta::Int, + mpert::Int, + mlow::Int, + n::Int +) + # Extract plasma surface geometry at this psi + r, z, delta, qa = extract_plasma_surface_at_psi(equil, psi, mtheta_eq) + + # Convert delta to ν for VacuumInput + # Relationship: delta = -ν/qa, so ν = -delta*qa + ν = -delta .* qa + + # Create VacuumInput structure + mhigh = mlow + mpert - 1 + + return VacuumInput( + r = r, + z = z, + ν = ν, + mlow = mlow, + mhigh = mhigh, + mpert = mpert, + n = n, + qa = qa, + mtheta_eq = mtheta_eq, + mtheta = mtheta, + force_wv_symmetry = true + ) +end + +""" + compute_greens_functions_only( + inputs::VacuumInput, + wall_settings::WallShapeSettings + ) -> (grri, grre) + +Compute only Green's functions without full vacuum response matrix. + +This is a lightweight version of compute_vacuum_response() that skips +the wv matrix calculation and only returns the Green's functions needed +for surface inductance calculations. + +## Arguments +- `inputs`: Vacuum input with plasma geometry +- `wall_settings`: Wall shape settings + +## Returns +Tuple of: +- `grri::Matrix{Float64}`: Interior Green's function [2*mtheta, 2*mpert] +- `grre::Matrix{Float64}`: Exterior Green's function [2*mtheta, 2*mpert] + +## Performance +Much faster than full compute_vacuum_response() since it skips: +- wv matrix construction +- Eigenvalue calculations +- Mode coupling matrices + +## Reference +Mimics GPEC's gpvacuum_flxsurf which only computes Green's functions +for surface inductance (gpvacuum.f line 279-283) +""" +function compute_greens_functions_only( + inputs::VacuumInput, + wall_settings::WallShapeSettings +) + # Reuse initialization from compute_vacuum_response + (; mtheta, mpert, mlow, n, qa) = inputs + plasma_surf = initialize_plasma_surface(inputs) + wall = initialize_wall(inputs, plasma_surf, wall_settings) + + # Compute Fourier basis coefficients + cslth, snlth = compute_fourier_coefficients(mtheta, mpert, mlow; n=n, qa=qa, delta=plasma_surf.delta) + + # Allocate arrays for both Green's functions + grri = zeros(2 * mtheta, 2 * mpert) + grre = zeros(2 * mtheta, 2 * mpert) + grad_greenfunction_mat = zeros(2 * mtheta, 2 * mtheta) + greenfunction_temp = zeros(mtheta, mtheta) + + # Plasma–Plasma block + j1, j2 = 1, 1 + kernel!(grad_greenfunction_mat, greenfunction_temp, plasma_surf.x, plasma_surf.z, plasma_surf.x, plasma_surf.z, j1, j2, n) + + # Fourier transform plasma-plasma block + fourier_transform!(grri, greenfunction_temp, cslth, 0, 0) + fourier_transform!(grri, greenfunction_temp, snlth, 0, mpert) + fourier_transform!(grre, greenfunction_temp, cslth, 0, 0) + fourier_transform!(grre, greenfunction_temp, snlth, 0, mpert) + + !wall.nowall && begin + # Plasma–Wall block + j1, j2 = 1, 2 + kernel!(grad_greenfunction_mat, greenfunction_temp, plasma_surf.x, plasma_surf.z, wall.x, wall.z, j1, j2, n) + + # Wall–Wall block + j1, j2 = 2, 2 + kernel!(grad_greenfunction_mat, greenfunction_temp, wall.x, wall.z, wall.x, wall.z, j1, j2, n) + + # Wall–Plasma block + j1, j2 = 2, 1 + kernel!(grad_greenfunction_mat, greenfunction_temp, wall.x, wall.z, plasma_surf.x, plasma_surf.z, j1, j2, n) + + # Fourier transform wall blocks + fourier_transform!(grri, greenfunction_temp, cslth, mtheta, 0) + fourier_transform!(grri, greenfunction_temp, snlth, mtheta, mpert) + fourier_transform!(grre, greenfunction_temp, cslth, mtheta, 0) + fourier_transform!(grre, greenfunction_temp, snlth, mtheta, mpert) + end + + # Add cn0 for n=0 modes if needed + cn0 = 1.0 + (abs(n) <= 1e-5 && !wall.nowall && wall.is_closed_toroidal) && begin + mth12 = wall.nowall ? mtheta : 2 * mtheta + for i in 1:mth12, j in 1:mth12 + grad_greenfunction_mat[i, j] += cn0 + end + end + + # Apply kernelsign transformations and invert to get Green's functions + # grri: interior (kernelsign=-1), grre: exterior (kernelsign=+1) + grad_greenfunction_mat_interior = copy(grad_greenfunction_mat) + grad_greenfunction_mat_exterior = copy(grad_greenfunction_mat) + + apply_kernelsign!(grad_greenfunction_mat_interior, -1.0, mtheta) # Interior + apply_kernelsign!(grad_greenfunction_mat_exterior, +1.0, mtheta) # Exterior + + # Invert the system for both cases + if wall.nowall + @views grri[1:mtheta, :] .= grad_greenfunction_mat_interior[1:mtheta, 1:mtheta] \ grri[1:mtheta, :] + @views grre[1:mtheta, :] .= grad_greenfunction_mat_exterior[1:mtheta, 1:mtheta] \ grre[1:mtheta, :] + else + grri .= grad_greenfunction_mat_interior \ grri + grre .= grad_greenfunction_mat_exterior \ grre + end + + return grri, grre +end diff --git a/src/Vacuum/fortran/vacuum_io.f b/src/Vacuum/fortran/vacuum_io.f index c73ba58c..342d328a 100644 --- a/src/Vacuum/fortran/vacuum_io.f +++ b/src/Vacuum/fortran/vacuum_io.f @@ -508,14 +508,14 @@ subroutine setahgdir ( directory ) end c----------------------------------------------------------------------- -c subprogram 7. set_dcon_params. +c subprogram 7. set_surface_params. c In memory io for DCON c Speedup over original ascii io (readahg) for multiple calls c----------------------------------------------------------------------- c----------------------------------------------------------------------- c declarations. c----------------------------------------------------------------------- - subroutine set_dcon_params ( mthin,lmin,lmax,nnin,qa1in,xin, + subroutine set_surface_params ( mthin,lmin,lmax,nnin,qa1in,xin, $ zin, deltain ) USE vglobal_mod, ONLY: r8 use vglobal_mod, only: dcon_set, mth_dcon, lmin_dcon,lmax_dcon, @@ -528,7 +528,7 @@ subroutine set_dcon_params ( mthin,lmin,lmax,nnin,qa1in,xin, c----------------------------------------------------------------------- mthin1 = mthin + 1 mthin5 = mthin1 + 4 - if(dcon_set) call unset_dcon_params + if(dcon_set) call unset_surface_params allocate(x_dcon(mthin5), z_dcon(mthin5), delta_dcon(mthin5)) mth_dcon = mthin lmin_dcon = lmin @@ -553,13 +553,13 @@ subroutine set_dcon_params ( mthin,lmin,lmax,nnin,qa1in,xin, end c----------------------------------------------------------------------- -c subprogram 8. unset_dcon_params. +c subprogram 8. unset_surface_params. c Tell vacuum to ignore in-memory dcon params and read ascii values c----------------------------------------------------------------------- c----------------------------------------------------------------------- c declarations. c----------------------------------------------------------------------- - subroutine unset_dcon_params + subroutine unset_surface_params use vglobal_mod, only: dcon_set, x_dcon, z_dcon, delta_dcon c----------------------------------------------------------------------- c computations. diff --git a/test/runtests_equil.jl b/test/runtests_equil.jl index edd7e1be..b28c46e0 100644 --- a/test/runtests_equil.jl +++ b/test/runtests_equil.jl @@ -7,7 +7,7 @@ # --- 1. Load EFIT Data (G-EQDSK format) --- @testset "Load EFIT Data" begin - efit_control = JPEC.Equilibrium.EquilibriumControl(; + efit_config = JPEC.Equilibrium.EquilibriumConfig(; eq_filename=joinpath(data_dir, "EQDSK_COCOS_02"), eq_type="efit", jac_type="boozer", @@ -15,7 +15,6 @@ psilow=0.01, psihigh=0.994 ) - efit_config = JPEC.Equilibrium.EquilibriumConfig(efit_control, JPEC.Equilibrium.EquilibriumOutput()) global plasma_eq_efit = JPEC.Equilibrium.setup_equilibrium(efit_config) @test plasma_eq_efit isa JPEC.Equilibrium.PlasmaEquilibrium @@ -24,7 +23,7 @@ # --- 2. Load CHEASE Binary Data --- @testset "Load CHEASE Binary" begin - binary_control = JPEC.Equilibrium.EquilibriumControl(; + binary_config = JPEC.Equilibrium.EquilibriumConfig(; eq_filename=joinpath(data_dir, "INP1_binary"), eq_type="chease_binary", jac_type="boozer", @@ -34,15 +33,14 @@ r0exp=6.8, b0exp=7.4 ) - chease_config_chease_binary = JPEC.Equilibrium.EquilibriumConfig(binary_control, JPEC.Equilibrium.EquilibriumOutput()) - global plasma_eq_binary = JPEC.Equilibrium.setup_equilibrium(chease_config_chease_binary) + global plasma_eq_binary = JPEC.Equilibrium.setup_equilibrium(binary_config) @test plasma_eq_binary isa JPEC.Equilibrium.PlasmaEquilibrium end # --- 3. Load CHEASE ASCII Data --- @testset "Load CHEASE ASCII" begin - ascii_control = JPEC.Equilibrium.EquilibriumControl(; + ascii_config = JPEC.Equilibrium.EquilibriumConfig(; eq_filename=joinpath(data_dir, "INP1_ascii"), eq_type="chease_ascii", jac_type="boozer", @@ -52,8 +50,7 @@ r0exp=6.8, b0exp=7.4 ) - chease_config_chease_ascii = JPEC.Equilibrium.EquilibriumConfig(ascii_control, JPEC.Equilibrium.EquilibriumOutput()) - global plasma_eq_ascii = JPEC.Equilibrium.setup_equilibrium(chease_config_chease_ascii) + global plasma_eq_ascii = JPEC.Equilibrium.setup_equilibrium(ascii_config) @test plasma_eq_ascii isa JPEC.Equilibrium.PlasmaEquilibrium end diff --git a/test/runtests_eulerlagrange.jl b/test/runtests_eulerlagrange.jl index 4a830e1b..03236894 100644 --- a/test/runtests_eulerlagrange.jl +++ b/test/runtests_eulerlagrange.jl @@ -1,7 +1,7 @@ using LinearAlgebra # TODO: perhaps this isn't the best place for this function? -# Should I do include("../dcon/utils.jl") instead? or maybe save these functions in a separate file? +# Should I do include("../ForceFreeStates/utils.jl") instead? or maybe save these functions in a separate file? # associated TODO: come up with Gaussian reduction test that doesn't rely on external data function load_u_matrix(filename) @@ -30,7 +30,7 @@ end # Test that resize_storage! doubles the size of storage arrays mpert = 3 numsteps_init = 10 - odet = JPEC.DCON.OdeState(mpert, numsteps_init, 10, 5) + odet = JPEC.ForceFreeStates.OdeState(mpert, numsteps_init, 10, 5) # Fill some data odet.step = 8 @@ -42,7 +42,7 @@ end end # Resize storage - JPEC.DCON.resize_storage!(odet) + JPEC.ForceFreeStates.resize_storage!(odet) # Check new size is doubled @test length(odet.psi_store) == 2 * numsteps_init @@ -59,7 +59,7 @@ end end # Check that you can resize again - JPEC.DCON.resize_storage!(odet) + JPEC.ForceFreeStates.resize_storage!(odet) @test length(odet.psi_store) == 4 * numsteps_init @test length(odet.q_store) == 4 * numsteps_init @test size(odet.u_store, 4) == 4 * numsteps_init @@ -70,7 +70,7 @@ end # Test that trim_storage! resizes arrays to actual step count mpert = 3 numsteps_init = 20 - odet = JPEC.DCON.OdeState(mpert, numsteps_init, 10, 5) + odet = JPEC.ForceFreeStates.OdeState(mpert, numsteps_init, 10, 5) # Set step to less than initial size odet.step = 12 @@ -82,7 +82,7 @@ end end # Trim storage - JPEC.DCON.trim_storage!(odet) + JPEC.ForceFreeStates.trim_storage!(odet) # Check sizes match step count @test length(odet.psi_store) == odet.step @@ -98,70 +98,70 @@ end @testset "compute_tols" begin # Test tolerance computation mpert = 3 - ctrl = JPEC.DCON.DconControl() + ctrl = JPEC.ForceFreeStates.ForceFreeStatesControl() ctrl.tol_r = 1e-6 ctrl.tol_nr = 1e-4 ctrl.crossover = 0.01 - intr = JPEC.DCON.DconInternal(; mpert=mpert) + intr = JPEC.ForceFreeStates.ForceFreeStatesInternal(; mpert=mpert) intr.msing = 2 - intr.sing = [JPEC.DCON.SingType(), JPEC.DCON.SingType()] + intr.sing = [JPEC.ForceFreeStates.SingType(), JPEC.ForceFreeStates.SingType()] intr.sing[1].q = 2.0 intr.sing[1].n = [1] intr.sing[2].q = 3.0 intr.sing[2].n = [1] - odet = JPEC.DCON.OdeState(mpert, 10, 10, 2) + odet = JPEC.ForceFreeStates.OdeState(mpert, 10, 10, 2) # Test 1: Far from singular surface (singfac > crossover) ising = 1 odet.q = 1.5 # Far from q=2.0 - rtol = JPEC.DCON.compute_tols(ctrl, intr, odet, ising) + rtol = JPEC.ForceFreeStates.compute_tols(ctrl, intr, odet, ising) @test rtol == ctrl.tol_nr # Should use non-resonant tolerance # Test 2: Close to singular surface (singfac < crossover) ising = 1 odet.q = 1.999 # Very close to q=2.0 - rtol = JPEC.DCON.compute_tols(ctrl, intr, odet, ising) + rtol = JPEC.ForceFreeStates.compute_tols(ctrl, intr, odet, ising) @test rtol == ctrl.tol_r # Should use resonant tolerance # Test 3: Between two singular surfaces ising = 2 odet.q = 2.5 # Between q=2.0 and q=3.0 - rtol = JPEC.DCON.compute_tols(ctrl, intr, odet, ising) + rtol = JPEC.ForceFreeStates.compute_tols(ctrl, intr, odet, ising) @test rtol == ctrl.tol_nr # Should use min distance to either surface # Test 4: Beyond all singular surfaces ising = 3 odet.q = 4.0 - rtol = JPEC.DCON.compute_tols(ctrl, intr, odet, ising) + rtol = JPEC.ForceFreeStates.compute_tols(ctrl, intr, odet, ising) @test rtol == ctrl.tol_nr # Edge case - no singular surfaces mpert = 2 - ctrl = JPEC.DCON.DconControl() + ctrl = JPEC.ForceFreeStates.ForceFreeStatesControl() ctrl.tol_r = 1e-6 ctrl.tol_nr = 1e-4 ctrl.crossover = 0.01 - intr = JPEC.DCON.DconInternal(; mpert=mpert) + intr = JPEC.ForceFreeStates.ForceFreeStatesInternal(; mpert=mpert) intr.msing = 0 intr.sing = [] - odet = JPEC.DCON.OdeState(mpert, 10, 10, 0) + odet = JPEC.ForceFreeStates.OdeState(mpert, 10, 10, 0) ising = 1 odet.q = 2.0 # Should return non-resonant tolerance when no singular surfaces - rtol = JPEC.DCON.compute_tols(ctrl, intr, odet, ising) + rtol = JPEC.ForceFreeStates.compute_tols(ctrl, intr, odet, ising) @test rtol == ctrl.tol_nr end @testset "transform_u!" begin # Test transformation of solution vectors mpert = 2 - intr = JPEC.DCON.DconInternal(; mpert=mpert, numpert_total=mpert) - odet = JPEC.DCON.OdeState(mpert, 10, 5, 2) + intr = JPEC.ForceFreeStates.ForceFreeStatesInternal(; mpert=mpert, numpert_total=mpert) + odet = JPEC.ForceFreeStates.OdeState(mpert, 10, 5, 2) # Set up a simple fixup scenario odet.ifix = 1 @@ -190,7 +190,7 @@ end u_orig = copy(odet.u_store) # Apply transformation - JPEC.DCON.transform_u!(odet, intr) + JPEC.ForceFreeStates.transform_u!(odet, intr) # Check that u_store was modified (transformation applied) @test !all(odet.u_store .== u_orig) @@ -205,16 +205,16 @@ end # Initialize to random u mpert = 5 ifix = 1 - odet = JPEC.DCON.OdeState(mpert, 10, 10, 10) + odet = JPEC.ForceFreeStates.OdeState(mpert, 10, 10, 10) odet.u = randn(ComplexF64, mpert, mpert, 2) odet.unorm = [norm(odet.u[:, i, 1]) for i in 1:mpert] odet.ifix = ifix odet.fixfac = zeros(ComplexF64, mpert, mpert, ifix) - intr = JPEC.DCON.DconInternal(; numpert_total=mpert) + intr = JPEC.ForceFreeStates.ForceFreeStatesInternal(; numpert_total=mpert) # Save copy of original u and run u_orig = copy(odet.u) - JPEC.DCON.apply_gaussian_reduction!(odet.u, odet, intr, false) + JPEC.ForceFreeStates.apply_gaussian_reduction!(odet.u, odet, intr, false) # Very simple tests @test !all(odet.u .== u_orig) # u should have changed @@ -224,7 +224,7 @@ end # --- Real Fortran data check --- mpert = 31 - odet = JPEC.DCON.OdeState(mpert, 10, 10, 10) + odet = JPEC.ForceFreeStates.OdeState(mpert, 10, 10, 10) # We'll load in Fortran data for u pulled before and after a fixup # Note that this was generated by manually setting # unorm = [norm(odet.u[:,i,1]) for i in 1:msol] in the Fortran to avoid @@ -233,9 +233,9 @@ end odet.unorm = [norm(odet.u[:, i, 1]) for i in 1:mpert] odet.ifix = ifix odet.fixfac = zeros(ComplexF64, mpert, mpert, ifix) - intr = JPEC.DCON.DconInternal(; numpert_total=mpert) + intr = JPEC.ForceFreeStates.ForceFreeStatesInternal(; numpert_total=mpert) - JPEC.DCON.apply_gaussian_reduction!(odet.u, odet, intr, false) + JPEC.ForceFreeStates.apply_gaussian_reduction!(odet.u, odet, intr, false) u_fortran = load_u_matrix(joinpath(@__DIR__, "test_data", "u_postfixup.dat")) # test that the outputs are approximately equivalent (1e-3 seems ok to account for loading differences) @@ -243,7 +243,7 @@ end # Test with a simple 2x2 case where we can predict the result mpert = 2 - odet = JPEC.DCON.OdeState(mpert, 10, 10, 10) + odet = JPEC.ForceFreeStates.OdeState(mpert, 10, 10, 10) # Set up a simple u matrix where first column has larger norm # u[:, 1, 1] = [3, 4] (norm = 5) @@ -255,11 +255,11 @@ end odet.unorm = [norm(odet.u[:, i, 1]) for i in 1:mpert] odet.ifix = 1 odet.fixfac = zeros(ComplexF64, mpert, mpert, 1) - intr = JPEC.DCON.DconInternal(; numpert_total=mpert) + intr = JPEC.ForceFreeStates.ForceFreeStatesInternal(; numpert_total=mpert) u_before = copy(odet.u) - JPEC.DCON.apply_gaussian_reduction!(odet.u, odet, intr, false) + JPEC.ForceFreeStates.apply_gaussian_reduction!(odet.u, odet, intr, false) # After fixup: # - index should sort by norm: [1, 2] (largest first) @@ -277,9 +277,9 @@ end @testset "compute_solution_norms!" begin mpert = 2 - odet = JPEC.DCON.OdeState(mpert, 10, 10, 10) - intr = JPEC.DCON.DconInternal(; mpert=mpert) - ctrl = JPEC.DCON.DconControl() + odet = JPEC.ForceFreeStates.OdeState(mpert, 10, 10, 10) + intr = JPEC.ForceFreeStates.ForceFreeStatesInternal(; mpert=mpert) + ctrl = JPEC.ForceFreeStates.ForceFreeStatesControl() ctrl.ucrit = 10.0 # Case 1: Basic norm computation @@ -287,7 +287,7 @@ end odet.u[:, 1, 1] .= [3, 4] # norm = 5 odet.u[:, 2, 1] .= [0, 2] # norm = 2 - JPEC.DCON.compute_solution_norms!(odet.u, odet, ctrl, intr, false) + JPEC.ForceFreeStates.compute_solution_norms!(odet.u, odet, ctrl, intr, false) # After the first run with new=True (default), unorm0 should be set to unorm # and new should be false @test odet.unorm[1:intr.mpert] ≈ [5, 2] @@ -297,27 +297,27 @@ end # Case 2: Error on zero norm odet.u[:, 1, 1] .= 0 odet.new = true - @test_throws ErrorException JPEC.DCON.compute_solution_norms!(odet.u, odet, ctrl, intr, false) + @test_throws ErrorException JPEC.ForceFreeStates.compute_solution_norms!(odet.u, odet, ctrl, intr, false) # Case 3: Normalization on second call odet.u[:, 1, 1] .= [3, 4] # norm = 5 odet.u[:, 2, 1] .= [0, 2] # norm = 2 odet.new = false - JPEC.DCON.compute_solution_norms!(odet.u, odet, ctrl, intr, false) + JPEC.ForceFreeStates.compute_solution_norms!(odet.u, odet, ctrl, intr, false) @test odet.unorm[1:intr.mpert] ≈ [1, 1] # Case 4: Trigger fixup via ucrit odet.unorm0 = ones(intr.mpert) odet.u[:, 1, 1] .= [1000, 0] # large norm odet.u[:, 2, 1] .= [1, 0] # small norm - JPEC.DCON.compute_solution_norms!(odet.u, odet, ctrl, intr, false) + JPEC.ForceFreeStates.compute_solution_norms!(odet.u, odet, ctrl, intr, false) @test odet.new == true # implies fixup ran # Case 5: Trigger fixup via sing_flag odet.new = false odet.u[:, 1, 1] .= [1, 0] odet.u[:, 2, 1] .= [1, 0] - JPEC.DCON.compute_solution_norms!(odet.u, odet, ctrl, intr, true) + JPEC.ForceFreeStates.compute_solution_norms!(odet.u, odet, ctrl, intr, true) @test odet.new == true # fixup triggered end @@ -328,7 +328,7 @@ end numunorms_init = 20 msing = 10 - odet = JPEC.DCON.OdeState(numpert_total, numsteps_init, numunorms_init, msing) + odet = JPEC.ForceFreeStates.OdeState(numpert_total, numsteps_init, numunorms_init, msing) # Check fields are initialized correctly @test odet.numpert_total == numpert_total @@ -356,26 +356,26 @@ end @testset "chunk_el_integration_bounds tests" begin # Helper to build a minimal control and internal structs - ctrl = JPEC.DCON.DconControl() + ctrl = JPEC.ForceFreeStates.ForceFreeStatesControl() ctrl.numsteps_init = 10 ctrl.numunorms_init = 5 # Case 1: No singular surfaces -> single chunk to edge - intr = JPEC.DCON.DconInternal(; mpert=1, numpert_total=1) + intr = JPEC.ForceFreeStates.ForceFreeStatesInternal(; mpert=1, numpert_total=1) intr.msing = 0 intr.psilim = 1.0 - odet = JPEC.DCON.OdeState(1, ctrl.numsteps_init, ctrl.numunorms_init, intr.msing) + odet = JPEC.ForceFreeStates.OdeState(1, ctrl.numsteps_init, ctrl.numunorms_init, intr.msing) odet.psifac = 0.0 ctrl.singfac_min = 1e-4 - chunks = JPEC.DCON.chunk_el_integration_bounds(odet, ctrl, intr) + chunks = JPEC.ForceFreeStates.chunk_el_integration_bounds(odet, ctrl, intr) @test length(chunks) == 1 @test chunks[1].needs_crossing == false # Case 2: One singular surface within limits -> crossing chunk then edge - intr = JPEC.DCON.DconInternal(; mpert=1, numpert_total=1) - s = JPEC.DCON.SingType() + intr = JPEC.ForceFreeStates.ForceFreeStatesInternal(; mpert=1, numpert_total=1) + s = JPEC.ForceFreeStates.SingType() s.psifac = 0.5 s.n = [1] s.m = [1] @@ -386,11 +386,11 @@ end intr.mlow = 1 intr.mhigh = 1 - odet = JPEC.DCON.OdeState(1, ctrl.numsteps_init, ctrl.numunorms_init, intr.msing) + odet = JPEC.ForceFreeStates.OdeState(1, ctrl.numsteps_init, ctrl.numunorms_init, intr.msing) odet.psifac = 0.0 ctrl.singfac_min = 1e-4 - chunks = JPEC.DCON.chunk_el_integration_bounds(odet, ctrl, intr) + chunks = JPEC.ForceFreeStates.chunk_el_integration_bounds(odet, ctrl, intr) @test length(chunks) == 2 @test chunks[1].needs_crossing == true @test chunks[2].needs_crossing == false @@ -398,31 +398,31 @@ end @test chunks[1].psi_end < intr.sing[1].psifac # Case 3: Multiple singular surfaces -> multiple crossing chunks - intr = JPEC.DCON.DconInternal(; mpert=1, numpert_total=1) - s1 = JPEC.DCON.SingType(; psifac=0.3, n=[1], m=[1], q1=1.5) - s2 = JPEC.DCON.SingType(; psifac=0.6, n=[1], m=[1], q1=2.5) + intr = JPEC.ForceFreeStates.ForceFreeStatesInternal(; mpert=1, numpert_total=1) + s1 = JPEC.ForceFreeStates.SingType(; psifac=0.3, n=[1], m=[1], q1=1.5) + s2 = JPEC.ForceFreeStates.SingType(; psifac=0.6, n=[1], m=[1], q1=2.5) intr.sing = [s1, s2] intr.msing = 2 intr.psilim = 1.0 intr.mlow = 1 intr.mhigh = 1 - odet = JPEC.DCON.OdeState(1, ctrl.numsteps_init, ctrl.numunorms_init, intr.msing) + odet = JPEC.ForceFreeStates.OdeState(1, ctrl.numsteps_init, ctrl.numunorms_init, intr.msing) odet.psifac = 0.0 ctrl.singfac_min = 1e-6 - chunks = JPEC.DCON.chunk_el_integration_bounds(odet, ctrl, intr) + chunks = JPEC.ForceFreeStates.chunk_el_integration_bounds(odet, ctrl, intr) @test length(chunks) == 3 @test all(c.needs_crossing == true for c in chunks[1:2]) @test chunks[3].needs_crossing == false # Case 4: singfac_min == 0 should disable crossing logic -> single chunk - intr = JPEC.DCON.DconInternal(; mpert=1, numpert_total=1) - intr.sing = [JPEC.DCON.SingType(; psifac=0.4, n=[1], m=[1], q1=2.0)] + intr = JPEC.ForceFreeStates.ForceFreeStatesInternal(; mpert=1, numpert_total=1) + intr.sing = [JPEC.ForceFreeStates.SingType(; psifac=0.4, n=[1], m=[1], q1=2.0)] intr.msing = 1 intr.psilim = 1.0 - odet = JPEC.DCON.OdeState(1, ctrl.numsteps_init, ctrl.numunorms_init, intr.msing) + odet = JPEC.ForceFreeStates.OdeState(1, ctrl.numsteps_init, ctrl.numunorms_init, intr.msing) odet.psifac = 0.0 ctrl.singfac_min = 0.0 - chunks = JPEC.DCON.chunk_el_integration_bounds(odet, ctrl, intr) + chunks = JPEC.ForceFreeStates.chunk_el_integration_bounds(odet, ctrl, intr) @test length(chunks) == 1 @test chunks[1].needs_crossing == false end diff --git a/test/runtests_fastinterp.jl b/test/runtests_fastinterp.jl index 8fc1d92b..43ed7cf1 100644 --- a/test/runtests_fastinterp.jl +++ b/test/runtests_fastinterp.jl @@ -24,7 +24,7 @@ end fs[ix, iy, 1] = x * cos(2 * 2π * y) end - fc = JPEC.Util.FourierCoefficients(xs, ys, fs, 4) + fc = JPEC.Utilities.FourierCoefficients(xs, ys, fs, 4) # Check structure @test fc.mband == 4 @@ -32,11 +32,11 @@ end @test length(fc.xs) == npsi # Mode 2 should have significant content at ipsi=10 (x=0.5) - c2 = JPEC.Util.get_complex_coeff(fc, 10, 2, 1) + c2 = JPEC.Utilities.get_complex_coeff(fc, 10, 2, 1) @test abs(real(c2)) > 0.1 # Should have cosine content # Get all coefficients out = zeros(ComplexF64, 5) - JPEC.Util.get_complex_coeffs!(out, fc, 10, 1) + JPEC.Utilities.get_complex_coeffs!(out, fc, 10, 1) @test out[3] == c2 # Mode 2 is at index 3 (0-indexed mode) end diff --git a/test/runtests_fullruns.jl b/test/runtests_fullruns.jl index 7196889f..5b8d7275 100644 --- a/test/runtests_fullruns.jl +++ b/test/runtests_fullruns.jl @@ -1,16 +1,16 @@ -# Run DCON.Main on the provided example directories and assert it completes without throwing. -@testset "Full DCON runs" begin +# Run JPEC.main on the provided example directories and assert it completes without throwing. +@testset "Full ForceFreeStates runs" begin ex1 = joinpath(@__DIR__, "test_data", "regression_solovev_ideal_example") @info "Running Solovev ideal example" @test begin - DCON.Main(ex1) + JPEC.main([ex1]) true end ex2 = joinpath(@__DIR__, "test_data", "regression_solovev_ideal_example_multi_n") @info "Running Solovev ideal multi-n example" @test begin - DCON.Main(ex2) + JPEC.main([ex2]) true end end diff --git a/test/runtests_vacuum_julia.jl b/test/runtests_vacuum_julia.jl index 0ead6dc8..b62abd01 100644 --- a/test/runtests_vacuum_julia.jl +++ b/test/runtests_vacuum_julia.jl @@ -12,7 +12,7 @@ using LinearAlgebra vac_in = VacuumInput() @test vac_in.mlow == 0 @test vac_in.n == 0 - @test vac_in.kernelsign == 1.0 + # NOTE: kernelsign field deprecated - compute_vacuum_response now computes both grri and grre @test vac_in.mtheta == 1 @test vac_in.force_wv_symmetry == true @@ -35,9 +35,9 @@ using LinearAlgebra plasma_surf = JPEC.Vacuum.initialize_plasma_surface(inputs) @test length(plasma_surf.x) == 4 @test length(plasma_surf.z) == 4 + @test length(plasma_surf.delta) == 4 @test length(plasma_surf.dx_dtheta) == 4 - @test size(plasma_surf.cos_ln_basis) == (4, 1) - @test size(plasma_surf.sin_ln_basis) == (4, 1) + @test length(plasma_surf.dz_dtheta) == 4 @test !any(isnan, plasma_surf.dx_dtheta) @test !any(isnan, plasma_surf.dz_dtheta) end @@ -264,12 +264,14 @@ using LinearAlgebra ) wall_settings = WallShapeSettings(shape="nowall") - wv, grri, xzpts = compute_vacuum_response(inputs, wall_settings) + wv, grri, grre, xzpts = compute_vacuum_response(inputs, wall_settings) @test size(wv) == (2, 2); @test !any(isnan, wv); @test size(grri) == (2 * 128, 2 * 2); @test !any(isnan, grri); + @test size(grre) == (2 * 128, 2 * 2); + @test !any(isnan, grre); @test size(xzpts) == (128, 4); @test !any(isnan, xzpts[:, 1:2]); end @@ -293,7 +295,7 @@ using LinearAlgebra # Use a conformal wall wall_settings = WallShapeSettings(shape="conformal", a=0.5) - wv, grri, xzpts = compute_vacuum_response(inputs, wall_settings) + wv, grri, grre, xzpts = compute_vacuum_response(inputs, wall_settings) @test size(wv) == (2, 2) @test !any(isnan, wv) diff --git a/test/test_data/regression_solovev_ideal_example/equil.toml b/test/test_data/regression_solovev_ideal_example/equil.toml deleted file mode 100644 index ee9366f2..00000000 --- a/test/test_data/regression_solovev_ideal_example/equil.toml +++ /dev/null @@ -1,30 +0,0 @@ -[EQUIL_CONTROL] -eq_type = "sol" # Type of the input 2D equilibrium file -eq_filename = "sol.toml" # path to equilibrium file - -jac_type = "pest" # Coordinate system (hamada, pest, boozer, equal_arc) -power_bp = 0 # del.B ~ B_p^power_bp * B^power_b / R^power_r -power_b = 0 # del.B ~ B_p^power_bp * B^power_b / R^power_r -power_r = 0 # del.B ~ B_p^power_bp * B^power_b / R^power_r - -grid_type = "ldp" # Radial grid packing -psilow = 1e-4 # Min psi (normalized) -psihigh = 0.99999 # Max psi (normalized) -mpsi = 16 # Radial grid intervals -mtheta = 256 # Poloidal grid intervals - -newq0 = 0 # Override q(0) -etol = 1e-7 # Reconstruction tolerance -use_classic_splines = false # Use classical spline (vs. tri-diagonal) - -input_only = false # Quit after input read - -[EQUIL_OUTPUT] -gse_flag = false # Output G-S equation accuracy diagnostics -out_eq_1d = false # ASCII output of 1D eq data -bin_eq_1d = false # Binary output of 1D eq data -out_eq_2d = false # ASCII output of 2D eq data -bin_eq_2d = true # Binary output of 2D eq data (used by GPEC) -out_2d = false # ASCII output of processed 2D data -bin_2d = false # Binary output of processed 2D data -dump_flag = false # Binary dump of equilibrium data diff --git a/test/test_data/regression_solovev_ideal_example/dcon.toml b/test/test_data/regression_solovev_ideal_example/jpec.toml similarity index 59% rename from test/test_data/regression_solovev_ideal_example/dcon.toml rename to test/test_data/regression_solovev_ideal_example/jpec.toml index 66e45d3f..fde89015 100644 --- a/test/test_data/regression_solovev_ideal_example/dcon.toml +++ b/test/test_data/regression_solovev_ideal_example/jpec.toml @@ -1,4 +1,30 @@ -[DCON_CONTROL] +[Equilibrium] +eq_type = "sol" # Type of the input 2D equilibrium file +eq_filename = "sol.toml" # Path to equilibrium file +jac_type = "pest" # Coordinate system (hamada, pest, boozer, equal_arc) +power_bp = 0 # Poloidal field power exponent for Jacobian +power_b = 0 # Toroidal field power exponent for Jacobian +power_r = 0 # Major radius power exponent for Jacobian +grid_type = "ldp" # Radial grid packing type +psilow = 1e-4 # Lower limit of normalized flux coordinate +psihigh = 0.99999 # Upper limit of normalized flux coordinate +mpsi = 16 # Number of radial grid points +mtheta = 256 # Number of poloidal grid points +newq0 = 0 # Override for on-axis safety factor (0 = use input value) +etol = 1e-7 # Error tolerance for equilibrium solver +force_termination = false # Terminate after equilibrium setup (skip stability calculations) + +[Wall] +shape = "conformal" # Wall shape (nowall, conformal, elliptical, dee, mod_dee, filepath) +a = 0.2415 # Distance from plasma (conformal) or shape parameter +aw = 0.05 # Half-thickness parameter for Dee-shaped walls +bw = 1.5 # Elongation parameter for wall shapes +cw = 0 # Offset of wall center from major radius +dw = 0.5 # Triangularity parameter for wall shapes +tw = 0.05 # Sharpness of wall corners (try 0.05 as initial value) +equal_arc_wall = true # Equal arc length distribution of nodes on wall + +[ForceFreeStates] bal_flag = false # Ideal MHD ballooning criterion for short wavelengths mat_flag = true # Construct coefficient matrices for diagnostic purposes ode_flag = true # Integrate ODE's for determining stability of internal long-wavelength mode (must be true for GPEC) diff --git a/test/test_data/regression_solovev_ideal_example/vac.in b/test/test_data/regression_solovev_ideal_example/vac.in deleted file mode 100644 index 7be7c808..00000000 --- a/test/test_data/regression_solovev_ideal_example/vac.in +++ /dev/null @@ -1,99 +0,0 @@ -! **** Running DCON's input *** - -&MODES - mth = 32 - xiin(1:9) = 0 0 0 0 0 0 0 1 0 - lsymz = .TRUE. - leqarcw = 1 - lzio = 0 - lgato = 0 - lrgato = 0 -/ -&DEBUGS - checkd = .FALSE. - check1 = .FALSE. - check2 = .FALSE. - checke = .FALSE. - checks = .FALSE. - wall = .FALSE. - lkplt = 0 -/ -&VACDAT - ishape = 6 - aw = 0.05 - bw = 1.5 - cw = 0 - dw = 0.5 - tw = 0.05 - nsing = 500 - epsq = 1e-05 - noutv = 37 - idgt = 6 - idot = 0 - idsk = 0 - delg = 15.01 - delfac = 0.001 - cn0 = 1 -/ -&SHAPE - ipshp = 0 - xpl = 100 - apl = 1 - a = 0.2415 - b = 170 - bpl = 1 - dpl = 0 - r = 1 - abulg = 0.932 - bbulg = 17.0 - tbulg = 0.02 - qain = 2.5 -/ -&DIAGNS - lkdis = .FALSE. - ieig = 0 - iloop = 0 - lpsub = 1 - nloop = 128 - nloopr = 0 - nphil = 3 - nphse = 1 - xofsl = 0 - ntloop = 32 - aloop = 0.01 - bloop = 1.6 - dloop = 0.5 - rloop = 1.0 - deloop = 0.001 - mx = 21 - mz = 21 - nph = 0 - nxlpin = 6 - nzlpin = 11 - epslp = 0.02 - xlpmin = 0.7 - xlpmax = 2.7 - zlpmin = -1.5 - zlpmax = 1.5 - linterior = 2 -/ -&SPRK - nminus = 0 - nplus = 0 - mphi = 16 - lwrt11 = 0 - civ = 0.0 - sp2sgn1 = 1 - sp2sgn2 = 1 - sp2sgn3 = 1 - sp2sgn4 = 1 - sp2sgn5 = 1 - sp3sgn1 = -1 - sp3sgn2 = -1 - sp3sgn3 = -1 - sp3sgn4 = 1 - sp3sgn5 = 1 - lff = 0 - ff = 1.6 - fv = 1.6 1.6 1.6 1.6 1.6 1.0 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 -/ diff --git a/test/test_data/regression_solovev_ideal_example_multi_n/equil.toml b/test/test_data/regression_solovev_ideal_example_multi_n/equil.toml deleted file mode 100644 index ee9366f2..00000000 --- a/test/test_data/regression_solovev_ideal_example_multi_n/equil.toml +++ /dev/null @@ -1,30 +0,0 @@ -[EQUIL_CONTROL] -eq_type = "sol" # Type of the input 2D equilibrium file -eq_filename = "sol.toml" # path to equilibrium file - -jac_type = "pest" # Coordinate system (hamada, pest, boozer, equal_arc) -power_bp = 0 # del.B ~ B_p^power_bp * B^power_b / R^power_r -power_b = 0 # del.B ~ B_p^power_bp * B^power_b / R^power_r -power_r = 0 # del.B ~ B_p^power_bp * B^power_b / R^power_r - -grid_type = "ldp" # Radial grid packing -psilow = 1e-4 # Min psi (normalized) -psihigh = 0.99999 # Max psi (normalized) -mpsi = 16 # Radial grid intervals -mtheta = 256 # Poloidal grid intervals - -newq0 = 0 # Override q(0) -etol = 1e-7 # Reconstruction tolerance -use_classic_splines = false # Use classical spline (vs. tri-diagonal) - -input_only = false # Quit after input read - -[EQUIL_OUTPUT] -gse_flag = false # Output G-S equation accuracy diagnostics -out_eq_1d = false # ASCII output of 1D eq data -bin_eq_1d = false # Binary output of 1D eq data -out_eq_2d = false # ASCII output of 2D eq data -bin_eq_2d = true # Binary output of 2D eq data (used by GPEC) -out_2d = false # ASCII output of processed 2D data -bin_2d = false # Binary output of processed 2D data -dump_flag = false # Binary dump of equilibrium data diff --git a/test/test_data/regression_solovev_ideal_example_multi_n/dcon.toml b/test/test_data/regression_solovev_ideal_example_multi_n/jpec.toml similarity index 59% rename from test/test_data/regression_solovev_ideal_example_multi_n/dcon.toml rename to test/test_data/regression_solovev_ideal_example_multi_n/jpec.toml index 3396fc58..5a41abf5 100644 --- a/test/test_data/regression_solovev_ideal_example_multi_n/dcon.toml +++ b/test/test_data/regression_solovev_ideal_example_multi_n/jpec.toml @@ -1,4 +1,30 @@ -[DCON_CONTROL] +[Equilibrium] +eq_type = "sol" # Type of the input 2D equilibrium file +eq_filename = "sol.toml" # Path to equilibrium file +jac_type = "pest" # Coordinate system (hamada, pest, boozer, equal_arc) +power_bp = 0 # Poloidal field power exponent for Jacobian +power_b = 0 # Toroidal field power exponent for Jacobian +power_r = 0 # Major radius power exponent for Jacobian +grid_type = "ldp" # Radial grid packing type +psilow = 1e-4 # Lower limit of normalized flux coordinate +psihigh = 0.99999 # Upper limit of normalized flux coordinate +mpsi = 16 # Number of radial grid points +mtheta = 256 # Number of poloidal grid points +newq0 = 0 # Override for on-axis safety factor (0 = use input value) +etol = 1e-7 # Error tolerance for equilibrium solver +force_termination = false # Terminate after equilibrium setup (skip stability calculations) + +[Wall] +shape = "conformal" # Wall shape (nowall, conformal, elliptical, dee, mod_dee, filepath) +a = 0.2415 # Distance from plasma (conformal) or shape parameter +aw = 0.05 # Half-thickness parameter for Dee-shaped walls +bw = 1.5 # Elongation parameter for wall shapes +cw = 0 # Offset of wall center from major radius +dw = 0.5 # Triangularity parameter for wall shapes +tw = 0.05 # Sharpness of wall corners (try 0.05 as initial value) +equal_arc_wall = true # Equal arc length distribution of nodes on wall + +[ForceFreeStates] bal_flag = false # Ideal MHD ballooning criterion for short wavelengths mat_flag = true # Construct coefficient matrices for diagnostic purposes ode_flag = true # Integrate ODE's for determining stability of internal long-wavelength mode (must be true for GPEC) diff --git a/test/test_data/regression_solovev_ideal_example_multi_n/vac.in b/test/test_data/regression_solovev_ideal_example_multi_n/vac.in deleted file mode 100644 index 7be7c808..00000000 --- a/test/test_data/regression_solovev_ideal_example_multi_n/vac.in +++ /dev/null @@ -1,99 +0,0 @@ -! **** Running DCON's input *** - -&MODES - mth = 32 - xiin(1:9) = 0 0 0 0 0 0 0 1 0 - lsymz = .TRUE. - leqarcw = 1 - lzio = 0 - lgato = 0 - lrgato = 0 -/ -&DEBUGS - checkd = .FALSE. - check1 = .FALSE. - check2 = .FALSE. - checke = .FALSE. - checks = .FALSE. - wall = .FALSE. - lkplt = 0 -/ -&VACDAT - ishape = 6 - aw = 0.05 - bw = 1.5 - cw = 0 - dw = 0.5 - tw = 0.05 - nsing = 500 - epsq = 1e-05 - noutv = 37 - idgt = 6 - idot = 0 - idsk = 0 - delg = 15.01 - delfac = 0.001 - cn0 = 1 -/ -&SHAPE - ipshp = 0 - xpl = 100 - apl = 1 - a = 0.2415 - b = 170 - bpl = 1 - dpl = 0 - r = 1 - abulg = 0.932 - bbulg = 17.0 - tbulg = 0.02 - qain = 2.5 -/ -&DIAGNS - lkdis = .FALSE. - ieig = 0 - iloop = 0 - lpsub = 1 - nloop = 128 - nloopr = 0 - nphil = 3 - nphse = 1 - xofsl = 0 - ntloop = 32 - aloop = 0.01 - bloop = 1.6 - dloop = 0.5 - rloop = 1.0 - deloop = 0.001 - mx = 21 - mz = 21 - nph = 0 - nxlpin = 6 - nzlpin = 11 - epslp = 0.02 - xlpmin = 0.7 - xlpmax = 2.7 - zlpmin = -1.5 - zlpmax = 1.5 - linterior = 2 -/ -&SPRK - nminus = 0 - nplus = 0 - mphi = 16 - lwrt11 = 0 - civ = 0.0 - sp2sgn1 = 1 - sp2sgn2 = 1 - sp2sgn3 = 1 - sp2sgn4 = 1 - sp2sgn5 = 1 - sp3sgn1 = -1 - sp3sgn2 = -1 - sp3sgn3 = -1 - sp3sgn4 = 1 - sp3sgn5 = 1 - lff = 0 - ff = 1.6 - fv = 1.6 1.6 1.6 1.6 1.6 1.0 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 -/ diff --git a/test/vac.in b/test/vac.in deleted file mode 100644 index 68e91e03..00000000 --- a/test/vac.in +++ /dev/null @@ -1,99 +0,0 @@ -! **** Running DCON's input *** -&MODES - mth = 480 - xiin(1:9) = 0 0 0 0 0 0 0 1 0 - lsymz = .TRUE. - leqarcw = 1 - lzio = 0 - lgato = 0 - lrgato = 0 -/ -&DEBUGS - checkd = .FALSE. - check1 = .FALSE. - check2 = .FALSE. - checke = .FALSE. - checks = .FALSE. - wall = .FALSE. - lkplt = 0 - verbose_timer_output = f -/ -&VACDAT - ishape = 6 - aw = 0.05 - bw = 1.5 - cw = 0 - dw = 0.5 - tw = 0.05 - nsing = 500 - epsq = 1e-05 - noutv = 37 - idgt = 6 - idot = 0 - idsk = 0 - delg = 15.01 - delfac = 0.001 - cn0 = 1 -/ -&SHAPE - ipshp = 0 - xpl = 100 - apl = 1 - a = 20 - b = 170 - bpl = 1 - dpl = 0 - r = 1 - abulg = 0.932 - bbulg = 17.0 - tbulg = 0.02 - qain = 2.5 -/ -&DIAGNS - lkdis = .FALSE. - ieig = 0 - iloop = 0 - lpsub = 1 - nloop = 128 - nloopr = 0 - nphil = 3 - nphse = 1 - xofsl = 0 - ntloop = 32 - aloop = 0.01 - bloop = 1.6 - dloop = 0.5 - rloop = 1.0 - deloop = 0.001 - mx = 21 - mz = 21 - nph = 0 - nxlpin = 6 - nzlpin = 11 - epslp = 0.02 - xlpmin = 0.7 - xlpmax = 2.7 - zlpmin = -1.5 - zlpmax = 1.5 - linterior = 2 -/ -&SPRK - nminus = 0 - nplus = 0 - mphi = 16 - lwrt11 = 0 - civ = 0.0 - sp2sgn1 = 1 - sp2sgn2 = 1 - sp2sgn3 = 1 - sp2sgn4 = 1 - sp2sgn5 = 1 - sp3sgn1 = -1 - sp3sgn2 = -1 - sp3sgn3 = -1 - sp3sgn4 = 1 - sp3sgn5 = 1 - lff = 0 - ff = 1.6 - fv = 1.6 1.6 1.6 1.6 1.6 1.0 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 1.6 -/