Mobile sites#

Note

The output under each example on this page is from runs on CHPC on 26 October 2026 (why it is recorded).

The mobile sites are instruments on TRAX light-rail trains (TRX01 to TRX03) and on buses (BUS01 and up) in the Salt Lake Valley. They have no fixed position: each measurement is placed with the vehicle’s GPS. They are read with the same functions as the fixed sites, and this page says what is different. Sites and instruments lists them and their instruments.

Where each measurement was#

get_obs() at a mobile site gives a geopandas.GeoDataFrame: the concentrations, for each row the instrument that logged it, its position (Latitude_deg, Longitude_deg and geometry), the vehicle’s speed and course, and GPS_Group:

>>> import uataq
>>> trx01 = uataq.get_site("TRX01")
>>> obs = trx01.get_obs(["CO2", "CH4", "O3"], time_range="2024-06-01")
>>> obs.columns.tolist()
['CO2d_ppm_cal', 'CO2d_ppm_raw', 'CH4d_ppm_cal', 'CH4d_ppm_raw', 'O3_ppb', 'instrument', 'Latitude_deg', 'Longitude_deg', 'Altitude_msl', 'Speed_m_s', 'Course_deg', 'Speed_Estimated', 'Course_Estimated', 'geometry', 'GPS_Group']
>>> obs["GPS_Group"].value_counts()
GPS_Group
horel    35135
lin       8611
Name: count, dtype: int64

It took 11 seconds. A row has values from one instrument, as at a fixed site (see Read data): here 8,611 rows of CO2 and CH4 and 35,135 of ozone. Where the GPS did not record a speed or course, they are computed from the positions, and Speed_Estimated and Course_Estimated are True on those rows.

Two groups, two clocks#

The trains carry instruments of both research groups (see Research group): here the CO2 and CH4 analyzer is the lin group’s and the ozone monitor is the horel group’s. Each group logs its instruments with its own clock and has its own GPS, so uataq places each group’s rows with that group’s GPS. GPS_Group says which one placed a row, and so which clock its Time_UTC is on:

  • lin: GPS time.

  • horel: the horel logger’s clock, which on TRX01 ran 1 to 20 seconds ahead of GPS time (dates sampled from 2019 to 2026), drifting between resets.

So two rows of different groups with the same Time_UTC can be seconds apart in truth. Each row’s position is right for its own time; to compare the two groups’ measurements, average over a period longer than that, such as a minute. On a few days the lin computer’s clock was hours or days off, and then a lin row can have a Time_UTC outside the range you asked for.

To read one group only, name it. Both groups’ GPS record the altitude, so the table has Altitude_msl either way (this output is from a run on 30 October 2026):

>>> o3 = trx01.get_obs("O3", group="horel", time_range="2024-06-01")
>>> o3.columns.tolist()
['O3_ppb', 'instrument', 'Latitude_deg', 'Longitude_deg', 'Altitude_msl', 'Speed_m_s', 'Speed_Estimated', 'Course_deg', 'Course_Estimated', 'geometry', 'GPS_Group']

TRX01’s CO2 and CH4#

TRX01’s CO2 and CH4 analyzer has two instrument names, for two ways it was calibrated, and the same columns mean different things in each:

Instrument

When it has data

Columns

lgr_ugga

Before November 2023, and May to August 2024

_cal is calibrated against reference tanks, _raw is not calibrated, as at the fixed sites.

lgr_ugga_manual_cal

18 November 2023 to March 2024, and from 27 August 2024

No tanks. _raw holds the value calibrated by the analyzer’s own software, and _cal is empty.

(The months are those of the files in the archive in October 2026.) get_obs() reads both, and its instrument column says which one a row came from. On 1 September 2024, _cal is empty:

>>> obs = trx01.get_obs(["CO2", "CH4", "O3"], time_range="2024-09-01")
>>> obs.groupby("instrument")[["CO2d_ppm_cal", "CO2d_ppm_raw", "O3_ppb"]].count()
                     CO2d_ppm_cal  CO2d_ppm_raw  O3_ppb
instrument
2b_205                          0             0   26954
lgr_ugga_manual_cal             0          7188       0

(The analyzer logged 7,200 rows that day; get_obs() keeps the 7,188 the GPS could place.) So take _cal from the rows of lgr_ugga and _raw from those of lgr_ugga_manual_cal. get_network_obs() has the same instrument column:

>>> net = uataq.get_network_obs(["TRX01"], "CO2", time_range="2024-09-01")
>>> net[["SID", "instrument", "CO2d_ppm_cal", "CO2d_ppm_raw", "GPS_Group"]].head(3)
                       SID           instrument  CO2d_ppm_cal  CO2d_ppm_raw GPS_Group
Time_UTC
2024-09-01 00:00:07  TRX01  lgr_ugga_manual_cal           NaN      432.9656       lin
2024-09-01 00:00:17  TRX01  lgr_ugga_manual_cal           NaN      432.9974       lin
2024-09-01 00:00:27  TRX01  lgr_ugga_manual_cal           NaN      432.6543       lin

Months that are only raw#

Some of the horel group’s data are only at the raw level, so get_obs(), which reads final, has nothing for them (see Data levels and flags):

  • The trains’ ozone and particles before 19 November 2018, from horel’s first (pilot) archive. TRX01’s ozone comes from the lin group until 27 October 2017 (see Research group), so at the final level it has a gap from then to 19 November 2018.

  • The month of each particle monitor swap, when the MetOne ES-642 was replaced by an ES-405: from the start of that month to the swap, on BUS03 in December 2024, BUS02 in March 2025, and TRX01, TRX02 and TRX03 in January 2025 (with the trains’ ozone too).

Read them with uataq.read_data(..., lvl="raw").

Months with no GPS fix#

A row that finds no fix of its group’s GPS in its second has no position, and get_obs() leaves it out. TRX01’s lin GPS has no final file for September and October 2018, nor for August 2017 (that month is only at qaqc), so the methane of 2018-09 and 2018-10 and the lin ozone of 2017-08 have no position at all. get_obs then returns an empty table with an EmptyResultWarning that says how many rows were read; get_obs(..., include_gps=False) returns them without a position.

Next#