Coverage for src / hallmd / data.py: 0%

37 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-03-19 21:56 +0000

1"""The `hallmd.data` package contains utilities for loading and processing Hall thruster experimental data. 

2Example data for the SPT-100 thruster lives in the [pem_data repo](https://github.com/JANUS-Institute/pem_dat). 

3 

4## Data conventions 

5The data used in the Hall Thruster PEM is expected to be in somewhat standard format. 

6This format may evolve over time to account for more data, but at present, when we read a CSV file, here is what we look for in the columns. 

7Note that we treat columns case-insensitively (except the units), so `Anode current (A)` is treated the same as `anode current (A)`. 

8Additionally, we automatically convert units to SI as best we can. 

9 

10### Operating conditions 

11 

12Data is imported into a dictionary that maps operating conditions to data. 

13An operating condition consists of a unique set of an anode mass flow rate, a background pressure, a discharge / anode voltage, and a magnetic field scale. 

14The flow rate and voltage are mandatory, while the pressure and field scale are optional (and assumed to be 0 and 1, respectively). 

15These can be provided in a few ways. 

16In cases where multiple options are allowed, the first matching column is chosen. 

17 

18#### Background pressure 

19We expect a column named 'background pressure (torr)' (not case sensitive). 

20We assume the pressure is in units of Torr. 

21 

22#### Anode mass flow rate 

23We look for the following column names in order: 

24 

251. A column named 'anode flow rate (mg/s)', 

262. A column named 'total flow rate (mg/s)' and a column named 'anode-cathode flow ratio', 

273. A column named 'total flow rate (mg/s)' and a column named 'cathode flow fraction'. 

28 

29In all cases, the unit of the flow rate is expected to be mass / time. 

30For option 2, the cathode flow fraction is expected as a fraction between zero and one. 

31For option 3, the anode-cathode flow ratio is unitless and is expected to be greater than one. 

32 

33#### Discharge voltage 

34We look for the following column names in order: 

35 

361. 'discharge voltage (v)', 

372. 'anode voltage (v)' 

38 

39In both cases, the unit of the voltage is expected to be Volts. 

40 

41### Data 

42 

43The following data-fields are all **optional**. 

44For each of these quantities, an uncertainty can be provided, either relative or absolute. 

45The formats for uncertainties for a quantity of the form '{quantity} ({unit})' are 

461. '{quantity} absolute uncertainty ({unit})' 

472. '{quantity} relative uncertainty' 

48 

49Relative uncertainties are fractions (so 0.2 == 20%) and absolute uncertainties are in the same units as the main quantity. 

50 

51As an example, thrust of 100 mN and a relative uncertainty of 0.05 represents 100 +/- 5 mN. 

52We assume the errors are normally distributed about the nominal value with the uncertainty representing two standard deviations. 

53In this case, the distribution of the experimentally-measured thrust would be T ~ N(100, 2.5). 

54If both relative and absolute uncertainties are provided, we use the absolute uncertainty. 

55If an uncertainty is not provided, a relative uncertainty of 2% is assumed. 

56 

57#### Thrust 

58We look for a column called 'thrust (mn)' or 'thrust (n)'. 

59We then convert the thrust to Newtons internally. 

60 

61#### Discharge current 

62We look for one of the following column names in order: 

63 

641. 'discharge current (a)' 

652. 'anode current (a)' 

66 

67#### Cathode coupling voltage 

68We look for a column called 'cathode coupling voltage (v)'. 

69 

70#### Ion current density 

71We look for three columns 

72 

731. The radial distance from the thruster exit plane. 

74Allowed keys: 'radial position from thruster exit (m)' 

75 

762. The angle relative to thruster centerline 

77Allowed keys: 'angular position from thruster centerline (deg)' 

78 

793. The current density. 

80Allowed keys: 'ion current density (ma/cm^2)' or 'ion current density (a/m^2)' 

81 

82We do not look for uncertainties for the radius and angle. 

83The current density is assumed to have units of mA / cm^2 or A / m^2, depending on the key. 

84If one or two of these quantities is provided, we throw an error. 

85 

86#### Ion velocity 

87We look for two columns: 

88 

891. Axial position from anode 

90Allowed keys: 'axial position from anode (m)', 'axial distance from anode (m)' 

91 

922. Ion velocity 

93Allowed keys: 'ion velocity (m/s)' 

94 

95We do not look for uncertainties for the axial position. 

96The ion velocity is assumed to have units of m/s. 

97If only one of these quantities is provided, we throw an error. 

98 

99""" # noqa: E501 

