MOVES emission factors in R, without a database

ef_moves() uses local MOVES rate data to evaluate running-exhaust emission factors. It runs in R on Linux, macOS and Windows. There is no database connection, SQL query, Java process, network download or MOVES installation involved in evaluating factors.

VEIN evaluates factors; it does not run MOVES. The bundled data are a small reference set. To produce factors for other counties, years, months, weather, fuels, pollutants or processes, use movesr, a standalone R port of the MOVES rate engine that writes these tables with movesr_export_vein() (see Generate factors with movesr).

Vehicle type, fuel and age

These inputs are explicit arguments of ef_moves():

Argument Meaning Bundled reference data
source_type_id MOVES vehicle category all 13 on-road types
fuel_type_id MOVES fuel type every valid combination (names accepted)
model_year Vehicle manufacturing year 2000–2025
road_type_id MOVES road category 5: urban unrestricted
temperature Temperature in degrees C 10, 20 or 30
rel_humidity Relative humidity in percent 50

Use moves_vehicles() to list the available source-type and fuel combinations with their names and IDs. The bundled data include 38 of the 48 MOVES6-valid vehicle/fuel combinations. The five fuels are gasoline, diesel, E-85, CNG and electricity; not every fuel is valid for every vehicle type. Combinations with no modelled running-exhaust output, such as heavy electric trucks and CNG or electric motor homes, are not listed.

Vehicle age is the inventory year minus model_year. The bundled inventory year is 2025, so model_year = 2020 represents a five-year-old vehicle. For other datasets, choose the inventory year with select = list(yearID = ...).

Use the reference factors

The small dataset shipped with VEIN contains MOVES6 running-exhaust rates for a documented U.S. reference scenario. The selected model year describes the vehicle’s year of manufacture; yearID is the inventory year, which determines vehicle age. They must not be interchanged.

library(vein)
x <- moves_factors()
moves_vehicles()
#>    sourceTypeID               sourceTypeName fuelTypeID
#> 1            11                   Motorcycle          1
#> 2            21                Passenger Car          1
#> 3            21                Passenger Car          2
#> 4            21                Passenger Car          5
#> 5            21                Passenger Car          9
#> 6            31              Passenger Truck          1
#> 7            31              Passenger Truck          2
#> 8            31              Passenger Truck          5
#> 9            31              Passenger Truck          9
#> 10           32       Light Commercial Truck          1
#> 11           32       Light Commercial Truck          2
#> 12           32       Light Commercial Truck          5
#> 13           32       Light Commercial Truck          9
#> 14           41                  Other Buses          1
#> 15           41                  Other Buses          2
#> 16           41                  Other Buses          3
#> 17           42                  Transit Bus          1
#> 18           42                  Transit Bus          2
#> 19           42                  Transit Bus          3
#> 20           43                   School Bus          1
#> 21           43                   School Bus          2
#> 22           43                   School Bus          3
#> 23           51                 Refuse Truck          1
#> 24           51                 Refuse Truck          2
#> 25           51                 Refuse Truck          3
#> 26           52 Single Unit Short-haul Truck          1
#> 27           52 Single Unit Short-haul Truck          2
#> 28           52 Single Unit Short-haul Truck          3
#> 29           53  Single Unit Long-haul Truck          1
#> 30           53  Single Unit Long-haul Truck          2
#> 31           53  Single Unit Long-haul Truck          3
#> 32           54                   Motor Home          1
#> 33           54                   Motor Home          2
#> 34           61 Combination Short-haul Truck          1
#> 35           61 Combination Short-haul Truck          2
#> 36           61 Combination Short-haul Truck          3
#> 37           62  Combination Long-haul Truck          2
#> 38           62  Combination Long-haul Truck          3
#>                    fuelTypeDesc
#> 1                      Gasoline
#> 2                      Gasoline
#> 3                   Diesel Fuel
#> 4                Ethanol (E-85)
#> 5                   Electricity
#> 6                      Gasoline
#> 7                   Diesel Fuel
#> 8                Ethanol (E-85)
#> 9                   Electricity
#> 10                     Gasoline
#> 11                  Diesel Fuel
#> 12               Ethanol (E-85)
#> 13                  Electricity
#> 14                     Gasoline
#> 15                  Diesel Fuel
#> 16 Compressed Natural Gas (CNG)
#> 17                     Gasoline
#> 18                  Diesel Fuel
#> 19 Compressed Natural Gas (CNG)
#> 20                     Gasoline
#> 21                  Diesel Fuel
#> 22 Compressed Natural Gas (CNG)
#> 23                     Gasoline
#> 24                  Diesel Fuel
#> 25 Compressed Natural Gas (CNG)
#> 26                     Gasoline
#> 27                  Diesel Fuel
#> 28 Compressed Natural Gas (CNG)
#> 29                     Gasoline
#> 30                  Diesel Fuel
#> 31 Compressed Natural Gas (CNG)
#> 32                     Gasoline
#> 33                  Diesel Fuel
#> 34                     Gasoline
#> 35                  Diesel Fuel
#> 36 Compressed Natural Gas (CNG)
#> 37                  Diesel Fuel
#> 38 Compressed Natural Gas (CNG)
x$runs[, c("masterVersion", "defaultDatabaseUsed", "massUnits", "distanceUnits")]
#>   masterVersion defaultDatabaseUsed massUnits distanceUnits
#> 1    MOVES6.0.0     movesdb20260729         g            mi
unique(x$rates[, c("yearID", "hourID", "temperature_C", "relHumidity")])
#>     yearID hourID temperature_C relHumidity
#> 1     2025      1            20          50
#> 481   2025      2            10          50
#> 961   2025      3            30          50
range(x$rates$modelYearID)
#> [1] 2000 2025

