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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/src/vacuum.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ wall_settings = GeneralizedPerturbedEquilibrium.Vacuum.WallShapeSettings(
)

# Compute vacuum response matrix
wv, grri, xzpts = GeneralizedPerturbedEquilibrium.Vacuum.compute_vacuum_response(inputs, wall_settings)
wv, _, _, _ = GeneralizedPerturbedEquilibrium.Vacuum.compute_vacuum_response(inputs, wall_settings)
```

### Vacuum Field Calculation at Observation Points
Expand Down
5 changes: 2 additions & 3 deletions docs/src/workflow.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,7 @@ The single `gpec.toml` file supplies user-selected options to every module. The

**Outputs**:
- `wv` — Vacuum response matrix (scaled by the singular factor (m - nq)(m' - nq), see Chance 1997)
- `grri` — Interior Green's function matrix (plasma boundary → plasma boundary)
- `grre` — Exterior Green's function matrix (plasma boundary → wall)
- `I_v` — Vacuum surface-current matrix Iᵛ when `compute_Iv=true` (otherwise zeros); PerturbedEquilibrium inverts this to surface inductance `L`

**Key references**: [Chance et al. (1997)](citations.md#Vacuum-Module), [Chance et al. (2007)](citations.md#Vacuum-Module)

Expand Down Expand Up @@ -182,5 +181,5 @@ All results are written to a single HDF5 file (default: `gpec.h5`). The file is
| `locstab/` | Local stability: Mercier criterion, shear |
| `integration/` | ODE integration results: energy matrices, eigenvalues |
| `singular/` | Per-surface data: ψ_s, m/n, Δ', small solution coefficients |
| `vacuum/` | Vacuum response matrices: wv, grri, grre |
| `vacuum/` | Vacuum response matrices: wv (and I_v when computed for PE) |
| `perturbed/` | Perturbed equilibrium: ξ, b in mode space, island diagnostics |
8 changes: 0 additions & 8 deletions src/ForceFreeStates/ForceFreeStatesStructs.jl
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@ 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{ComplexF64,2}` - Interior Green's function at this surface [mthvac, mpert]
- `grre::Array{ComplexF64,2}` - Exterior Green's function at this surface [mthvac, mpert]
- `delta_prime::Vector{ComplexF64}` - **STUB (not physically valid)**. Per-surface ca-based Δ' estimate retained for future work / debugging only. The physically valid Δ' is `ForceFreeStatesInternal.delta_prime_matrix`, computed via the STRIDE global BVP (Glasser 2018 PoP 25, 032501). Do not use this field for tearing-stability analysis; do not expect agreement with `delta_prime_matrix`.
- `delta_prime_col::Matrix{ComplexF64}` - **STUB (not physically valid)**. Per-surface ca-based Δ' column retained for future work / debugging only. Shape (numpert_total × n_res_modes); `delta_prime_col[j, i] = (ca_r[j,ipert_res_i,2] - ca_l[j,ipert_res_i,2]) / (4π²·psio)`. The diagonal element matches the (also stubbed) `delta_prime[i]`. Only populated for the Riccati/parallel FM paths. The physically valid Δ' is `ForceFreeStatesInternal.delta_prime_matrix`; this field exists for future development on intra-surface coupling diagnostics, not for production use.
"""
Expand All @@ -23,8 +21,6 @@ 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{ComplexF64,2} = Array{ComplexF64}(undef, 0, 0)
grre::Array{ComplexF64,2} = Array{ComplexF64}(undef, 0, 0)
delta_prime::Vector{ComplexF64} = ComplexF64[]
delta_prime_col::Matrix{ComplexF64} = Matrix{ComplexF64}(undef, 0, 0)
ua_left::Array{ComplexF64,3} = Array{ComplexF64}(undef, 0, 0, 0) # asymptotic basis at left inner-layer boundary
Expand Down Expand Up @@ -404,8 +400,6 @@ Populated in `Free.jl`.
- `et::Vector{ComplexF64}` - Total energy eigenvalues of the pencil (W, N): power-normalized and invariant to the working (Jacobian) coordinate; et = ep + ev per mode
- `n_tor_idx::Vector{Int}` - 0-based toroidal mode number index of each sorted eigenvalue (numpert_total). Needed in `write_imas`
- `vacuum_eigenvalue::Float64` - Least stable (minimum) eigenvalue of the pencil (wv, N), clamped to zero
- `grri::Array{ComplexF64, 2}` - Interior Green's function matrices (2 * mthvac * nzvac × numpert_total)
- `grre::Array{ComplexF64, 2}` - Exterior Green's function matrices (2 * mthvac * nzvac × numpert_total)
- `plasma_pts::Array{Float64, 3}` - Cartesian coordinates of plasma points, shape (mthvac * nzvac) × 3 for (x, y, z)
- `wall_pts::Array{Float64, 3}` - Cartesian coordinates of wall points, shape (mthvac * nzvac) × 3 for (x, y, z)
"""
Expand All @@ -423,8 +417,6 @@ Populated in `Free.jl`.
et::Vector{ComplexF64} = Vector{ComplexF64}(undef, numpert_total)
n_tor_idx::Vector{Int} = zeros(Int, numpert_total)
vacuum_eigenvalue::Float64 = NaN
grri::Array{ComplexF64,2} = Array{ComplexF64}(undef, 2 * numpoints, numpert_total)
grre::Array{ComplexF64,2} = Array{ComplexF64}(undef, 2 * numpoints, numpert_total)
plasma_pts::Array{Float64,2} = Array{Float64}(undef, numpoints, 3)
wall_pts::Array{Float64,2} = Array{Float64}(undef, numpoints, 3)
end
Expand Down
2 changes: 1 addition & 1 deletion src/ForceFreeStates/Free.jl
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ q-window minimum.

