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
« 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).
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.
10### Operating conditions
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.
18#### Background pressure
19We expect a column named 'background pressure (torr)' (not case sensitive).
20We assume the pressure is in units of Torr.
22#### Anode mass flow rate
23We look for the following column names in order:
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'.
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.
33#### Discharge voltage
34We look for the following column names in order:
361. 'discharge voltage (v)',
372. 'anode voltage (v)'
39In both cases, the unit of the voltage is expected to be Volts.
41### Data
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'
49Relative uncertainties are fractions (so 0.2 == 20%) and absolute uncertainties are in the same units as the main quantity.
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.
57#### Thrust
58We look for a column called 'thrust (mn)' or 'thrust (n)'.
59We then convert the thrust to Newtons internally.
61#### Discharge current
62We look for one of the following column names in order:
641. 'discharge current (a)'
652. 'anode current (a)'
67#### Cathode coupling voltage
68We look for a column called 'cathode coupling voltage (v)'.
70#### Ion current density
71We look for three columns
731. The radial distance from the thruster exit plane.
74Allowed keys: 'radial position from thruster exit (m)'
762. The angle relative to thruster centerline
77Allowed keys: 'angular position from thruster centerline (deg)'
793. The current density.
80Allowed keys: 'ion current density (ma/cm^2)' or 'ion current density (a/m^2)'
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.
86#### Ion velocity
87We look for two columns:
891. Axial position from anode
90Allowed keys: 'axial position from anode (m)', 'axial distance from anode (m)'
922. Ion velocity
93Allowed keys: 'ion velocity (m/s)'
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.
99""" # noqa: E501
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
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#==================================================================================================
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}
128HT_COORDS = {
129 "z": "m",
130 "r": "m",
131 "theta": "rad",
132}
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}
154FLOW_RATE_KEY = "anode mass flow rate"
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# ---------------------------------------------------------------------------
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"])
168def _flow_from_fraction(df):
169 """anode flow = total flow * (1 - cathode_fraction)."""
170 return df["total flow rate"] * (1 - df["cathode flow fraction"])
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]
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}
198#==================================================================================================
199# This section contains further utilities for working with the Hall thruster PEM and data
200#==================================================================================================
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.
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 )
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 )
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"""
246 data_entries: list[DataEntry] = []
248 for (i, opcond) in enumerate(operating_conditions):
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])
256 Id = xr.DataArray(outputs['I_d'][i])
257 Vcc = xr.DataArray(outputs['V_cc'][i])
259 z = outputs['u_ion_coords'][i]
260 uion = outputs['u_ion'][i]
261 uion_arr = xr.DataArray(uion, coords=[z], dims=["z"])
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"])
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 }
276 entry = DataEntry(operating_condition=opcond, data=instance)
277 data_entries.append(entry)
279 return data_entries