Select a vehicle and fuel by name or numeric ID. This example is a model-year 2020 diesel combination long-haul truck:

ef_moves(
  pollutant = "NOx",
  source_type_id = "Combination Long-haul Truck",
  fuel_type_id = "Diesel Fuel",
  model_year = 2020,
  speed = c(20, 40, 60),
  temperature = 20
)
#> Units: [g/km]
#> [1] 6.788542 3.318675 2.208248

Here is NOx from a model-year 2020 gasoline passenger car at the hour-1 reference conditions. Numeric input speeds use km/h; results have units of g/km.

ef_moves("NOx", model_year = 2020, speed = c(20, 40, 60),
         factors = x, select = list(hourID = 1))
#> Units: [g/km]
#> [1] 0.011671836 0.010784581 0.009399917

temperature (degrees C) and rel_humidity (percent) can select meteorological points directly. Any remaining varying dimensions must be selected explicitly using select. The function never averages together different scenarios, model years, fuels, road types or temperatures.

The reference fleet and fuel assumptions describe the U.S. scenario recorded in the dataset. Application to Ecuador requires a suitable vehicle/technology mapping and local fuel, meteorology, road load and inspection assumptions. The input metadata and production notes are installed with the data:

system.file("extdata", "moves", "README.md", package = "vein")
#> [1] "/tmp/RtmpeJi6yk/Rinstbc7400b3bad/vein/extdata/moves/README.md"

Speed functions for VEIN inventories

Omitting speed returns a function of speed giving numeric g/km. Build one function per model year in the same order as the columns of the vehicle matrix.

years <- c(2020, 2010)
curves <- EmissionFactorsList(lapply(years, function(y) {
  ef_moves("NOx", model_year = y, factors = x, select = list(hourID = 1))
}))
veh <- data.frame(MY2020 = c(100, 200), MY2010 = c(30, 50))
lengths <- units::set_units(c(0.5, 1), "km")
speeds <- data.frame(H1 = c(30, 50))
emissions <- emis(veh, lengths, ef = curves, speed = speeds,
                  profile = matrix(1, 1, 1), simplify = TRUE, verbose = FALSE)
emissions
#> This EmissionsArray has
#>  2 streets
#>  2 vehicle categories
#>  1 hours
#> , , 1
#> 
#>           [,1]      [,2]
#> [1,] 0.5585133 0.4211443
#> [2,] 1.9508197 1.2258597

Native bins and interpolation

The default method = "bin" selects the exported MOVES rate for the corresponding speed bin. It preserves valid zeros and fails if a requested bin is absent. Positive speeds up to 75 mph (120.7008 km/h) are accepted. Stationary idle needs an hour-based factor and matching activity; a distance factor cannot describe it.

