Data access and IMAS

vaft.machine_mapping is the layer that turns VEST machine reality — DAQ field codes, coil geometry tables, digitizer CSVs, MATLAB exports — into an IMAS-compliant ODS. It is the only place in VAFT that knows about VEST-specific things like “field code 102 is the Rogowski coil” or “PF2 changed height in 2025”. Everything downstream (vaft.process, vaft.formula, vaft.plot) consumes the ODS and never touches the raw database.

flowchart LR
    SQL[(VEST SQL raw DB<br/>field codes)] --> MM
    GEO[Packaged geometry<br/>*.yaml / *.mat / *.csv] --> MM
    MAT[Thomson / CXRS<br/>.mat exports] --> MM
    CSV[SXR digitizer CSV] --> MM
    MM[vaft.machine_mapping] --> ODS[omas.ODS<br/>magnetics · tf · pf_active · ...]
    ODS --> DOWN[vaft.process / vaft.formula / EFIT / CHEASE]

If you only want to read already-mapped shots, you do not need this page — see Data structures. Read this page when you are mapping a new shot, adding a new diagnostic, or debugging why a mapped signal looks wrong.


The two-layer API

Every diagnostic module follows the same shape.

1. The canonical IDS entry point — named exactly after the IDS it fills. This is what you should call.

from vaft.machine_mapping.magnetics import magnetics
from vaft.machine_mapping.tf import tf

magnetics(ods, shot, tstart, tend, dt)   # fills ods['magnetics']
tf(ods, shot, tstart, tend, dt)          # fills ods['tf']

2. The vfit_* builders — the static/dynamic split underneath. *_static writes geometry and metadata that do not depend on time; *_dynamic writes the time-dependent signals. The canonical entry point simply calls both. Reach for these when you want geometry without touching the database, or want to re-map only the waveforms.

from vaft.machine_mapping.tf import vfit_tf_static, vfit_tf_dynamic

vfit_tf_static(ods)                                  # tf.r0, ids_properties — no DB access
vfit_tf_dynamic(ods, shot, tstart, tend, dt)         # tf.coil.0.current, tf.b_field_tor_vacuum_r

All names are also re-exported lazily from the package root, so vaft.machine_mapping.vfit_md(...) works without importing the submodule. The explicit from vaft.machine_mapping.<module> import <name> form used above is what the production pipeline uses, and it is unambiguous — prefer it in scripts.


Coverage: which IDS, which module

IDS Module Canonical entry point
magnetics magnetics.py magnetics(ods, shot, tstart, tend, dt, processing_config=None)
tf tf.py tf(ods, shot, tstart, tend, dt)
pf_active pf_active.py pf_active(ods, shot, tstart, tend, dt, geometry_root=None)
barometry barometry.py barometry(ods, shot, tstart, tend, dt)
spectrometer_uv spectrometer_uv.py spectrometer_uv(ods, shot, t_start, t_end, dt)
thomson_scattering thomson_scattering.py thomson_scattering(ods, shotnumber, data_root=None, mat_file=None)
charge_exchange charge_exchange.py charge_exchange(ods, shotnumber, options='ces', data_root=None, mat_file=None)
soft_x_rays soft_x_rays.py soft_x_rays(ods, shot, daq_label, **kwargs)
pulse_schedule pulse_schedule/mapping.py pulse_schedule(ods, shot, data_root=None)
dataset_description dataset_description.py dataset_description(ods, source, options=None)
pf_passive pf_passive.py pf_passive(ods, source=None, options=None)
em_coupling em_coupling.py em_coupling(ods, source=None, options=None)
summary summary.py summary(ods, source, options=None)
mhd_linear mhd_linear.py mhd_linear(ods, source, options=None)

Note the argument shapes differ, and deliberately so — they follow the data source:

  • Raw-DAQ diagnostics (magnetics, tf, pf_active, barometry, spectrometer_uv) take (ods, shot, tstart, tend, dt): they resample DAQ traces onto a uniform window you choose.
  • File-backed diagnostics (thomson_scattering, charge_exchange, soft_x_rays) take a shot plus a file/root override: their timebase comes from the exported file, not from you.
  • pulse_schedule reads the per-shot ShotLog record FileDB keeps under legacy/shotlog/{shot}/ (python -m vaft.cli shotlog extract) and writes the operators’ planned triggers as pulse_schedule.event(:) on the DAQ clock. They are what was set, not measured; each event’s provenance names the workbook, sheet and cell it came from.
  • Reference-ODS diagnostics (pf_passive, em_coupling) take a source path to a reference ODS.