# Compute raw vacuum matrix at the actual scan psi (singfac NOT applied; free_compute_total applies it analytically)
vac_inputs = Vacuum.VacuumInput(equil, psi_array[i], ctrl.mthvac, ctrl.nzvac, intr.mlow:intr.mhigh, intr.nlow:intr.nhigh)
wv, _, _, _, _ = Vacuum.compute_vacuum_response(vac_inputs, intr.wall_settings)
wv, _, _, _ = Vacuum.compute_vacuum_response(vac_inputs, intr.wall_settings)
@views wv_array[i, :, :] .= wv
end

Expand Down
10 changes: 3 additions & 7 deletions src/PerturbedEquilibrium/Response.jl
Original file line number Diff line number Diff line change
Expand Up @@ -35,16 +35,12 @@ function compute_plasma_response!(
# Plasma inductance Lambda (wt0 formula, Fortran resp_induct_flag=TRUE default)
plasma_inductance = calc_plasma_inductance(vac_data, ffs_intr, equil.psio)

# Surface inductance L from Green's functions at psilim.
# Requires a 2D (nzvac=1) vacuum response so rows are theta points only,
# Surface inductance L from vacuum surface-current matrix at psilim.
nn = ffs_intr.nlow
vac_input_2d = Vacuum.VacuumInput(equil, ffs_intr.psilim, vac_data.mthvac, 1, ffs_intr.mlow:ffs_intr.mhigh, [nn])
wall_nowall = Vacuum.WallShapeSettings(; shape="nowall")
_, grri_2d_raw, grre_2d_raw, _, _ = Vacuum.compute_vacuum_response(vac_input_2d, wall_nowall)
grri_2d = Matrix{ComplexF64}(grri_2d_raw)
grre_2d = Matrix{ComplexF64}(grre_2d_raw)
ν_vac = Vacuum.PlasmaGeometry(vac_input_2d).ν
surface_inductance = compute_surface_inductance_from_greens(grri_2d, grre_2d, ffs_intr, nn, ν_vac)
_, I_v, _, _ = Vacuum.compute_vacuum_response(vac_input_2d, wall_nowall; compute_Iv=true)
surface_inductance = calc_surface_inductance(I_v)
permeability = calc_permeability(plasma_inductance, surface_inductance)

# Reluctance ϱ = L⁻¹·(Λ† − L)·L⁻¹ (Fortran gpresp_reluct: diff_indmats = CONJG(TRANSPOSE(plas_indmats)) − surf_indmats).
Expand Down
13 changes: 13 additions & 0 deletions src/PerturbedEquilibrium/ResponseMatrices.jl
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,19 @@ function calc_plasma_inductance(
return inv(temp2)
end

"""
calc_surface_inductance(I_v::Matrix{ComplexF64})::Matrix{ComplexF64}

Surface inductance from the vacuum surface-current matrix, Park 2007 eq. 7: `Φ_x = L·I_v`.
Columns of `I_v` are driven by unit flux harmonics which are consistent since the eq. 3
weight `1/(J|∇ψ|)` cancels the Jacobian in the vacuum solver's source density — so `Φ_x = 𝕀`
and `L = I_v⁻¹`. `I_v` is normalized in place by `μ₀(2π)²`.
"""
function calc_surface_inductance(I_v::Matrix{ComplexF64})::Matrix{ComplexF64}
μ₀ = 4π * 1e-7
return inv(I_v) .* μ₀ * (2π)^2
end

"""
calc_permeability(
plasma_inductance::Matrix{ComplexF64},
Expand Down
109 changes: 4 additions & 105 deletions src/PerturbedEquilibrium/SingularCoupling.jl
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,6 @@ function _hermite_cubic_val(u_a, u_b, du_a, du_b, psi_a, psi_b, psi)
return @. h00 * u_a + h * h10 * du_a + h01 * u_b + h * h11 * du_b
end

# Reflect a periodic theta-space vector θ → -θ (the theta reversal in gpvacuum_flxsurf).
_reverse_theta(v::AbstractVector) = circshift(reverse(v), 1)

"""
compute_singular_coupling_metrics!(
state::PerturbedEquilibriumState,
Expand Down Expand Up @@ -173,17 +170,12 @@ function compute_singular_coupling_metrics!(
continue
end

# Compute Green's functions at this surface for this n (once per pair)
# Surface-current matrix at this surface for this n (once per pair)
vac_input = Vacuum.VacuumInput(equil, sing_surf.psifac, mtheta, 1, mlow:mhigh, [nn])
_, grri_raw, grre_raw, _, _ = Vacuum.compute_vacuum_response(vac_input, wall_settings)
grri = Matrix{ComplexF64}(grri_raw)
grre = Matrix{ComplexF64}(grre_raw)

# Get ν on the vacuum theta grid (same ν used in the vacuum Fourier basis computation)
ν_vac = Vacuum.PlasmaGeometry(vac_input).ν
_, I_v, _, _ = Vacuum.compute_vacuum_response(vac_input, wall_settings; compute_Iv=true)
L_surf = calc_surface_inductance(I_v)

# Precompute L_surf; only the (m_res, m_res) diagonal element is needed for singflx
L_surf = compute_surface_inductance_from_greens(grri, grre, ffs_intr, nn, ν_vac)
# Extract the (m_res, m_res) diagonal element is needed for singflx
m_idx = m_res - mlow + 1
L_mm = L_surf[m_idx, m_idx]

Expand Down Expand Up @@ -432,99 +424,6 @@ function compute_current_density(
return j_c
end

"""
compute_surface_inductance_from_greens(
grri::Matrix{ComplexF64},
grre::Matrix{ComplexF64},
ffs_intr::ForceFreeStatesInternal,
nn::Int,
ν::Vector{Float64}
)::Matrix{ComplexF64}

Compute surface inductance matrix from Green's functions at flux surface.

Implements the GPEC `gpvacuum_flxsurf` algorithm.

The Julia vacuum code uses SFL Fourier basis `cos(m*θ - n*ν)` in the column transform,
so the row DFT must apply the matching toroidal phase correction `exp(-i*n*ν)` before
the DFT (matching Fortran `gpvacuum_flxsurf`'s `EXP(-ifac*nn*dphi)` phase correction).

## Arguments

- `grri`: Interior Green's function [mtheta, mpert]
- `grre`: Exterior Green's function [mtheta, mpert]
- `ffs_intr`: ForceFreeStates internal state
- `nn`: Toroidal mode number
- `ν`: Toroidal angle offset on the vacuum theta grid [mtheta]

## Returns

Surface inductance matrix [mpert × mpert]
"""
@with_pool pool function compute_surface_inductance_from_greens(
grri::Matrix{ComplexF64},
grre::Matrix{ComplexF64},
ffs_intr::ForceFreeStatesInternal,
nn::Int,
ν::Vector{Float64}
)::Matrix{ComplexF64}
mpert = ffs_intr.mpert
mtheta = length(ν)
μ₀ = 4π * 1e-7

ft = FourierTransforms.FourierTransform(mtheta, mpert, ffs_intr.mlow)

flux_matrix = zeros!(pool, ComplexF64, mpert, mpert)
current_matrix = zeros!(pool, ComplexF64, mpert, mpert)

kax = zeros!(pool, ComplexF64, mtheta)
grri_surf = @view grri[1:mtheta, :]
grre_surf = @view grre[1:mtheta, :]

# Toroidal phase correction: exp(-i*n*ν)
phase = cis.(-nn .* ν)

for i in 1:mpert
flux_matrix[i, i] = 1.0

# Complex grri/e stores exp(i(mθ-nν)) projection, need conjugate for exp(-i(mθ-nν))
kax .= conj.(grri_surf[:, i] .+ grre_surf[:, i]) ./ (μ₀ * (2π)^2)

# Port of Fortran gpvacuum_flxsurf: apply toroidal phase, reverse theta, forward-DFT.
g_phased = kax .* phase
current_matrix[:, i] = ft(_reverse_theta(g_phased))
end

# Compute surface inductance: L_surf = flux * inv(current) = inv(current)
L_surf = zeros(ComplexF64, mpert, mpert)

current_mag = maximum(abs.(current_matrix))

if current_mag < 1e-15
@warn "Current matrix is all zeros! Cannot compute surface inductance." maxlog=1
for i in 1:mpert
L_surf[i, i] = μ₀ * 1e-6
end
else
try
regularization = 1e-12 * current_mag
current_reg = current_matrix + regularization * I

L_surf = flux_matrix * inv(current_reg)

# Hermitianize (matches Fortran: temp1 = 0.5*(temp1 + CONJG(TRANSPOSE(temp1))))
L_surf = 0.5 * (L_surf + L_surf')
catch e
@warn "Surface inductance inversion failed: $e" maxlog=1
for i in 1:mpert
L_surf[i, i] = μ₀ * 1e-6
end
end
end

return L_surf
end

"""
compute_surface_area(
equil::Equilibrium.PlasmaEquilibrium,
Expand Down
3 changes: 2 additions & 1 deletion src/Vacuum/DataTypes.jl
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,8 @@ function VacuumInput(
r, z, ν = extract_plasma_surface_at_psi(equil, ψ)

# Remove the last point to go from the [0, 2π] grid to VACUUM's [0, 2π) grid
# and reverse the arrays for VACUUM's CW θ direction
# and reverse the arrays for VACUUM's CW θ direction (θ_VAC = -θ_GPEC). This handedness is why
# operators returned to GPEC (e.g. the surface-inductance current matrix) are conjugated.
return VacuumInput(;
x=reverse(r)[1:(end-1)],
z=reverse(z)[1:(end-1)],
Expand Down
4 changes: 3 additions & 1 deletion src/Vacuum/Kernel2D.jl
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,9 @@ grad_greenfunction is not zeroed since it fills a different block of the
end

# Normals need to point outward from vacuum region. In VACUUM clockwise θ convention, normal points
# out of vacuum for wall but inward for plasma, so we multiply by -1 for plasma sources
# out of vacuum for wall but inward for plasma, so we multiply by -1 for plasma sources.
# This normal orientation sets the sign of the surface current K = n̂×∇χ (Chance 1997 eq. 184),
# and is the origin of the leading minus in the current-matrix build.
if source isa PlasmaGeometry
grad_greenfunction_block .*= -1
end
Expand Down
Loading
Loading