100 

101import numpy as np 

102import xarray as xr 

103from pem_core.data import DataEntry, DataField, DataInstance, DerivedColumn, load_multiple_datasets, load_single_dataset 

104from pem_core.types import PathLike 

105 

106#================================================================================================== 

107# This section defines the operating conditions and QoIs of the Hall thruster PEMv1.  

108# For other Hall thruster PEMS, you can define your own versions of these dicts. 

109#================================================================================================== 

110 

111HT_OP_VARS = { 

112 "discharge voltage": { 

113 "unit": "V", 

114 }, 

115 "anode mass flow rate": { 

116 "unit": "kg/s", 

117 }, 

118 "background pressure": { 

119 "unit": "Torr", 

120 "default": 0.0, 

121 }, 

122 "magnetic field scale": { 

123 "unit": "", 

124 "default": 1.0, 

125 }, 

126} 

127 

128HT_COORDS = { 

129 "z": "m", 

130 "r": "m", 

131 "theta": "rad", 

132} 

133 

134HT_QOIS = { 

135 "cathode coupling voltage": { 

136 "unit": "V", 

137 }, 

138 "discharge current": { 

139 "unit": "A", 

140 }, 

141 "thrust": { 

142 "unit": "N", 

143 }, 

144 "ion velocity": { 

145 "unit": "m/s", 

146 "coords": ("z",), 

147 }, 

148 "ion current density": { 

149 "unit": "A/m", 

150 "coords": ("r", "theta"), 

151 }, 

152} 

153 

154FLOW_RATE_KEY = "anode mass flow rate" 

155 

156# --------------------------------------------------------------------------- 

157# Derived-column specs for Hall thruster data files. 

158# These teach the generic loader how to reconstruct the anode mass flow rate 

159# from alternative column combinations that appear in some data sources. 

160# Specs are tried in order; the first one whose required columns are all 

161# present in the CSV wins. 

162# --------------------------------------------------------------------------- 

163 

164def _flow_from_ratio(df): 

165 """anode flow = total flow * ratio / (1 + ratio), where ratio = anode/cathode.""" 

166 return df["total flow rate"] * df["anode-cathode flow ratio"] / (1 + df["anode-cathode flow ratio"]) 

167 

168def _flow_from_fraction(df): 

169 """anode flow = total flow * (1 - cathode_fraction).""" 

170 return df["total flow rate"] * (1 - df["cathode flow fraction"]) 

171 

172HT_DERIVED_COLS: list[DerivedColumn] = [ 

173 DerivedColumn( 

174 target=FLOW_RATE_KEY, 

175 required=["total flow rate", "anode-cathode flow ratio"], 

176 compute=_flow_from_ratio, 

177 unit_from="total flow rate", 

178 ), 

179 DerivedColumn( 

180 target=FLOW_RATE_KEY, 

181 required=["total flow rate", "cathode flow fraction"], 

182 compute=_flow_from_fraction, 

183 unit_from="total flow rate", 

184 ), 

185] 

186 

187HT_RENAME_MAP = { 

188 "anode voltage" : "discharge voltage", 

189 "anode current" : "discharge current", 

190 "anode flow rate" : FLOW_RATE_KEY, 

191 "axial distance from anode": "z", 

192 "axial position from anode": "z", 

193 "axial ion velocity": "ion velocity", 

194 "angular position from thruster centerline": "theta", 

195 "radial position from thruster exit": "r", 

196} 

