33#= ============== =#
44import 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 )
1316end
1417
1518mutable 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 }
1922end
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"""
6533function ActorDivertors (dd:: IMAS.dd , act:: ParametersAllActors ; kw... )
6634 actor = ActorDivertors (dd, act. ActorDivertors; kw... )
7240function 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 } ())
7644end
7745
7846function _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
15188end
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