Coverage for tcvx21/grillix_post/components/sources_m.py: 92%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1"""
2Routines to evaluate the density and temperature source strengths
4Since the sources were used with fixed positions and widths, these parameters are given
5default parameters matching the values used.
6"""
7import xarray as xr
8import numpy as np
11def smoothstep(test_point: float, step_centre: float, step_width: float, order=3):
12 """
13 Returns a hermite smoothstep of a specific order
15 The step will be exactly 0 for test_point values less than step_centre - step_width / 2
16 The step will be exactly 1 for test_point values greater than step_centre + step_width / 2
18 For a step of order A, the (A-1)^th derivative will also be zero at the endpoints
19 """
21 xn = (np.atleast_1d(test_point) - step_centre) / step_width + 0.5
23 # Ignore the runtime warning that comes from comparing NaN
24 # NaN will return False in any comparison
25 with np.errstate(invalid='ignore'):
26 xn[xn <= 0] = 0
27 xn[xn >= 1] = 1
29 if order == 1:
30 step_values = xn
31 elif order == 2:
32 step_values = -2.0 * xn ** 3 + 3.0 * xn ** 2
33 elif order == 3:
34 step_values = 6.0 * xn ** 5 - 15.0 * xn ** 4 + 10.0 * xn ** 3
35 else:
36 return NotImplemented
38 if isinstance(test_point, xr.DataArray):
39 return xr.DataArray(step_values, coords=test_point.coords, dims=test_point.dims, attrs=test_point.attrs)
40 else:
41 return step_values
44def density_source_function(rho, district, norm, source_strength, source_centre=0.915141, source_width=0.083797):
45 """Annular source for the density, mimicking neutral particle ionisation"""
46 annular_source = xr.DataArray(source_strength * np.exp(-((rho ** 2 - source_centre ** 2) / source_width) ** 2))
48 return xr.DataArray(annular_source.where(district == "CLOSED", 0.0)).assign_attrs(norm=norm.n0 / norm.tau_0)
51def core_temperature_source_function(rho, district, norm, source_strength, source_centre=0.3, source_width=0.3):
52 core_source = source_strength * xr.DataArray(1 - smoothstep(rho, source_centre, source_width))
53 return xr.DataArray(core_source.where(district == "CLOSED", 0.0)).assign_attrs(norm=norm.Te0 / norm.tau_0)
56def temperature_source_function(rho, district, norm, density: xr.DataArray, etemp: xr.DataArray, itemp: xr.DataArray,
57 density_source: xr.DataArray, source_strength, source_centre=0.3, source_width=0.3):
58 """
59 Smooth-step core power injection, mimicking Ohmic power deposition
60 The temperature source takes into account the power from the density source, to give a constant-power source
61 """
62 core_source = core_temperature_source_function(rho, district, norm, source_strength, source_centre, source_width)
64 total_source = xr.DataArray(core_source / density - density_source * (etemp + itemp) / density).assign_attrs(
65 norm=norm.Te0 / norm.tau_0)
66 core_source = xr.DataArray(core_source / density).assign_attrs(
67 norm=norm.Te0 / norm.tau_0)
68 annular_sink = xr.DataArray(-density_source * (etemp + itemp) / density).assign_attrs(
69 norm=norm.Te0 / norm.tau_0)
71 return total_source, core_source, annular_sink
74def _power_source_from_temperature_source(density_source: xr.DataArray, etemp_source: xr.DataArray,
75 density: xr.DataArray, etemp: xr.DataArray, itemp: xr.DataArray,
76 norm, kind: str = 'total'):
77 """
78 Returns the power source corresponding to a given density source and temperature source
80 Can return the sum of the power contributions from the etemp and density sources, or just the etemp_source and
81 density_source contributions, using the 'kind' parameter
82 """
83 if kind == 'total':
84 power_source = xr.DataArray(1.5 * (etemp_source * density + density_source * (etemp + itemp)))
85 elif kind == 'etemp_source':
86 power_source = xr.DataArray(1.5 * (etemp_source * density))
87 elif kind == 'density_source':
88 power_source = xr.DataArray(1.5 * (density_source * (etemp + itemp)))
89 else:
90 raise NotImplementedError(f"Power source kind {kind} not recognised")
92 return power_source.assign_attrs(norm=norm.n0 * norm.Te0 / norm.tau_0)
94def _integrated_power_source(grid, norm, density_source, etemp_source, mean_density, mean_etemp, mean_itemp,
95 kind: str = 'total'):
96 """
97 Computes the integrated power source
99 Can return the sum of the power contributions from the etemp and density sources, or just the etemp_source and
100 density_source contributions, using the 'kind' parameter
101 """
102 from tcvx21.grillix_post.observables.integrations_m import axisymmetric_cylindrical_integration
104 power_source = _power_source_from_temperature_source(density_source, etemp_source, density=mean_density,
105 etemp=mean_etemp, itemp=mean_itemp, norm=norm, kind=kind)
107 integrated_power_ = axisymmetric_cylindrical_integration(grid, norm, power_source)
109 return integrated_power_.to('kW')
112def integrated_sources(grid, equi, norm, params, snaps):
113 """
114 Compute the integrated particle and power sources
116 Integrated power = core_power + density_source_power - edge_sink_power
117 = core_power (since the edge sink and density source powers cancel out)
119 We check that this is the case, to within 1kW
120 """
121 from tcvx21.grillix_post.observables.integrations_m import axisymmetric_cylindrical_integration
123 rho = equi.normalised_flux_surface_label(grid.r_s, grid.z_s)
125 density_source_rate = params['params_srcsnk']['csrcn']
126 etemp_source_rate = params['params_srcsnk']['csrcte']
128 density_source = density_source_function(rho, grid.districts, norm, density_source_rate)
129 integrated_density_source = axisymmetric_cylindrical_integration(grid, norm, density_source)
130 print(f"The integrated particle source rate is {integrated_density_source:4.3e}")
132 mean_density = grid.shape(snaps.density.mean(dim=('phi', 'tau')))
133 mean_etemp = grid.shape(snaps.electron_temp.mean(dim=('phi', 'tau')))
134 mean_itemp = grid.shape(snaps.ion_temp.mean(dim=('phi', 'tau')))
136 etemp_source, etemp_core_source, etemp_edge_sink = temperature_source_function(
137 rho=rho, district=grid.districts, norm=norm,
138 density=mean_density, etemp=mean_etemp, itemp=mean_itemp,
139 density_source=density_source, source_strength=etemp_source_rate)
141 # Compute the power injected into the core as the (positive) etemp source
142 core_power = _integrated_power_source(grid, norm, density_source, etemp_core_source,
143 mean_density, mean_etemp, mean_itemp, kind='etemp_source')
145 total_power = _integrated_power_source(grid, norm, density_source, etemp_source,
146 mean_density, mean_etemp, mean_itemp, kind='total')
148 assert np.isclose(core_power.magnitude, total_power.magnitude, atol=1)
150 # Density source power
151 density_source_power = _integrated_power_source(grid, norm, density_source, etemp_source,
152 mean_density, mean_etemp, mean_itemp, kind='density_source')
154 edge_sink_power = _integrated_power_source(grid, norm, density_source, etemp_edge_sink,
155 mean_density, mean_etemp, mean_itemp, kind='etemp_source')
157 assert np.isclose((density_source_power + edge_sink_power).magnitude, 0, atol=1)
159 print(f"The integrated power source is {total_power:4.3f}\n"
160 f"The density source required {density_source_power:4.3f}, which was compensated by a temperature sink")
162 return {'density_source': integrated_density_source,
163 'power_source': total_power}