Skip to content

Commit c11dcd7

Browse files
Integrate changes with boundary model (#1001)
* Integrate changes with boundary model * Update BoundaryPlasmaModels compat * Update divertors_actor.jl fix(divertor): ensure SOL boundary results are written back via populate_divertor_targets_from_sol! * Update divertors_actor.jl fix(divertor): ensure SOL boundary results are written back via populate_divertor_targets_from_sol! * fix(divertor): allow ActorDivertors creation when dd.divertors is missing (experiment data only) * Revert "Update BoundaryPlasmaModels compat" This reverts commit a893baf. * Update src/actors/divertors/divertors_actor.jl Co-authored-by: Brendan Lyons <[email protected]> * Update src/actors/divertors/divertors_actor.jl Co-authored-by: Brendan Lyons <[email protected]> * fix bugs --------- Co-authored-by: Brendan Lyons <[email protected]>
1 parent 02c4a92 commit c11dcd7

File tree

1 file changed

+145
-101
lines changed

1 file changed

+145
-101
lines changed

src/actors/divertors/divertors_actor.jl

Lines changed: 145 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -3,64 +3,32 @@
33
#= ============== =#
44
import BoundaryPlasmaModels
55

6-
@actor_parameters_struct ActorDivertors{T} begin
7-
heat_flux_model::Switch{Symbol} = Switch{Symbol}([:lengyel, :stangeby], "-", "Divertor heat flux model"; default=:lengyel)
6+
Base.@kwdef mutable struct FUSEparameters__ActorDivertors{T<:Real} <: ParametersActor{T}
7+
_parent::WeakRef = WeakRef(nothing)
8+
_name::Symbol = :not_set
9+
_time::Float64 = NaN
10+
boundary_model::Switch{Symbol} = Switch{Symbol}([:slcoupled, :lengyel, :stangeby], "-", "SOL boundary model"; default=:slcoupled)
811
impurities::Entry{Vector{Symbol}} = Entry{Vector{Symbol}}("-", "Vector of impurity species"; default=Symbol[])
912
impurities_fraction::Entry{Vector{T}} = Entry{Vector{T}}("-", "Vector of impurity fractions"; default=T[])
10-
heat_spread_factor::Entry{T} = Entry{T}("-", "Heat flux expansion factor in the private flux region (eg. due to transport) should be >= 1.0"; default=1.0)
11-
thermal_power_extraction_efficiency::Entry{T} = Entry{T}("-", "Fraction of thermal power that is carried out by the coolant at the divertor interface, rather than being lost in the surrounding strutures."; default=1.0)
13+
heat_spread_factor::Entry{T} = Entry{T}("-", "Heat flux expansion factor in the private flux region (eg. due to transport) should be >= 1.0"; default=one(T))
14+
thermal_power_extraction_efficiency::Entry{T} = Entry{T}("-", "Fraction of thermal power that is carried out by the coolant at the divertor interface, rather than being lost in the surrounding strutures."; default=one(T))
1215
verbose::Entry{Bool} = act_common_parameters(verbose=false)
1316
end
1417

1518
mutable struct ActorDivertors{D,P} <: SingleAbstractActor{D,P}
1619
dd::IMAS.dd{D}
1720
par::OverrideParameters{P,FUSEparameters__ActorDivertors{P}}
18-
boundary_plasma_models::Vector{BoundaryPlasmaModels.DivertorHeatFluxModel}
21+
boundary_plasma_models::Vector{BoundaryPlasmaModels.SOLBoundaryModel}
1922
end
2023

2124
"""
2225
ActorDivertors(dd::IMAS.dd, act::ParametersAllActors; kw...)
2326
24-
Calculates divertor heat fluxes and power loads using reduced SOL transport models.
25-
26-
The actor evaluates the heat and particle fluxes striking divertor targets by tracing
27-
power flow from the main plasma through the scrape-off layer (SOL) to the material
28-
surfaces. It uses simplified two-point models calibrated for divertor physics.
29-
30-
Key physics modeled:
31-
- Power flow from separatrix through SOL to divertor targets
32-
- Heat flux spreading due to field line divergence and cross-field transport
33-
- Strike point geometry and magnetic field angle effects
34-
- Material surface heat loads and wetted area calculations
35-
36-
Heat flux models available:
37-
- `:lengyel`: Simple exponential decay model with flux expansion
38-
- `:stangeby`: More sophisticated model including impurity effects
39-
40-
Analysis workflow:
41-
1. **SOL Geometry**: Identifies strike surfaces and field line connections
42-
2. **Power Balance**: Calculates SOL power from core sources minus radiation
43-
3. **SOL Width**: Estimates λq (heat flux width) at outer midplane using Eich scaling
44-
4. **Field Line Mapping**: Traces flux expansion from midplane to targets
45-
5. **Heat Flux Calculation**: Applies model-specific physics for target fluxes
46-
6. **Surface Integration**: Calculates total power loads and peak heat fluxes
47-
48-
Key outputs:
49-
- Peak heat flux on each divertor target surface (W/m²)
50-
- Total incident power on each target (W)
51-
- Wetted area and flux expansion factors
52-
- Strike angles and field line geometry parameters
53-
- Thermal power extraction efficiency for cooling systems
54-
55-
Model parameters:
56-
- `heat_spread_factor`: Additional flux spreading in private regions
57-
- `impurities`: Impurity species concentrations affecting radiation
58-
- `thermal_power_extraction_efficiency`: Cooling system effectiveness
27+
Evaluates divertor loading and deposited power
5928
6029
!!! note
6130
62-
Stores detailed heat flux analysis in `dd.divertors` including peak fluxes,
63-
total power loads, target geometry, and cooling system requirements
31+
Stores data in `dd.divertors`
6432
"""
6533
function ActorDivertors(dd::IMAS.dd, act::ParametersAllActors; kw...)
6634
actor = ActorDivertors(dd, act.ActorDivertors; kw...)
@@ -72,7 +40,7 @@ end
7240
function ActorDivertors(dd::IMAS.dd, par::FUSEparameters__ActorDivertors; kw...)
7341
logging_actor_init(ActorDivertors)
7442
par = OverrideParameters(par; kw...)
75-
return ActorDivertors(dd, par, Vector{BoundaryPlasmaModels.DivertorHeatFluxModel}())
43+
return ActorDivertors(dd, par, Vector{BoundaryPlasmaModels.SOLBoundaryModel}())
7644
end
7745

7846
function _step(actor::ActorDivertors)
@@ -81,71 +49,147 @@ function _step(actor::ActorDivertors)
8149

8250
eqt = dd.equilibrium.time_slice[]
8351
cp1d = dd.core_profiles.profiles_1d[]
52+
cs = dd.core_sources
8453

8554
sol1 = IMAS.sol(eqt, dd.wall; levels=1)[:lfs][1] # first SOL open field line on the lfs
8655
Psol = IMAS.power_sol(dd.core_sources, cp1d)
8756
λ_omp = IMAS.widthSOL_eich(eqt, Psol)
57+
index_inner = 1
58+
index_outer = length(sol1.r)
59+
if sol1.r[1] < sol1.r[end]
60+
index_inner = 1
61+
index_outer = length(sol1.r)
62+
else
63+
index_inner = length(sol1.r)
64+
index_outer = 1
65+
end
8866

89-
identifiers = IMAS.identify_strike_surface(sol1, dd.divertors)
90-
identifiers = [(k_divertor, k_target) for (k_divertor, k_target, k_tile) in identifiers]
91-
92-
for (k_divertor, divertor) in enumerate(dd.divertors.divertor)
93-
for (k_target, target) in enumerate(divertor.target)
94-
95-
# identify which end of the field line strikes the divertor/target that we are considering
96-
id = (k_divertor, k_target)
97-
if id == identifiers[1]
98-
strike_index = 1
99-
elseif id == identifiers[2]
100-
strike_index = length(sol1.r)
101-
else
102-
@ddtime(target.power_conducted.data = 0.0)
103-
@ddtime(target.power_convected.data = 0.0)
104-
@ddtime(target.power_incident.data = 0.0)
105-
continue
106-
end
107-
108-
# Power conducted/convected by the plasma on this divertor target [W]
109-
@ddtime(target.power_conducted.data = Psol / 2.0)
110-
@ddtime(target.power_convected.data = 0.0)
111-
112-
# λq at the outer midplane [m]
113-
resize!(target.two_point_model)
114-
target.two_point_model[].sol_heat_decay_length = λ_omp
115-
116-
# target flux expansion [-] and wetted area [m²]
117-
flxexp = @ddtime(target.flux_expansion.data = sol1.total_flux_expansion[strike_index])
118-
λ_target = flxexp * λ_omp
119-
wetted_area = @ddtime(target.wetted_area.data = λ_target * 2π * sol1.r[strike_index])
120-
121-
# strike angles [rad]
122-
@ddtime(target.tilt_angle_tor.data = atan(sol1.Bp[strike_index] / sol1.Bt[strike_index]))
123-
@ddtime(target.tilt_angle_pol.data = sol1.strike_angles[strike_index == 1 ? 1 : 2])
124-
125-
# Setup model based on dd and run
126-
boundary_plasma_model = BoundaryPlasmaModels.DivertorHeatFluxModel(par.heat_flux_model)
127-
push!(actor.boundary_plasma_models, boundary_plasma_model)
128-
BoundaryPlasmaModels.setup_model(boundary_plasma_model, target, eqt, cp1d, sol1; par.impurities, par.impurities_fraction, par.heat_spread_factor)
129-
boundary_plasma_model() # run
130-
if par.verbose
131-
println(" == $(uppercase(target.name)) ==")
132-
BoundaryPlasmaModels.summary(boundary_plasma_model)
133-
println()
134-
end
135-
136-
# get the peak power heatflux [W.m²]
137-
power_flux_peak = @ddtime(target.power_flux_peak.data = boundary_plasma_model.results.q_perp_target_spread)
138-
139-
# get the total power on the divertor [W]
140-
power_incident = power_flux_peak * wetted_area
141-
@ddtime(target.power_incident.data = power_incident)
142-
end
143-
144-
# update totals
145-
IMAS.divertor_totals_from_targets!(divertor)
67+
strike_indices = (index_outer, index_inner)
68+
empty!(actor.boundary_plasma_models)
69+
for (i, strike_index) in enumerate(strike_indices)
70+
boundary_plasma_model = BoundaryPlasmaModels.SOLBoundaryModel(par.boundary_model)
71+
push!(actor.boundary_plasma_models, boundary_plasma_model)
72+
BoundaryPlasmaModels.setup_model(boundary_plasma_model, λ_omp, eqt, cp1d, cs, sol1; par.impurities, par.impurities_fraction, par.heat_spread_factor,strike_index)
73+
boundary_plasma_model() # run
74+
end
75+
# --- Decide how to write back results --- #
76+
if !_has_any_divertor(dd)
77+
write_divertor_totals_only_from_sol!(dd, sol1, λ_omp, actor.boundary_plasma_models, par)
78+
return actor
79+
end
14680

147-
@ddtime(divertor.power_thermal_extracted.data = par.thermal_power_extraction_efficiency * @ddtime(divertor.power_incident.data))
81+
if !_has_any_target(dd)
82+
write_divertor_totals_only_from_sol!(dd, sol1, λ_omp, actor.boundary_plasma_models, par)
83+
return actor
14884
end
14985

86+
populate_divertor_targets_from_sol!(dd, sol1, λ_omp, actor.boundary_plasma_models, Psol, par, strike_indices)
15087
return actor
15188
end
89+
90+
function populate_divertor_targets_from_sol!(
91+
dd,
92+
sol1,
93+
λ_omp,
94+
boundary_models::Vector,
95+
Psol,
96+
par,
97+
strike_indices::Tuple{Int,Int};
98+
)
99+
identifiers = try
100+
IMAS.identify_strike_surface(sol1, dd.divertors)
101+
catch err
102+
@warn "identify_strike_surface failed: $err"
103+
Tuple{Int,Int,Int}[(0,0,0),(0,0,0)]
104+
end
105+
106+
if length(identifiers) < 2 || any(==( (0,0,0) ), identifiers)
107+
@warn "No valid strike surfaces; skip per-target write-back."
108+
write_divertor_totals_only_from_sol!(dd, sol1, λ_omp, boundary_models, par)
109+
return nothing
110+
end
111+
112+
strike_angle_slot = (1, 2)
113+
114+
for i in 1:2
115+
(k_div, k_tgt) = identifiers[i]
116+
target = dd.divertors.divertor[k_div].target[k_tgt]
117+
118+
strike_index = strike_indices[i]
119+
bpm = boundary_models[i]
120+
121+
# λq @ OMP
122+
resize!(target.two_point_model)
123+
target.two_point_model[].sol_heat_decay_length = λ_omp
124+
125+
# flux expansion & wetted area
126+
flxexp = @ddtime(target.flux_expansion.data = sol1.total_flux_expansion[strike_index])
127+
λ_target = flxexp * λ_omp
128+
wetted_area = @ddtime(target.wetted_area.data = λ_target * 2π * sol1.r[strike_index])
129+
130+
# strike angles
131+
@ddtime(target.tilt_angle_tor.data = atan(sol1.Bp[strike_index] / sol1.Bt[strike_index]))
132+
@ddtime(target.tilt_angle_pol.data = sol1.strike_angles[strike_angle_slot[i]])
133+
134+
@ddtime(target.power_flux_peak.data = bpm.results.q_perp_target_spread)
135+
136+
power_incident = @ddtime(target.power_flux_peak.data) * wetted_area
137+
@ddtime(target.power_incident.data = power_incident)
138+
139+
@ddtime(target.power_conducted.data = Psol / 2.0)
140+
@ddtime(target.power_convected.data = 0.0)
141+
end
142+
143+
for divertor in dd.divertors.divertor
144+
IMAS.divertor_totals_from_targets!(divertor)
145+
@ddtime(divertor.power_thermal_extracted.data =
146+
par.thermal_power_extraction_efficiency * @ddtime(divertor.power_incident.data))
147+
end
148+
149+
return nothing
150+
end
151+
_has_any_divertor(dd) = !isempty(dd.divertors.divertor)
152+
153+
function _has_any_target(dd)
154+
for d in dd.divertors.divertor
155+
if !isempty(d.target); return true; end
156+
end
157+
return false
158+
end
159+
"""
160+
Write total divertor quantities using only SOL data
161+
(used when no target geometry is available)
162+
"""
163+
function write_divertor_totals_only_from_sol!(
164+
dd,
165+
sol1,
166+
λ_omp,
167+
boundary_models::Vector,
168+
par::OverrideParameters
169+
)
170+
function compute_incident_power(strike_index::Int, boundary_model)::Float64
171+
flux_expansion = sol1.total_flux_expansion[strike_index]
172+
λ_target = flux_expansion * λ_omp
173+
wetted_area = λ_target * 2π * sol1.r[strike_index]
174+
q_peak = boundary_model.results.q_perp_target_spread
175+
return q_peak * wetted_area
176+
end
177+
178+
index_inner = 1
179+
index_outer = length(sol1.r)
180+
if !(sol1.r[1] < sol1.r[end])
181+
index_inner, index_outer = length(sol1.r), 1
182+
end
183+
strike_indices = (index_outer, index_inner)
184+
185+
power_outer = compute_incident_power(strike_indices[1], boundary_models[1])
186+
power_inner = compute_incident_power(strike_indices[2], boundary_models[2])
187+
total_power_incident = power_outer + power_inner
188+
189+
for divertor in dd.divertors.divertor
190+
@ddtime(divertor.power_incident.data = total_power_incident)
191+
@ddtime(divertor.power_thermal_extracted.data =
192+
par.thermal_power_extraction_efficiency * @ddtime(divertor.power_incident.data))
193+
end
194+
return nothing
195+
end

0 commit comments

Comments
 (0)