equilibrium and pf_plasma exist as modules but their mapping functions raise NotImplementedError — equilibria are produced by EFIT/CHEASE, not by machine mapping. See Equilibrium.

Source: vaft/machine_mapping/


Mapping a shot end to end

This is the real production recipe, condensed from generate_diagnostics_ods.py.

from omas import ODS, save_omas_json

from vaft.machine_mapping.dataset_description import dataset_description
from vaft.machine_mapping.magnetics import magnetics
from vaft.machine_mapping.pf_active import pf_active
from vaft.machine_mapping.spectrometer_uv import spectrometer_uv
from vaft.machine_mapping.barometry import barometry
from vaft.machine_mapping.tf import tf

shot, tstart, tend, dt = 39915, 0.26, 0.36, 4e-5

ods = ODS()
dataset_description(ods, shot, {"source_type": "shot", "run": 1, "machine": "VEST"})
pf_active(ods, shot, tstart, tend, dt)
spectrometer_uv(ods, shot, tstart, tend, dt)
barometry(ods, shot, tstart, tend, dt)
tf(ods, shot, tstart, tend, dt)
magnetics(ods, shot, tstart, tend, dt)

save_omas_json(ods, "39915_diagnostics.json")

Order matters only for dataset_description (it stamps the pulse/run identity). The rest are independent and each writes into its own IDS.

dt = 4e-5 (25 kHz) matches the native DAQ rate — asking for a finer grid does not create information, it just interpolates.


Per-diagnostic detail

magnetics

The heaviest module, and the one you are most likely to need to reason about.

vfit_magnetics_static(ods) writes probe and loop geometry from packaged YAML: position.r, position.z, a probe length of 0.01 m, poloidal_angle of $3\pi/2$, and type.index = 2 (Mirnov). It also appends four toroidal reference Mirnov channels at $r = 0.796$ m, $z = 0.02$ m spaced at $\phi = 0,\ 2\pi/3,\ \pi,\ 4\pi/3$ — these are what make toroidal mode-number analysis possible.

vfit_magnetics_dynamic(ods, shot, tstart, tend, dt, processing_config=None) fills:

Node Notes
magnetics.flux_loop.:.flux.data integrated loop signal, multiplied by $2\pi$ to give poloidal flux in Wb
magnetics.b_field_pol_probe.:.field.data integrated probe signal, T
magnetics.b_field_pol_probe.:.voltage.data raw un-integrated Mirnov voltage at native DAQ timebase
magnetics.ip.0.data plasma current, A
magnetics.diamagnetic_flux.0.data diamagnetic flux, Wb

The raw voltage traces are written by vfit_mirnov_raw_dynamic(ods, shot) and kept on the acquisition timebase, not the resampled one — fluctuation analysis needs the full bandwidth. See fluctuation_diagnostics_analysis.ipynb.

Two lower-level helpers are worth knowing:

from vaft.machine_mapping.magnetics import detect_plasma_window, vfit_md, vfit_plasma_current, vest_diamagnetic_flux

time, flux_loops, probes = vfit_md(39915)          # -> (ndarray, list[ndarray], list[ndarray])
time, ip = vfit_plasma_current(39915)              # -> (ndarray, ndarray), amps
window = detect_plasma_window(39915, time, ip)     # the plasma window, with its source
time, dia = vest_diamagnetic_flux(39915, window.start, window.end)

Plasma current is not just the Rogowski trace. The bare Rogowski signal picks up the vacuum-vessel and coil contribution, so vfit_plasma_current subtracts a flux-loop-derived reference scaled by a mutual inductance, then removes a linear baseline fitted on a pre-plasma window. The mutual inductance value and the overall sign are both shot-range dependent (VEST rewired and re-polarised its diagnostics over the years), which is exactly the kind of machine trivia this module exists to hide. If a historical shot’s $I_p$ comes out inverted, that switch is the first thing to check.

