Skip to content

Commit 8e88cc6

Browse files
authored
feat: Add lambda_q Eich regression 9 (#114)
* Add Eich regression 9 * Backwards compatibility: add reg 9 to named options * Fix doctest * Call unitless func for reg 9 * Fix bug: hadn't passed lambda_q to reg 9 in wrapper * Add tests for lambda_q scalings * Py3.9 compatibility
1 parent 2d23478 commit 8e88cc6

File tree

6 files changed

+214
-16
lines changed

6 files changed

+214
-16
lines changed

cfspopcon/formulas/scrape_off_layer/__init__.py

+11-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
"""Routines to calculate the scrape-off-layer conditions and check divertor survivability."""
22

33
from .heat_flux_density import calc_B_pol_omp, calc_B_tor_omp, calc_fieldline_pitch_at_omp, calc_parallel_heat_flux_density, calc_q_perp
4-
from .lambda_q import calc_lambda_q
4+
from .lambda_q import (
5+
calc_lambda_q,
6+
calc_lambda_q_with_brunner,
7+
calc_lambda_q_with_eich_regression_9,
8+
calc_lambda_q_with_eich_regression_14,
9+
calc_lambda_q_with_eich_regression_15,
10+
)
511
from .reattachment_models import (
612
calc_ionization_volume_from_AUG,
713
calc_neutral_flux_density_factor,
@@ -25,6 +31,10 @@
2531
"two_point_model_fixed_qpart",
2632
"two_point_model_fixed_tet",
2733
"calc_lambda_q",
34+
"calc_lambda_q_with_brunner",
35+
"calc_lambda_q_with_eich_regression_14",
36+
"calc_lambda_q_with_eich_regression_15",
37+
"calc_lambda_q_with_eich_regression_9",
2838
"calc_separatrix_electron_density",
2939
"calc_separatrix_electron_temp",
3040
"calc_B_pol_omp",

cfspopcon/formulas/scrape_off_layer/lambda_q.py

+56-15
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
major_radius=ureg.meter,
1616
B_pol_out_mid=ureg.tesla,
1717
inverse_aspect_ratio=ureg.dimensionless,
18+
magnetic_field_on_axis=ureg.T,
19+
q_star=ureg.dimensionless,
1820
lambda_q_factor=ureg.dimensionless,
1921
),
2022
)
@@ -25,6 +27,8 @@ def calc_lambda_q(
2527
major_radius: float,
2628
B_pol_out_mid: float,
2729
inverse_aspect_ratio: float,
30+
magnetic_field_on_axis: float,
31+
q_star: float,
2832
lambda_q_factor: float = 1.0,
2933
) -> float:
3034
"""Calculate SOL heat flux decay length (lambda_q) from a scaling.
@@ -36,64 +40,101 @@ def calc_lambda_q(
3640
major_radius: [m] :term:`glossary link<major_radius>`
3741
B_pol_out_mid: [T] :term:`glossary link<B_pol_out_mid>`
3842
inverse_aspect_ratio: [~] :term:`glossary link<inverse_aspect_ratio>`
43+
magnetic_field_on_axis: [T] :term:`glossary link<magnetic_field_on_axis>`
44+
q_star: [~] :term:`glossary link<q_star>`
3945
lambda_q_factor: [~] :term:`glossary link<lambda_q_factor>`
4046
4147
Returns:
4248
:term:`lambda_q` [mm]
4349
"""
4450
if lambda_q_scaling == LambdaQScaling.Brunner:
45-
return lambda_q_factor * float(calc_lambda_q_with_brunner.unitless_func(average_total_pressure))
51+
lambda_q = calc_lambda_q_with_brunner.unitless_func(average_total_pressure=average_total_pressure, lambda_q_factor=lambda_q_factor)
52+
elif lambda_q_scaling == LambdaQScaling.EichRegression9:
53+
lambda_q = calc_lambda_q_with_eich_regression_9.unitless_func(
54+
magnetic_field_on_axis=magnetic_field_on_axis,
55+
q_star=q_star,
56+
power_crossing_separatrix=power_crossing_separatrix,
57+
lambda_q_factor=lambda_q_factor,
58+
)
4659
elif lambda_q_scaling == LambdaQScaling.EichRegression14:
47-
return lambda_q_factor * float(calc_lambda_q_with_eich_regression_14.unitless_func(B_pol_out_mid))
60+
lambda_q = calc_lambda_q_with_eich_regression_14.unitless_func(B_pol_out_mid=B_pol_out_mid, lambda_q_factor=lambda_q_factor)
4861
elif lambda_q_scaling == LambdaQScaling.EichRegression15:
49-
return lambda_q_factor * float(
50-
calc_lambda_q_with_eich_regression_15.unitless_func(
51-
power_crossing_separatrix, major_radius, B_pol_out_mid, inverse_aspect_ratio
52-
)
62+
lambda_q = calc_lambda_q_with_eich_regression_15.unitless_func(
63+
power_crossing_separatrix=power_crossing_separatrix,
64+
major_radius=major_radius,
65+
B_pol_out_mid=B_pol_out_mid,
66+
inverse_aspect_ratio=inverse_aspect_ratio,
67+
lambda_q_factor=lambda_q_factor,
5368
)
5469
else:
5570
raise NotImplementedError(f"No implementation for lambda_q scaling {lambda_q_scaling}")
5671

72+
return float(lambda_q)
73+
5774

75+
@Algorithm.register_algorithm(return_keys=["lambda_q"])
5876
@wraps_ufunc(
5977
return_units=dict(lambda_q=ureg.millimeter),
60-
input_units=dict(average_total_pressure=ureg.atm),
78+
input_units=dict(average_total_pressure=ureg.atm, lambda_q_factor=ureg.dimensionless),
6179
)
62-
def calc_lambda_q_with_brunner(average_total_pressure: float) -> float:
80+
def calc_lambda_q_with_brunner(average_total_pressure: float, lambda_q_factor: float = 1.0) -> float:
6381
"""Return lambda_q according to the Brunner scaling.
6482
6583
Equation 4 in :cite:`brunner_2018_heat_flux`
6684
"""
67-
return float(0.91 * average_total_pressure**-0.48)
85+
return float(lambda_q_factor * 0.91 * average_total_pressure**-0.48)
6886

6987

70-
@wraps_ufunc(return_units=dict(lambda_q=ureg.millimeter), input_units=dict(B_pol_out_mid=ureg.tesla))
71-
def calc_lambda_q_with_eich_regression_14(B_pol_out_mid: float) -> float:
88+
@Algorithm.register_algorithm(return_keys=["lambda_q"])
89+
@wraps_ufunc(
90+
return_units=dict(lambda_q=ureg.millimeter),
91+
input_units=dict(
92+
magnetic_field_on_axis=ureg.T,
93+
q_star=ureg.dimensionless,
94+
power_crossing_separatrix=ureg.megawatt,
95+
lambda_q_factor=ureg.dimensionless,
96+
),
97+
)
98+
def calc_lambda_q_with_eich_regression_9(
99+
magnetic_field_on_axis: float, q_star: float, power_crossing_separatrix: float, lambda_q_factor: float = 1.0
100+
) -> float:
101+
"""Return lambda_q according to Eich regression 9.
102+
103+
#9 in Table 2 in :cite:`eich_scaling_2013`
104+
"""
105+
return float(lambda_q_factor * 0.7 * magnetic_field_on_axis**-0.77 * q_star**1.05 * power_crossing_separatrix**0.09)
106+
107+
108+
@Algorithm.register_algorithm(return_keys=["lambda_q"])
109+
@wraps_ufunc(return_units=dict(lambda_q=ureg.millimeter), input_units=dict(B_pol_out_mid=ureg.tesla, lambda_q_factor=ureg.dimensionless))
110+
def calc_lambda_q_with_eich_regression_14(B_pol_out_mid: float, lambda_q_factor: float = 1.0) -> float:
72111
"""Return lambda_q according to Eich regression 14.
73112
74113
#14 in Table 3 in :cite:`eich_scaling_2013`
75114
"""
76-
return float(0.63 * B_pol_out_mid**-1.19)
115+
return float(lambda_q_factor * 0.63 * B_pol_out_mid**-1.19)
77116

78117

118+
@Algorithm.register_algorithm(return_keys=["lambda_q"])
79119
@wraps_ufunc(
80120
return_units=dict(lambda_q=ureg.millimeter),
81121
input_units=dict(
82122
power_crossing_separatrix=ureg.megawatt,
83123
major_radius=ureg.meter,
84124
B_pol_out_mid=ureg.tesla,
85125
inverse_aspect_ratio=ureg.dimensionless,
126+
lambda_q_factor=ureg.dimensionless,
86127
),
87128
)
88129
def calc_lambda_q_with_eich_regression_15(
89-
power_crossing_separatrix: float, major_radius: float, B_pol_out_mid: float, inverse_aspect_ratio: float
130+
power_crossing_separatrix: float, major_radius: float, B_pol_out_mid: float, inverse_aspect_ratio: float, lambda_q_factor: float = 1.0
90131
) -> float:
91132
"""Return lambda_q according to Eich regression 15.
92133
93134
#15 in Table 3 in :cite:`eich_scaling_2013`
94135
"""
95136
lambda_q = 1.35 * major_radius**0.04 * B_pol_out_mid**-0.92 * inverse_aspect_ratio**0.42
96137
if power_crossing_separatrix > 0:
97-
return float(lambda_q * power_crossing_separatrix**-0.02)
138+
return float(lambda_q_factor * lambda_q * power_crossing_separatrix**-0.02)
98139
else:
99-
return float(lambda_q)
140+
return float(lambda_q_factor * lambda_q)

cfspopcon/named_options.py

+1
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ class LambdaQScaling(Enum):
6363
"""Options for heat flux decay length scaling."""
6464

6565
Brunner = auto()
66+
EichRegression9 = auto()
6667
EichRegression14 = auto()
6768
EichRegression15 = auto()
6869

cfspopcon/variables.yaml

+19
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,7 @@ average_total_pressure:
153153
- calc_average_total_pressure
154154
used_by:
155155
- calc_lambda_q
156+
- calc_lambda_q_with_brunner
156157
B_pol_out_mid:
157158
default_units: tesla
158159
description:
@@ -162,6 +163,8 @@ B_pol_out_mid:
162163
used_by:
163164
- calc_fieldline_pitch_at_omp
164165
- calc_lambda_q
166+
- calc_lambda_q_with_eich_regression_14
167+
- calc_lambda_q_with_eich_regression_15
165168
- calc_power_crossing_separatrix_in_electron_channel
166169
B_t_out_mid:
167170
default_units: tesla
@@ -740,6 +743,7 @@ inverse_aspect_ratio:
740743
- calc_plasma_current_from_qstar
741744
- calc_q_star_from_plasma_current
742745
- calc_lambda_q
746+
- calc_lambda_q_with_eich_regression_15
743747
invmu_0_dLedR:
744748
default_units: dimensionless
745749
description:
@@ -838,6 +842,10 @@ lambda_q:
838842
- The near-scrape-off-layer heat-flux-density decay length.
839843
set_by:
840844
- calc_lambda_q
845+
- calc_lambda_q_with_brunner
846+
- calc_lambda_q_with_eich_regression_9
847+
- calc_lambda_q_with_eich_regression_14
848+
- calc_lambda_q_with_eich_regression_15
841849
used_by:
842850
- calc_parallel_heat_flux_density
843851
- calc_q_perp
@@ -850,6 +858,10 @@ lambda_q_factor:
850858
set_by: []
851859
used_by:
852860
- calc_lambda_q
861+
- calc_lambda_q_with_brunner
862+
- calc_lambda_q_with_eich_regression_9
863+
- calc_lambda_q_with_eich_regression_14
864+
- calc_lambda_q_with_eich_regression_15
853865
lambda_q_scaling:
854866
default_units: null
855867
description:
@@ -897,6 +909,8 @@ magnetic_field_on_axis:
897909
- calc_beta_normalized
898910
- calc_troyon_limit
899911
- calc_B_tor_omp
912+
- calc_lambda_q
913+
- calc_lambda_q_with_eich_regression_9
900914
- calc_SepOS_L_mode_density_limit
901915
- calc_SepOS_LH_transition
902916
- calc_SepOS_ideal_MHD_limit
@@ -940,6 +954,7 @@ major_radius:
940954
- calc_parallel_heat_flux_density
941955
- calc_q_perp
942956
- calc_lambda_q
957+
- calc_lambda_q_with_eich_regression_15
943958
- calc_SepOS_L_mode_density_limit
944959
- calc_SepOS_LH_transition
945960
- calc_SepOS_ideal_MHD_limit
@@ -1406,6 +1421,8 @@ power_crossing_separatrix:
14061421
- calc_parallel_heat_flux_density
14071422
- calc_q_perp
14081423
- calc_lambda_q
1424+
- calc_lambda_q_with_eich_regression_9
1425+
- calc_lambda_q_with_eich_regression_15
14091426
- calc_ratio_P_LH
14101427
- calc_ratio_P_LI
14111428
pumping_duct_neutral_pressure:
@@ -1456,6 +1473,8 @@ q_star:
14561473
- calc_PBpRnSq
14571474
- calc_bootstrap_fraction
14581475
- calc_plasma_current_from_qstar
1476+
- calc_lambda_q
1477+
- calc_lambda_q_with_eich_regression_9
14591478
radas_dir:
14601479
default_units: null
14611480
description:

docs/conf.py

+3
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,9 @@
5151
r"https://www.ipp.mpg.de/16701/jet",
5252
r"https://iopscience.iop.org/article/10.1088/1009-0630/13/1/01",
5353
r"https://www-internal.psfc.mit.edu/research/alcator/data/fst_cmod.pdf",
54+
# These bib resources fail due to "403 Client Error: Forbidden for url"
55+
r"https://doi.org/10.1103/PhysRevLett.121.055001",
56+
r"https://journals.aps.org/prl/abstract/10.1103/PhysRevLett.121.055001",
5457
]
5558
linkcheck_retries = 5
5659
linkcheck_timeout = 120