With method = "linear", factors are interpolated between nominal MOVES bin speeds: 2.5, 5, 10, …, 75 mph. Both neighboring bins must exist. Extrapolation is disabled. This is an approximation between exported rates, so speed-bin lookup is the appropriate method for exact reproduction of the reference table.

f <- ef_moves("NOx", model_year = 2020, factors = x,
              select = list(hourID = 1), method = "linear")
f(c(20, 40, 60))
#> [1] 0.011547791 0.010795777 0.009323479
attr(f, "moves_metadata")$scenario
#>           MOVESScenarioID MOVESRunID yearID monthID dayID hourID pollutantID
#> 76178 vein-reference-2025          4   2025       1     2      1           3
#>       processID sourceTypeID regClassID SCC fuelTypeID modelYearID roadTypeID
#> 76178         1           21          0  NA          1        2020          5
#>       temperature relHumidity temperature_C  locationID
#> 76178          68          50            20 zone:261610

Meteorology, model year and vehicle category are selected exactly; this first version does not interpolate those dimensions.

Start and evaporative factors

Start exhaust is grams per engine start, not g/km. Use the separate start data and function; there is deliberately no speed argument:

starts <- moves_start_factors()
#> Warning in require_bit64_if_needed(ans): Some columns are type 'integer64' but
#> package bit64 is not installed. Those columns will print as strange looking
#> floating point data. There is no need to reload the data. Simply
#> install.packages('bit64') to obtain the integer64 print method and print the
#> data again.
ef_moves_start(
  "NOx", model_year = 2020,
  source_type_id = "Passenger Car", fuel_type_id = "Gasoline",
  select = list(hourID = 1)
)
#>       MOVESScenarioID MOVESRunID yearID monthID dayID hourID zoneID
#> 4763 vein-events-2025          1   2025       1     2      1 261610
#>      sourceTypeID regClassID SCC fuelTypeID modelYearID pollutantID processID
#> 4763           21          0  NA          1        2020           3         2
#>      temperature relHumidity ratePerStart gperstart temperature_C basis
#> 4763          68          50      0.12535   0.12535            20 start

The evaporative data contain THC permeation (process 11), fuel vapor venting (process 12) and fuel leaks (process 13) of parked vehicles. All three are grams per vehicle in the hour: MOVES divides each hour’s emissions by the vehicle population. MOVES writes venting to a separate table (RatePerProfile) labelled with its temperature-profile ID, but its rate has the same basis.

evap <- moves_evap_factors()
#> Warning in require_bit64_if_needed(ans): Some columns are type 'integer64' but
#> package bit64 is not installed. Those columns will print as strange looking
#> floating point data. There is no need to reload the data. Simply
#> install.packages('bit64') to obtain the integer64 print method and print the
#> data again.
#> Warning in require_bit64_if_needed(ans): Some columns are type 'integer64' but
#> package bit64 is not installed. Those columns will print as strange looking
#> floating point data. There is no need to reload the data. Simply
#> install.packages('bit64') to obtain the integer64 print method and print the
#> data again.
#> Warning in require_bit64_if_needed(ans): Some columns are type 'integer64' but
#> package bit64 is not installed. Those columns will print as strange looking
#> floating point data. There is no need to reload the data. Simply
#> install.packages('bit64') to obtain the integer64 print method and print the
#> data again.
ef_moves_evap(
  "THC", model_year = 2020, process = "all",
  source_type_id = "Passenger Car", fuel_type_id = "Gasoline",
  select = list(hourID = 1)
)
#>       MOVESScenarioID MOVESRunID yearID monthID dayID hourID zoneID pollutantID
#> 837  vein-events-2025          1   2025       1     2      1 261610           1
#> 1899 vein-events-2025          1   2025      NA     2      1     NA           1
#> 2961 vein-events-2025          1   2025       1     2      1 261610           1
#>      processID sourceTypeID regClassID SCC fuelTypeID modelYearID temperature
#> 837         11           21          0  NA          1        2020          68
#> 1899        12           21          0  NA          1        2020          68
#> 2961        13           21          0  NA          1        2020          68
#>      relHumidity ratePerVehicle gpervehicle temperature_C temperatureProfileID
#> 837           50     0.00734352  0.00734352            20                   NA
#> 1899          50     0.00264464  0.00264464            20        1.292525e-314
#> 2961          50     0.00728413  0.00728413            20                   NA
#>        basis
#> 837  vehicle
#> 1899 profile
#> 2961 vehicle

