Transportation GFMs
The Transportation Gap Filling Modules calculate greenhouse gas emissions from transporting food products from origin to destination. These two modules work together: transportation_mode_distance_gfm calculates distances and route options, while transportation_decision_gfm selects the optimal transport mode and generates emission flows.
Quick Reference
| Property | TransportModeDistanceGFM | TransportDecisionGFM |
|---|---|---|
| Runs on | FoodProductFlowNode, FlowNode | Nodes with transport attribute |
| Dependencies | OriginGFM, MatchProductNameGFM, UnitWeightConversionGFM, ConservationGFM | TransportModeDistanceGFM |
| Key Input | Origin location, destination location | Transport distances, product perishability |
| Output | Distances and CO2 values per transport mode | Optimal transport mode, carriage flow nodes |
| External API | EcoTransit | None |
When They Run
TransportModeDistanceGFM triggers when:
- A
FoodProductFlowNodehas a valid origin location - The parent node has a valid destination location
- The product is not marked as a subdivision
- No transport mode has already been specified
TransportDecisionGFM triggers when:
- Transport distances have been calculated by TransportModeDistanceGFM
- At least one valid transport distance exists (non-zero)
- The node has a child node for pre-transported product
Key Output
The modules add multiple components to the calculation graph:
- Pre-carriage: Road transport from origin to departure port/airport
- Main carriage: Primary transport via selected mode (road, sea, air, rail)
- Post-carriage: Road transport from arrival port/airport to destination
- Cooling/Freezing: Additional emissions for temperature-controlled transport
- Infrastructure: Transport infrastructure emissions per ton-kilometer
Scientific Methodology
The transport model calculates emissions using a three-stage carriage approach with EcoTransit for route planning and emission factors.
Transport Mode Options
| Mode | EcoTransit Code | Travel Speed | Loading Time | Price per km | Loading Price |
|---|---|---|---|---|---|
| Road | road | 45 km/h | 3 hours | $0.0497/km | $84.62 |
| Sea | sea | 26 km/h | 48 hours | $0.0062/km | $119.66 |
| Air | air | 500 km/h | 6 hours | $0.0062/km | $2,085.00 |
| Rail | rail | 40 km/h | 24 hours | $0.0186/km | $99.58 |
Distance Calculation Methodology
The transport model uses a hierarchical approach to calculate distances:
EcoTransit Integration
For each transport mode, the system queries EcoTransit using the CCWG-Tradelane methodology (Clean Cargo Working Group):
ECOTRANSIT_CALCULATION_METHOD = "ccwg-tradelane"
ECOTRANSIT_CALCULATION_YEAR = 2024
Request Parameters:
- Departure coordinates (origin centroid or precise location)
- Destination coordinates (destination centroid or precise location)
- Transport mode (road, sea, air, rail)
- Cargo weight: 1 ton (emissions normalized per ton)
Location Resolution with GADM
Locations are resolved using the GADM (Global Administrative Areas Database):
- GADM Level 1-2 (Country/Region): Uses centroid coordinates for EcoTransit queries
- GADM Level 3+ (Detailed): Falls back to level 2 parent region centroid
- Caching Strategy: Region-to-region results are cached; country-to-country serves as fallback
# Cache key format
request_key = f"{start_location_gadm_term.xid}_{end_location_gadm_term.xid}_{mode.value}"
Pre/Post Carriage Distance Estimation
For sea and air transport, pre- and post-carriage distances (road transport to/from ports) are estimated using:
# For same-region transport (origin and destination in same GADM region)
estimated_distance = ribbon_factor * direct_distance
# For different regions
estimated_distance = max(
sqrt(region_area_km2) * scaling_factor, # Area-based estimate
distance_from_centroid_to_port # Centroid-based estimate
) * ribbon_factor
Scaling Factors by Transport Mode:
| Mode | Pre/Post Carriage Scaling Factor | Description |
|---|---|---|
| Sea | 0.29042 | sqrt(area)/2.43 |
| Air | 0.11782 | sqrt(area)/5.99 |
| Road (same region) | 0.56905 | For local transport |
Ribbon Factor (Detour Index):
The ribbon factor of 1.417 converts straight-line distances to realistic road distances, based on research from Ballou et al. (2002).
CO2 Emission Calculation
CO2 emissions are extracted directly from EcoTransit responses in tons CO2eq per ton of cargo:
# For each carriage segment
co2_value = section["emissions"]["co2Equivalents"][mode]["_value_1"]
distance = section["emissions"]["distances"][mode]["tankToWheel"]
# Pre/post carriage CO2 is scaled based on estimated distance
scaled_co2 = ecotransit_co2 * (estimated_distance / ecotransit_distance)
Transport Mode Selection
The TransportDecisionGFM selects the optimal mode by:
- Filter by Perishability: Exclude modes where travel time exceeds product storage time
- Calculate Total Cost: Sum distance costs and loading costs for each mode
- Select Cheapest: Choose the mode with the lowest total cost
# Total travel time calculation
travel_time = distance / travel_speed + loading_time
# Total cost calculation
total_cost = (distance_price * distance) + loading_price
Perishability Constraints
Products with storage time constraints (cooled, frozen) are filtered to modes that can deliver within the storage window:
# Check if mode qualifies
storage_time_hours = product.storage_time * time_conversion_factor
if travel_time < storage_time_hours:
qualifying_modes.append(mode)
If no mode qualifies within the storage constraint, the fastest mode is selected as a fallback.
Implementation Details
Default Transport Distances
When origin cannot be determined (unknown production FAO code), default distances based on Argentina to Germany via Portugal are used:
| Carriage | Distance (km) | CO2 (t/t) |
|---|---|---|
| Pre-carriage (Argentina centroid to port) | 666.02 | 0.0952 |
| Main carriage (Argentina port to Portugal port) | 11,428.68 | 0.1202 |
| Post-carriage (Portugal port to Germany centroid) | 2,569.74 | 0.1908 |
| Total | 14,664.44 | 0.4062 |
Non-Food Product Handling
Non-food products (identified by FoodEx2 terms EAT-0002 and EAT-0000) are excluded from transport calculations.
Same-Location Transport
When origin and destination are the same GADM region:
- Apply a 50km offset to origin and destination coordinates
- Force road transport mode
- Calculate distance using the ribbon factor
if start_location_gadm_term == end_location_gadm_term:
start_location = add_offset_to_endpoint(start_lon, start_lat, -50_000)
end_location = add_offset_to_endpoint(end_lon, end_lat, 50_000)
pre_defined_mode = ServiceTransportModeEnum.GROUND
Graph Mutations
The TransportDecisionGFM creates the following graph structure:
FoodProductFlowNode (ingredient)
|
+-- FoodProcessingActivityNode (transport activity)
|
+-- FoodProductFlowNode (pre-transported product)
| |
| +-- [original child nodes]
|
+-- PracticeFlowNode (Pre-carriage)
| +-- TransportActivityNode
| +-- FlowNode (Fuel consumption)
| +-- PracticeFlowNode (Infrastructure)
|
+-- PracticeFlowNode (Main carriage)
| +-- TransportActivityNode
| +-- FlowNode (Fuel consumption)
| +-- PracticeFlowNode (Infrastructure)
|
+-- PracticeFlowNode (Post-carriage)
| +-- TransportActivityNode
| +-- FlowNode (Fuel consumption)
| +-- PracticeFlowNode (Infrastructure)
|
+-- PracticeFlowNode (Cooling/Freezing) [optional]
Cooling and Freezing During Transport
For products tagged as cooled (J0131) or frozen (J0136), additional cooling emissions are added:
# Calculate travel time for selected mode
travel_time_hours = total_distance / travel_speed + loading_time
# Create cooling/freezing flow node with kg*hour amount
cooling_flow_node.amount = travel_time_hours # in kg*hour unit
Full Code Reference
EcoTransit Request Structure
complex_transport_request = {
"cargo": Cargo(weight=1, unit="ton"),
"transportDate": None, # Dates omitted to avoid temporal variations
"accountingVariant": AccountingVariantParameters(
variant="ccwg-tradelane",
year=2024,
),
"section": {
"route": {
"departure": RequestStation(
wgs84=Wgs84Coordinates(departure_longitude, departure_latitude)
),
"destination": RequestStation(
wgs84=Wgs84Coordinates(destination_longitude, destination_latitude)
),
},
"carriage": {
"preCarriage": PreOrPostCarriageParameters(road=RoadParameters()),
"mainCarriage": CarriageParameters(mode_params),
"postCarriage": PreOrPostCarriageParameters(road=RoadParameters()),
},
},
"output": Output(split=OutputSplit(), createKml=False),
}
Response Processing
The EcoTransit response is stripped to essential data for caching:
def strip_ecotransit_response(mode: str, response: dict) -> dict:
return {
"mainhaul": {
mode: [
{
"co2eq_value": section["emissions"]["co2Equivalents"][mode]["_value_1"],
"distance": section["emissions"]["distances"][mode]["tankToWheel"],
}
for section in mainhaul_sections
]
},
"mainhaul_start_location": response["mainhaul"][0]["route"]["startLocation"]["wgs84"],
"mainhaul_end_location": response["mainhaul"][-1]["route"]["endLocation"]["wgs84"],
"preCarriage": {ServiceTransportModeEnum.GROUND: [...]},
"postCarriage": {ServiceTransportModeEnum.GROUND: [...]},
}
Cost Calculation
async def get_minimal_cost_transportation_mode(
transport_modes_distances: TransportModesDistancesProp,
qualified_travel_modes: list,
) -> ServiceTransportModeEnum:
mile_to_km_constant = 0.621371192
# Prices per mile converted to km
distance_prices = {
ServiceTransportModeEnum.AIR: 0.01 * mile_to_km_constant,
ServiceTransportModeEnum.GROUND: 0.08 * mile_to_km_constant,
ServiceTransportModeEnum.SEA: 0.01 * mile_to_km_constant,
ServiceTransportModeEnum.TRAIN: 0.03 * mile_to_km_constant,
}
loading_prices = {
ServiceTransportModeEnum.AIR: 2085.0,
ServiceTransportModeEnum.GROUND: 84.62,
ServiceTransportModeEnum.SEA: 119.6646,
ServiceTransportModeEnum.TRAIN: 99.578,
}
total_prices = {}
for mode in qualified_travel_modes:
total_price = 0
# Add pre/post carriage costs (always road)
for carriage in ("pre_carriage", "post_carriage"):
if getattr(distances[mode], carriage).value > 0:
total_price += (
distance_prices[ServiceTransportModeEnum.GROUND]
* getattr(distances[mode], carriage).value
+ loading_prices[ServiceTransportModeEnum.GROUND]
)
# Add main carriage cost
total_price += (
distance_prices[mode] * distances[mode].main_carriage.value
+ loading_prices[mode]
)
total_prices[mode] = total_price
return min(total_prices, key=total_prices.get)
Travel Time Calculation
@staticmethod
def get_travel_time(distance: float, mode: str) -> float:
travel_speed = { # km/h
ServiceTransportModeEnum.AIR: 500,
ServiceTransportModeEnum.GROUND: 45,
ServiceTransportModeEnum.SEA: 26,
ServiceTransportModeEnum.TRAIN: 40,
}
travel_loading_time = { # hours
ServiceTransportModeEnum.AIR: 6,
ServiceTransportModeEnum.GROUND: 3,
ServiceTransportModeEnum.SEA: 48,
ServiceTransportModeEnum.TRAIN: 24,
}
return distance / travel_speed[mode] + travel_loading_time[mode]
Data Sources
EcoTransit API
EcoTransit World provides freight transport emission calculations:
- Methodology: GLEC Framework (Global Logistics Emissions Council)
- Calculation variant: CCWG-Tradelane (Clean Cargo Working Group)
- Coverage: Global freight routes for road, rail, sea, and air
- Data year: 2024 (configurable)
The CCWG methodology adds a 15% correction factor to sea distances, as the shortest route distance typically underestimates actual distances traveled.
GADM Geographic Database
GADM provides geographic boundaries and centroids:
- Country centroids for level 1
- Regional centroids for level 2
- Fallback from level 3+ to level 2 parent
FAO Codes for Special Cases
| FAO Code | Description | Transport Behavior |
|---|---|---|
100000 | Local production | No transport emissions |
200000 | Default production | Default distances used |
300000 | Unknown fish production | Default distances used |
400000 | Failed production matching | Default distances used |
Calculation Example
Scenario: 1 kg of oranges from Spain (Valencia) to Switzerland (Zurich)
Step 1: Resolve Locations
- Origin: Spain, Valencia region (GADM: ES.17_1)
- Centroid: -0.4, 39.5
- Destination: Switzerland, Zurich (GADM: CH.26_1)
- Centroid: 8.5, 47.4
Step 2: Query EcoTransit
Queries made for road, sea, and air modes:
Road Response:
{
"mainhaul": {
"road": [
{"distance": 1284.3, "co2eq_value": 0.1156}
]
}
}
Sea Response:
{
"mainhaul": {
"sea": [
{"distance": 2156.7, "co2eq_value": 0.0345}
]
},
"preCarriage": {"road": [{"distance": 312.4, "co2eq_value": 0.0281}]},
"postCarriage": {"road": [{"distance": 428.6, "co2eq_value": 0.0386}]}
}
Step 3: Calculate Transport Options
| Mode | Pre-carriage | Main carriage | Post-carriage | Total Distance | Total CO2 |
|---|---|---|---|---|---|
| Road | 0 km | 1,284.3 km | 0 km | 1,284.3 km | 0.1156 t |
| Sea | 312.4 km | 2,156.7 km | 428.6 km | 2,897.7 km | 0.1012 t |
Step 4: Calculate Costs
Road:
Cost = (1284.3 * 0.0497) + 84.62 = $148.45
Sea:
Pre-carriage: (312.4 * 0.0497) + 84.62 = $100.14
Main carriage: (2156.7 * 0.0062) + 119.66 = $133.03
Post-carriage: (428.6 * 0.0497) + 84.62 = $105.92
Total: $339.09
Step 5: Select Mode
Road is selected as the cheapest viable option ($148.45 vs $339.09).
Step 6: Create Graph Nodes
- Transport activity node created
- Main carriage flow node: 1,284.3 km road transport
- Fuel consumption node: 0.1156 t CO2eq
- Infrastructure node: 1.0 ton-km
Final Output
The module adds emissions of 0.1156 kg CO2eq per kg of oranges for transport.
Caching Strategy
Cache Levels
- Region-to-region cache: Full EcoTransit responses cached by GADM term pairs
- Country-to-country fallback: Used when region cache miss occurs
- Stripped response cache: Minimal data for quick lookups (loaded on boot)
# Full response cache (not loaded on boot)
await cache.set(request_key, response_data, load_on_boot=False)
# Stripped response cache (loaded on boot)
await cache.set(request_key + "_stripped", stripped_data, load_on_boot=True)
Background Calculation
When a cache miss occurs:
- Background task started for region-to-region calculation
- Check for country-to-country cache hit
- Use country fallback if available, otherwise wait for region calculation
# Start background calculation
region_calc_task = asyncio.create_task(
get_transport_mode_responses_cached(origin, destination, mode, cache_only=False)
)
# Check country fallback
country_result = await get_transport_mode_responses_cached(
origin_country, destination_country, mode, cache_only=True
)
Known Limitations
Geographic Coverage
- Depends on EcoTransit route database coverage
- Some remote locations may not have valid routes
- Sea routes require valid port connections
Mode Coverage
- Rail: Currently functional but less prioritized in cost model
- Inland waterways: Available in EcoTransit but not yet implemented
- Ferry: Handled as road transport (no distinction)
Model Assumptions
- Transport date not specified to avoid temporal variations in emission factors
- Load factor assumed at 60% (EcoTransit default)
- All pre/post carriage assumed to be road transport
- Same infrastructure mix used globally
Perishability Constraints
- Only applies when storage time tag is present on product
- Fastest mode selected as fallback when no mode meets constraints
- May result in air transport for highly perishable items
Data Freshness
- EcoTransit emission factors updated annually
- GADM boundaries may not reflect recent administrative changes
- Transport prices in cost model may not reflect current market rates
References
-
EcoTransit World. Environmental methodology and data update. https://www.ecotransit.org/
-
Smart Freight Centre (2019). Global Logistics Emissions Council Framework for Logistics Emissions Accounting and Reporting. https://www.smartfreightcentre.org/en/our-programs/global-logistics-emissions-council/
-
Clean Cargo Working Group. Container shipping methodology for calculating emissions. https://www.smartfreightcentre.org/en/our-programs/clean-cargo-1/
-
Ballou, R.H., Rahardja, H., & Sakai, N. (2002). Selected country circuity factors for road travel distance estimation. Transportation Research Part A, 36(9), 843-848.
-
GADM Database. Global Administrative Areas. https://gadm.org/
-
FAO. Food and Agriculture Organization trade statistics. https://www.fao.org/faostat/