Mapped plasma current

Plasma timing policy

Where the plasma is in a discharge is decided once, in the plasma_timing block of vest.yaml, and read by every consumer (issue #409). Its window names a diagnostics_time_policies window — plasma_analysis, 0.28–0.36 s, which no diagnostic maps onto and the stage’s tstart/tend never retune — and baseline_lead_s the stretch before it, [tstart − lead, tstart), whose samples are the baseline the detectors measure their noise on. The block then carries the detector recipes, keyed by the signal they were tuned on: h_alpha (median filter, threshold max(2 % peak, 5 σ), 0.5 ms persistence, width, prominence and integral floors, principal pulse), ip (zero-phase low-pass, principal pulse, pickup floor, 10 % end threshold, collapse fallback), per-line lines for impurity lines with their own morphology, the H-alpha usability floors (rail level, quantized baseline, validity) and the agreement tolerances between light and current. Every rule is validated against vaft.process.onset.active_window’s signature when the policy is loaded:

from vaft.machine_mapping.utils import resolve_plasma_timing_policy

policy = resolve_plasma_timing_policy()
policy.window.tstart, policy.window.tend, policy.baseline_start   # 0.28, 0.36, 0.26
policy.h_alpha, policy.ip                                          # keyword arguments of active_window

Both detector rules are principal-pulse rules: each keeps only the segment holding its own maximum, so a window is one run and the two sources answer the same question — where is the one main discharge. The light rule was an envelope rule until #842, which meant a record of scattered flashes reported a window spanning all of them and opened it at the first, and a record whose brightest feature is a refused spike now yields no light window rather than one built from lesser bursts.

The rules were tuned on a scan of the raw database, and that scan is reproducible: workflow/plasma_onset/scan_corpus.py --shots 39900-41700 runs the raw-side detector over every shot in the range and writes test/data/onset_corpus.json (one row per shot: the verdict, both detectors’ windows, why a shot could not be judged), which test/test_onset_corpus.py holds to the current policy and to the packaged products; --npz-dir also writes the per-shot records review_onset.py draws from. A retuned rule means a re-scan, not an edit of the quoted numbers.

Two readers consume it. vaft.omas.plasma_timing.plasma_timing(ods) works on a mapped product: it finds the H-alpha line by label, checks it is usable, takes the slow line as authoritative for onset and offset, the validated fast line as first fallback and the plasma-current principal pulse as final fallback and cross-check, and returns a PlasmaTiming with the window, its source, the agreement (consistent, ip_before_halpha, halpha_leads_ip_large, halpha_only, ip_only, none), the usability of every candidate and a fallback_reason — a missing filterscope is a normal state, and no plasma is found = False, never the range. vaft.machine_mapping.magnetics.detect_plasma_window(shot, ip_time, ip) applies the same rules to the raw records this layer already reads (the slow H-alpha field, negated, then the current) and returns a PlasmaWindowChoice whose source is h_alpha_raw, ip or analysis_range; the last is the whole range, flagged analysis_range_fallback. The diamagnetic-flux mapping anchors its reconstruction to that window and records it in magnetics.diamagnetic_flux[0].method_name (…; plasma window 0.3065-0.3308 s from h_alpha_raw), and the EFIT constraint script cuts its time slices from the range intersected with the ODS-side window.

Signal conditioning (integration, drift removal, smoothing) is delegated to vaft.process and is tunable through processing_config, a VestMagneticsProcessingConfig:

from vaft.process.magnetics import VestMagneticsProcessingConfig
from vaft.machine_mapping.magnetics import magnetics

magnetics(ods, 39915, 0.26, 0.36, 4e-5,
          processing_config=VestMagneticsProcessingConfig())

See Signal processing for what those knobs do, and Magnetics for plotting the result.

Discharge timing policy

The actuator events are a separate concept from the plasma window and sit in their own discharge_timing block of vest.yaml, read by vaft.omas.discharge_timing.discharge_timing(ods) (issue #409). It shares the plasma_analysis window and baseline_lead_s with the plasma policy, names the ohmic_coil (PF1) whose onset anchors the loop-voltage excursion, selects the loop (loop_voltage.selection: inboard_midplane — the inboard_flux_loop family member nearest the midplane, Flux Loop #10 on VEST — with a stored voltage.data preferred to $-d\Phi/dt$), and carries two rules: coil (keyword arguments of vaft.process.onset.active_window, run on |I - baseline| so an idle coil yields no onset) and vloop (keyword arguments of vaft.process.onset.zero_crossing_after_excursion: the sustained |V| run starting within anchor_tolerance_s of the ohmic onset is the solenoid excursion, the event is the first sign change after its extremum, and a decay that comes within approach_fraction of zero and climbs back above approach_hysteresis times that level before crossing is flagged approached_without_crossing). These are measured onsets, never triggers; EC power and gas injection have no mapped actuator signal and are reported as not_present. The events never enter the plasma hierarchy as fallbacks.

from vaft.omas.discharge_timing import discharge_timing

events = discharge_timing(ods)
events.oh_onset, events.vloop.zero_crossing, events.vloop.flags
{coil.name: coil.time for coil in events.pf_onsets}      # None for a coil that did not fire

Plasma features policy

The representative peaks of the plasma phase are a third concept beside the timing and the actuator events, configured in the plasma_features block of vest.yaml and read by vaft.omas.plasma_features.plasma_features(ods) (issue #409). Its window is plasma_timing — the only value: every peak is measured strictly inside the window the timing found, and when the timing found no plasma nothing is measured (computed = False with the timing’s fallback_reason; the analysis range is never reinterpreted as a window). The rule blocks ip, h_alpha, each lines[label] and diamagnetic are keyword arguments of vaft.process.onset.robust_peak, keyed by the signal they were tuned on — an impurity line never borrows the H-alpha values, and lines may not carry the H-alpha label. The H-alpha peak is measured on the line that answered for the plasma (the slow line, the validated fast line as fallback); lines are found by their stored processed_line label, and a configured label the product lacks is absent while an unconfigured one simply does not appear. Every optical feature notes what the mapper did to its channel (resampled, native_rate_hz, time_shift_s for the fast filterscope) and whether the channel’s digitizer clipped inside the window (railed, judged on the raw maximum against spectrometer_uv.CHANNEL_RAIL_LEVEL; None for a channel with no documented rail). The diamagnetic value is the signed sample at the largest |deviation| (polarity: absolute): the stored flux is positive for a paramagnetic plasma (#1196), so a diamagnetic shot would come back negative. ip_ramp_end is reserved and never required.

from vaft.omas.plasma_features import plasma_features

features = plasma_features(ods)
features.computed, features.window
features.ip.value, features.ip.time                 # A, s -- the representative peak, not the loudest sample
features.h_alpha.value, features.h_alpha.flags       # 39915: the raw maximum is a spike on the window edge
features.lines["CIII_1909"].value, features.diamagnetic.value

tf

tf reconstructs the toroidal field from a Hall probe, not from a coil current shunt. The raw signal is scaled by a Hall gain, baselined against the first samples, low-pass filtered and smoothed, then converted:

\[B_{t} R = \frac{\mu_0 N_{TF} I_{TF}}{2\pi}, \qquad N_{TF} = 24\]

which is written to tf.b_field_tor_vacuum_r (units T·m). The VEST reference radius tf.r0 is 0.4 m, so $B_t$ on axis is tf.b_field_tor_vacuum_r / tf.r0. The reconstructed coil current also lands in tf.coil.0.current.

from vaft.machine_mapping.tf import vfit_tf_current, vfit_tf_bt_r

time, i_tf = vfit_tf_current(39915)   # A
time, bt_r = vfit_tf_bt_r(39915)      # T·m

pf_active

Ten PF coils. vfit_pf_active_static loads a discretized coil geometry MATLAB table and writes each coil as a set of rectangular elements (geometry.geometry_type = 2) with turns_with_sign and area. Coil resistance is computed from copper resistivity as

\[R = \frac{2\pi \rho R_{coil}}{A_{coil}}\]

Crucially, which geometry file is loaded depends on the shot number — VEST’s PF set was modified, so coil heights differ between the older and newer builds. Passing a shot to vfit_pf_active_static is what selects the right one; the canonical pf_active(...) does this for you. You can point at a different asset tree with geometry_root, and resolve a packaged asset directly:

from vaft.machine_mapping.pf_active import resolve_geometry_asset, vfit_pf

path = resolve_geometry_asset("VEST_DiscretizedCoilGeometry_Full_ver_2507.mat")
time, currents = vfit_pf(39915)   # currents: list of 10 arrays, A

Per-coil gains and noise reduction are applied while building the waveforms, so vfit_pf output is already calibrated.

soft_x_rays

SXR does not come from the SQL DAQ — it comes from digitizer CSV files named digitizer_{daq_label}_{shot}.csv. The mapper builds the timebase itself from a sample rate (125 MHz / 128 by default) and a trigger offset, so daq_label is a required argument: it selects which physical array the digitizer channels belong to.

from vaft.machine_mapping.soft_x_rays import soft_x_rays, soft_x_rays_from_digitizer_csv

soft_x_rays(ods, shot=39915, daq_label="22577", data_root="/path/to/sxr")

# or build a fresh ODS in one call
ods_sxr = soft_x_rays_from_digitizer_csv(39915, "22577", data_root="/path/to/sxr")

Line-of-sight geometry (channel.:.line_of_sight.first_point / .second_point) is filled from a packaged endpoint table, and the default energy band is 0–20 keV. Channels the geometry table does not know about are still stored as brightness traces, just without LOS metadata — so an unmapped channel degrades gracefully rather than failing the run. Override the channel assignment with channel_map, and flip a mis-wired array with polarity=-1.0.

Worked example: soft_x_ray_signal_analysis.ipynb.

camera_visible

The FAST camera comes from per-shot BMP frames {shot}_{frame:08d}.bmp and a {shot}_bmp.txt header, not from the SQL DAQ. The header gives each frame’s time: either the original export’s Top Frame, rows or, for a BatchConv2 re-export (TopFrame: keys), frame / Frame_Rate. Only raw frames are stored (frame[:].image_raw); there is no radiometric calibration.

Frame selection convention

A camera product holds only the frames from the first to the last plasma-lit frame of the recording, plus two frames of padding on each side. Unlit frames inside that range stay. The kept frames are renumbered from 0, and each one keeps the time of its original frame index.

The default rule, background-relative-v3, compares each frame with the shot’s own view before the plasma:

  • Reference: the per-pixel median of the first 10 frames. The camera is triggered before breakdown, so these frames show the vessel without plasma, including room light and the sensor floor.
  • Plasma-lit frame: its mean rises above the reference by more than max(3, 6·σ_ref) DN, or its 99th-percentile pixel rises by more than 15 DN. σ_ref is the spread of the reference frames’ own mean rise.
  • Plasma end: when the shot’s diagnostics product gives an I_p window (|I_p| > 5 kA), the last lit frame is capped at the first frame at or after the end of I_p. A lit vessel fades slowly after the plasma ends, and without the cap that afterglow is kept. The start is never capped, so the breakdown flash before the I_p onset stays.

A dark-level rule cannot separate an empty but lit vessel from plasma light. Shot 48914 has a ~58 DN floor under room light and kept all 101 frames under v2; v3 keeps frames 26-61. A frozen recording (48913: 101 copies of one saturated image) has no lit frame under v3 either.

When the reference cannot stand for “no plasma”, the selection falls back to relative-dark-v2, and the comment names that rule. This happens when:

  • fewer than 11 frames exist;
  • any of the first 10 frames is missing (most original exports were already trimmed to the plasma by the operators);
  • σ_ref > 2 DN;
  • a later frame’s mean falls below the reference by more than the threshold, meaning the camera started during the plasma.

Under relative-dark-v2, a frame is near-black when more than 98 % of its pixels are below dark_level + 20 DN. dark_level is the lower quartile over frames of each frame’s 1st-percentile pixel, capped at 60 DN. Passing threshold= restores the donor’s fixed 35 DN (fixed-v1).

A shot with no lit frame raises CameraFrameSelectionError and gets no product.

Every product records which rule built it, so it can be checked without loading frames:

  • camera_visible.ids_properties.comment holds rule=, threshold, percentage, buffer_frames, and plasma_end_frame (none when nothing was capped). It also holds dark_level (v2), or peak_threshold, reference_frames and reference_sigma (v3), and retained original frame indices [a, b] out of N. Products built before the rule had a name have no rule= and read as fixed-v1.
  • The stage manifest holds the same fields under provenance.frame_selection. The I_p window the ingest used is under provenance.plasma_current_window_s.
from vaft.machine_mapping.camera_visible import parse_frame_selection, select_valid_frames

parse_frame_selection(ods["camera_visible.ids_properties.comment"])
# {'rule': 'background-relative-v3', 'first_retained': 31, 'last_retained': 57, 'total_frames': 101, ...}
select_valid_frames(frames, plasma_end_frame=55)      # v3, afterglow capped at I_p end
select_valid_frames(frames, rule="relative-dark-v2")  # the dark-level rule, for comparison
select_valid_frames(frames, threshold=35)             # the old fixed rule

workflow/automatic_pipeline_2_corrective_data_update/audit_camera_visible_frames.py checks each FileDB product, its HSDS replication and the copy in HSDS main against this rule, using the raw frames and the plasma-current window.

thomson_scattering and charge_exchange

Both read MATLAB exports rather than the DAQ, so they take a shotnumber and an optional data_root / mat_file. Both write uncertainties alongside values when the export provides errors.

from vaft.machine_mapping.thomson_scattering import thomson_scattering
from vaft.machine_mapping.charge_exchange import charge_exchange

thomson_scattering(ods, 39915)                    # -> thomson_scattering.channel.:.{t_e, n_e}
charge_exchange(ods, 39915, options="ces")        # 'ces' or Doppler modes

charge_exchange supports more than one instrument: options selects between the CES export and the ion Doppler spectroscopy exports, and read_doppler_single / read_doppler_profile are available if you want to load Doppler data directly. These two IDSs are the kinetic input to profile fitting — profile_fitting_using_equilibrium_and_kinetic_diagnostics.ipynb.

pf_passive and em_coupling

These two are not measured — they are machine constants. Both are copied from a packaged reference ODS that ships with VAFT:

from vaft.machine_mapping.pf_passive import pf_passive
from vaft.machine_mapping.em_coupling import em_coupling

pf_passive(ods)     # vessel/passive-loop geometry
em_coupling(ods)    # mutual inductance matrices

pf_passive deliberately deletes the loop currents and the time axis from the reference while keeping the geometry. That is not a bug: the reference ODS contains one historical shot’s eddy currents, and you want the geometry so you can compute your shot’s eddy currents fresh. Pass source (or options['reference_ods']) to substitute your own reference.

em_coupling supplies the mutual inductances that eddy-current reconstruction needs; see Signal processing and EM modeling.

dataset_description

Stamps pulse identity. vfit_dataset_description is the explicit form:

from vaft.machine_mapping.dataset_description import vfit_dataset_description

vfit_dataset_description(ods, shot=39915, run=0, machine="VEST", pulse_type="pulse")
# -> dataset_description.data_entry.{machine, pulse, pulse_type, run, user}

Static assets and the channel table

Machine mapping is only as good as its geometry tables. They live under vaft/data/geometry/ and are resolved through resolve_data_root():

Asset Used for
VEST_MagneticsGeometry_Full_ver_2302.yaml probe / flux-loop positions, and the inboard–side–outboard grouping
MD.yaml magnetic-diagnostic channel list used by vfit_md
table.yaml field code → channel name lookup
VEST_DiscretizedCoilGeometry_Full_ver_1906.mat, ..._ver_2507.mat PF coil elements (shot-dependent; 2507 from shot 45968)
VEST_static_geometry.json.gz, VEST_passive_wall_2409.npz passive wall: 950 loops, plus 15 from shot 43017 (wall 2409)
line_of_sight_endpoints.csv SXR lines of sight

A separate vest.yaml shipped inside vaft/machine_mapping/ carries per-shot DAQ channel metadata (label, field code, gain). It is keyed by shot, with a 0 block holding defaults that shot-specific blocks are deep-merged over — so a recalibrated channel is a small YAML override, not a code change. Query it with:

from vaft.machine_mapping.utils import raw_database_info

info = raw_database_info("vest.yaml", 39915, "b_field_pol_probe")
info["labels"], info["fields"], info["gains"]   # dicts keyed by channel index

Probes and loops are grouped by position — inboard, side, outboard — and that grouping is what drives per-group uncertainties below.


Uncertainties for equilibrium reconstruction

EFIT needs an error bar on every constraint. machine_mapping attaches them, because the machine is what determines them: an outboard Mirnov is not as trustworthy as a Rogowski coil.

from vaft.machine_mapping.utils import (
    DEFAULT_CONSTRAINT_UNCERTAINTIES,
    apply_default_constraint_uncertainties,
)

apply_default_constraint_uncertainties(ods)   # annotates pf_active, tf and magnetics in one call

The defaults are relative errors:

Constraint key Default
pf_active_current 1e-4
tf_b_field_tor_vacuum_r 1e-4
magnetics_ip 5e-2
magnetics_diamagnetic_flux 3e-2
magnetics_bpol_inboard 1e-2
magnetics_bpol_side 1e-1
magnetics_bpol_outboard 1e-2
magnetics_flux_loop_inboard 1e-1
magnetics_flux_loop_outboard 1e-2

The pattern is physical: coil currents are known to a part in $10^4$ because they are driven and metered, while the side probes and inboard flux loops get 10% — they sit where the field is weakest and the pickup worst, so down-weighting them is what keeps a reconstruction stable.

Override selectively with a mapping (unknown keys raise), or wholesale with a 9-element vector in the table’s order:

apply_default_constraint_uncertainties(ods, {"magnetics_ip": 0.02})
apply_default_constraint_uncertainties(ods, [1e-4, 1e-4, 2e-2, 3e-2, 1e-2, 1e-1, 1e-2, 1e-1, 1e-2])

Finer control is available per subsystem — apply_pf_active_current_uncertainties, apply_tf_uncertainties, and apply_magnetics_uncertainties (which additionally accepts fl_correct_coeff to divide out per-loop calibration coefficients). normalize_constraint_uncertainties turns either input form into the canonical dict if you want to inspect it first.


Offline mapping and reproducibility

Live SQL access is not always available or desirable — CI, archived reprocessing, and anyone outside the VEST network need to work from a dump. Two environment variables control this, and the production pipeline sets both:

import os

os.environ["VAFT_RAW_SAMPLE_PATH"] = "/data/raw_dumps/39915.json.gz"
os.environ["VAFT_RAW_OFFLINE_ONLY"] = "1"     # never attempt a live SQL connection

# ... mapping calls now read from the dump

VAFT_RAW_SAMPLE_PATH accepts a {shot} placeholder, so one template serves a whole reprocessing campaign. With VAFT_RAW_OFFLINE_ONLY set, a missing channel yields a zero waveform on a default timebase instead of an exception — mapping completes and the gap is visible in the data rather than as a crash. When you see an identically-zero trace in an offline run, suspect a missing channel in the dump before you suspect the machine.

Database access itself is documented in vest_raw_signal_sql_database.ipynb.


Adding a new diagnostic

The contract to satisfy, in order:

  1. Create vaft/machine_mapping/<ids_name>.py, named after the IMAS IDS, not the instrument.
  2. Write vfit_<ids>_static(ods, ...) for geometry/metadata and vfit_<ids>_dynamic(ods, shot, ...) for signals. Use set_path from .utils rather than assigning into the ODS directly — it keeps path handling uniform.
  3. Write the canonical <ids_name>(ods, shot, tstart, tend, dt) that calls both.
  4. Put geometry in vaft/data/geometry/ as YAML or CSV. Do not hardcode positions in Python.
  5. Register the public names in _EXPORT_MAP and __all__ in __init__.py so they are reachable from the package root.
  6. Degrade gracefully when a channel is absent — return an empty or zero signal, do not raise.

Related pages

results matching ""

    No results matching ""