tests/test_scrape_off_layer_model.py

+124
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
import numpy as np
2+
import pytest
3+
4+
from cfspopcon import formulas
5+
from cfspopcon.named_options import LambdaQScaling
6+
from cfspopcon.unit_handling import ureg
7+
8+
lambda_q_tests = {
9+
LambdaQScaling.Brunner: 0.4332283874128845 * ureg.mm,
10+
LambdaQScaling.EichRegression14: 0.20533809707365488 * ureg.mm,
11+
LambdaQScaling.EichRegression15: 0.34842497310813536 * ureg.mm,
12+
LambdaQScaling.EichRegression9: 0.5865460692254366 * ureg.mm,
13+
}
14+
15+
16+
@pytest.fixture()
17+
def average_total_pressure():
18+
return 732028.9793 * ureg.Pa
19+
20+
21+
@pytest.fixture()
22+
def power_crossing_separatrix():
23+
return 25.57417052 * ureg.MW
24+
25+
26+
@pytest.fixture()
27+
def major_radius():
28+
return 1.85 * ureg.m
29+
30+
31+
@pytest.fixture()
32+
def B_pol_out_mid():
33+
return 3.052711915 * ureg.T
34+
35+
36+
@pytest.fixture()
37+
def inverse_aspect_ratio():
38+
return 0.3081000000
39+
40+
41+
@pytest.fixture()
42+
def magnetic_field_on_axis():
43+
return 12.20000000 * ureg.T
44+
45+
46+
@pytest.fixture()
47+
def q_star():
48+
return 3.290275716
49+
50+
51+
@pytest.fixture()
52+
def lambda_q_factor():
53+
return 1.23
54+
55+
56+
@pytest.mark.parametrize(["scaling", "result"], lambda_q_tests.items(), ids=[key.name for key in lambda_q_tests.keys()])
57+
def test_lambda_q_scalings(
58+
scaling,
59+
result,
60+
average_total_pressure,
61+
power_crossing_separatrix,
62+
major_radius,
63+
B_pol_out_mid,
64+
inverse_aspect_ratio,
65+
magnetic_field_on_axis,
66+
q_star,
67+
lambda_q_factor,
68+
):
69+
lambda_q = formulas.scrape_off_layer.calc_lambda_q(
70+
lambda_q_scaling=scaling,
71+
average_total_pressure=average_total_pressure,
72+
power_crossing_separatrix=power_crossing_separatrix,
73+
major_radius=major_radius,
74+
B_pol_out_mid=B_pol_out_mid,
75+
inverse_aspect_ratio=inverse_aspect_ratio,
76+
magnetic_field_on_axis=magnetic_field_on_axis,
77+
q_star=q_star,
78+
lambda_q_factor=lambda_q_factor,
79+
)
80+
81+
assert np.isclose(lambda_q, result)
82+
83+
84+
@pytest.mark.parametrize(["scaling", "result"], lambda_q_tests.items(), ids=[key.name for key in lambda_q_tests.keys()])
85+
def test_lambda_q_scalings_with_algorithms(
86+
scaling,
87+
result,
88+
average_total_pressure,
89+
power_crossing_separatrix,
90+
major_radius,
91+
B_pol_out_mid,
92+
inverse_aspect_ratio,
93+
magnetic_field_on_axis,
94+
q_star,
95+
lambda_q_factor,
96+
):
97+
if scaling == LambdaQScaling.Brunner:
98+
lambda_q = formulas.scrape_off_layer.lambda_q.calc_lambda_q_with_brunner(
99+
average_total_pressure=average_total_pressure, lambda_q_factor=lambda_q_factor
100+
)
101+
elif scaling == LambdaQScaling.EichRegression14:
102+
lambda_q = formulas.scrape_off_layer.lambda_q.calc_lambda_q_with_eich_regression_14(
103+
B_pol_out_mid=B_pol_out_mid,
104+
lambda_q_factor=lambda_q_factor,
105+
)
106+
elif scaling == LambdaQScaling.EichRegression15:
107+
lambda_q = formulas.scrape_off_layer.lambda_q.calc_lambda_q_with_eich_regression_15(
108+
power_crossing_separatrix=power_crossing_separatrix,
109+
major_radius=major_radius,
110+
B_pol_out_mid=B_pol_out_mid,
111+
inverse_aspect_ratio=inverse_aspect_ratio,
112+
lambda_q_factor=lambda_q_factor,
113+
)
114+
elif scaling == LambdaQScaling.EichRegression9:
115+
lambda_q = formulas.scrape_off_layer.lambda_q.calc_lambda_q_with_eich_regression_9(
116+
magnetic_field_on_axis=magnetic_field_on_axis,
117+
q_star=q_star,
118+
power_crossing_separatrix=power_crossing_separatrix,
119+
lambda_q_factor=lambda_q_factor,
120+
)
121+
else:
122+
raise NotImplementedError(f"Add the algorithm for {scaling.name}.")
123+
124+
assert np.isclose(lambda_q, result)

0 commit comments

Comments
 (0)