197 

198#================================================================================================== 

199# This section contains further utilities for working with the Hall thruster PEM and data 

200#================================================================================================== 

201 

202def load_ht_dataset( 

203 file: PathLike, 

204 op_vars: dict | None = None, 

205 qois: dict | None = None, 

206) -> list[DataEntry]: 

207 """Load a Hall thruster CSV data file using the standard HT schema. 

208 

209 Wraps :func:`pem_core.data.load_single_dataset` with the Hall-thruster 

210 defaults (`HT_OP_VARS`, `HT_QOIS`, `HT_COORDS`, `HT_RENAME_MAP`, 

211 `HT_DERIVED_COLS`) so that callers don't have to supply them manually. 

212 Custom ``op_vars`` / ``qois`` dicts, if provided, *replace* (not extend) 

213 the defaults. 

214 """ 

215 return load_single_dataset( 

216 file, 

217 operating_vars=op_vars if op_vars is not None else HT_OP_VARS, 

218 qois=qois if qois is not None else HT_QOIS, 

219 coords=HT_COORDS, 

220 rename_map=HT_RENAME_MAP, 

221 derived_cols=HT_DERIVED_COLS, 

222 ) 

223 

224def load_ht_datasets( 

225 files: list[PathLike], 

226 op_vars: dict | None = None, 

227 qois: dict | None = None, 

228) -> list[DataEntry]: 

229 """Load and merge multiple Hall thruster CSV files. See `load_ht_dataset`.""" 

230 return load_multiple_datasets( 

231 files, 

232 operating_vars=op_vars if op_vars is not None else HT_OP_VARS, 

233 qois=qois if qois is not None else HT_QOIS, 

234 coords=HT_COORDS, 

235 rename_map=HT_RENAME_MAP, 

236 derived_cols=HT_DERIVED_COLS, 

237 ) 

238 

239def pem_to_xarray( 

240 operating_conditions: list[dict[str, float]], 

241 outputs: dict, sweep_radii: np.ndarray, 

242 use_corrected_thrust: bool = True 

243 ) -> list[DataEntry]: 

244 """Convert the outputs of the Hall thruster PEM to xarrays so that we can compare them to data""" 

245 

246 data_entries: list[DataEntry] = [] 

247 

248 for (i, opcond) in enumerate(operating_conditions): 

249 

250 if use_corrected_thrust: 

251 # With multiple radii, we have multiple thrusts. Pick the last one as sweep_radii are sorted. 

252 thrust = xr.DataArray(np.atleast_1d(outputs['T_c'][i])[-1]) 

253 else: 

254 thrust = xr.DataArray(outputs['T'][i]) 

255 

256 Id = xr.DataArray(outputs['I_d'][i]) 

257 Vcc = xr.DataArray(outputs['V_cc'][i]) 

258 

259 z = outputs['u_ion_coords'][i] 

260 uion = outputs['u_ion'][i] 

261 uion_arr = xr.DataArray(uion, coords=[z], dims=["z"]) 

262 

263 theta = outputs['j_ion_coords'][i] 

264 r = sweep_radii 

265 jion = np.atleast_3d(outputs['j_ion'])[i, :, :].T 

266 jion_arr = xr.DataArray(jion, coords=[r, theta], dims=["r", "theta"]) 

267 

268 instance: DataInstance = { 

269 "discharge current": DataField(val=Id, unit="A"), 

270 "cathode coupling voltage": DataField(val=Vcc, unit="V"), 

271 "thrust": DataField(val=thrust, unit="N"), 

272 "ion velocity": DataField(val=uion_arr, unit="m/s"), 

273 "ion current density": DataField(val=jion_arr, unit="A/m^2"), 

274 } 

275 

276 entry = DataEntry(operating_condition=opcond, data=instance) 

277 data_entries.append(entry) 

278 

279 return data_entries