These event-based factors cannot be passed to emis(), whose factors are g/km. Multiply start factors by the number of starts, and evaporative factors by the number of vehicles in each hour. Start and evaporative data are separate MOVES runs; their provenance is recorded in movesrun_events.csv.

MOVES builds vapor venting only from the hours selected in a run: a vehicle whose cold soak started outside those hours contributes nothing. The bundled venting comes from a three-hour run with artificial temperatures and is illustrative only; generate full-day factors (all 24 hours) for inventories.

Brake, tire and evaporative emissions while driving

Brakewear (process 9), tirewear (10) and evaporative emissions while driving (11–13) are also distance factors in MOVES RatePerDistance. Select them with process; PM2.5 and PM10 then refer to brake or tire particles (pollutants 116/106 and 117/107). The bundled reference set has running exhaust only, so this example needs factors exported by movesr or MOVES:

x <- moves_factors("factors/rateperdistance.csv", "factors/movesrun.csv",
                   "factors/vehicles.csv")
ef_moves("PM2.5", model_year = 2020, speed = 50, factors = x,
         process = "brake", select = list(hourID = 16))
ef_moves("THC", model_year = 2020, speed = 50, factors = x,
         process = "venting", select = list(hourID = 16))

Generate factors with movesr

movesr reproduces MOVES6 rates in R (running and start exhaust, brake and tire wear, evaporative permeation, vapor venting and leaks, HC species, CO2, SO2 and PM species) from the MOVES default database stored as Parquet, without Java, MariaDB or MOVES. It reproduces the running and start factors bundled with VEIN. Local weather, fuel supply, barometric pressure and I/M program can replace the U.S. county defaults.

# remotes::install_github("atmoschem/movesr")
library(movesr)
db <- movesr_database("movesdb-parquet")
met <- movesr_default_meteorology(db, county_id = 11001, month_id = 7)
running <- movesr_running_rates(db, county_id = 11001, month_id = 7,
                                meteorology = met, pollutant_id = c(2, 3, 87))
start <- movesr_start_rates(db, county_id = 11001, month_id = 7,
                            meteorology = met, pollutant_id = c(2, 3, 87))
evap <- movesr_evap(db, county_id = 11001, month_id = 7)
movesr_export_vein("factors", running = running, start = start, evap = evap,
                   database = db)

x <- vein::moves_factors("factors/rateperdistance.csv", "factors/movesrun.csv",
                         "factors/vehicles.csv")
s <- vein::moves_start_factors("factors/rateperstart.csv",
                               "factors/movesrun_events.csv",
                               "factors/vehicles.csv")
e <- vein::moves_evap_factors("factors/ratepervehicle.csv",
                              "factors/rateperprofile.csv",
                              "factors/movesrun_events.csv",
                              "factors/vehicles.csv")

Supply additional local data

Data producers can run MOVES (or movesr) once and export RatePerDistance and MOVESRun tables. R users only need those local files or a saved RDS object. The importer reads units from MOVESRun, preserves all original rate-table dimensions, converts pollutant mass rates to g/km, and checks for duplicate keys and missing provenance. It supports plain or gzip-compressed CSV and TSV files.

x <- moves_factors("my_rateperdistance.csv", "my_movesrun.csv")
saveRDS(x, "my_moves_factors.rds")

# This file can be copied to an R installation on another OS.
x <- readRDS("my_moves_factors.rds")
f <- ef_moves("NOx", model_year = 2020, factors = x,
              select = list(MOVESRunID = 1, yearID = 2025, hourID = 1))

This interface accepts the distance-based processes (running exhaust 1, brakewear 9, tirewear 10 and evaporative 11–13 while driving), road types 2–5, and pollutant mass rates. Starts and parked evaporative emissions have their own functions; stationary idle and energy need their own rate bases and activity denominators. MOVES base operating-mode rates also need activity weighting and adjustments before they become distance-based factors.

EPA describes external use of rate tables in section 4.2.2 of the MOVES6 technical guidance.