Input Assumptions for Modelling the Transport Sector in the negaWatt-BE Scenario
ENERGY DEMAND FOR THE TRANSPORT SECTOR IN BELGIUM
Developer: LATERRE Antoine, BERNAERTS Valentine, QUOILIN Sylvain, MEYER Sébastien
To ensure transparency and consistency within the negaWatt-BE project, this notebook documents all key input assumptions related to the transport sector. It computes the energy demand of passengers and freight transport under sufficiency and efficiency measures over the energy transition period (2020–2050). Projections are based on statistical data from the starting reference year, and the resulting energy demand values are directly used as inputs for the PyPSA model.
Note: The reference year is 2019 instead of 2020, due to the disruptions caused by the COVID-19 pandemic in 2020. This choice ensures greater consistency and realism in the baseline and future projections.
- Introduction
- 1.1. Sufficiency vs Efficiency
- 1.2. Global Demands
- 1.2.1. Passenger Mobility
- 1.2.2. Freight Transport
- 1.2.3. Maritime Bunkers
- Passenger Mobility
- 2.1. Modal Shares
- 2.2. Carriers Shares
- 2.3. Final Energy Consumption
- Freight Transport
- 3.1. Modal Shares
- 3.2. Carriers Shares
- 3.3. Final Energy Consumption
- Maritime Bunkers
- Total Final Energy Consumption
- Short List of Sufficiency Assumptions
- [1] Rozsai, Mate; Jaxa-Rozen, Marc; Salvucci, Raffaele; Sikora, Przemyslaw; Gea Bermudez, Juan; Neuwahl, Frederik (2025). JRC-IDEES-2023. European Commission, Joint Research Centre (JRC). [Dataset].
PID: http://data.europa.eu/89h/1f0b480c-6d21-4d95-897d-20c7ca33df6f - [2] Rozsai, Mate; Jaxa-Rozen, Marc; Salvucci, Raffaele; Sikora, Przemyslaw; Tattini, Jacopo; Neuwahl, Frederik (2024). JRC-IDEES-2021: the Integrated Database of the European Energy System – Data update and technical documentation, Publications Office of the European Union, Luxembourg, 2024. [Report].
DOI: https://doi.org/10.2760/614599 - [3] Service public fédéral Mobilité et Transports (2019). Enquête monitor sur la mobilité des Belges. Bruxelles: SPF Mobilité et Transports [Report].
Retrieved from: https://mobilit.belgium.be/fr/publications/enquete-monitor-sur-la-mobilite-des-belges
- [4] Service public fédéral Mobilité et Transports (2025). Enquête fédérale sur la mobilité en Belgique. Bruxelles: SPF Mobilité et Transports [Report].
Retrieved from: https://mobilit.belgium.be/fr/publications/enquete-federale-sur-la-mobilite-en-belgique
- [5] Bureau fédéral du Plan (2022). Perspectives de la demande de transport à l'horizon 2040. Bruxelles: BFP [Report].
Retrieved from: https://www.plan.be/fr/publications/perspectives-de-la-demande-de-transport-en
- [6] Association négaWatt (2021). Scénario négaWatt 2022-2050 [Report].
Availale at: https://www.negawatt.org/Scenario-negaWatt-2022
- [7] Service public fédéral Mobilité et Transports (2022). Vision Rail 2040 – Le rail : la colonne vertébrale de la mobilité en Belgique. Bruxelles: SPF Mobilité et Transports [Report].
Retrieved from: https://mobilit.belgium.be/fr/publications/le-rail-la-colonne-vertebrale-de-la-mobilite-en-belgique
- [8] Transport & Mobility Leuven (2020). Elektrificatie van het Belgische spoorwegnet of het gebruik van andere duurzamere vervoerswijzen om de dieseltractie te vervangen. Leuven: Christophe Heyndrickx, Sebastiaan Boschmans [Report].
Retrieved from: https://mobilit.belgium.be/sites/default/files/publicaties%20en%20statistieken/studie_nl%20(1).pdf
- [9] Cheimariotis, Ilias (2025). Decarbonising European heavy-duty transport - A stakeholder analysis of technology readiness and future R&I priorities for zero-emission vehicles and infrastructure, Publications Office of the European Union, Luxembourg, 2025. [Report].
URI: https://publications.jrc.ec.europa.eu/repository/handle/JRC143054 - [10] DNV (2024). Maritime forecast to 2050: Energy transition outlook 2024. Hovik, Norway: DNV AS [Report].
Retrieved from: https://www.isesassociation.com/wp-content/uploads/2024/08/DNV_Maritime_Forecast_2050_2024-final-3.pdf
Contacts and experts in the field
- Christophe Pauwels (christophe.pauwels@mobilit.fgov.be): person in charge of collecting and managing data related to mobility at the Belgian Federal Public Service for Mobility and Transport (enquetewwv@mobilit.fgov.be, stats.enquetes@mobilit.fgov.be), section Sustainable Mobility and Rail.
# Automatically reload the file if it is modified:
%load_ext autoreload
%autoreload 2
# Load the macro parameters and necessary packages:
%run ./nW_BE_demand_model_macro.ipynb
# Option to print and plot the results:
post_process = True
The autoreload extension is already loaded. To reload it, use: %reload_ext autoreload
The method used to generate the energy demand projections for the transport sector follows the approach of the Joint Research Centre (JRC) of the European Commission for building the Integrated Database of the European Energy System (IDEES) [1,2]. This database compiles statistics related to passenger transport, freight transport, and maritime bunkers.
1.1. Sufficiency vs Efficiency
To reduce primary energy consumption in the transport sector, the negaWatt-BE approach relies on two complementary strategies: energy sufficiency, acting on usefull energy demand, and energy efficiency, acting on conversion losses. These strategies are implemented through several levers, presented below, which guide the transition towards a low-energy transport system.
- Energy Sufficiency refers to a reduction in the demand for energy-intensive activities. It is achieved through behavioral and systemic changes that reduce their usage, including modal shifts.
- Energy Efficiency refers to technical improvements that reduce the amount of primary energy consumed per kilometer travelled or ton transported, without reducing the final transport demand.
This section defines the demand in terms of final services. For passenger mobility, this corresponds to the equivalent annual distance travelled by the population. For freight transport and maritime bunkers, it corresponds to the equivalent annual distance travelled by all transported goods.
Passenger mobility is measured by the total annual distance travelled, representing the total number of kilometers covered each year by the population across all transport modes. In this study, only navigation is excluded, as it is negligible in Belgium. The passenger mobility is expressed in passenger-kilometers [pkm] for each year and includes commuting, leisure, and other mobility needs. The passenger mobility intensity reports the passenger mobility over the population and is expressed in passenger-kilometers per person [pkm/person].
To generate passenger mobility statistics for the reference year (2019), we rely on two main sources: (i) the JRC-IDEES database, whose most recent figures date from 2023 [1,2], and (ii) the two most recent Belgian Federal Mobility Surveys, conducted in 2017 and over the period 2024–2025 [3,4]. These two sources are complementary, as the former excludes data related to active modes (walking, cycling), while the latter focuses on daily commuting within Belgium and excludes extra-territorial trips, aviation being the main representative. Although not strictly required to assess the final energy consumption of the transport sector, gentle active modes are key levers in a transition based on sufficiency. As they help illustrate their contribution to reducing the sector's energy consumption, they are therefore included in this notebook.
For 2019, JRC-IDEES [1,2] reports a value of 172,657 Gpkm covering mobility by road, rail and air. Based on reports from the Belgian Federal Public Service for Mobility and Transport [3,4], we estimate that in the same year additional contributions amounted at 7,000 Gpkm for cycling and 3,000 Gpkm for walking. The total therefore amounts at 182,657 Gpkm, which corresponds to an average total mobility intensity of 15.979 pkm/person.
# Inputs - Define Sufficiency Scenario Data (SUF)
# 2019 total = 172.65652 Gpkm from JRC-IDEES (road, rail, air) + 3.00000 walking + 5.04167
# cycling (section 2.1; the cycling value is the reconstruction of nW_BE_demand_data_aux.ipynb)
ref_PM_spe = 180.69819e+9/population_dict[2019] # [pkm/person]
Comment: Data from the surveys made by the Belgian Federal Public Service for Mobility and Transport [3,4] are exclusively used to assess the passenger-kilometers related to walking and cycling as proper commuting modes (not leisure, sport, etc.).
Comment: The Belgian Federal Public Service for Mobility and Transport plans on publishing modal passenger-kilometers for commuting by 2027 (cfr. christophe.pauwels@mobilit.fgov.be).
Sufficiency Assumption
Excluding gentle modes, from 2000 to 2019, the total mobility intensity has grown by about +780 pkm/person, which is equivalent +5.5% [1,2]. This is driven by aviation (intra and extra Europe), which has grown by about +1160 pkm/person (or +8.1% w.r.t. total mobility intensity), and also due to rail (passenger trains and urban light rails) which has grown by about +340 pkm/person/year (or +2.4% w.r.t. total mobility intensity). In the meantime, road (two-wheeelers, cars and buses) has decreased by about -720 pkm/person (or -5.0% w.r.t. total mobility intensity) [1,2]. For its part, the Federal Planning Bureau, excluding aviation but including active modes, estimates that from 1990 to 2019 the total mobility intensity has grown by about +1830 pkm/person [5].
Despite this increase in the mobility intensity for the past 30 years, we believe this trend can be reversed, notably because of the expansion of teleworking [5], and through a higher share of urban housing and a prioritisation of closer tourist destinations. Population ageing and increased transportation costs should also reduce the mobility intensity [5]. This reversed trend assumption is further supported by the fact that the Federal Planning Bureau estimates that, under unchanged policies and excluding aviation from the projection, mobility intensity could decrease by around -100 pkm/person by 2040 [5].
In light of this, we project a -10% reduction in global mobility intensity by 2050 w.r.t. the 2019 level. This -10% assumption leads to an estimated 14.227 pkm/person by 2050 (-1.581 pkm/person). We attribute a significant share of this reduction (around half of it) to a decrease in the number of long-haul flights, for which modal shift is hardly feasible and whose decarbonisation remains uncertain (limited availability of Sustainable Aviation Fuels (SAF), etc.). More details are provided below.
# Inputs - Define Sufficiency Scenario Data (SUF)
pro_PM_spe = -0.10
# Outputs - Sufficiency Scenario Data (SUF)
SUF_data = {"PM intensity [pkm/person]": linear_growth(2019, ref_PM_spe,
2050, ref_PM_spe*(1+pro_PM_spe), years)}
df_SUF = pd.DataFrame(SUF_data, index=years)
df_SUF["population [person]"] = df_SUF.index.map(population_dict)
df_SUF["PM total [Gpkm]"] = df_SUF["PM intensity [pkm/person]"]*df_SUF["population [person]"]*1e-9
Comment: This -10% assumption is further supported by the post-COVID-19 recovery of mobility: it took more than five years for the mobility intensity to return to its pre-crisis level (it had dropped by about -30% from 2019 to 2020) [1,2]. This moderate pace of recovery illustrates the feasibility of reducing the mobility intensity.
Comment: Our -10% reduction is a conservative assumption with respect to the négaWatt 2022-2050 scenario [6] for France, which assumes a 23% reduction.
/!\ Need to check if we can find this -23% in the nW scenario - haven't found it so far /!\
Comment: Should also further motivate the -10% drop: teleworking and urban housing are maybe not enough.
Comment: A recent review from Peeters et al. (UAntrwerpen, UGent & VUB, 2026, https://doi.org/10.1016/j.erss.2026.104580) investigated the minimum levels for a sustainable ground based passengers mobility. Their results first highlighted the lack of consensus on minimum values, as these depend heavily on the local context (urbanisation rate, population density, etc.). The proposed range is from 5.000 to 15.000 pkm per person per year. Our value of 12.653 pkm (total excluding aviation) is therefore close to the upper limit, which indicates the realistic nature of our assumption.
Freight transport is measured by the total annual distance over which goods are transported, representing the total number of kilometers covered each year by the mass of all goods across all transport modes. In this study, international transport through maritine bunkers is adressed in a separate section due to the disproportionate contribution of the Port of Antwerp compared with other activities in Belgium. This indicator is expressed in ton-kilometers [tkm].
To generate freight transport statistics for the reference year (2019), we rely on the JRC-IDEES database, whose most recent figures date from 2023 [1,2].
For 2019, JRC-IDEES [1,2] reports a value of 80,176 Gtkm covering transport by road, rail, air and domestic navigation. Reported to the population, this corresponds to an average total tansport intensity of 7.014 tkm/person.
# Inputs - Define Sufficiency Scenario Data (SUF)
ref_FT_spe = 80.17566e+9/population_dict[2019] # [tkm/person]
Sufficiency Assumption
From 2000 to 2019, the average tansport intensity increased by more than +25%, rising from 5.548 tkm/person in 2000 to 7.014 tkm/person in 2019 [1,2]. Although the 2008 economic crisis caused this value to drop to 5.703 tkm/person in 2009, the growth slowdown was later offset, such that the upward trend remained relatively steady overall between 2000 and 2016 (with a peak of 7.532 tkm/person reached in that year). Since then, the upward trend has levelled off, with the value fluctuating around 7.000 tkm/person/year.
In view of the current stabilisation of this metric, we consider it reasonable to project a slight decrease. This is motivated, on the one hand, by a reduction in the consumption of material goods and, on the other hand, by the relocation of certain manufacturing activities and the development of short supply chains and local consumption patterns (e.g. local agriculture, etc.).
In light of this, we project a -10% reduction in global transport intensity by 2050 w.r.t. the 2019 level. This -10% assumption leads to an estimated 6.312 tkm/person by 2050 (-701 tkm/person).
# Inputs - Define Sufficiency Scenario Data (SUF)
pro_FT_spe = -0.10
# Outputs - Sufficiency Scenario Data (SUF)
df_SUF["FT intensity [tkm/person]"] = linear_growth(2019, ref_FT_spe,
2050, ref_FT_spe*(1+pro_FT_spe), years)
df_SUF["FT total [Gtkm]"] = df_SUF["FT intensity [tkm/person]"]*df_SUF["population [person]"]*1e-9
Comment: Need to compare this projection with other scenarios.
Comment: Freight is usually reported to GWP instead of population [5]. Yet, GWP is out of the scope of this work. We hence stay with population.
Comment: Note that the value for 2023 is 6.455 tkm/person, which is -8% compared to 2019. The -10% is thus perfectly realistic.
This section defines the modal shares (type of transport mode) and carrier shares (type of powertrain) to allocate the Final Energy Consumption across the different transport modes and carriers.
This section defines the modal shares, which are expressed in percentages of total passenger-kilometers. Walking and cycling (gentle modes) are explicitly included in the distribution, as well as aviation.
The modal shares of the powered modes for the reference year (2019) are based on JRC-IDEES [1,2], while walking is based on the last two surveys from the Belgian Federal Public Service for Mobility and Transport [3,4], and cycling on a reconstruction from the regional surveys (Flanders, Brussels, Wallonia), scaled to the Federal Public Service's estimate for 2024-2025 — see nW_BE_demand_data_aux.ipynb, "Cycling, 2000-2024: a reconstruction". Note that domestic flights and navigation are excluded due to their negligible contribution.
# Inputs - Modal Repartition [Gpkm] for reference year (2019)
ref_PM_mod_abs = {
'pedestrian': 3.00000, # From Belgian Federal Public Service for Mobility and Transport
'bicycle': 5.04167, # Reconstruction from the regional surveys, nW_BE_demand_data_aux.ipynb
'two-wheeler': 1.75208, # From JRC-IDEES
'tram&metro': 1.47211, # From JRC-IDEES
'bus&coach': 13.68139, # From JRC-IDEES
'car': 107.44465, # From JRC-IDEES
'train-conventional': 10.43600, # From JRC-IDEES
'train-high speed': 1.56400, # From JRC-IDEES
'plane-intra EU': 15.59146, # From JRC-IDEES
'plane-extra EU': 20.71483, # From JRC-IDEES
}
ref_PM_abs = sum(ref_PM_mod_abs.values())
if abs((ref_PM_abs-df_SUF["PM total [Gpkm]"][2019])/df_SUF["PM total [Gpkm]"][2019]*100) > 1e-5:
print("Entry values in 'ref_PM_mod_abs' are not correct!")
# Outputs - Modal Shares [%] and Modal Repartition [pkm/person] for reference year (2019)
ref_PM_mod_rel = {k: v/ref_PM_abs*100 for k, v in ref_PM_mod_abs.items()} # [%]
ref_PM_mod_spe = {k: v/df_SUF["population [person]"][2019]*1e+9 for k, v in ref_PM_mod_abs.items()} # [pkm/person]
Comment: The Belgian Federal Public Service for Mobility and Transport gives 10,84900 Gtkm for train (https://mobilit.belgium.be/fr/mobilite-durable/enquetes-et-resultats/chiffres-cles-de-la-mobilite, from SNCB). This is quite similar to our JRC-IDEES value!
Comment: For 2017, the MONITOR survey from Belgian Federal Public Service for Mobility and Transport gives 107,033 Gpkm by car (= 35 km/day/person x (55% + 19% modal shares) x 365 days x 11.322.088 people) [3]. The same year, IDEES gives 106,940 Gpkm [1,2]. This is about -0.1% deviation, which is more than acceptable!
But regarding train, the MONITOR survey gives 17,357 Gpkm (= 35 km/day/person x 12% modal share x 365 days x 11.322.088 people) [3], while IDEES gives 10,364 Gpkm [1,2]. This is about -40% deviation. The origin of this discrepency should be further investigated!
Comment: The Belgian Federal Public Service for Mobility and Transport (christophe.pauwels@mobilit.fgov.be) has told us that estimates for 2024-2025 are 7,2 Gpkm for cycling and 4 Gpkm for walking. The 7,0 Gpkm of cycling used here for 2019 until October 2026 therefore described 2024 rather than 2019: every indicator grows by +30-55% between 2017-2019 and 2023-2024. Cycling in 2019 is now 5,04 Gpkm (1,21 km/person/day), a reconstruction from the regional surveys scaled to the 7,2 Gpkm of 2024-2025 (see
nW_BE_demand_data_aux.ipynb). Walking (3,0 Gpkm) still needs refining.
Comment: The Federal Planning Bureau estimates that 172,7 Gpkm were travelled in 2019 (excluding aviation) [5]. Of these, 5.7 Gpkm were travelled on foot and by bicycle.
Comment: The Federal Planning Bureau estimates a mobility intensity of 15.105 pkm/person in 2019 (excluding aviation) [5]. Accounting for aviation (3.176 pkm/person), this value is 2.302 pkm/person higher than ours (+18%).
Global Sufficiency Assumption
By 2019, 58,8% of all passenger-kilometres were made by car while 19,9% were by plane. The remaining 21,3% were shared among public transport, cycling, walking, and other modes. Cars (with an average occupancy rate of 1,20 [3,4] - 1,22 [1,2]) and aviation together thus accounted for nearly 80% of total passenger-kilometres in 2019. As these modes of transport are also the most energy-intensive and therefore the most greenhouse-gas-emitting, it is essential to promote modal shift whenever possible, and to limit their use when not.
Although fossil-fuelled cars are gradually being replaced by battery electric vehicles, we believe that a fair transition should limit reliance on this individual mode of transport. Replacing all internal combustion engine cars with electric cars and without any modal shift does not seem desirable for several reasons, the main ones being (i) the pressure on raw materials (some of which are critical and also needed for other clean technologies) and (ii) the very large land footprint of car-based mobility, particularly in urban areas.
With regard to aviation, we believe that short-haul flights should shift towards less energy-intensive modes of transport wherever possible. The use of Sustainable Aviation Fuels (SAF) should be prioritised for long-haul flights, for which modal shift is hardly feasible. Nevertheless, due to the limited availability and potential of SAF, as well as the technological and economic challenges of alternative propulsion systems (batteries, hydrogen, etc.), a reduction in very long-distance travel appears essential to reduce the overall environmental footprint.
We therefore set an overall objective of reducing the individual demand for extra-Europe flights by -40%, and of achieving a modal shift for 50% of the intra-Europe flights towards international train services. Regarding passenger cars, we aim at a modal shift for 30% of the displacements and an increased adoption of carpooling. More details are provided below.
Note: Assuming a round trip to a destination 9.000 km away (such as Brussels–Shanghai or Brussels–Los Angeles), i.e. 18.000 km in total, and using 2019 data (20.715 Gpkm of long-haul flights [1,2]), we can infer that about 150.0833 people could have made such a trip in that year, i.e. exactly 10% of the population. If the total volume of long-haul flights were reduced by -40% by 2050, decreasing to 12,429 Gpkm, then only about 690.500 people could make such a trip, i.e. about 5.5% of the population. Looked at from another perspective, for the same overall mobility budget, the entire Belgian population could make such a trip roughly once every 18 years, i.e. nearly five times over a lifetime. This target of a 40% reduction in long-haul flights therefore appears both realistic and socially acceptable.
# Inputs - Define Sufficiency Scenario Data (SUF)
pro_PM_spe_avi_lng = -0.40
# Outputs - Sufficiency Scenario Data (SUF)
trg_PM_mod_spe = {'plane-extra EU': (1+pro_PM_spe_avi_lng)*ref_PM_mod_spe['plane-extra EU']} # [pkm/person]
trg_PM_mod_abs = {'plane-extra EU': trg_PM_mod_spe['plane-extra EU']*df_SUF["population [person]"][2050]*1e-9} # [Gpkm]
trg_PM_mod_rel = {'plane-extra EU': trg_PM_mod_spe['plane-extra EU']/df_SUF["PM intensity [pkm/person]"][2050]*100} # [%]
Therefore, from the overall reduction in mobility intensity of -1.581 pkm/person between 2019 and 2050, we infer that about 46% of this decrease is due to the reduction in extra-European flights (-725 pkm/person).
For the remainder of the analysis, we assume that the remaining 55% of the reduction in mobility intensity (-873 pkm/person) comes from all other transport modes combined. This means that, out of the targeted 10% reduction in overall mobility intensity, 5,5% still needs to be achieved through these other modes. Thus, of the 1.364 pkm/person travelled by intra-European flights in 2019, 1.280 pkm/person are initially projected for 2050.
# Calculation of reductions in mobility intensity
dlt_PM_spe_avi_lng = pro_PM_spe_avi_lng*ref_PM_mod_spe['plane-extra EU'] # [pkm/person]
dlt_PM_spe = pro_PM_spe *ref_PM_spe # [pkm/person]
# The reduction left for every mode other than long-haul aviation. Written as a
# difference rather than as (1 - a/d)*d: the two are algebraically identical, but
# the ratio form is 0/0 when pro_PM_spe == 0 and silently turns the whole 2050
# projection into NaN -- so the model could not be run with no overall mobility
# reduction at all, which a sensitivity sweep will want to do.
rem_dlt_PM_abs = dlt_PM_spe - dlt_PM_spe_avi_lng # [pkm/person]
rem_dlt_PM_rel = rem_dlt_PM_abs/dlt_PM_spe if dlt_PM_spe else 1.0 # [-] reporting only
red_PM_rel = (1+rem_dlt_PM_abs/(ref_PM_spe-ref_PM_mod_spe['plane-extra EU'])) # [-]
trg_PM_mod_spe_ped = red_PM_rel*ref_PM_mod_spe['pedestrian'] # [pkm/person]
trg_PM_mod_spe_bic = red_PM_rel*ref_PM_mod_spe['bicycle'] # [pkm/person]
trg_PM_mod_spe_mot = red_PM_rel*ref_PM_mod_spe['two-wheeler'] # [pkm/person]
trg_PM_mod_spe_car = red_PM_rel*ref_PM_mod_spe['car'] # [pkm/person]
trg_PM_mod_spe_bus_cch = red_PM_rel*ref_PM_mod_spe['bus&coach'] # [pkm/person]
trg_PM_mod_spe_trm_met = red_PM_rel*ref_PM_mod_spe['tram&metro'] # [pkm/person]
trg_PM_mod_spe_trn_cnv = red_PM_rel*ref_PM_mod_spe['train-conventional'] # [pkm/person]
trg_PM_mod_spe_trn_spd = red_PM_rel*ref_PM_mod_spe['train-high speed'] # [pkm/person]
trg_PM_mod_spe_avi_srt = red_PM_rel*ref_PM_mod_spe['plane-intra EU'] # [pkm/person]
sum_PM_gen = trg_PM_mod_spe_ped + trg_PM_mod_spe_bic
sum_PM_rod = trg_PM_mod_spe_mot + trg_PM_mod_spe_car + trg_PM_mod_spe_bus_cch
sum_PM_ral = trg_PM_mod_spe_trm_met + trg_PM_mod_spe_trn_cnv + trg_PM_mod_spe_trn_spd
sum_PM_avi = trg_PM_mod_spe_avi_srt
sum_PM_tot = sum_PM_gen + sum_PM_rod + sum_PM_ral + sum_PM_avi
if abs((sum_PM_tot + trg_PM_mod_spe['plane-extra EU']-df_SUF["PM intensity [pkm/person]"][2050])/df_SUF["PM intensity [pkm/person]"][2050]*100) > 1e-5:
print("There is an error in forecasting the 2050 mobility intensity!")
Comment: Check that these calculations are correct and the modal shifts are effectively reported below.
Sufficiency Assumption
As introduced above, we aim to achieve a modal shift for 50% of these intra-Europe trips (total 1.280 pkm/person in 2050). Only 640 pkm/person therefore remain allocated to intra-European flights. Of this 50% modal shift, 25% is allocated to high-speed rail (320 pkm/person), 20% to conventional trains (256 pkm/person), and finally 5% to coaches (64 pkm/person).
Why the larger share goes to high-speed rail. The average intra-European flight departing Belgium covered 1.150 km in 2019, and 1.216 km in 2023 (JRC-IDEES-2023, Transport workbook for Belgium, sheet
TrAvia_act, row "Distance travelled per flight", International - Intra-EEAwCHUK). At that distance the question is not whether a rail connection exists but how fast it is. The evidence is one-sided: reviewing the literature, the EEA reports that rail dominates below 2,5 hours of journey time, competes between 2,5 and 4,5 hours, and that high-speed rail can hold 50% or more of the combined market up to about 3,5 hours; and its own city-pair table shows every large rail win on a genuine high-speed corridor - Paris-Lyon 0,64 million air passengers against 3,4 million by rail, Madrid-Barcelona 2,47 against 3,9, Amsterdam-Paris 1,40 against 2,0 - while comparable distances served by conventional track lose outright: Berlin-Vienna (603 km) 1,05 million by air against 20-50 thousand by rail, and Budapest-Frankfurt (917 km) 0,66 million against 1-5 thousand [EEA, Transport and Environment Report 2020: Train or plane?, p. 17 and tabl. 5.1 p. 50: https://www.eea.europa.eu/en/analysis/publications/transport-and-environment-report-2020/transport-and-environment-report-2020/@@download/file]. Conventional rail does not take passengers off aircraft at these distances; fast rail does. On energy per passenger there is no penalty to fear: the newest high-speed stock (Alstom AGV, 300 km/h) comes out at 0,033 kWh per seat-km, the same as a 200 km/h Pendolino and better than the 1990s TGV and Eurostar sets, because lower mass and more seats offset the speed [ATOC for Greengauge21, Energy consumption and CO2 impacts of high speed rail, tabl. 1: https://www.greengauge21.net/wp-content/uploads/Energy-Consumption-and-CO2-impacts.pdf]. That is consistent with the 0,0704 kWh/pkm this model gives high-speed rail in 2050 against 0,0764 for conventional.
And why conventional rail still keeps 20 points. Three reasons, all of them arguments against pushing the high-speed share higher than 25. First, delivery risk: the European Court of Auditors found the EU high-speed network to be "not a reality but an ineffective patchwork", with trains running at about 45% of the line's design speed on average and the cross-border Figueres-Perpignan link at 36% - which is why Barcelona-Paris (945 km), nominally a TGV corridor, still carries 2,5 million passengers by air against 20 thousand by rail [ECA, Special Report 19/2018, §VI and §40-48: https://www.eca.europa.eu/Lists/ECADocuments/SR18_19/SR_HIGH_SPEED_RAIL_EN.pdf]. Second, what the EU has actually legislated is more modest than high speed: the revised TEN-T regulation requires 160 km/h on the core and extended-core passenger network by 2040, and the connection of every airport above 12 million passengers to long-distance rail [European Parliament Research Service, briefing on Regulation (EU) 2024/1679: https://www.europarl.europa.eu/RegData/etudes/ATAG/2025/769545/EPRS_ATA(2025)769545_EN.pdf]. Third, Belgium's own federal Vision Rail 2040 has 3.615 km of main line and only 214 km of high-speed line, and when it describes the international offer it deliberately hedges across "TGV relations, classic international trains, or night trains", naming Berlin, Hamburg, Zurich, Basel, Copenhagen, Rome and Milan - distances of 600 to 1.200 km, exactly the band where the outcome is uncertain [SPF Mobilité et Transports, Vision Rail 2040, 6 May 2022, p. 9 and p. 16: https://mobilit.belgium.be/sites/default/files/publicaties%20en%20statistieken/20220506_vision_rail_2040_-_versionlongue_fr.pdf]. The 20 points on conventional rail are where the night trains and the legs no fast line serves belong.
Note: until 2026-09-21 the sentence above and the cell below disagreed - the text named high-speed rail for the 25% and conventional rail for the 20%, the code did the reverse. The code was corrected to the text on the evidence quoted above. In energy the choice is nearly neutral - the two destinations differ by 0,006 kWh/pkm in 2050, so the swap moves the 2050 inland demand by about 0,004 TWh, from 23,076 to 23,072 TWh - but the assumption is now coherent and defensible. The same pair still has to be swapped in
scripts/nW_BE.py(lines 103-104) of the PyPSA-Eur forks, or their consistency check will fail on the rail rows ofdata/energy_totals_overrides.csv.
# Inputs - Define Sufficiency Scenario Data (SUF)
pro_PM_spe_avi_srt = -0.50
sft_PM_rel_avi_srt_to_trn_cnv = +0.20 # conventional rail: the shorter legs and the night trains
sft_PM_rel_avi_srt_to_trn_spd = +0.25 # high speed: the 1 150 km average intra-EU flight -- see above
sft_PM_rel_avi_srt_to_cch = +0.05
if abs(pro_PM_spe_avi_srt+sft_PM_rel_avi_srt_to_trn_cnv+sft_PM_rel_avi_srt_to_trn_spd+sft_PM_rel_avi_srt_to_cch) > 1e-15:
print("There is an error in the modal shift for the 2050 intra-Europe flights!")
# Outputs - Sufficiency Scenario Data (SUF)
trg_PM_mod_spe['plane-intra EU'] = (1+pro_PM_spe_avi_srt)*trg_PM_mod_spe_avi_srt # [pkm/person]
trg_PM_mod_abs['plane-intra EU'] = trg_PM_mod_spe['plane-intra EU']*df_SUF["population [person]"][2050]*1e-9 # [Gpkm]
trg_PM_mod_rel['plane-intra EU'] = trg_PM_mod_spe['plane-intra EU']/df_SUF["PM intensity [pkm/person]"][2050]*100 # [%]
# Modal shit - Report to other modes
sft_PM_abs_avi_srt_to_trn_cnv = sft_PM_rel_avi_srt_to_trn_cnv*trg_PM_mod_spe_avi_srt
sft_PM_abs_avi_srt_to_trn_spd = sft_PM_rel_avi_srt_to_trn_spd*trg_PM_mod_spe_avi_srt
sft_PM_abs_avi_srt_to_cch = sft_PM_rel_avi_srt_to_cch *trg_PM_mod_spe_avi_srt
Car travel accounted for nearly 60% of the modal share in 2019 (9.399 pkm/person), making it by far the dominant mode of transport.
Sufficiency Assumption
As discussed above, we aim to reduce the modal share of cars for daily travel. We therefore target a modal shift for 30% of trips currently made by car by 2050 (-2.646 pkm/person). We prioritise a shift towards public transport (+882 pkm/person for buses, +706 pkm/person for trains, and +265 pkm/person/year for trams and metro), towards active modes (+617 pkm/person for cycling and +88 pkm/person for walking), and towards two-wheelers (+88 pkm/person).
# Inputs - Define Sufficiency Scenario Data (SUF)
pro_PM_spe_car = -0.30
sft_PM_rel_car_to_bus = +0.10
sft_PM_rel_car_to_trn_cnv = +0.08
sft_PM_rel_car_to_byc = +0.07
sft_PM_rel_car_to_trm_met = +0.03
sft_PM_rel_car_to_mot = +0.01
sft_PM_rel_car_to_ped = +0.01
# Outputs - Sufficiency Scenario Data (SUF)
trg_PM_mod_spe['car'] = (1+pro_PM_spe_car)*trg_PM_mod_spe_car # [pkm/person]
trg_PM_mod_abs['car'] = trg_PM_mod_spe['car']*df_SUF["population [person]"][2050]*1e-9 # [Gpkm]
trg_PM_mod_rel['car'] = trg_PM_mod_spe['car']/df_SUF["PM intensity [pkm/person]"][2050]*100 # [%]
# Modal shit - Report to other modes
sft_PM_abs_car_to_bus = sft_PM_rel_car_to_bus *trg_PM_mod_spe_car
sft_PM_abs_car_to_trn_cnv = sft_PM_rel_car_to_trn_cnv*trg_PM_mod_spe_car
sft_PM_abs_car_to_bic = sft_PM_rel_car_to_byc *trg_PM_mod_spe_car
sft_PM_abs_car_to_trm_met = sft_PM_rel_car_to_trm_met*trg_PM_mod_spe_car
sft_PM_abs_car_to_mot = sft_PM_rel_car_to_mot *trg_PM_mod_spe_car
sft_PM_abs_car_to_ped = sft_PM_rel_car_to_ped *trg_PM_mod_spe_car
Comment: The modal shift from cars to cycling could be greater. Conversely, the shift towards buses, trams and metro may be ambitious if service coverage and frequency are not significantly improved.
Comment: Check that the modal shifts are effectively reported below.
As a result of the modal shifts away from aviation and cars, the modal share of rail (both conventional and high-speed) increases. Passenger-kilometres for conventional trains increase by +969 pkm/person (doubled), while high-speed trains increase by +248 pkm/person (tripled).
# Outputs - Sufficiency Scenario Data (SUF)
trg_PM_mod_spe['train-conventional'] = trg_PM_mod_spe_trn_cnv+sft_PM_abs_avi_srt_to_trn_cnv+sft_PM_abs_car_to_trn_cnv # [pkm/person]
trg_PM_mod_abs['train-conventional'] = trg_PM_mod_spe['train-conventional']*df_SUF["population [person]"][2050]*1e-9 # [Gpkm]
trg_PM_mod_rel['train-conventional'] = trg_PM_mod_spe['train-conventional']/df_SUF["PM intensity [pkm/person]"][2050]*100 # [%]
trg_PM_mod_spe['train-high speed'] = trg_PM_mod_spe_trn_spd+sft_PM_abs_avi_srt_to_trn_spd # [pkm/person]
trg_PM_mod_abs['train-high speed'] = trg_PM_mod_spe['train-high speed'] *df_SUF["population [person]"][2050]*1e-9 # [Gpkm]
trg_PM_mod_rel['train-high speed'] = trg_PM_mod_spe['train-high speed'] /df_SUF["PM intensity [pkm/person]"][2050]*100 # [%]
Comment: In this scenario, modal shifts from aviation and car travel increase rail demand from 10,436 Gpkm to 22,923 Gpkm (more than doubled) for conventional rail and from 1,564 Gpkm to 5,652 Gpkm/year for high-speed rail (×3,6). In its 'Vision Rail 2040' policy note published in 2022, the Belgian Minister of Mobility, Georges Gilkinet, stated the objective of increasing the modal share of rail from 8% to 15% by 2040 (aviation excluded) [7]. Few quantitative details are provided; however, assuming that conventional rail intensity was 919 pkm/person in 2019 [7], doubling the modal share would bring this value to around 1.840 pkm/person by 2040. In light of the performance observed in neighbouring countries, this appears achievable through an increase in service supply, without necessarily expanding network density. The projected value of 1.819 pkm/person in 2050 for conventional rail is therefore fully consistent with the stated ambition.
Comment: It should be noted that, in its business-as-usual outlook, the Belgian Federal Planning Bureau [5] estimates that total rail demand would decrease by 2,8%. Our assumption of more than doubling rail demand therefore requires strong public policies.
As a result of the modal shifts away from cars, the modal share of gentle modes (both pedestrian and walking) increases. Passenger-kilometres for pedestrian increase by +72 pkm/person, while cycling increases by +591 pkm/person. The increase in walking intensity results from more multimodal mobility patterns centred on public transport, which require short walking trips, as well as from the expansion of pedestrian-friendly and car-free areas in city centres, in line with current urban mobility policies. The growth in bicycles is supported by the rapid adoption of electric bikes and major investments in cycling infrastructure, including dedicated cycle lanes and secure bike parking.
# Outputs - Sufficiency Scenario Data (SUF)
trg_PM_mod_spe['pedestrian'] = trg_PM_mod_spe_ped+sft_PM_abs_car_to_ped # [pkm/person]
trg_PM_mod_abs['pedestrian'] = trg_PM_mod_spe['pedestrian']*df_SUF["population [person]"][2050]*1e-9 # [Gpkm]
trg_PM_mod_rel['pedestrian'] = trg_PM_mod_spe['pedestrian']/df_SUF["PM intensity [pkm/person]"][2050]*100 # [%]
trg_PM_mod_spe['bicycle'] = trg_PM_mod_spe_bic+sft_PM_abs_car_to_bic # [pkm/person]
trg_PM_mod_abs['bicycle'] = trg_PM_mod_spe['bicycle'] *df_SUF["population [person]"][2050]*1e-9 # [Gpkm]
trg_PM_mod_rel['bicycle'] = trg_PM_mod_spe['bicycle'] /df_SUF["PM intensity [pkm/person]"][2050]*100 # [%]
Comment: Cycling grows ×2,3, from 1,21 to 2,83 km/person/day. We could be more ambitious! It should nevertheless be noted that, under unchanged policies, the Federal Planning Bureau only anticipates a very slight increase in the modal share of this mode (+2 Gpkm from 2019 to 2040) [5].
Comment: The "FietsDNA 2025" report (https://fietsberaad.be/wp-content/uploads/FietsDNA_2025-UK.pdf) from the Kenniscentrum voor het fietsbeleid in Vlaanderen (Fietsberaad Vlaanderen) recalls that the objective is to reach 30% of the journeys done by bicycles in Flanders by 2040. According to them, this figure was close to 10% in whole Belgium by 2021. The exact definition of cycling journey is however not provided. This is probably different than the proper modal share.
As a result of the modal shift away from cars, the modal share of light rail transport systems (both metro and tram) increases. Passenger-kilometres for metro and tram increase by +257 pkm/person.
# Outputs - Sufficiency Scenario Data (SUF)
trg_PM_mod_spe['tram&metro'] = trg_PM_mod_spe_trm_met+sft_PM_abs_car_to_trm_met # [pkm/person]
trg_PM_mod_abs['tram&metro'] = trg_PM_mod_spe['tram&metro']*df_SUF["population [person]"][2050]*1e-9 # [Gpkm]
trg_PM_mod_rel['tram&metro'] = trg_PM_mod_spe['tram&metro']/df_SUF["PM intensity [pkm/person]"][2050]*100 # [%]
Comment: Despite the development of new tram lines in Liège and other cities, this value is likely too high (it is tripled w.r.t. 2019).
As a result of the modal shift away from cars and intra-Europe aviation, the modal share of buses and coaches increases. Passenger-kilometres for bus and coach increase by +872 pkm/person. This growth is considered plausible if rural and suburban areas are served by an expanded network of high-frequency bus services. Coach travels within Europe also develop.
# Outputs - Sufficiency Scenario Data (SUF)
trg_PM_mod_spe['bus&coach'] = trg_PM_mod_spe_bus_cch+sft_PM_abs_avi_srt_to_cch+sft_PM_abs_car_to_bus # [pkm/person]
trg_PM_mod_abs['bus&coach'] = trg_PM_mod_spe['bus&coach']*df_SUF["population [person]"][2050]*1e-9 # [Gpkm]
trg_PM_mod_rel['bus&coach'] = trg_PM_mod_spe['bus&coach']/df_SUF["PM intensity [pkm/person]"][2050]*100 # [%]
As a result of the modal shift away from cars, the modal share of powered two-wheelers (motorcycles) increases. Passenger-kilometres for powered two-wheelers increase by +79 pkm/person. This could be due to an increased use of two-wheelers for commuting in urban and peri-urban areas, particularly given the growing availability of low-emission or electric models.
# Outputs - Sufficiency Scenario Data (SUF)
trg_PM_mod_spe['two-wheeler'] = trg_PM_mod_spe_mot+sft_PM_abs_car_to_mot # [pkm/person]
trg_PM_mod_abs['two-wheeler'] = trg_PM_mod_spe['two-wheeler']*df_SUF["population [person]"][2050]*1e-9 # [Gpkm]
trg_PM_mod_rel['two-wheeler'] = trg_PM_mod_spe['two-wheeler']/df_SUF["PM intensity [pkm/person]"][2050]*100 # [%]
# Processing - Modal Shares (in %)
modes_PM = {
'pedestrian': linear_growth(2019,ref_PM_mod_rel['pedestrian'],
2050,trg_PM_mod_rel['pedestrian'], years),
'bicycle': linear_growth(2019,ref_PM_mod_rel['bicycle'],
2050,trg_PM_mod_rel['bicycle'], years),
'two-wheeler': linear_growth(2019,ref_PM_mod_rel['two-wheeler'],
2050,trg_PM_mod_rel['two-wheeler'], years),
'tram&metro': linear_growth(2019,ref_PM_mod_rel['tram&metro'],
2050,trg_PM_mod_rel['tram&metro'], years),
'bus&coach': linear_growth(2019,ref_PM_mod_rel['bus&coach'],
2050,trg_PM_mod_rel['bus&coach'], years),
'car': linear_growth(2019,ref_PM_mod_rel['car'],
2050,trg_PM_mod_rel['car'], years),
'train-conventional': linear_growth(2019,ref_PM_mod_rel['train-conventional'],
2050,trg_PM_mod_rel['train-conventional'],years),
'train-high speed': linear_growth(2019,ref_PM_mod_rel['train-high speed'],
2050,trg_PM_mod_rel['train-high speed'], years),
'plane-intra EU': linear_growth(2019,ref_PM_mod_rel['plane-intra EU'],
2050,trg_PM_mod_rel['plane-intra EU'], years),
'plane-extra EU': linear_growth(2019,ref_PM_mod_rel['plane-extra EU'],
2050,trg_PM_mod_rel['plane-extra EU'], years),
}
# Processing - Modal Shares DataFrame
df_PM_MOD = pd.DataFrame(modes_PM, index=years)
df_PM_MOD = df_PM_MOD.round(4).transpose()
# Processing - Global DataFrame
df_PM_GPKM = pd.DataFrame({year: df_SUF["PM total [Gpkm]"][year]*df_PM_MOD[year]*1e-2 for year in years}, index=df_PM_MOD.index).round(6)
df_PM_PKMP = pd.DataFrame({year: df_PM_GPKM[year]*1e+9/population_dict[year] for year in years}, index=df_PM_MOD.index).round(3)
arrays_PM =[np.repeat(df_PM_MOD.index, 3), ['% of total', 'pkm/person', 'Gpkm'] * len(df_PM_MOD)]
mi_PM = pd.MultiIndex.from_arrays(arrays_PM, names=['Mode', 'Unit'])
data_PM_rows = []
for mode in df_PM_MOD.index:
data_PM_rows.append(df_PM_MOD .loc[mode].values) # modal percentages
data_PM_rows.append(df_PM_PKMP.loc[mode].values) # pkm per person
data_PM_rows.append(df_PM_GPKM.loc[mode].values) # Gpkm values
data_PM = np.vstack(data_PM_rows)
df_PM = pd.DataFrame(data_PM, index=mi_PM, columns=years)
if post_process:
# === Full table ===
df_PM_r = df_PM.reset_index()
total_Gpkm = df_SUF["PM total [Gpkm]"]
total_pkmp = df_SUF["PM intensity [pkm/person]"]
rows = []
for unit in ['% of total', 'pkm/person', 'Gpkm']:
if unit == '% of total':
vals = [100] * len(years)
elif unit == 'Gpkm':
vals = [total_Gpkm[year] for year in years]
else:
vals = [total_pkmp [year] for year in years]
rows.append(pd.DataFrame([['TOTAL', unit, *vals]], columns=df_PM_r.columns))
df_PM_r = pd.concat([df_PM_r] + rows, ignore_index=True)
mode_full = df_PM_r['Mode'].tolist()
df_PM_r['Mode'] = np.where(df_PM_r['Unit'] == '% of total', df_PM_r['Mode'], '')
styled = (
df_PM_r.style
.apply(highlight_lines, axis=1)
.set_properties(subset=['Mode'], **{'font-weight':'bold'})
.set_properties(subset=['Unit'], **{'font-style':'italic','color':'gray'})
.format({year:"{:.2f}" for year in years})
#.set_caption("Modal Shares")
.hide(axis='index')
.set_table_attributes('style="width:100%;table-layout:fixed;"')
)
display(styled)
# === Breakdown bar chart - relative values ===
dfmp = df_PM_MOD.transpose()
fig1, ax = plt.subplots(figsize=(14, 5))
bottom = np.zeros(len(dfmp))
colors = plt.cm.tab10.colors # default matplotlib palette
# Thicker bars → control via height argument
bar_height = 2.0 # default is ~0.8; increase to 0.9–1.0 for thicker bars
for i, mode in enumerate(dfmp.columns):
ax.barh(dfmp.index, dfmp[mode],
left=bottom, label=mode,
color=colors[i % len(colors)],
height=bar_height)
bottom += dfmp[mode]
ax.set_xlabel("Modal Share [%]", fontsize=10, ha='right', va='top')
ax.xaxis.set_label_coords(1, -0.1)
ax.set_ylabel("Years", fontsize=10, rotation=0, ha='left', va='bottom')
ax.yaxis.set_label_coords(-0.05, 1)
ax.set_yticks(years)
ax.yaxis.set_inverted(True) # inverted axis with autoscaling
ax.set_title("Evolution of Modal Shares for Passenger Mobility (negaWatt-BE Scenario)", fontsize=11, pad=15)
ax.legend(bbox_to_anchor=(1, 1), loc='upper left', title="Mode", frameon=False)
ax.grid(axis='x', linestyle='--', alpha=0.6)
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)
ax.spines['bottom'].set_visible(False)
ax.spines['left'].set_visible(False)
fig1.tight_layout()
plt.show()
# === Breakdown bar chart - absolute values ===
dfmp = df_PM.transpose()
fig2, ax = plt.subplots(figsize=(14, 5))
bottom = np.zeros(len(dfmp))
colors = plt.cm.tab10.colors # default matplotlib palette
# Thicker bars → control via height argument
bar_height = 2.0 # default is ~0.8; increase to 0.9–1.0 for thicker bars
j = 0
for i, mode in enumerate(dfmp.columns):
if mode[1] == 'Gpkm':
ax.barh(dfmp.index, dfmp[mode],
left=bottom, label=mode[0],
color=colors[j % len(colors)],
height=bar_height)
bottom += dfmp[mode]
j += 1
ax.set_xlabel("Total number of passenger-kilometres [Gpkm]", fontsize=10, ha='right', va='top')
ax.xaxis.set_label_coords(1, -0.1)
ax.set_ylabel("Years", fontsize=10, rotation=0, ha='left', va='bottom')
ax.yaxis.set_label_coords(-0.05, 1)
ax.set_yticks(years)
ax.yaxis.set_inverted(True) # inverted axis with autoscaling
ax.set_title("Evolution of Modal Shares for Passenger Mobility (negaWatt-BE Scenario)", fontsize=11, pad=15)
ax.legend(bbox_to_anchor=(1, 1), loc='upper left', title="Mode", frameon=False)
ax.grid(axis='x', linestyle='--', alpha=0.6)
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)
ax.spines['bottom'].set_visible(False)
ax.spines['left'].set_visible(False)
fig2.tight_layout()
plt.show()
# === Breakdown bar chart - comparison of relative values in 2019 and 2050 ===
labels = df_PM_MOD.index.tolist()
sizes_2019 = df_PM_MOD[2019].values # Modal shares for 2019
sizes_2050 = df_PM_MOD[2050].values # Modal shares for 2050
x = np.arange(len(labels)) # x locations for each mode
width = 0.35 # bar width
fig, ax = plt.subplots(figsize=(14, 5))
# Bars for 2019 and 2050
ax.bar(x - width/2, sizes_2019, width, label='2019')
ax.bar(x + width/2, sizes_2050, width, label='2050')
# Labels and Title
ax.set_ylabel('Modal Share (%)')
ax.set_title('Modal Shares Comparison: 2019 vs 2050 (negaWatt-BE Scenario)')
# X-ticks: mode names, rotated for readability
ax.set_xticks(x)
ax.set_xticklabels(labels, rotation=45, ha='right')
# Legend to identify each year
ax.legend()
# Tight layout to avoid clipping
plt.tight_layout()
plt.show()
| Mode | Unit | 2019 | 2025 | 2030 | 2035 | 2040 | 2045 | 2050 |
|---|---|---|---|---|---|---|---|---|
| pedestrian | % of total | 1.64 | 1.77 | 1.89 | 2.00 | 2.10 | 2.22 | 2.33 |
| pkm/person | 262.37 | 278.13 | 290.51 | 302.32 | 313.56 | 324.39 | 334.49 | |
| Gpkm | 3.00 | 3.29 | 3.49 | 3.68 | 3.87 | 4.05 | 4.21 | |
| bicycle | % of total | 3.83 | 4.70 | 5.41 | 6.13 | 6.85 | 7.57 | 8.29 |
| pkm/person | 612.29 | 735.67 | 834.38 | 929.38 | 1020.53 | 1108.12 | 1192.01 | |
| Gpkm | 7.00 | 8.69 | 10.03 | 11.33 | 12.60 | 13.84 | 15.02 | |
| two-wheeler | % of total | 0.96 | 1.09 | 1.19 | 1.30 | 1.40 | 1.51 | 1.61 |
| pkm/person | 153.23 | 170.17 | 183.55 | 196.54 | 208.84 | 220.75 | 231.96 | |
| Gpkm | 1.75 | 2.01 | 2.21 | 2.40 | 2.58 | 2.76 | 2.92 | |
| tram&metro | % of total | 0.81 | 1.17 | 1.47 | 1.77 | 2.08 | 2.38 | 2.68 |
| pkm/person | 128.79 | 183.17 | 226.70 | 268.68 | 309.24 | 348.10 | 385.40 | |
| Gpkm | 1.47 | 2.16 | 2.73 | 3.27 | 3.82 | 4.35 | 4.86 | |
| bus&coach | % of total | 7.49 | 8.82 | 9.94 | 11.05 | 12.16 | 13.28 | 14.39 |
| pkm/person | 1196.79 | 1382.81 | 1531.60 | 1674.49 | 1811.81 | 1943.24 | 2069.09 | |
| Gpkm | 13.68 | 16.34 | 18.42 | 20.41 | 22.37 | 24.27 | 26.07 | |
| car | % of total | 58.82 | 55.75 | 53.18 | 50.62 | 48.06 | 45.49 | 42.93 |
| pkm/person | 9399.02 | 8735.28 | 8196.45 | 7671.00 | 7158.75 | 6659.72 | 6173.89 | |
| Gpkm | 107.44 | 103.22 | 98.55 | 93.48 | 88.39 | 83.18 | 77.80 | |
| train-conventional | % of total | 5.71 | 7.14 | 8.33 | 9.52 | 10.71 | 11.90 | 13.09 |
| pkm/person | 912.85 | 1118.94 | 1283.78 | 1442.64 | 1595.22 | 1741.82 | 1882.28 | |
| Gpkm | 10.44 | 13.22 | 15.44 | 17.58 | 19.70 | 21.76 | 23.72 | |
| train-high speed | % of total | 0.86 | 1.21 | 1.50 | 1.79 | 2.09 | 2.38 | 2.67 |
| pkm/person | 136.78 | 189.28 | 231.33 | 271.86 | 310.88 | 348.39 | 384.39 | |
| Gpkm | 1.56 | 2.24 | 2.78 | 3.31 | 3.84 | 4.35 | 4.84 | |
| plane-intra EU | % of total | 8.54 | 7.75 | 7.09 | 6.43 | 5.77 | 5.11 | 4.45 |
| pkm/person | 1363.92 | 1213.58 | 1092.06 | 973.93 | 859.21 | 747.87 | 639.94 | |
| Gpkm | 15.59 | 14.34 | 13.13 | 11.87 | 10.61 | 9.34 | 8.06 | |
| plane-extra EU | % of total | 11.34 | 10.61 | 10.00 | 9.39 | 8.78 | 8.17 | 7.56 |
| pkm/person | 1812.12 | 1662.35 | 1541.00 | 1422.94 | 1307.88 | 1195.95 | 1087.32 | |
| Gpkm | 20.72 | 19.64 | 18.53 | 17.34 | 16.15 | 14.94 | 13.70 | |
| TOTAL | % of total | 100.00 | 100.00 | 100.00 | 100.00 | 100.00 | 100.00 | 100.00 |
| pkm/person | 15978.48 | 15669.22 | 15411.50 | 15153.79 | 14896.07 | 14638.35 | 14380.63 | |
| Gpkm | 182.66 | 185.15 | 185.31 | 184.68 | 183.92 | 182.84 | 181.21 |
This section presents the internal distribution of each transport mode by energy carrier and powertrain type, as a percentage of the total Gpkm for that mode. These shares are used to calculate the weighted energy demand and infrastructure needs per technology.
The bicycle mode is split between human-powered (mechanical) and electric bicycles. Shares are given as a percentage of total Gpkm covered by bicycles (see Section 2.1.8.).
Few figures are available regarding the share of trips made by electric bicycles (standard e-bikes with assistance up to 25 km/h and speed pedelecs) in 2019. The 2017 survey by the Belgian Federal Public Service for Mobility and Transport indicates that electric bicycles accounted for 9,3% of all trips made by bicycle [3]. The 2025 survey by the Belgian Federal Public Service for Mobility and Transport, by contrast, shows that electric bicycles now account for 64% of total kilometres travelled by bicycle [4]. The distinction between trips and kilometres travelled is meaningful when considering the average distances covered per trip by bicycle type: 4 km for conventional bicycles, 5 km for electric bicycles, and 10 km for speed pedelecs [4]. The increased use of electric bicycles is undoubtedly linked to the continuous growth in sales up to the COVID period, followed by a sharp increase afterwards. It should nevertheless be noted that sales have since stabilised, while the share of electric bicycle sales has clearly overtaken that of conventional bicycles since 2023.
Due to the lack of more precise data, we estimate that electric bicycles accounted for 25% of total bicycle kilometres travelled in 2019.
Comfort Assumption
We believe that the widespread deployment of cycling as a major mode of transport by 2050 will be enabled by the adoption of electric bicycles, as they increase user comfort and average travel speed. Nevertheless, given the short length of many trips, the favourable geography of a large part of the country, and the use of folding bikes as multimodal tools, we consider that the share of electric bicycles should not exceed 80%. We assume that this level will already be reached by 2040.
# Inputs - Define Sufficiency Scenario Data (SUF)
sta_PM_bic = 25 # [%]
end_PM_bic = 80 # [%]
mid_PM_bic = 2040
# Outputs - Sufficiency Scenario Data (SUF)
carriers_PM_bic = {
'electrical': linear_with_middle_point(2019, sta_PM_bic, mid_PM_bic, end_PM_bic, 2050, end_PM_bic, years), # [%]
'mechanical': [100-x for x in linear_with_middle_point(2019, sta_PM_bic, mid_PM_bic, end_PM_bic, 2050, end_PM_bic, years)] # [%]
}
df_PM_bic = pd.DataFrame(carriers_PM_bic, index=years).T.round(3)
The two-wheelers mode is divided between liquid-fuel and electric two-wheelers. Shares refer to the percentage of total Gpkm covered by two-wheelers (see Section 2.1.8.).
The breakdown of propulsion types for two-wheelers in 2019 is not provided by JRC-IDEES [1,2], which does not distinguish propulsion technologies for this transport mode. We assume their contribution to be low, or even negligible, and therefore set it to 0% for 2019.
Efficiency Assumption
The goal is to minimise liquid fuel use as much as possible by 2050. The electrification of two-wheelers has already begun, particularly for urban models. However, full phase-out is unlikely, notably due to their use for leisure purposes. Nevertheless, fossil gasoline can gradually be replaced by bio- and e-fuels. We project that only 15% of kilometres travelled by two-wheelers will still be powered by liquid fuels (gasoline type). The remainder will be powered by battery-electric propulsion.
# Inputs - Define Sufficiency Scenario Data (SUF)
sta_PM_mot = 100 # [%]
end_PM_mot = 15 # [%]
# Outputs - Sufficiency Scenario Data (SUF)
carriers_PM_mot = {
'liquid-gasoline': linear_growth(2019, sta_PM_mot, 2050, end_PM_mot, years), # [%]
'electrical': [100-x for x in linear_growth(2019, sta_PM_mot, 2050, end_PM_mot, years)], # [%]
}
df_PM_mot = pd.DataFrame(carriers_PM_mot, index=years).T.round(3)
The metro and tram mode contains only electric vehicles. Shares refer to the percentage of total Gpkm covered by metros and trams (see Section 2.1.8.).
Comment: Heavy metros are directly fed via a third rail (Brussels) and light metros (pre-metros) are directly fed via a catenary (Charleroi, Brussels, Antwerp). Trams are fed via a catenary (coastal tram, Brussels, Antwerp, Ghent, Charleroi) and hybrid catenary-battery (Liège).
# Inputs - Define Sufficiency Scenario Data (SUF)
sta_PM_trm_met = 100 # [%]
end_PM_trm_met = 100 # [%]
# Outputs - Sufficiency Scenario Data (SUF)
carriers_PM_trm_met = {
'electrical': linear_growth(2019, sta_PM_trm_met, 2050, end_PM_trm_met, years), # [%]
}
df_PM_trm_met = pd.DataFrame(carriers_PM_trm_met, index=years).T.round(3)
In 2019, diesel propulsion dominated buses and coaches, accounting for more than 99% of the fleet:
- Diesel engines: 99,27%
- Battery Electric Vehicles (BEV): 0,39%
- Gasoline engines: 0,25%
- Natural Gas (CNG/LNG) engines: 0,09%
The shares refer to the percentage of total Gpkm covered by buses and coaches (see Section 2.1.8.). The breakdown of propulsion types for 2019 is provided by JRC-IDEES [1,2].
Efficiency Assumption
The electrification of buses and coaches is already well underway in Belgium, with their share reaching 2,21% of total passenger-kilometres in 2023 (5,71 times higher than in 2019). The adoption of battery electric vehicles for urban lines is progressing rapidly, with their share of sales increasing from 9% in 2019 to 78% in 2024 (see Transport & Environment, https://www.transportenvironment.org/articles/half-of-new-eu-city-buses-were-zero-emission-in-2024). A share above 90% for battery electric buses in new sales could therefore be reached quickly in Belgium (this has already been the case in Denmark since 2022 and in other EU countries).
The figures also show an increase in the sales share of hydrogen fuel-cell buses in-the EU-27 (see Solaris Urbino 18 Hydrogen), rising from 1% over the 2021-2023 period to 3% in 2024 (still 0% in Belgium at this stage). This share remains confidential but could increase for applications where range is a key issue (such as coaches) and due to logistical constraints (limited access to charging infrastructure, charging times). Several manufacturers are developing solutions in this area (see Irizar i6S Efficient-Hydrogen and Daimler-Setra H2 Coach). Nevertheless, given the already high ranges exceeding 600 km offered by electric coaches (see Volvo BZR electric and MAN Lion's Coach E), it is also very likely that battery electric coaches will dominate over hydrogen in the future. uture.
For our projections, we mainly rely on battery electric vehicles, which are assumed to account for 90% of the fleet by 2050. As no trolleybus projects (with or without onboard batteries) are currently being discussed in Belgium, this option is not considered. Regarding hydrogen, the outlook is highly uncertain due to competition with battery electric solutions and the difficulties in scaling up the hydrogen value chain. We nevertheless assume a 5% share by 2050. For the remaining share, we assume that gasoline-powered drivetrains will completely disappear. We also retain a small 3% share for liquid-fuel propulsion, considering that biodiesel could facilitate the use of locally produced energy while enabling longer distance operations. Similarly, we allocate a 2% share to gas-powered vehicles, assuming the potential valorisation of biogas.
# Inputs - Define Sufficiency Scenario Data (SUF)
sta_PM_bus_cch_lfd = 99.27 # [%]
end_PM_bus_cch_lfd = 3.00 # [%]
sta_PM_bus_cch_lfg = 0.25 # [%]
end_PM_bus_cch_lfg = 0.00 # [%]
sta_PM_bus_cch_h2 = 0.00 # [%]
end_PM_bus_cch_h2 = 5.00 # [%]
sta_PM_bus_cch_cng = 0.09 # [%]
end_PM_bus_cch_cng = 2.00 # [%]
slope_f_PM_bus_cch = 0.9
# Outputs - Sufficiency Scenario Data (SUF)
carriers_bus = {
'liquid-diesel': s_curve_growth(2019, sta_PM_bus_cch_lfd, 2050, end_PM_bus_cch_lfd, years, slope_f_PM_bus_cch), # [%]
'liquid-gasoline': s_curve_growth(2019, sta_PM_bus_cch_lfg, 2050, end_PM_bus_cch_lfg, years, slope_f_PM_bus_cch), # [%]
'hydrogen': s_curve_growth(2019, sta_PM_bus_cch_h2, 2050, end_PM_bus_cch_h2, years, slope_f_PM_bus_cch), # [%]
'gas-NG': s_curve_growth(2019, sta_PM_bus_cch_cng, 2050, end_PM_bus_cch_cng, years, slope_f_PM_bus_cch), # [%]
'electrical': [100-w-x-y-z for w, x, y, z in zip(*[s_curve_growth(2019, sta_PM_bus_cch_lfd, 2050, end_PM_bus_cch_lfd, years, slope_f_PM_bus_cch),
s_curve_growth(2019, sta_PM_bus_cch_lfg, 2050, end_PM_bus_cch_lfg, years, slope_f_PM_bus_cch),
s_curve_growth(2019, sta_PM_bus_cch_h2, 2050, end_PM_bus_cch_h2, years, slope_f_PM_bus_cch),
s_curve_growth(2019, sta_PM_bus_cch_cng, 2050, end_PM_bus_cch_cng, years, slope_f_PM_bus_cch)])], # [%]
}
df_PM_bus_cch = pd.DataFrame(carriers_bus, index=years).T.round(3)
Comment: Shares for sales of zero-emissions urban buses for 2025 have been released by T&E (see: https://www.transportenvironment.org/articles/past-the-inflection-point-electric-now-clearly-dominates-the-city-bus-market). BEV represented 94% in Belgium, 56% at the EU level. Also note that at the EU level, H2 buses represented 4% of the sales. These figures could help uptading projections.
In 2019, diesel and gasoline propulsion dominated passenger cars, accounting for more than 98% of the fleet:
- Diesel engines: 63,25%
- Gasoline engines: 35,00%
- Liquefied Petroleum Gas (LPG) engines: 0,69%
- Plug-in Hybrid electric Vehicles (PHV): 0,53%
- Natural Gas (CNG/LNG) engines: 0,29%
- Battery Electric Vehicles: 0,24%
The shares refer to the percentage of total Gpkm covered by cars (see Section 2.1.8.). The breakdown of propulsion types for 2019 is provided by JRC-IDEES [1,2].
Efficiency Assumption
In light of current technological developments, sales trends, and the regulations imposed by the European Commission, it appears realistic that the majority of cars sold in 2040 will be battery electric vehicles. Consequently, most of the vehicle fleet in 2050 should consist of battery-powered vehicles. We nevertheless retain a small share (1,5% each) for diesel, gasoline, and gas engines, as these could potentially be fueled with renewable fuels (biofuels, or even e-fuels). We however exclude hydrogen, as current prospects do not appear favorable for this drivetrain.
We aim for a gradual phase-out of gasoline and diesel engines, reaching a combined floor value of 3% by 2050. Given current trends, which point to a rapid decline of diesel (-3,1%/year of total shares), likely reinforced by the Dieselgate scandal, its minimum share of 1,5% should be reached by 2040. Gasoline gradually replaces diesel during the transition, although the total share of internal combustion engines continues to decline.
For the Belgian market, we consider plug-in hybrid vehicles as transition technologies between internal combustion and electric vehicles. We expect their share to peak at 6% by 2030.
# Inputs - Define Sufficiency Scenario Data (SUF)
sta_PM_car_lfd = 63.25 # [%]
end_PM_car_lfd = 1.50 # [%]
mid_PM_car_lfd = 2040
sta_PM_car_lfg = 35.00 # [%]
end_PM_car_lfg = 1.50 # [%]
sta_PM_car_lpg = 0.69 # [%]
end_PM_car_lpg = 0.00 # [%]
sta_PM_car_phv = 0.53 # [%]
end_PM_car_phv = 0.00 # [%]
mid_PM_car_phv = 6.00 # [%]
sta_PM_car_cng = 0.29 # [%]
end_PM_car_cng = 1.50 # [%]
# Outputs - Sufficiency Scenario Data (SUF)
carriers_PM_car = {
'liquid-diesel': linear_with_middle_point(2019, sta_PM_car_lfd, mid_PM_car_lfd, end_PM_car_lfd, 2050, end_PM_car_lfd, years), # [%]
'liquid-gasoline': [x-y for x, y in zip(*[s_curve_growth(2019, sta_PM_car_lfd +sta_PM_car_lfg, 2050, end_PM_car_lfd+end_PM_car_lfg, years,0.8),
linear_with_middle_point(2019, sta_PM_car_lfd, mid_PM_car_lfd, end_PM_car_lfd, 2050, end_PM_car_lfd, years)])], # [%]
'gas-LPG': linear_growth(2019, sta_PM_car_lpg, 2050, end_PM_car_lpg, years), # [%]
'gas-NG': linear_growth(2019, sta_PM_car_cng, 2050, end_PM_car_cng, years), # [%]
'hybrid-plug-in': b_curve_with_control_value(2019, sta_PM_car_phv, 2030, mid_PM_car_phv, 2050, end_PM_car_phv, years,1.5),# [%]
'electrical': [100-w-x-y-z for w, x, y, z in zip(*[s_curve_growth(2019, sta_PM_car_lfd+sta_PM_car_lfg, 2050, end_PM_car_lfd+end_PM_car_lfg, years,slope_f_PM_bus_cch),
linear_growth(2019, sta_PM_car_lpg, 2050, end_PM_car_lpg, years),
linear_growth(2019, sta_PM_car_cng, 2050, end_PM_car_cng, years),
b_curve_with_control_value(2019, sta_PM_car_phv, 2030, mid_PM_car_phv, 2050, end_PM_car_phv, years,1.5)])], # [%]
}
df_PM_car = pd.DataFrame(carriers_PM_car, index=years).T.round(3)
The train mode is divided between liquid-fuel and electric trains. Shares refer to the percentage of total Gpkm covered by trains (see Section 2.1.8.). Pr).
The breakdown of propulsion types for conveareional trains in 2019 is provided by JRC-IDEES [1,2]: over the total 10,436 Gpkm achieved, 9,97826 Gpkm (95,61%) were done through electric trains while the remaining 0,457 Gpkm (4,39%) were covered by diesel trains. Note that the line between Hasselt and Mol was electrified in 2023. All high speed trains are electric.
Efficiency Assumption
The rail operator SNCB has committed not to renewing its AR41 diesel railcars. The main barrier to fully replacing them with electric traction remains the lack of electrification of just under 10% of the network, mai-ly around Gh-nt (Eeklo–Ronse, Ghen-–Geraardsbergen, Aalst–Burst) and on the Charleroi–Couvin line. Full electrification of the network appears to be the most appropriate solution for phasing out diesel, even if it is also the most costly one [8]. The limited use of battery-electric trains also appears to be an interesting option when direct electrification is not possible. The hydrogen train option is set aside due to high operational costs. In the scenarios studied by Transport & Mobility Leuven, full electrification could be achieved by 2035, provided that appropriate policies are implemented [8]. We therefore propose to set a target of 100% electrification by 2035 (direct catenary + battery).
# Inputs - Define Sufficiency Scenario Data (SUF)
sta_PM_trn_cnv = 4.39 # [%]
end_PM_trn_cnv = 0.00 # [%]
mid_PM_trn_cnv = 2035 # [%]
# Outputs - Sufficiency Scenario Data (SUF)
carriers_PM_trn_cnv = {
'liquid-diesel': linear_with_middle_point(2019, sta_PM_trn_cnv, mid_PM_trn_cnv, end_PM_trn_cnv, 2050, end_PM_trn_cnv, years), # [%]
'electrical': [100-x for x in linear_with_middle_point(2019, sta_PM_trn_cnv, mid_PM_trn_cnv, end_PM_trn_cnv, 2050, end_PM_trn_cnv, years)], # [%]
}
carriers_PM_trn_spd = {
'electrical': linear_growth(2019, 100, 2050, 100, years), # [%]
}
df_PM_trn_cnv = pd.DataFrame(carriers_PM_trn_cnv, index=years).T.round(3)
df_PM_trn_spd = pd.DataFrame(carriers_PM_trn_spd, index=years).T.round(3)
Compared with other transport modes, the options and potential for defossilising aviation are currently far less mature and all the more uncertain. While Sustainable Aviation Fuels (SAF)-which are renewable (bio-based and synthetic) drop-in substitutes for conventional jet fuel-appear to be the most straightforward option, their real global potential remains uncertain, notably due to competing uses (land use, biomass availability).
The use of e-fuels, such as hydrogen- or ammonia-powered aircraft, is also being considered. These options nevertheless represent a considerable technical challenge, both in terms of adapting propulsion systems and in terms of fuel storage capacity and safety (energy density, flammability risks, toxicity, pollution managemec.), as well as the required adaptation of airport infrastructure. Although the aviation sector has stated its ambition to achieve carbon neutrality by 2050, projects involving alternative propulsion aircraft are facing delays. For example, Airbus's ZEROe project, which aimed to bring short-haul a hydrogen-powered aircraft with a capacity of 100 passengers and a range 1,850 km into service by 2035, has recently been postponed by 5 to 10 years, and its budget has been reduced.
Finally, battery-electric aircraft do not appear to be a viable option for defossilising the aviation sector. Ongoing projects rather focus on developing new uses, particularly in and around urban centers.
In its Commitment to Fly Net Zero by 2050, the International Air Transport Association (IATA) states that around 65% of GHG reductions would be achieved through SAF, 13% through electric and hydrogen aircraft, 19% through carbon capture, and 3% through operational efficiency improvements. These 13% attributed to new aircraft technologies therefore represent only a limited contribution, which likely also reflects a lack of confidence in their large-scale deployment.
Efficiency Assumption
For our scenario, in view of the delays affecting the hydrogen aviation sector, we assume that by 2050 only, 5% of intra-European flights will be powered by hydrogen (representing less than 2% of total flights). The remaining flights are assumed to rely on liquid fuels such as kerosene, without requiring significant changes to propulsion systems. The PyPSA model will then autonomously determine the decarbonisation strategy (use of SAF, carbon capture).
# Inputs - Define Sufficiency Scenario Data (SUF)
sta_PM_avi_srt = 0 # [%]
end_PM_avi_srt = 5 # [%]
mid_PM_avi_srt = 2045
# Outputs - Sufficiency Scenario Data (SUF)
carriers_PM_avi_srt = {
'hydrogen': linear_with_middle_point(2019, sta_PM_avi_srt, mid_PM_avi_srt, sta_PM_avi_srt, 2050, end_PM_avi_srt, years), # [%]
'liquid-kerosene': [100-x for x in linear_with_middle_point(2019, sta_PM_avi_srt, mid_PM_avi_srt, sta_PM_avi_srt, 2050, end_PM_avi_srt, years)], # [%]
}
carriers_PM_avi_lng = {
'liquid-kerosene': linear_growth(2019, 100, 2050, 100, years), # [%]
}
df_PM_avi_srt = pd.DataFrame(carriers_PM_avi_srt, index=years).T.round(3)
df_PM_avi_lng = pd.DataFrame(carriers_PM_avi_lng, index=years).T.round(3)
# Processing - Carriers Shares (in %)
df_PM_ped = pd.DataFrame({'human': [100] * len(years)}, index=years).T
df_PM_carriers = {
'pedestrian': df_PM_ped,
'bicycle': df_PM_bic,
'two-wheeler': df_PM_mot,
'tram&metro': df_PM_trm_met,
'bus&coach': df_PM_bus_cch,
'car': df_PM_car,
'train-conventional': df_PM_trn_cnv,
'train-high speed': df_PM_trn_spd,
'plane-intra EU': df_PM_avi_srt,
'plane-extra EU': df_PM_avi_lng,
}
rows = []
for mode, df in df_PM_carriers.items():
temp = df.copy()
temp['Mode'] = mode
temp['Powertrain'] = temp.index
temp = temp.reset_index(drop=True)
rows.append(temp)
df_PM_carrier = pd.concat(rows, ignore_index=True)
if post_process:
df_carrier = df_PM_carrier.copy()
df_carrier['Mode'] = df_carrier['Mode'].mask(df_carrier['Mode'].duplicated(), '')
df_carrier_r = df_carrier[['Mode', 'Powertrain'] + list(years)]
styled = (
df_carrier_r.style
.hide(axis='index')
#.set_caption("Carrier Shares (%)")
.set_table_attributes('style="width:100%;table-layout:fixed;"')
.apply(highlight_mode_separator, axis=1)
.apply(lambda row: [bold_mode(cell, row['Mode'], col) for col, cell in zip(df_carrier_r.columns, row)], axis=1)
.set_properties(subset=['Powertrain'], **{'font-style':'italic'})
.format({year: "{:.2f}%" for year in years})
)
display(styled)
| Mode | Powertrain | 2019 | 2025 | 2030 | 2035 | 2040 | 2045 | 2050 |
|---|---|---|---|---|---|---|---|---|
| pedestrian | human | 100.00% | 100.00% | 100.00% | 100.00% | 100.00% | 100.00% | 100.00% |
| bicycle | electrical | 25.00% | 40.71% | 53.81% | 66.91% | 80.00% | 80.00% | 80.00% |
| mechanical | 75.00% | 59.29% | 46.19% | 33.09% | 20.00% | 20.00% | 20.00% | |
| two-wheeler | liquid-gasoline | 100.00% | 83.55% | 69.84% | 56.13% | 42.42% | 28.71% | 15.00% |
| electrical | 0.00% | 16.45% | 30.16% | 43.87% | 57.58% | 71.29% | 85.00% | |
| tram&metro | electrical | 100.00% | 100.00% | 100.00% | 100.00% | 100.00% | 100.00% | 100.00% |
| bus&coach | liquid-diesel | 99.27% | 94.48% | 79.38% | 47.57% | 18.50% | 6.38% | 3.00% |
| liquid-gasoline | 0.25% | 0.24% | 0.20% | 0.12% | 0.04% | 0.01% | 0.00% | |
| hydrogen | 0.00% | 0.25% | 1.03% | 2.69% | 4.20% | 4.83% | 5.00% | |
| gas-NG | 0.09% | 0.18% | 0.48% | 1.12% | 1.69% | 1.93% | 2.00% | |
| electrical | 0.39% | 4.85% | 18.91% | 48.51% | 75.57% | 86.86% | 90.00% | |
| car | liquid-diesel | 63.25% | 45.61% | 30.91% | 16.20% | 1.50% | 1.50% | 1.50% |
| liquid-gasoline | 35.00% | 46.58% | 45.56% | 31.24% | 18.97% | 5.89% | 1.50% | |
| gas-LPG | 0.69% | 0.56% | 0.45% | 0.33% | 0.22% | 0.11% | 0.00% | |
| gas-NG | 0.29% | 0.52% | 0.72% | 0.92% | 1.11% | 1.30% | 1.50% | |
| hybrid-plug-in | 0.53% | 3.72% | 6.00% | 3.75% | 0.90% | 0.07% | 0.00% | |
| electrical | 0.24% | 1.69% | 14.27% | 47.91% | 79.44% | 92.17% | 95.50% | |
| train-conventional | liquid-diesel | 4.39% | 2.74% | 1.37% | 0.00% | 0.00% | 0.00% | 0.00% |
| electrical | 95.61% | 97.26% | 98.63% | 100.00% | 100.00% | 100.00% | 100.00% | |
| train-high speed | electrical | 100.00% | 100.00% | 100.00% | 100.00% | 100.00% | 100.00% | 100.00% |
| plane-intra EU | hydrogen | 0.00% | 0.00% | 0.00% | 0.00% | 0.00% | 0.00% | 5.00% |
| liquid-kerosene | 100.00% | 100.00% | 100.00% | 100.00% | 100.00% | 100.00% | 95.00% | |
| plane-extra EU | liquid-kerosene | 100.00% | 100.00% | 100.00% | 100.00% | 100.00% | 100.00% | 100.00% |
In this section, the Gpkm associated with each powertrain and fuel are converted into equivalent TWh.
Comment: The modelling assumptions still need to be specified!*
The specific fuel consumption [kWh/km] for each propulsion type for 2019 is provided by JRC-IDEES [1,2]. The occucpancy ratio is defined as the ratio between the specific fuel consumption [kWh/km] and the activity energy intensity [kWh/pkm].
# ===== Bicycle =====
# -> Projections of efficiency and occupancy
cons_fuel_PM_bic = pd.DataFrame({
'electrical': linear_growth(2019, 1.00/100, 2050, 1.00/100, years), # [kWh/km] -> sta = https://ecoquery.ecoinvent.org/3.11/cutoff/dataset/7742/documentation, end = nW-BE
'mechanical': linear_growth(2019, 0.00/100, 2050, 0.00/100, years), # [kWh/km] -> sta = nW-BE
}, index=years).T
occupancy_PM_bic = pd.DataFrame({
'electrical': linear_growth(2019, 1, 2050, 1, years), # [p] -> sta = nW-BE, end = nW-BE
'mechanical': linear_growth(2019, 1, 2050, 1, years), # [p] -> sta = nW-BE, end = nW-BE
}, index=years).T
# -> Conversion to TWh
df_PM_bic_TWh = df_PM_carrier[df_PM_carrier['Mode'] == 'bicycle'].copy()
df_PM_bic_TWh[years] *= df_PM_GPKM.loc['bicycle', years].values/100 # Gpkm
df_PM_bic_TWh = df_PM_bic_TWh.set_index('Powertrain') # Gpkm
df_PM_bic_TWh[years] *= cons_fuel_PM_bic[years]/occupancy_PM_bic[years] # TWh
# -> Make total for sanity check
new_index = len(df_PM_bic_TWh)
df_PM_bic_TWh.loc[new_index, years] = df_PM_bic_TWh[years].sum()
# -> Clean and process
if 'Powertrain' not in df_PM_bic_TWh.columns: df_PM_bic_TWh = df_PM_bic_TWh.reset_index()
df_PM_bic_TWh = df_PM_bic_TWh.rename(columns={'index': 'Powertrain'})
last_row_index = df_PM_bic_TWh.index[-1]
df_PM_bic_TWh.loc[last_row_index, 'Powertrain'] = 'TOTAL'
df_PM_bic_TWh.loc[last_row_index, 'Mode'] = 'bicycle'
df_PM_bic_TWh = df_PM_bic_TWh[['Mode','Powertrain'] + years]
df_PM_bic_TWh = df_PM_bic_TWh.round(3)
# ===== Two-wheeler =====
# -> Settings
redu_fuel_PM_mot = 0.95
# -> Projections of efficiency and occupancy
cons_fuel_PM_mot = pd.DataFrame({
'liquid-gasoline': linear_growth(2019, 3.52/100*kgoe_to_kWh, 2050, 3.52/100*kgoe_to_kWh*redu_fuel_PM_mot, years), # [kWh/km] -> sta = JRC-IDEES, end = nW-BE
'electrical': linear_growth(2019, 6.00/100, 2050, 6.00/100, years), # [kWh/km] -> sta = 10.1088/1757-899X/1306/1/012032, end = nW-BE
}, index=years).T
occupancy_PM_mot = pd.DataFrame({
'liquid-gasoline': linear_growth(2019, 1.151, 2050, 1.151, years), # [p] -> sta = JRC-IDEES, end = nW-BE
'electrical': linear_growth(2019, 1.151, 2050, 1.151, years), # [p] -> sta = nW-BE, end = nW-BE
}, index=years).T
# -> Conversion to TWh
df_PM_mot_TWh = df_PM_carrier[df_PM_carrier['Mode'] == 'two-wheeler'].copy()
df_PM_mot_TWh[years] *= df_PM_GPKM.loc['two-wheeler', years].values/100 # Gpkm
df_PM_mot_TWh = df_PM_mot_TWh.set_index('Powertrain') # Gpkm
df_PM_mot_TWh[years] *= cons_fuel_PM_mot[years]/occupancy_PM_mot[years] # TWh
# -> Make total for sanity check
new_index = len(df_PM_mot_TWh)
df_PM_mot_TWh.loc[new_index, years] = df_PM_mot_TWh[years].sum()
# -> Clean and process
if 'Powertrain' not in df_PM_mot_TWh.columns: df_PM_mot_TWh = df_PM_mot_TWh.reset_index()
df_PM_mot_TWh = df_PM_mot_TWh.rename(columns={'index': 'Powertrain'})
last_row_index = df_PM_mot_TWh.index[-1]
df_PM_mot_TWh.loc[last_row_index, 'Powertrain'] = 'TOTAL'
df_PM_mot_TWh.loc[last_row_index, 'Mode'] = 'two-wheeler'
df_PM_mot_TWh = df_PM_mot_TWh[['Mode','Powertrain'] + years]
df_PM_mot_TWh = df_PM_mot_TWh.round(3)
# ===== Tram and metro =====
# -> Settings
redu_fuel_PM_trm_met = 0.95
occu_trgt_PM_trm_met = 1.20
# -> Projections of efficiency and occupancy
cons_fuel_PM_trm_met = pd.DataFrame({
'electrical': linear_growth(2019, 39.141/100*kgoe_to_kWh, 2050, 39.141/100*kgoe_to_kWh*redu_fuel_PM_trm_met, years), # [kWh/km] -> sta = JRC-IDEES, end = nW-BE
}, index=years).T
occupancy_PM_trm_met = pd.DataFrame({
'electrical': linear_growth(2019, 81.938, 2050, 81.938*occu_trgt_PM_trm_met, years), # [p] -> sta = JRC-IDEES, end = nW-BE
}, index=years).T
# -> Conversion to TWh
df_PM_trm_met_TWh = df_PM_carrier[df_PM_carrier['Mode'] == 'tram&metro'].copy()
df_PM_trm_met_TWh[years] *= df_PM_GPKM.loc['tram&metro', years].values/100 # Gpkm
df_PM_trm_met_TWh = df_PM_trm_met_TWh.set_index('Powertrain') # Gpkm
df_PM_trm_met_TWh[years] *= cons_fuel_PM_trm_met[years]/occupancy_PM_trm_met[years] # TWh
# -> Make total for sanity check
new_index = len(df_PM_trm_met_TWh)
df_PM_trm_met_TWh.loc[new_index, years] = df_PM_trm_met_TWh[years].sum()
# -> Clean and process
if 'Powertrain' not in df_PM_trm_met_TWh.columns: df_PM_trm_met_TWh = df_PM_trm_met_TWh.reset_index()
df_PM_trm_met_TWh = df_PM_trm_met_TWh.rename(columns={'index': 'Powertrain'})
last_row_index = df_PM_trm_met_TWh.index[-1]
df_PM_trm_met_TWh.loc[last_row_index, 'Powertrain'] = 'TOTAL'
df_PM_trm_met_TWh.loc[last_row_index, 'Mode'] = 'tram&metro'
df_PM_trm_met_TWh = df_PM_trm_met_TWh[['Mode','Powertrain'] + years]
df_PM_trm_met_TWh = df_PM_trm_met_TWh.round(3)
The specific fuel consumption [kWh/km] for each propulsion type for 2019 is provided by JRC-IDEES [1,2]. The occucpancy ratio is defined as the ratio between the specific fuel consumption [kWh/km] and the activity energy intensity [kWh/pkm].
Comment: The electricity consumption for BEV seems really high. In passenger cars, the ratio between diesel and electrical is about 3. For JRC-IDEES data, this is only 2.3. For electrical buses, studies report values of about 1-2 kWh/km (https://doi.org/10.1016/j.treng.2023.100223). We therefore select 2 kWh/km, instead of the 3.06 kWh/km reported by JRC-IDEES.
# ===== Bus and coach =====
# -> Settings
redu_fuel_PM_bus_cch = 0.95
occu_trgt_PM_bus_cch = 1.20
# -> Projections of efficiency and occupancy
cons_fuel_PM_bus_cch = pd.DataFrame({
'liquid-diesel': linear_growth(2019, 59.773/100*kgoe_to_kWh, 2050, 59.773/100*kgoe_to_kWh*redu_fuel_PM_bus_cch, years), # [kWh/km] -> sta = JRC-IDEES, end = nW-BE
'liquid-gasoline': linear_growth(2019, 18.065/100*kgoe_to_kWh, 2050, 18.065/100*kgoe_to_kWh*redu_fuel_PM_bus_cch, years), # [kWh/km] -> sta = JRC-IDEES, end = nW-BE
'hydrogen': linear_growth(2019, 8.000/100*kgh2_to_kWh, 2050, 8.000/100*kgh2_to_kWh*redu_fuel_PM_bus_cch, years), # [kWh/km] -> sta = https://doi.org/10.1016/j.ijhydene.2024.11.460, end = nW-BE
'gas-NG': linear_growth(2019, 64.939/100*kgoe_to_kWh, 2050, 64.939/100*kgoe_to_kWh*redu_fuel_PM_bus_cch, years), # [kWh/km] -> sta = JRC-IDEES, end = nW-BE
#'electrical': linear_growth(2019, 26.331/100*kgoe_to_kWh, 2050, 26.331/100*kgoe_to_kWh*redu_fuel_PM_bus_cch, years), # [kWh/km] -> sta = JRC-IDEES, end = nW-BE
'electrical': linear_growth(2019, 2.0, 2050, 2.0 *redu_fuel_PM_bus_cch, years), # [kWh/km] -> sta = https://doi.org/10.1016/j.treng.2023.100223, end = nW-BE
}, index=years).T
# Solaris Urbino 18 Hydrogen (city bus): 8.53kg/100km (51.2kg/600km, total tanks are 2.142m³, 51.2kg at 350 bar and 15°C)
# Irizar i6S Efficient Hydrogen (coach): NA kg/100km (NA kg/1000km)
# Daimler-Setra H2 Coach (coach): 5.75kg/100km (46.0kg/800km, total tanks are m³, 46.0kg at bar and °C)
occupancy_PM_bus_cch = pd.DataFrame({
'liquid-diesel': linear_growth(2019, 21.845, 2050, 21.845*occu_trgt_PM_bus_cch, years), # [p] -> sta = JRC-IDEES, end = nW-BE
'liquid-gasoline': linear_growth(2019, 8.369, 2050, 8.369*occu_trgt_PM_bus_cch, years), # [p] -> sta = JRC-IDEES, end = nW-BE
'hydrogen': linear_growth(2019, 21.845, 2050, 21.845*occu_trgt_PM_bus_cch, years), # [p] -> sta = nW-BE, end = nW-BE
'gas-NG': linear_growth(2019, 21.845, 2050, 21.845*occu_trgt_PM_bus_cch, years), # [p] -> sta = JRC-IDEES, end = nW-BE
'electrical': linear_growth(2019, 21.845, 2050, 21.845*occu_trgt_PM_bus_cch, years), # [p] -> sta = JRC-IDEES, end = nW-BE
}, index=years).T
# -> Conversion to TWh
df_PM_bus_cch_TWh = df_PM_carrier[df_PM_carrier['Mode'] == 'bus&coach'].copy()
df_PM_bus_cch_TWh[years] *= df_PM_GPKM.loc['bus&coach', years].values/100 # Gpkm
df_PM_bus_cch_TWh = df_PM_bus_cch_TWh.set_index('Powertrain') # Gpkm
df_PM_bus_cch_TWh[years] *= cons_fuel_PM_bus_cch[years]/occupancy_PM_bus_cch[years] # TWh
# -> Make total for sanity check
new_index = len(df_PM_bus_cch_TWh)
df_PM_bus_cch_TWh.loc[new_index, years] = df_PM_bus_cch_TWh[years].sum()
# -> Clean and process
if 'Powertrain' not in df_PM_bus_cch_TWh.columns: df_PM_bus_cch_TWh = df_PM_bus_cch_TWh.reset_index()
df_PM_bus_cch_TWh = df_PM_bus_cch_TWh.rename(columns={'index': 'Powertrain'})
last_row_index = df_PM_bus_cch_TWh.index[-1]
df_PM_bus_cch_TWh.loc[last_row_index, 'Powertrain'] = 'TOTAL'
df_PM_bus_cch_TWh.loc[last_row_index, 'Mode'] = 'bus&coach'
df_PM_bus_cch_TWh = df_PM_bus_cch_TWh[['Mode','Powertrain'] + years]
df_PM_bus_cch_TWh = df_PM_bus_cch_TWh.round(3)
The specific fuel consumption [kWh/km] for each propulsion type for 2019 is provided by JRC-IDEES [1,2]. The occucpancy ratio is defined as the ratio between the specific fuel consumption [kWh/km] and the activity energy intensity [kWh/pkm].
Accoring to JRC-IDEES [1,2], the share of electricity as a primary energy source in plug-in hybrid vehicles increased from around 40% in 2012 to nearly 60% in 2023. In this work, we therefore assume a 50/50 split to convert energy consumption into equivalent amounts of gasoline and electricity.
Sufficiency and Efficiency Assumptions
redu_fuel_PM_car multiplies the 2050 kWh/km of each propulsion type separately, so it
carries the energy needed to move the vehicle itself — speed, mass, aerodynamics, driving
style. The choice of propulsion is counted elsewhere, in the powertrain shares of section
2.2.5; this parameter is what comes on top of it.
Overall, -25% fuel consumption, from three ingredients that multiply:
redu_fuel_PM_car = redu_fuel_PM_car_speed × redu_fuel_PM_car_driving × redu_fuel_PM_car_size.
The total came first and is what the model uses; the split below is a reading of it, made so
that each ingredient can be discussed (and asked in the workshop) on its own. The first two are
taken from the literature, and the third is the residual, so the -25% and every result
downstream of it are unchanged.
| Ingredient | Multiplier | Cut | Basis |
|---|---|---|---|
Lower speeds (redu_fuel_PM_car_speed) |
0,95 | -5% | Motorways 120 → 100 km/h and rural roads 90 → 80 km/h, enforced; 30 km/h zones in town are roughly neutral for energy (Davis, 2018). A 90 km/h motorway limit cuts car energy per motorway km by 4-10% (KiM, 2023), and 120 → 90 km/h on the Liège-Antwerp motorway cut CO₂ by 8,4% (Degraeuwe et al., 2012, in Vias, 2017). But only ~38% of Belgian vehicle-km are driven on motorways (Vias, 2017, §2.5), so the effect over all trips is a few percent: 1,8-4,5% in KiM's own fleet estimate for the Netherlands, 1-6% of national car fuel per 10 km/h for the IEA (2026). Belgium's higher starting point (120 km/h) brings it to about 5%. |
Eco-driving (redu_fuel_PM_car_driving) |
0,93 | -7% | The lasting effect, not the day-one one: training gains fade (4,6% → 2,5% within ten months, Xu et al., 2021), while permanent in-car feedback keeps 5-10% over the long term (DfT, 2016); ECOWILL measured 14% on the training day and about 7,5% long-term. Assumes assistance is fitted to the whole fleet by 2050. Not higher, because it partly overlaps with lower speeds and regenerative braking shrinks the gain for electric cars. |
Smaller, lighter cars (redu_fuel_PM_car_size) |
0,849 (residual) | -15,1% | What is left to reach -25%. At 4% less energy per 100 kg (sens_mass_PM_car, mid-range of ICCT 2017's 2,8-4,9% and close to Del Pero et al. 2020's ~3,6% for electric cars, the smaller frontal area of a smaller car included), this is an average mass of about 1 050 kg (trg_mass_PM_car) against 1 430 kg for a new car sold in Belgium in 2019 (ref_mass_PM_car, EEA, mass in running order) — below the EU 2001 average of ~1 275 kg. Plausible only as real downsizing, not by removing SUVs alone: in the EU they use only ~10% more energy than a medium car (IEA, 2020). |
In tangible units (workshop, D64). The two physical ingredients are also asked in their own units, with linear conversions:
- speed: about 0,9% energy per km per km/h of average speed reduction (
sens_speed_PM_car), for a mostly electric 2050 fleet — an electric car's consumption rises by ~26% from 100 to 120 km/h (ADAC, 2026), ~1,1%/km/h on a test bench (Konzept et al., 2022); combustion cars are about half as sensitive (KiM, 2023). The scenario's -5% is therefore an average reduction of 5,6 km/h over all car-km (speed_cut_PM_car_kmh). For scale (kmh_PM_car_package): Belgian cars drive ~36% of their km on motorways, ~40% on regional roads and ~24% in town (SERV, 2007; Vias, 2017; IWEPS, 2024), at measured free-flow averages of 119, 69-93 and 38-52 km/h (Vias, 2021). A 100 km/h motorway limit lowers actual speeds there by ~13 km/h, i.e. -4,7 km/h averaged over all car-km; 80 instead of 90 on regional roads -3,3 km/h there (Vias, 2022), -1,3 km/h overall; 30 km/h everywhere in town lowers actual speeds by about a quarter of the 20 km/h (Davis, 2018), -1,2 km/h overall; - mass: 4% energy per 100 kg against the 2019 average of 1 430 kg (above).
Comment: the residual falls on the least documented ingredient, and the observed trend runs the other way (+19% mass for new EU cars between 2001 and 2022). It is the weak point of the -25%. Sources: KiM (2023), p. 10-11 and annex A; Vias (2017), §2.5 and §3.6.2; IEA (2026), Sheltering from oil shocks, measure 2; Davis (2018), Welsh Government, p. 10; Xu et al. (2021), Sensors, §4.1.3 and §4.2; DfT (2016), Efficient driving, p. iv-v; ECOWILL (2013), p. 7; ICCT (2017), Lightweighting, p. 2; ICCT (2023), Pocketbook 2023/24, p. 5; EEA, CO2 monitoring of new cars, 2019; Del Pero et al. (2020), Machines; ADAC, Elektroauto Reichweite; Konzept et al. (2022), Vehicles; Vias (2021), Nationale gedragsmeting snelheid, p. 9; Vias (2022), 90 naar 70 buiten de bebouwde kom, p. 7; SERV, Kerncijfers Vlaamse mobiliteit, tabel 6; IWEPS, Transport routier; IEA (2020), SUVs.
Note: earlier versions of this section also listed "more efficient power trains (including regenerative braking, non-plug-in hybrid)" among the ingredients of the -25%. That is removed: powertrain efficiency belongs with the propulsion mix, not with the energy required to move the car, and the workshop asks the two questions separately.
Sufficiency Assumption
Occupancy from about 1,2 to 2,0 thanks to carpooling.
# ===== Passenger Car =====
# -> Settings
share_elec_plug_in = 0.50
redu_fuel_PM_car = 0.75
# -> The three ingredients of redu_fuel_PM_car (see the markdown above). Each one
# multiplies the kWh/km of every powertrain. The first two are read from the
# literature; the third is what is left, so that their product -- and every
# result downstream of it -- stays exactly at redu_fuel_PM_car. Only the product
# enters the model; the workshop asks for the three separately (D61).
redu_fuel_PM_car_speed = 0.95 # lower speed limits, all roads together -> KiM (2023), Vias (2017), IEA (2026)
redu_fuel_PM_car_driving = 0.93 # eco-driving, lasting effect -> DfT (2016), ECOWILL (2013)
redu_fuel_PM_car_size = redu_fuel_PM_car / (redu_fuel_PM_car_speed * redu_fuel_PM_car_driving) # smaller, lighter cars (residual)
assert abs(redu_fuel_PM_car_speed * redu_fuel_PM_car_driving * redu_fuel_PM_car_size
- redu_fuel_PM_car) < 1e-12
# -> Two of the ingredients in tangible units, for the workshop (D64). Linear
# conversions only: the factors above remain the assumption.
# Speed: % energy per km for each km/h of average speed reduction, km-weighted over
# all car-km, for a mostly electric 2050 fleet (ADAC 2026; Konzept et al. 2022; KiM
# 2023 gives roughly half of it for combustion cars).
sens_speed_PM_car = 0.9
speed_cut_PM_car_kmh = (1 - redu_fuel_PM_car_speed) * 100 / sens_speed_PM_car # km/h
# Where such km/h can come from, averaged over all car-km: the share of car-km on each
# road type (SERV 2007, Vias 2017, IWEPS 2024) times the fall of the actual average
# speed when the limit is cut (Vias 2021 and 2022, Davis 2018).
share_km_PM_car_road = {'motorway': 0.36, 'rural': 0.40, 'urban': 0.24}
kmh_PM_car_package = {
'motorway': share_km_PM_car_road['motorway'] * 13.0, # 120 -> 100 km/h: 119 -> ~106 actual
'rural': share_km_PM_car_road['rural'] * 3.3, # 90 -> 80 km/h on regional roads
'urban': share_km_PM_car_road['urban'] * 5.0, # 50 -> 30 km/h in town: actual speed
} # falls by a quarter of the limit cut
# Mass: the 2019 average of new cars registered in Belgium, in running order (EEA CO2
# monitoring; no Belgian figure exists for the whole stock), and % energy per 100 kg
# for a smaller, lighter car -- mass and the smaller frontal area that comes with it
# (ICCT 2017: 2.8-4.9 %; Del Pero et al. 2020: ~3.6 % for electric cars).
ref_mass_PM_car = 1430.0
sens_mass_PM_car = 4.0
trg_mass_PM_car = ref_mass_PM_car - (1 - redu_fuel_PM_car_size) * 100 / sens_mass_PM_car * 100 # kg
assert 900 < trg_mass_PM_car < 1200, trg_mass_PM_car
occu_trgt_PM_car = 2.0
# -> 2019 occupancy per powertrain [persons/car], from JRC-IDEES [1,2].
# Named (instead of inlined below) so the workshop module can export them
# and cross-check them against the fleet average quoted in section 2.1.
# See docs/workshop_module.md.
ref_occu_PM_car = {
'liquid-diesel': 1.242,
'liquid-gasoline': 1.188,
'gas-LPG': 1.184,
'gas-NG': 1.184,
'hybrid-plug-in': 1.194,
'electrical': 1.099,
}
# -> Projections of efficiency and occupancy
cons_fuel_PM_car = pd.DataFrame({
'liquid-diesel': linear_growth(2019, 5.523/100*kgoe_to_kWh, 2050, 5.523/100*kgoe_to_kWh*redu_fuel_PM_car, years), # [kWh/km] -> sta = JRC-IDEES, end = nW-BE
'liquid-gasoline': linear_growth(2019, 5.798/100*kgoe_to_kWh, 2050, 5.798/100*kgoe_to_kWh*redu_fuel_PM_car, years), # [kWh/km] -> sta = JRC-IDEES, end = nW-BE
'gas-LPG': linear_growth(2019, 7.477/100*kgoe_to_kWh, 2050, 7.477/100*kgoe_to_kWh*redu_fuel_PM_car, years), # [kWh/km] -> sta = JRC-IDEES, end = nW-BE
'gas-NG': linear_growth(2019, 9.107/100*kgoe_to_kWh, 2050, 9.107/100*kgoe_to_kWh*redu_fuel_PM_car, years), # [kWh/km] -> sta = JRC-IDEES, end = nW-BE
'hybrid-plug-in': linear_growth(2019, 3.735/100*kgoe_to_kWh, 2050, 3.735/100*kgoe_to_kWh*redu_fuel_PM_car, years), # [kWh/km] -> sta = JRC-IDEES, end = nW-BE
'electrical': linear_growth(2019, 1.818/100*kgoe_to_kWh, 2050, 1.818/100*kgoe_to_kWh*redu_fuel_PM_car, years), # [kWh/km] -> sta = JRC-IDEES, end = nW-BE
}, index=years).T
occupancy_PM_car = pd.DataFrame({ # [p] -> sta = JRC-IDEES (ref_occu_PM_car), end = nW-BE
_pt: linear_growth(2019, _occ, 2050, occu_trgt_PM_car, years)
for _pt, _occ in ref_occu_PM_car.items()
}, index=years).T
# -> Conversion to TWh
df_PM_car_TWh = df_PM_carrier[df_PM_carrier['Mode'] == 'car'].copy()
df_PM_car_TWh[years] *= df_PM_GPKM.loc['car', years].values/100 # Gpkm
df_PM_car_TWh = df_PM_car_TWh.set_index('Powertrain') # Gpkm
df_PM_car_TWh[years] *= cons_fuel_PM_car[years]/occupancy_PM_car[years] # TWh
# -> Disaggregate hybrid plug-in
df_PM_car_TWh.loc['electrical', years] += df_PM_car_TWh.loc['hybrid-plug-in', years] * share_elec_plug_in
df_PM_car_TWh.loc['liquid-gasoline', years] += df_PM_car_TWh.loc['hybrid-plug-in', years] * (1-share_elec_plug_in)
df_PM_car_TWh = df_PM_car_TWh.drop('hybrid-plug-in')
# -> Make total for sanity check
new_index = len(df_PM_car_TWh)
df_PM_car_TWh.loc[new_index, years] = df_PM_car_TWh[years].sum()
# -> Clean and process
if 'Powertrain' not in df_PM_car_TWh.columns: df_PM_car_TWh = df_PM_car_TWh.reset_index()
df_PM_car_TWh = df_PM_car_TWh.rename(columns={'index': 'Powertrain'})
last_row_index = df_PM_car_TWh.index[-1]
df_PM_car_TWh.loc[last_row_index, 'Powertrain'] = 'TOTAL'
df_PM_car_TWh.loc[last_row_index, 'Mode'] = 'car'
df_PM_car_TWh = df_PM_car_TWh[['Mode','Powertrain'] + years]
df_PM_car_TWh = df_PM_car_TWh.round(3)
# ===== Train (conventional) =====
# -> Settings
redu_fuel_PM_trn_cnv = 0.90
occu_trgt_PM_trn_cnv = 1.20
# -> Projections of efficiency and occupancy
cons_fuel_PM_trn_cnv = pd.DataFrame({
'liquid-diesel': linear_growth(2019, 201.155/100*kgoe_to_kWh, 2050, 201.155/100*kgoe_to_kWh, years), # [kWh/km] -> sta = JRC-IDEES, end = nW-BE
'electrical': linear_growth(2019, 115.233/100*kgoe_to_kWh, 2050, 115.233/100*kgoe_to_kWh*redu_fuel_PM_trn_cnv, years), # [kWh/km] -> sta = JRC-IDEES, end = nW-BE
}, index=years).T
occupancy_PM_trn_cnv = pd.DataFrame({
'liquid-diesel': linear_growth(2019, 82.898, 2050, 82.898*occu_trgt_PM_trn_cnv, years), # [p] -> sta = JRC-IDEES, end = nW-BE
'electrical': linear_growth(2019, 131.549, 2050, 131.549*occu_trgt_PM_trn_cnv, years), # [p] -> sta = JRC-IDEES, end = nW-BE
}, index=years).T
# -> Conversion to TWh
df_PM_trn_cnv_TWh = df_PM_carrier[df_PM_carrier['Mode'] == 'train-conventional'].copy()
df_PM_trn_cnv_TWh[years] *= df_PM_GPKM.loc['train-conventional', years].values/100 # Gpkm
df_PM_trn_cnv_TWh = df_PM_trn_cnv_TWh.set_index('Powertrain') # Gpkm
df_PM_trn_cnv_TWh[years] *= cons_fuel_PM_trn_cnv[years]/occupancy_PM_trn_cnv[years] # TWh
# -> Make total for sanity check
new_index = len(df_PM_trn_cnv_TWh)
df_PM_trn_cnv_TWh.loc[new_index, years] = df_PM_trn_cnv_TWh[years].sum()
# -> Clean and process
if 'Powertrain' not in df_PM_trn_cnv_TWh.columns: df_PM_trn_cnv_TWh = df_PM_trn_cnv_TWh.reset_index()
df_PM_trn_cnv_TWh = df_PM_trn_cnv_TWh.rename(columns={'index': 'Powertrain'})
last_row_index = df_PM_trn_cnv_TWh.index[-1]
df_PM_trn_cnv_TWh.loc[last_row_index, 'Powertrain'] = 'TOTAL'
df_PM_trn_cnv_TWh.loc[last_row_index, 'Mode'] = 'train-conventional'
df_PM_trn_cnv_TWh = df_PM_trn_cnv_TWh[['Mode','Powertrain'] + years]
df_PM_trn_cnv_TWh = df_PM_trn_cnv_TWh.round(3)
# ===== Train (high speed) =====
# -> Settings
redu_fuel_PM_trn_spd = 0.90
occu_trgt_PM_trn_spd = 1.05
# -> Projections of efficiency and occupancy
cons_fuel_PM_trn_spd = pd.DataFrame({
'electrical': linear_growth(2019, 213.694/100*kgoe_to_kWh, 2050, 213.694/100*kgoe_to_kWh*redu_fuel_PM_trn_spd, years), # [kWh/km] -> sta = JRC-IDEES, end = nW-BE
}, index=years).T
occupancy_PM_trn_spd = pd.DataFrame({
'electrical': linear_growth(2019, 302.477, 2050, 302.477*occu_trgt_PM_trn_spd, years), # [p] -> sta = JRC-IDEES, end = nW-BE
}, index=years).T
# -> Conversion to TWh
df_PM_trn_spd_TWh = df_PM_carrier[df_PM_carrier['Mode'] == 'train-high speed'].copy()
df_PM_trn_spd_TWh[years] *= df_PM_GPKM.loc['train-high speed', years].values/100 # Gpkm
df_PM_trn_spd_TWh = df_PM_trn_spd_TWh.set_index('Powertrain') # Gpkm
df_PM_trn_spd_TWh[years] *= cons_fuel_PM_trn_spd[years]/occupancy_PM_trn_spd[years] # TWh
# -> Make total for sanity check
new_index = len(df_PM_trn_spd_TWh)
df_PM_trn_spd_TWh.loc[new_index, years] = df_PM_trn_spd_TWh[years].sum()
# -> Clean and process
if 'Powertrain' not in df_PM_trn_spd_TWh.columns: df_PM_trn_spd_TWh = df_PM_trn_spd_TWh.reset_index()
df_PM_trn_spd_TWh = df_PM_trn_spd_TWh.rename(columns={'index': 'Powertrain'})
last_row_index = df_PM_trn_spd_TWh.index[-1]
df_PM_trn_spd_TWh.loc[last_row_index, 'Powertrain'] = 'TOTAL'
df_PM_trn_spd_TWh.loc[last_row_index, 'Mode'] = 'train-high speed'
df_PM_trn_spd_TWh = df_PM_trn_spd_TWh[['Mode','Powertrain'] + years]
df_PM_trn_spd_TWh = df_PM_trn_spd_TWh.round(3)
# ===== Aviation (intra EU) =====
# -> Settings
redu_fuel_PM_avi_intra = 1.05 # + 9.4% from 2000 to 2023 (from 578.8 kgoe/100km to 633.0 kgoe/100km)
occu_trgt_PM_avi_intra = 1.25 # +49.5% from 2000 to 2023 (from 87.5p to 130.8p)
# -> Projections of efficiency and occupancy
cons_fuel_PM_avi_intra = pd.DataFrame({
'hydrogen': linear_growth(2019, 130.000/100*kgh2_to_kWh, 2050, 130.000/100*kgh2_to_kWh, years), # [kWh/km] -> sta = Airbus Zero-E, end = nW-BE
'liquid-kerosene': linear_growth(2019, 593.771/100*kgoe_to_kWh, 2050, 593.771/100*kgoe_to_kWh*redu_fuel_PM_avi_intra, years), # [kWh/km] -> sta = JRC-IDEES, end = nW-BE
}, index=years).T
occupancy_PM_avi_intra = pd.DataFrame({
'hydrogen': linear_growth(2019, 100.000, 2050, 100.000, years), # [p] -> sta = Airbus Zero-E, end = nW-BE
'liquid-kerosene': linear_growth(2019, 121.927, 2050, 121.927*occu_trgt_PM_avi_intra, years), # [p] -> sta = JRC-IDEES, end = nW-BE
}, index=years).T
# -> Conversion to TWh
df_PM_avi_srt_TWh = df_PM_carrier[df_PM_carrier['Mode'] == 'plane-intra EU'].copy()
df_PM_avi_srt_TWh[years] *= df_PM_GPKM.loc['plane-intra EU', years].values/100 # Gpkm
df_PM_avi_srt_TWh = df_PM_avi_srt_TWh.set_index('Powertrain') # Gpkm
df_PM_avi_srt_TWh[years] *= cons_fuel_PM_avi_intra[years]/occupancy_PM_avi_intra[years] # TWh
# -> Make total for sanity check
new_index = len(df_PM_avi_srt_TWh)
df_PM_avi_srt_TWh.loc[new_index, years] = df_PM_avi_srt_TWh[years].sum()
# -> Clean and process
if 'Powertrain' not in df_PM_avi_srt_TWh.columns: df_PM_avi_srt_TWh = df_PM_avi_srt_TWh.reset_index()
df_PM_avi_srt_TWh = df_PM_avi_srt_TWh.rename(columns={'index': 'Powertrain'})
last_row_index = df_PM_avi_srt_TWh.index[-1]
df_PM_avi_srt_TWh.loc[last_row_index, 'Powertrain'] = 'TOTAL'
df_PM_avi_srt_TWh.loc[last_row_index, 'Mode'] = 'plane-intra EU'
df_PM_avi_srt_TWh = df_PM_avi_srt_TWh[['Mode','Powertrain'] + years]
df_PM_avi_srt_TWh = df_PM_avi_srt_TWh.round(3)
# ===== Aviation (extra EU) =====
# -> Settings
redu_fuel_PM_avi_extra = 0.84 # -32.8% from 2000 to 2023 (from 937.0 kgoe/100km to 629.5 kgoe/100km)
occu_trgt_PM_avi_extra = 1.17 # +33.7% from 2000 to 2023 (from 154.0p to 205.5p)
# -> Projections of efficiency and occupancy
cons_fuel_PM_avi_extra = pd.DataFrame({
'liquid-kerosene': linear_growth(2019, 578.489/100*kgoe_to_kWh, 2050, 578.489/100*kgoe_to_kWh*redu_fuel_PM_avi_extra, years), # [kWh/km] -> sta = JRC-IDEES, end = nW-BE
}, index=years).T
occupancy_PM_avi_extra = pd.DataFrame({
'liquid-kerosene': linear_growth(2019, 187.817, 2050, 187.817*occu_trgt_PM_avi_extra, years), # [p] -> sta = JRC-IDEES, end = nW-BE
}, index=years).T
# -> Conversion to TWh
df_PM_avi_lng_TWh = df_PM_carrier[df_PM_carrier['Mode'] == 'plane-extra EU'].copy()
df_PM_avi_lng_TWh[years] *= df_PM_GPKM.loc['plane-extra EU', years].values/100 # Gpkm
df_PM_avi_lng_TWh = df_PM_avi_lng_TWh.set_index('Powertrain') # Gpkm
df_PM_avi_lng_TWh[years] *= cons_fuel_PM_avi_extra[years]/occupancy_PM_avi_extra[years] # TWh
# -> Make total for sanity check
new_index = len(df_PM_avi_lng_TWh)
df_PM_avi_lng_TWh.loc[new_index, years] = df_PM_avi_lng_TWh[years].sum()
# -> Clean and process
if 'Powertrain' not in df_PM_avi_lng_TWh.columns: df_PM_avi_lng_TWh = df_PM_avi_lng_TWh.reset_index()
df_PM_avi_lng_TWh = df_PM_avi_lng_TWh.rename(columns={'index': 'Powertrain'})
last_row_index = df_PM_avi_lng_TWh.index[-1]
df_PM_avi_lng_TWh.loc[last_row_index, 'Powertrain'] = 'TOTAL'
df_PM_avi_lng_TWh.loc[last_row_index, 'Mode'] = 'plane-extra EU'
df_PM_avi_lng_TWh = df_PM_avi_lng_TWh[['Mode','Powertrain'] + years]
df_PM_avi_lng_TWh = df_PM_avi_lng_TWh.round(3)
df_PM_TWh_lst = [df_PM_bic_TWh,
df_PM_mot_TWh,
df_PM_trm_met_TWh,
df_PM_bus_cch_TWh,
df_PM_car_TWh,
df_PM_trn_cnv_TWh,
df_PM_trn_spd_TWh,
df_PM_avi_srt_TWh,
df_PM_avi_lng_TWh
]
df_PM_TWh_all = pd.concat(df_PM_TWh_lst, ignore_index=True)
df_PM_TWh_flt = df_PM_TWh_all[~df_PM_TWh_all['Powertrain'].isin(['TOTAL', 'mechanical'])].copy()
df_PM_TWh_agr = df_PM_TWh_flt.groupby('Powertrain').sum(numeric_only=True)
custom_order = [
'electrical',
'liquid-gasoline',
'liquid-diesel',
'liquid-kerosene',
'gas-NG',
'gas-LPG',
'hydrogen'
]
df_PM_TWh_agr = df_PM_TWh_agr.reindex(custom_order)
if post_process:
# === Full table ===
df_fec = df_PM_TWh_all
df_fec['Mode'] = df_fec['Mode'].mask(df_fec['Mode'].duplicated(), '')
df_fec_r = df_fec[['Mode', 'Powertrain'] + list(years)]
styled = (
df_fec_r.style
.hide(axis='index')
#.set_caption("Carrier Shares (%)")
.set_table_attributes('style="width:100%;table-layout:fixed;"')
.apply(highlight_mode_separator, axis=1)
.apply(lambda row: [bold_mode(cell, row['Mode'], col) for col, cell in zip(df_fec_r.columns, row)], axis=1)
.set_properties(subset=['Powertrain'], **{'font-style':'italic'})
.format({year: "{:.3f} TWh" for year in years})
)
display(styled)
# === Breakdown bar chart - absolute values ===
dfmp = df_PM_TWh_agr.transpose()
fig, ax = plt.subplots(figsize=(14, 5))
bottom = np.zeros(len(dfmp))
colors = plt.cm.tab10.colors # default matplotlib palette
# Thicker bars → control via height argument
bar_height = 2.0 # default is ~0.8; increase to 0.9–1.0 for thicker bars
j = 0
for i, fuel in enumerate(dfmp.columns):
ax.barh(dfmp.index, dfmp[fuel],
left=bottom, label=fuel,
color=colors[j % len(colors)],
height=bar_height)
bottom += dfmp[fuel]
j += 1
ax.set_xlabel("Final Energy Consumption [TWh]", fontsize=10, ha='right', va='top')
ax.xaxis.set_label_coords(1, -0.1)
ax.set_ylabel("Years", fontsize=10, rotation=0, ha='left', va='bottom')
ax.yaxis.set_label_coords(-0.05, 1)
ax.set_yticks(years)
ax.yaxis.set_inverted(True) # inverted axis with autoscaling
ax.set_title("Evolution of Energy Consumption for Passenger Mobility (negaWatt-BE Scenario)", fontsize=11, pad=15)
ax.legend(bbox_to_anchor=(1, 1), loc='upper left', title="Carrier", frameon=False)
ax.grid(axis='x', linestyle='--', alpha=0.6)
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)
ax.spines['bottom'].set_visible(False)
ax.spines['left'].set_visible(False)
fig.tight_layout()
plt.show()
| Mode | Powertrain | 2019 | 2025 | 2030 | 2035 | 2040 | 2045 | 2050 |
|---|---|---|---|---|---|---|---|---|
| bicycle | electrical | 0.017 TWh | 0.035 TWh | 0.054 TWh | 0.076 TWh | 0.101 TWh | 0.111 TWh | 0.120 TWh |
| mechanical | 0.000 TWh | 0.000 TWh | 0.000 TWh | 0.000 TWh | 0.000 TWh | 0.000 TWh | 0.000 TWh | |
| TOTAL | 0.017 TWh | 0.035 TWh | 0.054 TWh | 0.076 TWh | 0.101 TWh | 0.111 TWh | 0.120 TWh | |
| two-wheeler | liquid-gasoline | 0.622 TWh | 0.591 TWh | 0.538 TWh | 0.466 TWh | 0.376 TWh | 0.270 TWh | 0.148 TWh |
| electrical | 0.000 TWh | 0.017 TWh | 0.035 TWh | 0.055 TWh | 0.077 TWh | 0.102 TWh | 0.130 TWh | |
| TOTAL | 0.622 TWh | 0.608 TWh | 0.573 TWh | 0.521 TWh | 0.454 TWh | 0.372 TWh | 0.278 TWh | |
| tram&metro | electrical | 0.082 TWh | 0.115 TWh | 0.139 TWh | 0.161 TWh | 0.180 TWh | 0.198 TWh | 0.214 TWh |
| TOTAL | 0.082 TWh | 0.115 TWh | 0.139 TWh | 0.161 TWh | 0.180 TWh | 0.198 TWh | 0.214 TWh | |
| bus&coach | liquid-diesel | 4.322 TWh | 4.684 TWh | 4.266 TWh | 2.728 TWh | 1.120 TWh | 0.404 TWh | 0.197 TWh |
| liquid-gasoline | 0.009 TWh | 0.009 TWh | 0.008 TWh | 0.005 TWh | 0.002 TWh | 0.000 TWh | 0.000 TWh | |
| hydrogen | 0.000 TWh | 0.005 TWh | 0.021 TWh | 0.059 TWh | 0.097 TWh | 0.117 TWh | 0.126 TWh | |
| gas-NG | 0.004 TWh | 0.010 TWh | 0.028 TWh | 0.070 TWh | 0.111 TWh | 0.133 TWh | 0.143 TWh | |
| electrical | 0.005 TWh | 0.069 TWh | 0.292 TWh | 0.800 TWh | 1.317 TWh | 1.584 TWh | 1.701 TWh | |
| TOTAL | 4.340 TWh | 4.777 TWh | 4.617 TWh | 3.662 TWh | 2.648 TWh | 2.238 TWh | 2.167 TWh | |
| car | liquid-diesel | 35.128 TWh | 20.707 TWh | 11.792 TWh | 5.185 TWh | 0.403 TWh | 0.338 TWh | 0.281 TWh |
| liquid-gasoline | 21.439 TWh | 23.537 TWh | 19.471 TWh | 11.079 TWh | 5.484 TWh | 1.402 TWh | 0.295 TWh | |
| gas-LPG | 0.545 TWh | 0.354 TWh | 0.236 TWh | 0.147 TWh | 0.082 TWh | 0.034 TWh | 0.000 TWh | |
| gas-NG | 0.279 TWh | 0.406 TWh | 0.464 TWh | 0.491 TWh | 0.497 TWh | 0.486 TWh | 0.463 TWh | |
| electrical | 0.153 TWh | 0.862 TWh | 2.704 TWh | 5.680 TWh | 7.313 TWh | 6.908 TWh | 5.907 TWh | |
| TOTAL | 57.543 TWh | 45.867 TWh | 34.666 TWh | 22.583 TWh | 13.779 TWh | 9.168 TWh | 6.946 TWh | |
| train-conventional | liquid-diesel | 0.129 TWh | 0.099 TWh | 0.056 TWh | 0.000 TWh | 0.000 TWh | 0.000 TWh | 0.000 TWh |
| electrical | 1.016 TWh | 1.237 TWh | 1.397 TWh | 1.540 TWh | 1.647 TWh | 1.739 TWh | 1.812 TWh | |
| TOTAL | 1.146 TWh | 1.335 TWh | 1.453 TWh | 1.540 TWh | 1.647 TWh | 1.739 TWh | 1.812 TWh | |
| train-high speed | electrical | 0.128 TWh | 0.178 TWh | 0.217 TWh | 0.252 TWh | 0.284 TWh | 0.314 TWh | 0.341 TWh |
| TOTAL | 0.128 TWh | 0.178 TWh | 0.217 TWh | 0.252 TWh | 0.284 TWh | 0.314 TWh | 0.341 TWh | |
| plane-intra EU | hydrogen | 0.000 TWh | 0.000 TWh | 0.000 TWh | 0.000 TWh | 0.000 TWh | 0.000 TWh | 0.175 TWh |
| liquid-kerosene | 8.831 TWh | 7.822 TWh | 6.952 TWh | 6.108 TWh | 5.312 TWh | 4.557 TWh | 3.645 TWh | |
| TOTAL | 8.831 TWh | 7.822 TWh | 6.952 TWh | 6.108 TWh | 5.312 TWh | 4.557 TWh | 3.819 TWh | |
| plane-extra EU | liquid-kerosene | 7.420 TWh | 6.601 TWh | 5.904 TWh | 5.239 TWh | 4.625 TWh | 4.055 TWh | 3.524 TWh |
| TOTAL | 7.420 TWh | 6.601 TWh | 5.904 TWh | 5.239 TWh | 4.625 TWh | 4.055 TWh | 3.524 TWh |
# Inputs - Modal Repartition [Gtkm] for reference year (2019)
ref_FT_mod_abs = {
'truck-light commercial': 1.09462, # From JRC-IDEES
'truck-heavy duty': 52.43700, # From JRC-IDEES
'train': 14.70000, # From JRC-IDEES
'plane-intra EU': 0.27957, # From JRC-IDEES
'plane-extra EU': 3.70473, # From JRC-IDEES
'navigation-coastal': 0.19274, # From JRC-IDEES
'navigation-inland': 7.76700, # From JRC-IDEES
}
ref_FT_abs = sum(ref_FT_mod_abs.values())
if abs((ref_FT_abs-df_SUF["FT total [Gtkm]"][2019])/df_SUF["FT total [Gtkm]"][2019]*100) > 1e-5:
print("Entry values in 'ref_FT_mod_abs' are not correct!")
# Outputs - Modal Shares [%] and Modal Repartition [tkm/person] for reference year (2019)
ref_FT_mod_rel = {k: v/ref_FT_abs*100 for k, v in ref_FT_mod_abs.items()} # [%]
ref_FT_mod_spe = {k: v/df_SUF["population [person]"][2019]*1e+9 for k, v in ref_FT_mod_abs.items()} # [tkm/person]
Comment: For trucks, Statbel gives 34,829 Gtkm, combining national and international transport (see https://statbel.fgov.be/en/themes/mobility/transport/road-freight-transport#panel-11). This huge discrepency (+60% for JRC-IDEES) should be further investigated! This could be due to the fact that Statbel only considers 1ton+ vehicles registered in Belgium. Instead, the Federal Planning Bureau gives 49,1 Gtkm [5]. This is much closer to JRC-IDEES.
Comment: For trains, the value reported by JRC-IDEES [1,2] corresponds to the gross tonnage. Gross tonnage includes both the weight of the rolling stock (locomotive, wagons, container carriers) and the weight of the transported goods (including the weight of the container in the case of container transport). The Belgian Federal Public Service for Mobility and Transport gives 14,70385 Gtkm (https://mobilit.belgium.be/fr/mobilite-durable/enquetes-et-resultats/chiffres-cles-de-la-mobilite, data from Infrabel), which corresponds to the value from JRC-IDEES. While we get a modal share of 18,3%, the Vision Rail report claim that it was 12,1% (from Eurostat, https://ec.europa.eu/eurostat/databrowser/view/tran_hv_frmod/default/table) [7]. Knowing that they exlude aviation, this is even less when compared to our data. The difference might come from Gtkm by truck. The Federal Planning Bureau gives 6,5 Gtkm [5]. This is much less than JRC-IDEES! The difference should be further investigated.
Comment: For inland navigation, the Belgian Federal Public Service for Mobility and Transport gives 7,77650 Gtkm (https://mobilit.belgium.be/fr/mobilite-durable/enquetes-et-resultats/chiffres-cles-de-la-mobilite, data from Statbel, https://statbel.fgov.be/fr/themes/mobilite/transport/navigation-interieure#panel-12), which is the same as JRC-IDEES! The Federal Planning Bureau gives 7,9 Gtkm [5], which is also close to JRC-IDEES.
Global Sufficiency Assumption
By 2019, 66,8% of all ton-kilometres were made on the road. The remaining 33,2% were shared among rail (18,3%), domestic navigation (9,9%), and aviation (5,0%).
After aviation, road transport is the second most energy-intensive mode, and therefore greenhouse-gas-emitting. It is thus essential to operate modal shift towards less energy intensive modes, such as rail and navigation.
Although fossil-fuelled trucks should gradually be replaced by battery electric vehicles, we believe that replacing all internal combustion engine trucks with electric trucks without operating any modal shift is not desirable for several reasons, the main ones being (i) the pressure on raw materials (some of which are critical and also needed for other clean technologies) and (ii) the very large land footprint of truck-based transport.
We therefore set an overall objective of achieving a modal shift for 25% of the heavy duty trucks towards trains and inland waterways. More details are provided below.
Comment: We should maybe consider reductions of aviation intensity, and other modal shifts.
Accounting for the overall -10% reduction in transport intensity between 2019 and 2050 (-701 tkm/person), operating a modal shift for 25% the transport intensity of heavy duty trucks by 2050 means shifting from 4.587 tkm/person in 2019 to 3.096 tkm/person (-1.491 tkm/person). The modal share of heavy duty trucks hence decreases from 65,4% in 2019 to 49,1% in 2050.
# Inputs - Define Sufficiency Scenario Data (SUF)
pro_FT_spe_trk_hvy = -0.25
# Outputs - Sufficiency Scenario Data (SUF)
trg_FT_mod_spe = {'truck-heavy duty': (1+pro_FT_spe_trk_hvy)*(1+pro_FT_spe)*ref_FT_mod_spe['truck-heavy duty']} # [tkm/person]
trg_FT_mod_abs = {'truck-heavy duty': trg_FT_mod_spe['truck-heavy duty']*df_SUF["population [person]"][2050]*1e-9} # [Gtkm]
trg_FT_mod_rel = {'truck-heavy duty': trg_FT_mod_spe['truck-heavy duty']/df_SUF["FT intensity [tkm/person]"][2050]*100} # [%]
Of the 25% of heavy-duty truck tonne-kilometres to be shifted by 2050 (1.032 tkm/person), we project that 15% will be shifted to rail (+619 tkm/person) and 10% to inland waterways (+413 tkm/person).
# Inputs - Define Sufficiency Scenario Data (SUF)
sft_FT_rel_trk_hvy_to_trn = +0.15
sft_FT_rel_trk_hvy_to_nav_ild = +0.10
if abs(pro_FT_spe_trk_hvy+sft_FT_rel_trk_hvy_to_trn+sft_FT_rel_trk_hvy_to_nav_ild) > 1e-15:
print("There is an error in the modal shift for the 2050 heavy duty trucking!")
# Modal shit - Report to other modes
sft_FT_abs_trk_hvy_to_trn = sft_FT_rel_trk_hvy_to_trn *(1+pro_FT_spe)*ref_FT_mod_spe['truck-heavy duty']
sft_FT_abs_trk_hvy_to_nav_ild = sft_FT_rel_trk_hvy_to_nav_ild*(1+pro_FT_spe)*ref_FT_mod_spe['truck-heavy duty']
Comment: Check that these calculations are correct and the modal shifts are effectively reported below.
As regards light commercial vehicles, we do not, at this stage, envisage any modal shift.
# Outputs - Sufficiency Scenario Data (SUF)
trg_FT_mod_spe['truck-light commercial'] = (1+pro_FT_spe)*ref_FT_mod_spe['truck-light commercial'] # [tkm/person]
trg_FT_mod_abs['truck-light commercial'] = trg_FT_mod_spe['truck-light commercial']*df_SUF["population [person]"][2050]*1e-9 # [Gtkm]
trg_FT_mod_rel['truck-light commercial'] = trg_FT_mod_spe['truck-light commercial']/df_SUF["FT intensity [tkm/person]"][2050]*100 # [%]
# Outputs - Sufficiency Scenario Data (SUF)
trg_FT_mod_spe['train'] = (1+pro_FT_spe)*ref_FT_mod_spe['train']+sft_FT_abs_trk_hvy_to_trn # [tkm/person]
trg_FT_mod_abs['train'] = trg_FT_mod_spe['train']*df_SUF["population [person]"][2050]*1e-9 # [Gtkm]
trg_FT_mod_rel['train'] = trg_FT_mod_spe['train']/df_SUF["FT intensity [tkm/person]"][2050]*100 # [%]
Comment: This growth may appear ambitious, given that this intensity has on average declined between 2000 and 2023.
# Outputs - Sufficiency Scenario Data (SUF)
trg_FT_mod_spe['navigation-inland'] = (1+pro_FT_spe)*ref_FT_mod_spe['navigation-inland']+sft_FT_abs_trk_hvy_to_nav_ild # [tkm/person]
trg_FT_mod_abs['navigation-inland'] = trg_FT_mod_spe['navigation-inland']*df_SUF["population [person]"][2050]*1e-9 # [Gtkm]
trg_FT_mod_rel['navigation-inland'] = trg_FT_mod_spe['navigation-inland']/df_SUF["FT intensity [tkm/person]"][2050]*100 # [%]
Comment: This growth may appear ambitious, given that this intensity has on average declined by more than -15,7% between 2000 and 2023, decreasing from 705 tkm/person to 594 tkm/person.
However, it should be noted that a peak of 980 tkm/person was reached in 2017, meaning that the intensity increased by 39.1% between 2000 and 2017. This tends to confirm that an increase of +20% is entirely realistic, especially since it is also driven by a relocation of industrial activities.
As regards coastal shiping, we do not, at this stage, envisage any modal shift.
# Outputs - Sufficiency Scenario Data (SUF)
trg_FT_mod_spe['navigation-coastal'] = (1+pro_FT_spe)*ref_FT_mod_spe['navigation-coastal'] # [tkm/person]
trg_FT_mod_abs['navigation-coastal'] = trg_FT_mod_spe['navigation-coastal']*df_SUF["population [person]"][2050]*1e-9 # [Gtkm]
trg_FT_mod_rel['navigation-coastal'] = trg_FT_mod_spe['navigation-coastal']/df_SUF["FT intensity [tkm/person]"][2050]*100 # [%]
Air freight is small in tonne-kilometres — 5,0% of the 2019 total — but it is by far the most energy-intensive mode of the whole freight system, at about 0,90 kWh/tkm in 2019 against 0,46 for a heavy truck, 0,20 for inland navigation and 0,04 for rail. It is also the only freight flow that the scenario leaves essentially untouched: with no assumption of its own it simply follows the overall -10% reduction in transport intensity, so it still accounts for roughly a third of the international mobility energy demand in 2050.
pro_FT_spe_avi is the change in air-freight tonne-kilometres per person on top of that overall
reduction, and sft_FT_rel_avi_to_trn sends the corresponding tonne-kilometres to rail —
the one alternative the model represents and which exists in practice on the Eurasian corridor
(the China-Europe rail freight services). Deep-sea shipping, the other real alternative, is not
represented in the demand model at all, so it cannot be a destination here.
Sufficiency Assumption
At this stage we envisage no modal shift for air freight: both parameters are zero, and the scenario's numbers are exactly what they were before these parameters existed. They are written explicitly rather than left implicit so that the assumption is visible, so that a sensitivity run needs one number rather than a rewrite, and so that the interactive workshop can ask the question.
# Inputs - Define Sufficiency Scenario Data (SUF)
pro_FT_spe_avi = -0.00 # [-] change in air-freight tkm/person, on top of pro_FT_spe
sft_FT_rel_avi_to_trn = +0.00 # [-] share of it reported to rail
if abs(pro_FT_spe_avi+sft_FT_rel_avi_to_trn) > 1e-15:
print("There is an error in the modal shift for the 2050 air freight!")
# Modal shift - Report to other modes
ref_FT_mod_spe_avi = ref_FT_mod_spe['plane-intra EU'] + ref_FT_mod_spe['plane-extra EU']
sft_FT_abs_avi_to_trn = sft_FT_rel_avi_to_trn*(1+pro_FT_spe)*ref_FT_mod_spe_avi
# Outputs - Sufficiency Scenario Data (SUF)
for mode_FT_avi in ['plane-intra EU', 'plane-extra EU']:
trg_FT_mod_spe[mode_FT_avi] = (1+pro_FT_spe_avi)*(1+pro_FT_spe)*ref_FT_mod_spe[mode_FT_avi] # [tkm/person]
trg_FT_mod_abs[mode_FT_avi] = trg_FT_mod_spe[mode_FT_avi]*df_SUF["population [person]"][2050]*1e-9 # [Gtkm]
trg_FT_mod_rel[mode_FT_avi] = trg_FT_mod_spe[mode_FT_avi]/df_SUF["FT intensity [tkm/person]"][2050]*100 # [%]
# The tonne-kilometres taken off aviation land on rail, whose target was computed in 3.1.2:
# it is updated here rather than there so that this assumption stays in one place.
trg_FT_mod_spe['train'] += sft_FT_abs_avi_to_trn # [tkm/person]
trg_FT_mod_abs['train'] = trg_FT_mod_spe['train']*df_SUF["population [person]"][2050]*1e-9 # [Gtkm]
trg_FT_mod_rel['train'] = trg_FT_mod_spe['train']/df_SUF["FT intensity [tkm/person]"][2050]*100 # [%]
# The modal shares must still add up to the freight intensity they are shares of.
# The tolerance is loose on purpose: the denominator is df_SUF's rounded freight
# intensity, so the sum lands a few 1e-6 off 100 even when the shift balances
# exactly. A real imbalance would be off by whole percentage points.
if abs(sum(trg_FT_mod_rel.values())-100) > 1e-3:
print("The 2050 freight modal shares no longer add up to 100%!")
Comment: A modal shift for fast deliveries is difficult, which is why the parameters above are written as a reduction in intensity with rail as the destination rather than as a shift between two equivalent modes. The two candidates for a real reduction are the goods that do not need to fly (e-commerce parcels, fresh produce out of season) and the Eurasian rail corridor.
We assume a linear evolution of modal shares [%] between the reference year (2019) and the end of the transition period (2050).
# Processing - Modal Shares (in %)
modes_FT = {
'train': linear_growth(2019,ref_FT_mod_rel['train'],
2050,trg_FT_mod_rel['train'], years),
'navigation-coastal': linear_growth(2019,ref_FT_mod_rel['navigation-coastal'],
2050,trg_FT_mod_rel['navigation-coastal'], years),
'navigation-inland': linear_growth(2019,ref_FT_mod_rel['navigation-inland'],
2050,trg_FT_mod_rel['navigation-inland'], years),
'truck-light commercial': linear_growth(2019,ref_FT_mod_rel['truck-light commercial'],
2050,trg_FT_mod_rel['truck-light commercial'],years),
'truck-heavy duty': linear_growth(2019,ref_FT_mod_rel['truck-heavy duty'],
2050,trg_FT_mod_rel['truck-heavy duty'], years),
'plane-intra EU': linear_growth(2019,ref_FT_mod_rel['plane-intra EU'],
2050,trg_FT_mod_rel['plane-intra EU'], years),
'plane-extra EU': linear_growth(2019,ref_FT_mod_rel['plane-extra EU'],
2050,trg_FT_mod_rel['plane-extra EU'], years),
}
# Processing - Modal Shares DataFrame
df_FT_MOD = pd.DataFrame(modes_FT, index=years)
df_FT_MOD = df_FT_MOD.round(4).transpose()
# Processing - Global DataFrame
df_FT_GTKM = pd.DataFrame({year: df_SUF["FT total [Gtkm]"][year]*df_FT_MOD[year]*1e-2 for year in years}, index=df_FT_MOD.index).round(6)
df_FT_TKMP = pd.DataFrame({year: df_FT_GTKM[year]*1e+9/population_dict[year] for year in years}, index=df_FT_MOD.index).round(3)
arrays_FT =[np.repeat(df_FT_MOD.index, 3), ['% of total', 'tkm/person', 'Gtkm'] * len(df_FT_MOD)]
mi_FT = pd.MultiIndex.from_arrays(arrays_FT, names=['Mode', 'Unit'])
data_FT_rows = []
for mode in df_FT_MOD.index:
data_FT_rows.append(df_FT_MOD .loc[mode].values) # modal percentages
data_FT_rows.append(df_FT_TKMP.loc[mode].values) # tkm per person
data_FT_rows.append(df_FT_GTKM.loc[mode].values) # Gtkm values
data_FT = np.vstack(data_FT_rows)
df_FT = pd.DataFrame(data_FT, index=mi_FT, columns=years)
if post_process:
# === Full table ===
df_FT_r = df_FT.reset_index()
total_Gtkm = df_SUF["FT total [Gtkm]"]
total_tkmp = df_SUF["FT intensity [tkm/person]"]
rows = []
for unit in ['% of total', 'tkm/person', 'Gtkm']:
if unit == '% of total':
vals = [100] * len(years)
elif unit == 'Gtkm':
vals = [total_Gtkm[year] for year in years]
else:
vals = [total_tkmp [year] for year in years]
rows.append(pd.DataFrame([['TOTAL', unit, *vals]], columns=df_FT_r.columns))
df_FT_r = pd.concat([df_FT_r] + rows, ignore_index=True)
mode_full = df_FT_r['Mode'].tolist()
df_FT_r['Mode'] = np.where(df_FT_r['Unit'] == '% of total', df_FT_r['Mode'], '')
styled = (
df_FT_r.style
.apply(highlight_lines, axis=1)
.set_properties(subset=['Mode'], **{'font-weight':'bold'})
.set_properties(subset=['Unit'], **{'font-style':'italic','color':'gray'})
.format({year:"{:.2f}" for year in years})
#.set_caption("Modal Shares")
.hide(axis='index')
.set_table_attributes('style="width:100%;table-layout:fixed;"')
)
display(styled)
# === Breakdown bar chart - relative values ===
dftf = df_FT_MOD.transpose()
fig1, ax = plt.subplots(figsize=(14, 5))
bottom = np.zeros(len(dftf))
colors = plt.cm.tab10.colors # default matplotlib palette
# Thicker bars → control via height argument
bar_height = 2.0 # default is ~0.8; increase to 0.9–1.0 for thicker bars
for i, mode in enumerate(dftf.columns):
ax.barh(dftf.index, dftf[mode],
left=bottom, label=mode,
color=colors[i % len(colors)],
height=bar_height)
bottom += dftf[mode]
ax.set_xlabel("Modal Share [%]", fontsize=10, ha='right', va='top')
ax.xaxis.set_label_coords(1, -0.1)
ax.set_ylabel("Years", fontsize=10, rotation=0, ha='left', va='bottom')
ax.yaxis.set_label_coords(-0.05, 1)
ax.set_yticks(years)
ax.yaxis.set_inverted(True) # inverted axis with autoscaling
ax.set_title("Evolution of Modal Shares for Freight Transport (negaWatt-BE Scenario)", fontsize=11, pad=15)
ax.legend(bbox_to_anchor=(1, 1), loc='upper left', title="Mode", frameon=False)
ax.grid(axis='x', linestyle='--', alpha=0.6)
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)
ax.spines['bottom'].set_visible(False)
ax.spines['left'].set_visible(False)
fig1.tight_layout()
plt.show()
# === Breakdown bar chart - absolute values ===
dftf = df_FT.transpose()
fig2, ax = plt.subplots(figsize=(14, 5))
bottom = np.zeros(len(dftf))
colors = plt.cm.tab10.colors # default matplotlib palette
# Thicker bars → control via height argument
bar_height = 2.0 # default is ~0.8; increase to 0.9–1.0 for thicker bars
j = 0
for i, mode in enumerate(dftf.columns):
if mode[1] == 'Gtkm':
ax.barh(dftf.index, dftf[mode],
left=bottom, label=mode[0],
color=colors[j % len(colors)],
height=bar_height)
bottom += dftf[mode]
j += 1
ax.set_xlabel("Total number of ton-kilometres [Gtkm]", fontsize=10, ha='right', va='top')
ax.xaxis.set_label_coords(1, -0.1)
ax.set_ylabel("Years", fontsize=10, rotation=0, ha='left', va='bottom')
ax.yaxis.set_label_coords(-0.05, 1)
ax.set_yticks(years)
ax.yaxis.set_inverted(True) # inverted axis with autoscaling
ax.set_title("Evolution of Modal Shares for Freight Transport (negaWatt-BE Scenario)", fontsize=11, pad=15)
ax.legend(bbox_to_anchor=(1, 1), loc='upper left', title="Mode", frameon=False)
ax.grid(axis='x', linestyle='--', alpha=0.6)
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)
ax.spines['bottom'].set_visible(False)
ax.spines['left'].set_visible(False)
fig2.tight_layout()
plt.show()
# === Breakdown bar chart - comparison of relative values in 2019 and 2050 ===
labels = df_FT_MOD.index.tolist()
sizes_2019 = df_FT_MOD[2019].values # Modal shares for 2019
sizes_2050 = df_FT_MOD[2050].values # Modal shares for 2050
x = np.arange(len(labels)) # x locations for each mode
width = 0.35 # bar width
fig, ax = plt.subplots(figsize=(14, 5))
# Bars for 2019 and 2050
ax.bar(x - width/2, sizes_2019, width, label='2019')
ax.bar(x + width/2, sizes_2050, width, label='2050')
# Labels and Title
ax.set_ylabel('Modal Share (%)')
ax.set_title('Modal Shares Comparison: 2019 vs 2050 (negaWatt-BE Scenario)')
# X-ticks: mode names, rotated for readability
ax.set_xticks(x)
ax.set_xticklabels(labels, rotation=45, ha='right')
# Legend to identify each year
ax.legend()
# Tight layout to avoid clipping
plt.tight_layout()
plt.show()
| Mode | Unit | 2019 | 2025 | 2030 | 2035 | 2040 | 2045 | 2050 |
|---|---|---|---|---|---|---|---|---|
| train | % of total | 18.34 | 20.23 | 21.82 | 23.40 | 24.98 | 26.56 | 28.14 |
| tkm/person | 1285.95 | 1391.67 | 1475.80 | 1556.35 | 1633.32 | 1706.78 | 1776.59 | |
| Gtkm | 14.70 | 16.44 | 17.74 | 18.97 | 20.17 | 21.32 | 22.39 | |
| navigation-coastal | % of total | 0.24 | 0.24 | 0.24 | 0.24 | 0.24 | 0.24 | 0.24 |
| tkm/person | 16.83 | 16.51 | 16.23 | 15.96 | 15.69 | 15.42 | 15.15 | |
| Gtkm | 0.19 | 0.20 | 0.20 | 0.19 | 0.19 | 0.19 | 0.19 | |
| navigation-inland | % of total | 9.69 | 10.95 | 12.01 | 13.06 | 14.12 | 15.17 | 16.23 |
| tkm/person | 679.41 | 753.34 | 812.31 | 868.90 | 923.11 | 974.92 | 1024.36 | |
| Gtkm | 7.77 | 8.90 | 9.77 | 10.59 | 11.40 | 12.18 | 12.91 | |
| truck-light commercial | % of total | 1.36 | 1.36 | 1.36 | 1.36 | 1.36 | 1.36 | 1.36 |
| tkm/person | 95.74 | 93.88 | 92.34 | 90.80 | 89.25 | 87.71 | 86.16 | |
| Gtkm | 1.09 | 1.11 | 1.11 | 1.11 | 1.10 | 1.10 | 1.09 | |
| truck-heavy duty | % of total | 65.40 | 62.24 | 59.60 | 56.96 | 54.33 | 51.69 | 49.05 |
| tkm/person | 4587.12 | 4280.66 | 4031.86 | 3789.04 | 3552.11 | 3321.22 | 3096.29 | |
| Gtkm | 52.44 | 50.58 | 48.48 | 46.18 | 43.86 | 41.48 | 39.02 | |
| plane-intra EU | % of total | 0.35 | 0.35 | 0.35 | 0.35 | 0.35 | 0.35 | 0.35 |
| tkm/person | 24.48 | 24.00 | 23.61 | 23.21 | 22.82 | 22.43 | 22.03 | |
| Gtkm | 0.28 | 0.28 | 0.28 | 0.28 | 0.28 | 0.28 | 0.28 | |
| plane-extra EU | % of total | 4.62 | 4.62 | 4.62 | 4.62 | 4.62 | 4.62 | 4.62 |
| tkm/person | 324.10 | 317.83 | 312.60 | 307.37 | 302.14 | 296.92 | 291.69 | |
| Gtkm | 3.70 | 3.76 | 3.76 | 3.75 | 3.73 | 3.71 | 3.68 | |
| TOTAL | % of total | 100.00 | 100.00 | 100.00 | 100.00 | 100.00 | 100.00 | 100.00 |
| tkm/person | 7013.63 | 6877.88 | 6764.76 | 6651.64 | 6538.51 | 6425.39 | 6312.27 | |
| Gtkm | 80.18 | 81.27 | 81.34 | 81.06 | 80.73 | 80.26 | 79.54 |
This section presents the internal distribution of each transport mode by energy carrier and powertrain type, as a percentage of the total Gtkm for that mode. These shares are used to calculate the weighted energy demand and infrastructure needs per technology.
In 2019, diesel (including 5,5% biofuels) is the only propulsion mode used for heavy duty trucks. Among the total 52,437 Gtkm achieved that year, 41,44% were for domestic transport while the remaing 58,56% were for international transport [1,2].
Efficiency Assumption
To estimate the 2050 fuel mix, this analysis draws on the JRC's 'Decarbonising European heavy-duty transport' report (2025) [9], which indicate that battery electric vehicles have the highest maturity, complemented by hydrogen for long-haul and a minor role for biofuels. Nevertheless, recent opinion surveys indicate that, due to the latest technological advances, the views of freight transport companies have recently shifted from confidence in hydrogen toward electric solutions.
The electrification of trucks is already well underway, particularly for regional transport. Current models can offer autonomies from 300 km (Volvo FH Electric) to up to more than 500 km (Scania, Renault Trucks E-Tech T, MAN eTGX, DAF XG Electric, Mercedes eACTROS600). For long distance transport, manufacturers also develop hydrogen lorries (DAF XF Hydrogen, Zepp Europa with 700 km autonomy), both with fuel cells and internal combustion engines. Hydrogen is also considered for shorter distances (Hyundai XCIENT Fuel Cell, 400 km autonomy). Nevertheless, commercialization is not expected before the end of this decade, at best.
For our projections, we mainly rely on battery electric vehicles, which are assumed to account for 90% of the fleet by 2050. Regarding hydrogen, the outlook is highly uncertain due to competition with battery electric solutions and the difficulties in scaling up the hydrogen value chain. We nevertheless assume a 5% share by 2050. We also retain a small 3% share for liquid-fuel propulsion, considering that biodiesel could facilitate the use of locally produced energy while enabling longer-distance operations. Similarly, we allocate a 2% share to gas-powered vehicles, assuming the potential valorisation of biogas.
# Inputs - Define Sufficiency Scenario Data (SUF)
sta_FT_trk_hvy_ele = 0.00 # [%]
end_FT_trk_hvy_ele = 90.00 # [%]
sta_FT_trk_hvy_h2 = 0.00 # [%]
end_FT_trk_hvy_h2 = 5.00 # [%]
sta_FT_trk_hvy_cng = 0.00 # [%]
end_FT_trk_hvy_cng = 2.00 # [%]
slope_f_FT_trk_hvy = 0.9
# Outputs - Sufficiency Scenario Data (SUF)
carriers_trk_hvy = {
'electrical': s_curve_growth(2019, sta_FT_trk_hvy_ele, 2050, end_FT_trk_hvy_ele, years, slope_f_FT_trk_hvy), # [%]
'hydrogen': linear_with_middle_point(2019, sta_FT_trk_hvy_h2, 2035, sta_FT_trk_hvy_h2, 2050, end_FT_trk_hvy_h2, years), # [%]
'gas-NG': s_curve_growth(2019, sta_FT_trk_hvy_cng, 2050, end_FT_trk_hvy_cng, years, slope_f_FT_trk_hvy), # [%]
'liquid-diesel': [100-x-y-z for x, y, z in zip(*[s_curve_growth(2019, sta_FT_trk_hvy_ele, 2050, end_FT_trk_hvy_ele, years, slope_f_FT_trk_hvy),
linear_with_middle_point(2019, sta_FT_trk_hvy_h2, 2035, sta_FT_trk_hvy_h2, 2050, end_FT_trk_hvy_h2, years),
s_curve_growth(2019, sta_FT_trk_hvy_cng, 2050, end_FT_trk_hvy_cng, years, slope_f_FT_trk_hvy)])], # [%]
}
df_FT_trk_hvy = pd.DataFrame(carriers_trk_hvy, index=years).T.round(3)
3.2.2. Light Commercial Trucks
In 2019, diesel propulsion dominated light commercial trucks, accounting for more than 95% of the fleet:
- Diesel engines: 96,38%
- Gasoline engines: 2,80%
- Liquefied Petroleum Gas (LPG) engines: 0,42%
- Natural Gas (CNG/LNG) engines: 0,27%
- Battery Electric Vehicles (BEV): 0,13%
Efficiency Assumption
We bet on a strong electrification, leaving little room to biofuels.
# Inputs - Define Sufficiency Scenario Data (SUF)
sta_FT_trk_lgt_lfd = 96.38# [%]
end_FT_trk_lgt_lfd = 1.50 # [%]
sta_FT_trk_lgt_lfg = 2.80 # [%]
end_FT_trk_lgt_lfg = 1.50 # [%]
sta_FT_trk_lgt_lpg = 0.42 # [%]
end_FT_trk_lgt_lpg = 0.00 # [%]
sta_FT_trk_lgt_cng = 0.27 # [%]
end_FT_trk_lgt_cng = 1.50 # [%]
slope_f_FT_trk_lgt = 0.9
# Outputs - Sufficiency Scenario Data (SUF)
carriers_trk_lgt = {
'liquid-diesel': s_curve_growth(2019, sta_FT_trk_lgt_lfd, 2050, end_FT_trk_lgt_lfd, years, slope_f_FT_trk_lgt), # [%]
'liquid-gasoline': s_curve_growth(2019, sta_FT_trk_lgt_lfg, 2050, end_FT_trk_lgt_lfg, years, slope_f_FT_trk_lgt), # [%]
'gas-LPG': s_curve_growth(2019, sta_FT_trk_lgt_lpg, 2050, end_FT_trk_lgt_lpg, years, slope_f_FT_trk_lgt), # [%]
'gas-NG': s_curve_growth(2019, sta_FT_trk_lgt_cng, 2050, end_FT_trk_lgt_cng, years, slope_f_FT_trk_lgt), # [%]
'electrical': [100-w-x-y-z for w, x, y, z in zip(*[s_curve_growth(2019, sta_FT_trk_lgt_lfd, 2050, end_FT_trk_lgt_lfd, years, slope_f_FT_trk_lgt),
s_curve_growth(2019, sta_FT_trk_lgt_lfg, 2050, end_FT_trk_lgt_lfg, years, slope_f_FT_trk_lgt),
s_curve_growth(2019, sta_FT_trk_lgt_lpg, 2050, end_FT_trk_lgt_lpg, years, slope_f_FT_trk_lgt),
s_curve_growth(2019, sta_FT_trk_lgt_cng, 2050, end_FT_trk_lgt_cng, years, slope_f_FT_trk_lgt)])], # [%]
}
df_FT_trk_lgt = pd.DataFrame(carriers_trk_lgt, index=years).T.round(3)
The train mode is divided between diesel and electric trains. Shares refer to the percentage of total Gtkm covered by trains (see Section 3.1.5.).
Propulsion types for conventional trains in 2019 are provided by JRC-IDEES [1,2]: over the total 14,700 Gtkm achieved, 12,546 Gpkm (85,35%) were done through electric trains while the remaining 2,154 Gpkm (14,65%) were covered by diesel.
Efficiency Assumption
The study on the phase-out of diesel in the Belgian rail sector [8] shows that full electrification of the network, while effective from an energy and environmental perspective, is a costly option for low-traffic lines. Nevertheless, since the passenger network is expected to be electrified, only a few freight lines remain concerned (such as Genk-Bilzen).
The study also considers that hybrid diesel-electric locomotives (such as the Siemens Vectron Dual Mode), hydrogen–diesel locomotives (BeHydro project) for heavy freight, or battery-powered solutions could have potential, although these options are currently limited. Hydrogen-based solutions are, however, significantly more uncertain.
We therefore favor a combined approach based on hybrid diesel-electric trains and the electrification of key lines. We believe this strategy should make it possible to reduce the share of liquid fuels -bio-diesel) to 5% by 2050.
# Inputs - Define Sufficiency Scenario Data (SUF)
sta_FT_trn = 14.65 # [%]
end_FT_trn = 5.00 # [%]
# Outputs - Sufficiency Scenario Data (SUF)
carriers_FT_trn = {
'liquid-diesel': linear_growth(2019, sta_FT_trn, 2050, end_FT_trn, years), # [%]
'electrical': [100-x for x in linear_growth(2019, sta_FT_trn, 2050, end_FT_trn, years)], # [%]
}
df_FT_trn = pd.DataFrame(carriers_FT_trn, index=years).T.round(3)
# Inputs - Define Sufficiency Scenario Data (SUF)
sta_FT_avi_srt = 0 # [%]
end_FT_avi_srt = 5 # [%]
mid_FT_avi_srt = 2045
# Outputs - Sufficiency Scenario Data (SUF)
carriers_FT_avi_srt = {
'hydrogen': linear_with_middle_point(2019, sta_FT_avi_srt, mid_FT_avi_srt, sta_FT_avi_srt, 2050, end_FT_avi_srt, years), # [%]
'liquid-kerosene': [100-x for x in linear_with_middle_point(2019, sta_FT_avi_srt, mid_FT_avi_srt, sta_FT_avi_srt, 2050, end_FT_avi_srt, years)], # [%]
}
carriers_FT_avi_lng = {
'liquid-kerosene': linear_growth(2019, 100, 2050, 100, years), # [%]
}
df_FT_avi_srt = pd.DataFrame(carriers_FT_avi_srt, index=years).T.round(3)
df_FT_avi_lng = pd.DataFrame(carriers_FT_avi_lng, index=years).T.round(3)
In 2019, 100% of coastal and inland waterways freight relied on conventional liquid fuels (diesel like, including gas oil and fuel oil).
To estimate the evolution of the maritime fuel mix, this analysis draws on two authoritative references: the JRC's EU Reference Scenario (2023) [10] and DNV's Maritime Forecast to 2050 (2024) [11]. Both sources indicate a progressive but profound transition from fossil fuels toward low- and zero-carbon alternatives, dominated by ammonia, biofuels, and e-fuels, complemented by limited use of hydrogen.
We apply the same carrier share for inland and coastal navigations.
# Inputs - Define Sufficiency Scenario Data (SUF)
sta_FT_nav_lfa = 0.00 # [%]
end_FT_nav_lfa = 17.50 # [%]
sta_FT_nav_h2 = 0.00 # [%]
end_FT_nav_h2 = 7.50 # [%]
sta_FT_nav_lfm = 0.00 # [%]
end_FT_nav_lfm = 35.00 # [%]
mid_y = 2035
# Outputs - Sufficiency Scenario Data (SUF)
carriers_nav = {
'ammonia': linear_with_middle_point(2019, sta_FT_nav_lfa, mid_y, sta_FT_nav_lfa, 2050, end_FT_nav_lfa, years), # [%]
'hydrogen': linear_with_middle_point(2019, sta_FT_nav_h2, mid_y, sta_FT_nav_h2, 2050, end_FT_nav_h2, years), # [%]
'methanol': linear_with_middle_point(2019, sta_FT_nav_lfm, mid_y, sta_FT_nav_lfm, 2050, end_FT_nav_lfm, years), # [%]
'liquid-diesel': [100-x-y-z for x, y, z in zip(*[linear_with_middle_point(2019, sta_FT_nav_lfa, mid_y, sta_FT_nav_lfa, 2050, end_FT_nav_lfa, years),
linear_with_middle_point(2019, sta_FT_nav_h2, mid_y, sta_FT_nav_h2, 2050, end_FT_nav_h2, years),
linear_with_middle_point(2019, sta_FT_nav_lfm, mid_y, sta_FT_nav_lfm, 2050, end_FT_nav_lfm, years)])], # [%]
}
df_FT_nav = pd.DataFrame(carriers_nav, index=years).T.round(3)
# Processing - Carriers Shares (in %)
df_FT_carriers = {
'train': df_FT_trn,
'navigation-coastal': df_FT_nav,
'navigation-inland': df_FT_nav,
'truck-light commercial': df_FT_trk_lgt,
'truck-heavy duty': df_FT_trk_hvy,
'plane-intra EU': df_FT_avi_srt,
'plane-extra EU': df_FT_avi_lng,
}
rows = []
for mode, df in df_FT_carriers.items():
temp = df.copy()
temp['Mode'] = mode
temp['Powertrain'] = temp.index
temp = temp.reset_index(drop=True)
rows.append(temp)
df_FT_carrier = pd.concat(rows, ignore_index=True)
if post_process:
df_carrier = df_FT_carrier.copy()
df_carrier['Mode'] = df_carrier['Mode'].mask(df_carrier['Mode'].duplicated(), '')
df_carrier_r = df_carrier[['Mode', 'Powertrain'] + list(years)]
styled = (
df_carrier_r.style
.hide(axis='index')
#.set_caption("Carrier Shares (%)")
.set_table_attributes('style="width:100%;table-layout:fixed;"')
.apply(highlight_mode_separator, axis=1)
.apply(lambda row: [bold_mode(cell, row['Mode'], col) for col, cell in zip(df_carrier_r.columns, row)], axis=1)
.set_properties(subset=['Powertrain'], **{'font-style':'italic'})
.format({year: "{:.2f}%" for year in years})
)
display(styled)
| Mode | Powertrain | 2019 | 2025 | 2030 | 2035 | 2040 | 2045 | 2050 |
|---|---|---|---|---|---|---|---|---|
| train | liquid-diesel | 14.65% | 12.78% | 11.23% | 9.67% | 8.11% | 6.56% | 5.00% |
| electrical | 85.35% | 87.22% | 88.77% | 90.33% | 91.89% | 93.44% | 95.00% | |
| navigation-coastal | ammonia | 0.00% | 0.00% | 0.00% | 0.00% | 5.83% | 11.67% | 17.50% |
| hydrogen | 0.00% | 0.00% | 0.00% | 0.00% | 2.50% | 5.00% | 7.50% | |
| methanol | 0.00% | 0.00% | 0.00% | 0.00% | 11.67% | 23.33% | 35.00% | |
| liquid-diesel | 100.00% | 100.00% | 100.00% | 100.00% | 80.00% | 60.00% | 40.00% | |
| navigation-inland | ammonia | 0.00% | 0.00% | 0.00% | 0.00% | 5.83% | 11.67% | 17.50% |
| hydrogen | 0.00% | 0.00% | 0.00% | 0.00% | 2.50% | 5.00% | 7.50% | |
| methanol | 0.00% | 0.00% | 0.00% | 0.00% | 11.67% | 23.33% | 35.00% | |
| liquid-diesel | 100.00% | 100.00% | 100.00% | 100.00% | 80.00% | 60.00% | 40.00% | |
| truck-light commercial | liquid-diesel | 96.38% | 91.66% | 76.77% | 45.43% | 16.77% | 4.83% | 1.50% |
| liquid-gasoline | 2.80% | 2.73% | 2.53% | 2.10% | 1.71% | 1.55% | 1.50% | |
| gas-LPG | 0.42% | 0.40% | 0.33% | 0.19% | 0.07% | 0.01% | 0.00% | |
| gas-NG | 0.27% | 0.33% | 0.52% | 0.93% | 1.30% | 1.46% | 1.50% | |
| electrical | 0.13% | 4.87% | 19.84% | 51.35% | 80.15% | 92.15% | 95.50% | |
| truck-heavy duty | electrical | 0.00% | 4.48% | 18.60% | 48.33% | 75.51% | 86.84% | 90.00% |
| hydrogen | 0.00% | 0.00% | 0.00% | 0.00% | 1.67% | 3.33% | 5.00% | |
| gas-NG | 0.00% | 0.10% | 0.41% | 1.07% | 1.68% | 1.93% | 2.00% | |
| liquid-diesel | 100.00% | 95.42% | 80.99% | 50.59% | 21.14% | 7.89% | 3.00% | |
| plane-intra EU | hydrogen | 0.00% | 0.00% | 0.00% | 0.00% | 0.00% | 0.00% | 5.00% |
| liquid-kerosene | 100.00% | 100.00% | 100.00% | 100.00% | 100.00% | 100.00% | 95.00% | |
| plane-extra EU | liquid-kerosene | 100.00% | 100.00% | 100.00% | 100.00% | 100.00% | 100.00% | 100.00% |
In this section, the Gtkm associated with each powertrain and fuel are converted into equivalent TWh.
Comment: The modelling assumptions still need to be specified!
The specific fuel consumption [kWh/km] for each propulsion type for 2019 is provided by JRC-IDEES [1,2]. The payload is defined as the ratio between the specific fuel consumption [kWh/km] and the activity energy intensity [kWh/tkm].ensity [kWh/tkm].
# ===== Train =====
# -> Settings
redu_fuel_FT_trn = 0.90
pyld_trgt_FT_trn = 1.00
# -> Projections of efficiency and payload
cons_fuel_FT_trn = pd.DataFrame({
'liquid-diesel': linear_growth(2019, 536.484/100*kgoe_to_kWh, 2050, 536.484/100*kgoe_to_kWh, years), # [kWh/km] -> sta = JRC-IDEES, end = nW-BE
'electrical': linear_growth(2019, 190.096/100*kgoe_to_kWh, 2050, 190.096/100*kgoe_to_kWh*redu_fuel_FT_trn, years), # [kWh/km] -> sta = JRC-IDEES, end = nW-BE
}, index=years).T
payload_FT_trn = pd.DataFrame({
'liquid-diesel': linear_growth(2019, 770.099, 2050, 770.099 *pyld_trgt_FT_trn, years), # [t] -> sta = JRC-IDEES, end = nW-BE
'electrical': linear_growth(2019, 732.106, 2050, 732.106 *pyld_trgt_FT_trn, years), # [t] -> sta = JRC-IDEES, end = nW-BE
}, index=years).T
# -> Conversion to TWh
df_FT_trn_TWh = df_FT_carrier[df_FT_carrier['Mode'] == 'train'].copy()
df_FT_trn_TWh[years] *= df_FT_GTKM.loc['train', years].values/100 # Gtkm
df_FT_trn_TWh = df_FT_trn_TWh.set_index('Powertrain') # Gtkm
df_FT_trn_TWh[years] *= cons_fuel_FT_trn[years]/payload_FT_trn[years] # TWh
# -> Make total for sanity check
new_index = len(df_FT_trn_TWh)
df_FT_trn_TWh.loc[new_index, years] = df_FT_trn_TWh[years].sum()
# -> Clean and process
if 'Powertrain' not in df_FT_trn_TWh.columns: df_FT_trn_TWh = df_FT_trn_TWh.reset_index()
df_FT_trn_TWh = df_FT_trn_TWh.rename(columns={'index': 'Powertrain'})
last_row_index = df_FT_trn_TWh.index[-1]
df_FT_trn_TWh.loc[last_row_index, 'Powertrain'] = 'TOTAL'
df_FT_trn_TWh.loc[last_row_index, 'Mode'] = 'train'
df_FT_trn_TWh = df_FT_trn_TWh[['Mode','Powertrain'] + years]
df_FT_trn_TWh = df_FT_trn_TWh.round(3)
# ===== Coastal Navigation =====
# -> Settings
redu_fuel_FT_nav_cst = 1.00
pyld_trgt_FT_nav_cst = 1.00
# -> Projections of efficiency and payload
cons_fuel_FT_nav_cst = pd.DataFrame({
'liquid-diesel': linear_growth(2019, 12355.078/100*kgoe_to_kWh, 2050, 12355.078/100*kgoe_to_kWh, years), # [kWh/km] -> sta = JRC-IDEES, end = nW-BE
'hydrogen': linear_growth(2019, 12355.078/100*kgoe_to_kWh, 2050, 12355.078/100*kgoe_to_kWh*redu_fuel_FT_nav_cst, years), # [kWh/km] -> sta = nW-BE, end = nW-BE
'methanol': linear_growth(2019, 12355.078/100*kgoe_to_kWh, 2050, 12355.078/100*kgoe_to_kWh*redu_fuel_FT_nav_cst, years), # [kWh/km] -> sta = nW-BE, end = nW-BE
'ammonia': linear_growth(2019, 12355.078/100*kgoe_to_kWh, 2050, 12355.078/100*kgoe_to_kWh*redu_fuel_FT_nav_cst, years), # [kWh/km] -> sta = nW-BE, end = nW-BE
}, index=years).T
# Ammonia and methanol are also ICE, so we assume same efficiency.
# Hydrogen could be ICE and FC: following [10], hydrogen blends could be burned in ICE
payload_FT_nav_cst = pd.DataFrame({
'liquid-diesel': linear_growth(2019, 3108.677, 2050, 3108.677*pyld_trgt_FT_nav_cst, years), # [t] -> sta = JRC-IDEES, end = nW-BE
'hydrogen': linear_growth(2019, 3108.677, 2050, 3108.677*pyld_trgt_FT_nav_cst, years), # [t] -> sta = nW-BE, end = nW-BE
'methanol': linear_growth(2019, 3108.677, 2050, 3108.677*pyld_trgt_FT_nav_cst, years), # [t] -> sta = nW-BE, end = nW-BE
'ammonia': linear_growth(2019, 3108.677, 2050, 3108.677*pyld_trgt_FT_nav_cst, years), # [t] -> sta = nW-BE, end = nW-BE
}, index=years).T
# -> Conversion to TWh
df_FT_nav_cst_TWh = df_FT_carrier[df_FT_carrier['Mode'] == 'navigation-coastal'].copy()
df_FT_nav_cst_TWh[years] *= df_FT_GTKM.loc['navigation-coastal', years].values/100 # Gtkm
df_FT_nav_cst_TWh = df_FT_nav_cst_TWh.set_index('Powertrain') # Gtkm
df_FT_nav_cst_TWh[years] *= cons_fuel_FT_nav_cst[years]/payload_FT_nav_cst[years] # TWh
# -> Make total for sanity check
new_index = len(df_FT_nav_cst_TWh)
df_FT_nav_cst_TWh.loc[new_index, years] = df_FT_nav_cst_TWh[years].sum()
# -> Clean and process
if 'Powertrain' not in df_FT_nav_cst_TWh.columns: df_FT_nav_cst_TWh = df_FT_nav_cst_TWh.reset_index()
df_FT_nav_cst_TWh = df_FT_nav_cst_TWh.rename(columns={'index': 'Powertrain'})
last_row_index = df_FT_nav_cst_TWh.index[-1]
df_FT_nav_cst_TWh.loc[last_row_index, 'Powertrain'] = 'TOTAL'
df_FT_nav_cst_TWh.loc[last_row_index, 'Mode'] = 'navigation-coastal'
df_FT_nav_cst_TWh = df_FT_nav_cst_TWh[['Mode','Powertrain'] + years]
df_FT_nav_cst_TWh = df_FT_nav_cst_TWh.round(3)
# ===== Inland Navigation =====
# -> Settings
redu_fuel_FT_nav_ild = 1.00
pyld_trgt_FT_nav_ild = 1.00
# -> Projections of efficiency and payload
cons_fuel_FT_nav_ild = pd.DataFrame({
'liquid-diesel': linear_growth(2019, 1079.015/100*kgoe_to_kWh, 2050, 1079.015/100*kgoe_to_kWh, years), # [kWh/km] -> sta = JRC-IDEES, end = nW-BE
'hydrogen': linear_growth(2019, 1079.015/100*kgoe_to_kWh, 2050, 1079.015/100*kgoe_to_kWh*redu_fuel_FT_nav_ild, years), # [kWh/km] -> sta = nW-BE, end = nW-BE
'methanol': linear_growth(2019, 1079.015/100*kgoe_to_kWh, 2050, 1079.015/100*kgoe_to_kWh*redu_fuel_FT_nav_ild, years), # [kWh/km] -> sta = nW-BE, end = nW-BE
'ammonia': linear_growth(2019, 1079.015/100*kgoe_to_kWh, 2050, 1079.015/100*kgoe_to_kWh*redu_fuel_FT_nav_ild, years), # [kWh/km] -> sta = nW-BE, end = nW-BE
}, index=years).T
# Ammonia and methanol are also ICE, so we assume same efficiency.
# Hydrogen could be ICE and FC: following [10], hydrogen blends could be burned in ICE
payload_FT_nav_ild = pd.DataFrame({
'liquid-diesel': linear_growth(2019, 617.310, 2050, 617.310*pyld_trgt_FT_nav_ild, years), # [t] -> sta = JRC-IDEES, end = nW-BE
'hydrogen': linear_growth(2019, 617.310, 2050, 617.310*pyld_trgt_FT_nav_ild, years), # [t] -> sta = nW-BE, end = nW-BE
'methanol': linear_growth(2019, 617.310, 2050, 617.310*pyld_trgt_FT_nav_ild, years), # [t] -> sta = nW-BE, end = nW-BE
'ammonia': linear_growth(2019, 617.310, 2050, 617.310*pyld_trgt_FT_nav_ild, years), # [t] -> sta = nW-BE, end = nW-BE
}, index=years).T
# -> Conversion to TWh
df_FT_nav_ild_TWh = df_FT_carrier[df_FT_carrier['Mode'] == 'navigation-inland'].copy()
df_FT_nav_ild_TWh[years] *= df_FT_GTKM.loc['navigation-inland', years].values/100 # Gtkm
df_FT_nav_ild_TWh = df_FT_nav_ild_TWh.set_index('Powertrain') # Gtkm
df_FT_nav_ild_TWh[years] *= cons_fuel_FT_nav_ild[years]/payload_FT_nav_ild[years] # TWh
# -> Make total for sanity check
new_index = len(df_FT_nav_ild_TWh)
df_FT_nav_ild_TWh.loc[new_index, years] = df_FT_nav_ild_TWh[years].sum()
# -> Clean and process
if 'Powertrain' not in df_FT_nav_ild_TWh.columns: df_FT_nav_ild_TWh = df_FT_nav_ild_TWh.reset_index()
df_FT_nav_ild_TWh = df_FT_nav_ild_TWh.rename(columns={'index': 'Powertrain'})
last_row_index = df_FT_nav_ild_TWh.index[-1]
df_FT_nav_ild_TWh.loc[last_row_index, 'Powertrain'] = 'TOTAL'
df_FT_nav_ild_TWh.loc[last_row_index, 'Mode'] = 'navigation-inland'
df_FT_nav_ild_TWh = df_FT_nav_ild_TWh[['Mode','Powertrain'] + years]
df_FT_nav_ild_TWh = df_FT_nav_ild_TWh.round(3)
The specific fuel consumption [kWh/km] for each propulsion type for 2019 is provided by JRC-IDEES [1,2]. The payload is defined as the ratio between the specific fuel consumption [kWh/km] and the activity energy intensity [kWh/tkm].
Sufficiency Assumption (heavy trucks)
pyld_trgt_FT_trk_hvy = 1.05: +5% load per heavy-truck kilometre in 2050, empty runs
included. Read as a filling rate — the load carried over every vehicle-km, empty ones
included, divided by the payload capacity of the trucks that drive them (0% = empty,
100% = loaded to the maximum payload, by weight) — this is 53% in 2019 → 56% in 2050
(ref_fill_FT_trk_hvy, trg_fill_FT_trk_hvy). The fleet is assumed unchanged, so the
filling rate moves exactly like the payload.
How the capacity is aggregated. Trucks of all sizes are weighted by the kilometres they
drive: capacity = Σ (laden vehicle-km × payload capacity) / Σ laden vehicle-km, from Eurostat
road_go_ta_lc (vehicle-km by load-capacity class, each class counted at the middle of its
range, 32 t for the open-ended top class). This gives 23,8 t for Belgian trucks
(cap_FT_trk_hvy) and 21,5 t for the EU (2024). Dividing totals by totals is what makes the
size mix aggregate correctly: a 40-tonne articulated truck weighs more in the result because it
offers more capacity.
For scale (Eurostat road_go_ta_tott, heavy goods vehicles by country of registration):
| Belgium | EU27 | |
|---|---|---|
| Filling rate, all trips | 53% (2019) | 52% (2008), 53% (2024) |
| Filling rate when loaded | 60% (56-59% reported for 2010-2014) | 66% (2024) |
| Share of vehicle-km driven empty (2024) | 11,6% | 19,8% |
Never driving empty would lift the Belgian average to the loaded rate, 60% (fill_FT_trk_laden).
Read as fewer empty runs alone, +5% means bringing Belgian empty running from 11,6% to about
7% (empty_share_FT_trk_equiv). It is a deliberately cautious assumption: filling trucks better
means pooling loads between competing shippers, and the EU rate gained under two points in
sixteen years.
Comment: two weak points. (1) The Belgian load-capacity classification stops holding in 2015 (the average load of a class exceeds its capacity), so the Belgian capacity is taken from the 2010-2014 fleet structure; it was stable over those years (23,4-24,1 t). (2) The Belgian empty-running figure nearly doubled in a single year (6,7% in 2021, 10,9% in 2022) while its neighbours stayed flat, so that series breaks in 2022. The load reference (
ref_pyld_FT_trk_hvy, 12,65 t per vehicle-km over all trips in 2019, JRC-IDEES) matches Eurostat's 12,41 t. A filling rate by weight also understates how full a truck of light goods is: it can be full to the roof well below 100%. Sources: Eurostat, road_go_ta_lc; Eurostat, road_go_ta_tott.
# ===== Light Commercial Trucks =====
# -> Settings
redu_fuel_FT_trk_lgt = 0.90
pyld_trgt_FT_trk_lgt = 1.05
# -> Projections of efficiency and payload
cons_fuel_FT_trk_lgt = pd.DataFrame({
'liquid-diesel': linear_growth(2019, 9.099/100*kgoe_to_kWh, 2050, 9.099/100*kgoe_to_kWh*redu_fuel_FT_trk_lgt, years), # [kWh/km] -> sta = JRC-IDEES, end = nW-BE
'liquid-gasoline': linear_growth(2019, 8.203/100*kgoe_to_kWh, 2050, 8.203/100*kgoe_to_kWh*redu_fuel_FT_trk_lgt, years), # [kWh/km] -> sta = JRC-IDEES, end = nW-BE
'electrical': linear_growth(2019, 1.996/100*kgoe_to_kWh, 2050, 1.996/100*kgoe_to_kWh*redu_fuel_FT_trk_lgt, years), # [kWh/km] -> sta = JRC-IDEES, end = nW-BE
'gas-LPG': linear_growth(2019, 12.830/100*kgoe_to_kWh, 2050, 12.830/100*kgoe_to_kWh*redu_fuel_FT_trk_lgt, years), # [kWh/km] -> sta = JRC-IDEES, end = nW-BE
'gas-NG': linear_growth(2019, 12.876/100*kgoe_to_kWh, 2050, 12.876/100*kgoe_to_kWh*redu_fuel_FT_trk_lgt, years), # [kWh/km] -> sta = JRC-IDEES, end = nW-BE
}, index=years).T
payload_FT_trk_lgt = pd.DataFrame({
'liquid-diesel': linear_growth(2019, 0.095, 2050, 0.095*pyld_trgt_FT_trk_lgt, years), # [t] -> sta = JRC-IDEES, end = nW-BE
'liquid-gasoline': linear_growth(2019, 0.071, 2050, 0.071*pyld_trgt_FT_trk_lgt, years), # [t] -> sta = JRC-IDEES, end = nW-BE
'electrical': linear_growth(2019, 0.072, 2050, 0.072*pyld_trgt_FT_trk_lgt, years), # [t] -> sta = JRC-IDEES, end = nW-BE
'gas-LPG': linear_growth(2019, 0.073, 2050, 0.073*pyld_trgt_FT_trk_lgt, years), # [t] -> sta = JRC-IDEES, end = nW-BE
'gas-NG': linear_growth(2019, 0.073, 2050, 0.073*pyld_trgt_FT_trk_lgt, years), # [t] -> sta = JRC-IDEES, end = nW-BE
}, index=years).T
# -> Conversion to TWh
df_FT_trk_lgt_TWh = df_FT_carrier[df_FT_carrier['Mode'] == 'truck-light commercial'].copy()
df_FT_trk_lgt_TWh[years] *= df_FT_GTKM.loc['truck-light commercial', years].values/100 # GTKM
df_FT_trk_lgt_TWh = df_FT_trk_lgt_TWh.set_index('Powertrain') # GTKM
df_FT_trk_lgt_TWh[years] *= cons_fuel_FT_trk_lgt[years]/payload_FT_trk_lgt[years] # TWh
# -> Make total for sanity check
new_index = len(df_FT_trk_lgt_TWh)
df_FT_trk_lgt_TWh.loc[new_index, years] = df_FT_trk_lgt_TWh[years].sum()
# -> Clean and process
if 'Powertrain' not in df_FT_trk_lgt_TWh.columns: df_FT_trk_lgt_TWh = df_FT_trk_lgt_TWh.reset_index()
df_FT_trk_lgt_TWh = df_FT_trk_lgt_TWh.rename(columns={'index': 'Powertrain'})
last_row_index = df_FT_trk_lgt_TWh.index[-1]
df_FT_trk_lgt_TWh.loc[last_row_index, 'Powertrain'] = 'TOTAL'
df_FT_trk_lgt_TWh.loc[last_row_index, 'Mode'] = 'truck-light commercial'
df_FT_trk_lgt_TWh = df_FT_trk_lgt_TWh[['Mode','Powertrain'] + years]
df_FT_trk_lgt_TWh = df_FT_trk_lgt_TWh.round(3)
# ===== Heavy Duty Trucks =====
# -> Settings
redu_fuel_FT_trk_hvy = 0.95
pyld_trgt_FT_trk_hvy = 1.05
# -> 2019 average payload of a loaded heavy truck [t], from JRC-IDEES [1,2];
# identical for every powertrain. Named (instead of inlined below) so the
# workshop module can export it. See docs/workshop_module.md.
ref_pyld_FT_trk_hvy = 12.654
# -> The filling rate (see the markdown above): the average load over every vehicle-km,
# empty runs included, divided by the payload capacity of the trucks driving them.
# 0% is an empty truck, 100% a truck loaded to its maximum payload.
# Eurostat road_go_ta_lc gives laden vehicle-km [Mio] by load-capacity class; each
# class is counted at the middle of its range, the open-ended last one at 32 t.
cap_class_FT_trk = [6.0, 12.5, 18.0, 23.0, 28.0, 32.0] # t: <=9.5, 9.6-15.5, 15.6-20.5, 20.6-25.5, 25.6-30.5, >30.5
# Belgium 2010-2014 only: from 2015 the Belgian classification no longer holds (the
# average load of a class exceeds its capacity), so the fleet structure of its last
# five valid years stands for today's.
ref_vkm_class_FT_trk_BE = {
2010: [312, 286, 106, 234, 682, 859],
2011: [466, 130, 94, 235, 599, 869],
2012: [373, 221, 151, 179, 547, 856],
2013: [345, 220, 119, 176, 546, 971],
2014: [357, 269, 96, 162, 485, 1005],
}
ref_vkm_class_FT_trk_EU = {
2008: [17215, 17992, 7537, 29262, 36439, 13789],
2024: [16532, 19166, 6491, 40612, 26934, 20951],
}
def _capacity_FT_trk(vkm):
"""Average payload capacity [t] per laden vehicle-km of a fleet, by class."""
return sum(v * c for v, c in zip(vkm, cap_class_FT_trk)) / sum(vkm)
cap_FT_trk_hvy = float(np.mean([_capacity_FT_trk(v) for v in ref_vkm_class_FT_trk_BE.values()]))
ref_fill_FT_trk_hvy = ref_pyld_FT_trk_hvy / cap_FT_trk_hvy # 2019, all trips
trg_fill_FT_trk_hvy = ref_fill_FT_trk_hvy * pyld_trgt_FT_trk_hvy # 2050, same trucks
# -> For scale. Eurostat road_go_ta_tott, heavy goods vehicles by country of registration:
ref_empty_share_FT_trk_BE = 0.116 # share of vehicle-km driven empty, Belgium, 2024
ref_empty_share_FT_trk_EU = 0.198 # same, EU27, 2024
ref_load_FT_trk_EU = {2008: 10.99, 2024: 11.47} # t per vehicle-km, all trips, EU27
ref_laden_load_FT_trk_EU = 14.30 # t per laden vehicle-km, EU27, 2024
fill_FT_trk_EU = {_y: ref_load_FT_trk_EU[_y] / _capacity_FT_trk(ref_vkm_class_FT_trk_EU[_y])
for _y in ref_load_FT_trk_EU}
fill_FT_trk_EU_laden = ref_laden_load_FT_trk_EU / _capacity_FT_trk(ref_vkm_class_FT_trk_EU[2024])
fill_FT_trk_laden = ref_fill_FT_trk_hvy / (1 - ref_empty_share_FT_trk_BE) # Belgian truck, when loaded
# -> The scenario's +5% read as fewer empty runs alone, loaded trucks no fuller:
empty_share_FT_trk_equiv = 1 - (1 - ref_empty_share_FT_trk_BE) * pyld_trgt_FT_trk_hvy
print(f"heavy trucks: capacity {cap_FT_trk_hvy:.1f} t, filled {ref_fill_FT_trk_hvy:.1%} in 2019 "
f"-> {trg_fill_FT_trk_hvy:.1%} in 2050 (EU {fill_FT_trk_EU[2008]:.1%} in 2008, "
f"{fill_FT_trk_EU[2024]:.1%} in 2024)")
# -> Projections of efficiency and payload
cons_fuel_FT_trk_hvy = pd.DataFrame({
'liquid-diesel': linear_growth(2019, 49.763/100*kgoe_to_kWh, 2050, 49.763/100*kgoe_to_kWh, years), # [kWh/km] -> sta = JRC-IDEES, end = nW-BE
'electrical': linear_growth(2019,130.000/100, 2050,130.000/100 *redu_fuel_FT_trk_hvy, years), # [kWh/km] -> sta = nW-BE, end = nW-BE
'hydrogen': linear_growth(2019, 8.400/100*kgh2_to_kWh, 2050, 8.400/100*kgh2_to_kWh*redu_fuel_FT_trk_hvy, years), # [kWh/km] -> sta = nW-BE, end = nW-BE
'gas-NG': linear_growth(2019, 25.000/100*kgLNG_to_kWh,2050, 25.000/100*kgLNG_to_kWh*redu_fuel_FT_trk_hvy,years), # [kWh/km] -> sta = nW-BE, end = nW-BE
}, index=years).T
# Renault E-Tech T 780 (semitrailer): 130 kWh/100km (780kWh for 600km)
# Renault E-Tech T 585 (semitrailer): 127 kWh/100km (585kWh for 460km)
# MAN eTGX (semitrailer): 96 kWh/100km (480kWh for 500km)
# Mercedes eACTROS 600 (semitrailer): 124 kWh/100km (621kWh for 500km)
# Zepp Europa (semitrailer): 8.4 kg/100km (58.8kg for 700km)
# Volvo FH Aero LNG (semitrailer): 22.5 kg/100km (225kg for 1000km)
payload_FT_trk_hvy = pd.DataFrame({
'liquid-diesel': linear_growth(2019, ref_pyld_FT_trk_hvy, 2050, ref_pyld_FT_trk_hvy*pyld_trgt_FT_trk_hvy, years), # [t] -> sta = JRC-IDEES, end = nW-BE
'electrical': linear_growth(2019, ref_pyld_FT_trk_hvy, 2050, ref_pyld_FT_trk_hvy*pyld_trgt_FT_trk_hvy, years), # [t] -> sta = nW-BE, end = nW-BE
'hydrogen': linear_growth(2019, ref_pyld_FT_trk_hvy, 2050, ref_pyld_FT_trk_hvy*pyld_trgt_FT_trk_hvy, years), # [t] -> sta = nW-BE, end = nW-BE
'gas-NG': linear_growth(2019, ref_pyld_FT_trk_hvy, 2050, ref_pyld_FT_trk_hvy*pyld_trgt_FT_trk_hvy, years), # [t] -> sta = nW-BE, end = nW-BE
}, index=years).T
# -> Conversion to TWh
df_FT_trk_hvy_TWh = df_FT_carrier[df_FT_carrier['Mode'] == 'truck-heavy duty'].copy()
df_FT_trk_hvy_TWh[years] *= df_FT_GTKM.loc['truck-heavy duty', years].values/100 # Gtkm
df_FT_trk_hvy_TWh = df_FT_trk_hvy_TWh.set_index('Powertrain') # Gtkm
df_FT_trk_hvy_TWh[years] *= cons_fuel_FT_trk_hvy[years]/payload_FT_trk_hvy[years] # TWh
# -> Make total for sanity check
new_index = len(df_FT_trk_hvy_TWh)
df_FT_trk_hvy_TWh.loc[new_index, years] = df_FT_trk_hvy_TWh[years].sum()
# -> Clean and process
if 'Powertrain' not in df_FT_trk_hvy_TWh.columns: df_FT_trk_hvy_TWh = df_FT_trk_hvy_TWh.reset_index()
df_FT_trk_hvy_TWh = df_FT_trk_hvy_TWh.rename(columns={'index': 'Powertrain'})
last_row_index = df_FT_trk_hvy_TWh.index[-1]
df_FT_trk_hvy_TWh.loc[last_row_index, 'Powertrain'] = 'TOTAL'
df_FT_trk_hvy_TWh.loc[last_row_index, 'Mode'] = 'truck-heavy duty'
df_FT_trk_hvy_TWh = df_FT_trk_hvy_TWh[['Mode','Powertrain'] + years]
df_FT_trk_hvy_TWh = df_FT_trk_hvy_TWh.round(3)
# ===== Aviation (intra EU) =====
# -> Settings
redu_fuel_FT_avi_intra = 1.00
pyld_trgt_FT_avi_intra = 1.00
# -> Projections of efficiency and payload
cons_fuel_FT_avi_intra = pd.DataFrame({
'hydrogen': linear_growth(2019, 130.000/100*kgh2_to_kWh, 2050, 130.000/100*kgh2_to_kWh, years), # [kWh/km] -> sta = nW-BE, end = nW-BE
'liquid-kerosene': linear_growth(2019, 665.675/100*kgoe_to_kWh, 2050, 665.675/100*kgoe_to_kWh*redu_fuel_FT_avi_intra, years), # [kWh/km] -> sta = JRC-IDEES, end = nW-BE
}, index=years).T
payload_FT_avi_intra = pd.DataFrame({
'hydrogen': linear_growth(2019, 100.000/121.927*25.200, 2050, 100.000/121.927*25.200, years), # [t] -> sta = nW-BE, end = nW-BE
'liquid-kerosene': linear_growth(2019, 25.200, 2050, 25.200*pyld_trgt_FT_avi_intra, years), # [t] -> sta = JRC-IDEES, end = nW-BE
}, index=years).T
# Payload of Airbus Zero-E is scaled from passenger aviation data
# -> Conversion to TWh
df_FT_avi_srt_TWh = df_FT_carrier[df_FT_carrier['Mode'] == 'plane-intra EU'].copy()
df_FT_avi_srt_TWh[years] *= df_FT_GTKM.loc['plane-intra EU', years].values/100 # Gtkm
df_FT_avi_srt_TWh = df_FT_avi_srt_TWh.set_index('Powertrain') # Gtkm
df_FT_avi_srt_TWh[years] *= cons_fuel_FT_avi_intra[years]/payload_FT_avi_intra[years] # TWh
# -> Make total for sanity check
new_index = len(df_FT_avi_srt_TWh)
df_FT_avi_srt_TWh.loc[new_index, years] = df_FT_avi_srt_TWh[years].sum()
# -> Clean and process
if 'Powertrain' not in df_FT_avi_srt_TWh.columns: df_FT_avi_srt_TWh = df_FT_avi_srt_TWh.reset_index()
df_FT_avi_srt_TWh = df_FT_avi_srt_TWh.rename(columns={'index': 'Powertrain'})
last_row_index = df_FT_avi_srt_TWh.index[-1]
df_FT_avi_srt_TWh.loc[last_row_index, 'Powertrain'] = 'TOTAL'
df_FT_avi_srt_TWh.loc[last_row_index, 'Mode'] = 'plane-intra EU'
df_FT_avi_srt_TWh = df_FT_avi_srt_TWh[['Mode','Powertrain'] + years]
df_FT_avi_srt_TWh = df_FT_avi_srt_TWh.round(3)
# ===== Aviation (extra EU) =====
# -> Settings
redu_fuel_FT_avi_extra = 1.00
pyld_trgt_FT_avi_extra = 1.00
# -> Projections of efficiency and payload
cons_fuel_FT_avi_extra = pd.DataFrame({
'liquid-kerosene': linear_growth(2019, 618.493/100*kgoe_to_kWh, 2050, 618.493/100*kgoe_to_kWh*redu_fuel_FT_avi_extra, years), # [kWh/km] -> sta = JRC-IDEES, end = nW-BE
}, index=years).T
payload_FT_avi_extra = pd.DataFrame({
'liquid-kerosene': linear_growth(2019, 98.258, 2050, 98.258*pyld_trgt_FT_avi_extra, years), # [t] -> sta = JRC-IDEES, end = nW-BE
}, index=years).T
# -> Conversion to TWh
df_FT_avi_lng_TWh = df_FT_carrier[df_FT_carrier['Mode'] == 'plane-extra EU'].copy()
df_FT_avi_lng_TWh[years] *= df_FT_GTKM.loc['plane-extra EU', years].values/100 # Gtkm
df_FT_avi_lng_TWh = df_FT_avi_lng_TWh.set_index('Powertrain') # Gtkm
df_FT_avi_lng_TWh[years] *= cons_fuel_FT_avi_extra[years]/payload_FT_avi_extra[years] # TWh
# -> Make total for sanity check
new_index = len(df_FT_avi_lng_TWh)
df_FT_avi_lng_TWh.loc[new_index, years] = df_FT_avi_lng_TWh[years].sum()
# -> Clean and process
if 'Powertrain' not in df_FT_avi_lng_TWh.columns: df_FT_avi_lng_TWh = df_FT_avi_lng_TWh.reset_index()
df_FT_avi_lng_TWh = df_FT_avi_lng_TWh.rename(columns={'index': 'Powertrain'})
last_row_index = df_FT_avi_lng_TWh.index[-1]
df_FT_avi_lng_TWh.loc[last_row_index, 'Powertrain'] = 'TOTAL'
df_FT_avi_lng_TWh.loc[last_row_index, 'Mode'] = 'plane-extra EU'
df_FT_avi_lng_TWh = df_FT_avi_lng_TWh[['Mode','Powertrain'] + years]
df_FT_avi_lng_TWh = df_FT_avi_lng_TWh.round(3)
df_FT_TWh_lst = [df_FT_trn_TWh,
df_FT_nav_ild_TWh,
df_FT_nav_cst_TWh,
df_FT_trk_hvy_TWh,
df_FT_trk_lgt_TWh,
df_FT_avi_srt_TWh,
df_FT_avi_lng_TWh
]
df_FT_TWh_all = pd.concat(df_FT_TWh_lst, ignore_index=True)
df_FT_TWh_flt = df_FT_TWh_all[~df_FT_TWh_all['Powertrain'].isin(['TOTAL'])].copy()
df_FT_TWh_agr = df_FT_TWh_flt.groupby('Powertrain').sum(numeric_only=True)
custom_order = [
'electrical',
'liquid-gasoline',
'liquid-diesel',
'liquid-kerosene',
'gas-NG',
'gas-LPG',
'hydrogen',
'methanol',
'ammonia',
]
df_FT_TWh_agr = df_FT_TWh_agr.reindex(custom_order)
if post_process:
# === Full table ===
df_fec = df_FT_TWh_all
df_fec['Mode'] = df_fec['Mode'].mask(df_fec['Mode'].duplicated(), '')
df_fec_r = df_fec[['Mode', 'Powertrain'] + list(years)]
styled = (
df_fec_r.style
.hide(axis='index')
#.set_caption("Carrier Shares (%)")
.set_table_attributes('style="width:100%;table-layout:fixed;"')
.apply(highlight_mode_separator, axis=1)
.apply(lambda row: [bold_mode(cell, row['Mode'], col) for col, cell in zip(df_fec_r.columns, row)], axis=1)
.set_properties(subset=['Powertrain'], **{'font-style':'italic'})
.format({year: "{:.3f} TWh" for year in years})
)
display(styled)
# === Breakdown bar chart - absolute values ===
dftf = df_FT_TWh_agr.transpose()
fig, ax = plt.subplots(figsize=(14, 5))
bottom = np.zeros(len(dftf))
colors = plt.cm.tab10.colors # default matplotlib palette
# Thicker bars → control via height argument
bar_height = 2.0 # default is ~0.8; increase to 0.9–1.0 for thicker bars
j = 0
for i, fuel in enumerate(dftf.columns):
ax.barh(dftf.index, dftf[fuel],
left=bottom, label=fuel,
color=colors[j % len(colors)],
height=bar_height)
bottom += dftf[fuel]
j += 1
ax.set_xlabel("Final Energy Consumption [TWh]", fontsize=10, ha='right', va='top')
ax.xaxis.set_label_coords(1, -0.1)
ax.set_ylabel("Years", fontsize=10, rotation=0, ha='left', va='bottom')
ax.yaxis.set_label_coords(-0.05, 1)
ax.set_yticks(years)
ax.yaxis.set_inverted(True) # inverted axis with autoscaling
ax.set_title("Evolution of Energy Consumption for Freight Transport (negaWatt-BE Scenario)", fontsize=11, pad=15)
ax.legend(bbox_to_anchor=(1, 1), loc='upper left', title="Carrier", frameon=False)
ax.grid(axis='x', linestyle='--', alpha=0.6)
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)
ax.spines['bottom'].set_visible(False)
ax.spines['left'].set_visible(False)
fig.tight_layout()
plt.show()
| Mode | Powertrain | 2019 | 2025 | 2030 | 2035 | 2040 | 2045 | 2050 |
|---|---|---|---|---|---|---|---|---|
| train | liquid-diesel | 0.174 TWh | 0.170 TWh | 0.161 TWh | 0.149 TWh | 0.133 TWh | 0.113 TWh | 0.091 TWh |
| electrical | 0.379 TWh | 0.425 TWh | 0.459 TWh | 0.491 TWh | 0.522 TWh | 0.551 TWh | 0.578 TWh | |
| TOTAL | 0.553 TWh | 0.595 TWh | 0.620 TWh | 0.639 TWh | 0.654 TWh | 0.664 TWh | 0.669 TWh | |
| navigation-inland | ammonia | 0.000 TWh | 0.000 TWh | 0.000 TWh | 0.000 TWh | 0.135 TWh | 0.289 TWh | 0.459 TWh |
| hydrogen | 0.000 TWh | 0.000 TWh | 0.000 TWh | 0.000 TWh | 0.058 TWh | 0.124 TWh | 0.197 TWh | |
| methanol | 0.000 TWh | 0.000 TWh | 0.000 TWh | 0.000 TWh | 0.270 TWh | 0.578 TWh | 0.918 TWh | |
| liquid-diesel | 1.579 TWh | 1.810 TWh | 1.985 TWh | 2.153 TWh | 1.854 TWh | 1.485 TWh | 1.050 TWh | |
| TOTAL | 1.579 TWh | 1.810 TWh | 1.985 TWh | 2.153 TWh | 2.317 TWh | 2.475 TWh | 2.624 TWh | |
| navigation-coastal | ammonia | 0.000 TWh | 0.000 TWh | 0.000 TWh | 0.000 TWh | 0.005 TWh | 0.010 TWh | 0.015 TWh |
| hydrogen | 0.000 TWh | 0.000 TWh | 0.000 TWh | 0.000 TWh | 0.002 TWh | 0.004 TWh | 0.007 TWh | |
| methanol | 0.000 TWh | 0.000 TWh | 0.000 TWh | 0.000 TWh | 0.010 TWh | 0.021 TWh | 0.031 TWh | |
| liquid-diesel | 0.089 TWh | 0.090 TWh | 0.090 TWh | 0.090 TWh | 0.072 TWh | 0.053 TWh | 0.035 TWh | |
| TOTAL | 0.089 TWh | 0.090 TWh | 0.090 TWh | 0.090 TWh | 0.090 TWh | 0.089 TWh | 0.088 TWh | |
| truck-heavy duty | electrical | 0.000 TWh | 0.228 TWh | 0.894 TWh | 2.177 TWh | 3.179 TWh | 3.402 TWh | 3.264 TWh |
| hydrogen | 0.000 TWh | 0.000 TWh | 0.000 TWh | 0.000 TWh | 0.151 TWh | 0.281 TWh | 0.391 TWh | |
| gas-NG | 0.000 TWh | 0.012 TWh | 0.048 TWh | 0.117 TWh | 0.170 TWh | 0.182 TWh | 0.175 TWh | |
| liquid-diesel | 23.981 TWh | 21.863 TWh | 17.642 TWh | 10.415 TWh | 4.102 TWh | 1.437 TWh | 0.510 TWh | |
| TOTAL | 23.981 TWh | 22.103 TWh | 18.584 TWh | 12.708 TWh | 7.603 TWh | 5.303 TWh | 4.339 TWh | |
| truck-light commercial | liquid-diesel | 11.747 TWh | 10.994 TWh | 8.972 TWh | 5.203 TWh | 1.862 TWh | 0.518 TWh | 0.155 TWh |
| liquid-gasoline | 0.412 TWh | 0.394 TWh | 0.359 TWh | 0.288 TWh | 0.229 TWh | 0.200 TWh | 0.187 TWh | |
| gas-LPG | 0.094 TWh | 0.088 TWh | 0.072 TWh | 0.040 TWh | 0.014 TWh | 0.003 TWh | 0.000 TWh | |
| gas-NG | 0.061 TWh | 0.073 TWh | 0.114 TWh | 0.195 TWh | 0.267 TWh | 0.288 TWh | 0.285 TWh | |
| electrical | 0.005 TWh | 0.169 TWh | 0.676 TWh | 1.689 TWh | 2.578 TWh | 2.867 TWh | 2.851 TWh | |
| TOTAL | 12.318 TWh | 11.718 TWh | 10.193 TWh | 7.416 TWh | 4.950 TWh | 3.876 TWh | 3.478 TWh | |
| plane-intra EU | hydrogen | 0.000 TWh | 0.000 TWh | 0.000 TWh | 0.000 TWh | 0.000 TWh | 0.000 TWh | 0.029 TWh |
| liquid-kerosene | 0.860 TWh | 0.871 TWh | 0.872 TWh | 0.869 TWh | 0.866 TWh | 0.861 TWh | 0.810 TWh | |
| TOTAL | 0.860 TWh | 0.871 TWh | 0.872 TWh | 0.869 TWh | 0.866 TWh | 0.861 TWh | 0.839 TWh | |
| plane-extra EU | liquid-kerosene | 2.712 TWh | 2.749 TWh | 2.752 TWh | 2.742 TWh | 2.731 TWh | 2.715 TWh | 2.691 TWh |
| TOTAL | 2.712 TWh | 2.749 TWh | 2.752 TWh | 2.742 TWh | 2.731 TWh | 2.715 TWh | 2.691 TWh |
# Aggregate global demand data
custom_order = [
'population [person]',
'PM intensity [pkm/person]',
'PM total [Gpkm]',
'FT intensity [tkm/person]',
'FT total [Gtkm]',
]
df_SUF = df_SUF.T.reindex(custom_order)
# Aggregate global energy data
df_transport_TWh_tot = df_PM_TWh_agr.add(df_FT_TWh_agr, fill_value=0)
df_transport_TWh_tot.loc[ len(df_transport_TWh_tot)] = df_transport_TWh_tot.sum(numeric_only=True)
df_transport_TWh_tot = df_transport_TWh_tot.rename(index={9: 'TOTAL'})
# Optional: Re-apply your custom order to the final total
custom_order = [
'electrical',
'liquid-gasoline', 'liquid-diesel', 'liquid-kerosene',
'gas-NG', 'gas-LPG',
'hydrogen', 'methanol', 'ammonia',
'TOTAL'
]
df_transport_TWh_tot = df_transport_TWh_tot.reindex(custom_order)
df_transport_TWh_tot
| 2019 | 2025 | 2030 | 2035 | 2040 | 2045 | 2050 | |
|---|---|---|---|---|---|---|---|
| Powertrain | |||||||
| electrical | 1.785 | 3.335 | 6.867 | 12.921 | 17.198 | 17.776 | 16.918 |
| liquid-gasoline | 22.482 | 24.531 | 20.376 | 11.838 | 6.091 | 1.872 | 0.630 |
| liquid-diesel | 77.149 | 60.417 | 44.964 | 25.923 | 9.546 | 4.348 | 2.319 |
| liquid-kerosene | 19.823 | 18.043 | 16.480 | 14.958 | 13.534 | 12.188 | 10.670 |
| gas-NG | 0.344 | 0.501 | 0.654 | 0.873 | 1.045 | 1.089 | 1.066 |
| gas-LPG | 0.639 | 0.442 | 0.308 | 0.187 | 0.096 | 0.037 | 0.000 |
| hydrogen | 0.000 | 0.005 | 0.021 | 0.059 | 0.308 | 0.526 | 0.925 |
| methanol | 0.000 | 0.000 | 0.000 | 0.000 | 0.280 | 0.599 | 0.949 |
| ammonia | 0.000 | 0.000 | 0.000 | 0.000 | 0.140 | 0.299 | 0.474 |
| TOTAL | 122.222 | 107.274 | 89.670 | 66.759 | 48.238 | 38.734 | 33.951 |
if post_process:
# === Breakdown bar chart - absolute values ===
dft = df_transport_TWh_tot.loc[['electrical','liquid-gasoline','liquid-diesel','liquid-kerosene','gas-NG','gas-LPG','hydrogen','methanol','ammonia',]].transpose()
fig, ax = plt.subplots(figsize=(14, 5))
bottom = np.zeros(len(dft))
colors = plt.cm.tab10.colors # default matplotlib palette
# Thicker bars → control via height argument
bar_height = 2.0 # default is ~0.8; increase to 0.9–1.0 for thicker bars
j = 0
for i, fuel in enumerate(dft.columns):
ax.barh(dft.index, dft[fuel],
left=bottom, label=fuel,
color=colors[j % len(colors)],
height=bar_height)
bottom += dft[fuel]
j += 1
ax.set_xlabel("Final Energy Consumption [TWh]", fontsize=10, ha='right', va='top')
ax.xaxis.set_label_coords(1, -0.1)
ax.set_ylabel("Years", fontsize=10, rotation=0, ha='left', va='bottom')
ax.yaxis.set_label_coords(-0.05, 1)
ax.set_yticks(years)
ax.yaxis.set_inverted(True) # inverted axis with autoscaling
ax.set_title("Evolution of Energy Consumption for Transport Sector (negaWatt-BE Scenario)", fontsize=11, pad=15)
ax.legend(bbox_to_anchor=(1, 1), loc='upper left', title="Carrier", frameon=False)
ax.grid(axis='x', linestyle='--', alpha=0.6)
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)
ax.spines['bottom'].set_visible(False)
ax.spines['left'].set_visible(False)
fig.tight_layout()
plt.show()
6. Short List of Sufficiency Assumptions
The cell below collects the elementary transport hypotheses (current value vs.
2050 target) and exports them to the public website
(website/data/transport.js). Re-run it after changing any assumption above so
that the website stays in sync. It also prints a short summary table of the
assumptions. See the Public sufficiency website section of README.md for the
data contract.
# ===== Website export: build website/data/transport.js =====
# Uses make_hypothesis() / write_hypotheses_js() from the sub-functions module.
# All numbers are read from the DataFrames computed above, so editing an
# assumption and re-running the notebook updates the public website automatically.
_NB = "../notebooks/nW_BE_demand_model_transports.html"
_H = []
def _add(*a, **k):
_H.append(make_hypothesis(*a, **k))
def _pts(v0, v1):
return ("%+.1f" % (v1 - v0)) + " pts"
def _dir(v0, v1):
return "down" if v1 < v0 else ("up" if v1 > v0 else "flat")
# Year columns are taken positionally so the export works whether the columns
# are integers (2019, 2050) or strings ("2019", "2050").
Y0, Y1 = df_SUF.columns[0], df_SUF.columns[-1]
# --- Global demand intensities (df_SUF) ---
_v0 = float(df_SUF.loc["PM intensity [pkm/person]", Y0])
_v1 = float(df_SUF.loc["PM intensity [pkm/person]", Y1])
_add("pm-intensity", "Passenger mobility intensity", "Passenger / demand",
round(_v0, 1), round(_v1, 1), unit="pkm/person",
notebook=_NB + "#section_1", reference="nW-BE \u00a71.2.1")
_v0 = float(df_SUF.loc["FT intensity [tkm/person]", Y0])
_v1 = float(df_SUF.loc["FT intensity [tkm/person]", Y1])
_add("ft-intensity", "Freight transport intensity", "Freight / demand",
round(_v0, 1), round(_v1, 1), unit="tkm/person",
notebook=_NB + "#section_3", reference="nW-BE \u00a71.2.2")
# --- Passenger modal shares (% of pkm) ---
_pm_modes = {
"mod-car": ("car", "Car"),
"mod-bus": ("bus&coach", "Bus & coach"),
"mod-train-conv": ("train-conventional", "Conventional train"),
"mod-train-hs": ("train-high speed", "High-speed train"),
"mod-tram": ("tram&metro", "Tram & metro"),
"mod-bicycle": ("bicycle", "Bicycle"),
"mod-pedestrian": ("pedestrian", "Walking"),
"mod-twowheeler": ("two-wheeler", "Two-wheeler"),
"mod-plane-intra": ("plane-intra EU", "Intra-EU aviation"),
"mod-plane-extra": ("plane-extra EU", "Extra-EU aviation"),
}
for _hid, (_m, _lab) in _pm_modes.items():
_v0 = float(df_PM.loc[(_m, "% of total"), Y0])
_v1 = float(df_PM.loc[(_m, "% of total"), Y1])
_add(_hid, _lab + " modal share", "Passenger / modal split",
round(_v0, 1), round(_v1, 1), unit="% of pkm",
change_label=_pts(_v0, _v1), direction=_dir(_v0, _v1),
notebook=_NB + "#section_2", reference="nW-BE \u00a72.1")
# --- Freight modal shares (% of tkm) ---
_ft_modes = {
"ft-truck-hvy": ("truck-heavy duty", "Heavy-duty trucks"),
"ft-truck-lgt": ("truck-light commercial", "Light commercial vehicles"),
"ft-train": ("train", "Rail freight"),
"ft-nav-inland": ("navigation-inland", "Inland navigation"),
"ft-nav-coastal": ("navigation-coastal", "Coastal navigation"),
"ft-plane-intra": ("plane-intra EU", "Intra-EU air freight"),
"ft-plane-extra": ("plane-extra EU", "Extra-EU air freight"),
}
for _hid, (_m, _lab) in _ft_modes.items():
_v0 = float(df_FT.loc[(_m, "% of total"), Y0])
_v1 = float(df_FT.loc[(_m, "% of total"), Y1])
_add(_hid, _lab + " modal share", "Freight / modal split",
round(_v0, 1), round(_v1, 1), unit="% of tkm",
change_label=_pts(_v0, _v1), direction=_dir(_v0, _v1),
notebook=_NB + "#section_3", reference="nW-BE \u00a73.1")
# --- Powertrain / carrier shares (% of fleet or fuel) ---
def _carrier(df, row, hid, label, cat, section):
_v0 = float(df.loc[row, Y0])
_v1 = float(df.loc[row, Y1])
_add(hid, label, cat, round(_v0, 1), round(_v1, 1), unit="% share",
change_label=_pts(_v0, _v1), direction=_dir(_v0, _v1),
notebook=_NB + section, reference="nW-BE carrier mix")
_carrier(df_PM_car, "electrical", "car-bev", "Battery-electric cars", "Passenger / powertrain", "#section_2")
_carrier(df_PM_bus_cch, "electrical", "bus-bev", "Electric buses & coaches", "Passenger / powertrain", "#section_2")
_carrier(df_PM_bic, "electrical", "bike-electric", "Electric bicycles (e-bikes)", "Passenger / powertrain", "#section_2")
_carrier(df_PM_mot, "electrical", "moto-electric", "Electric two-wheelers", "Passenger / powertrain", "#section_2")
_carrier(df_PM_trn_cnv, "electrical", "train-cnv-electric", "Conventional train electrification", "Passenger / powertrain", "#section_2")
_carrier(df_PM_avi_srt, "hydrogen", "avi-intra-h2", "Hydrogen on intra-EU flights", "Passenger / powertrain", "#section_2")
_carrier(df_FT_trk_hvy, "electrical", "ft-truck-hvy-bev", "Battery-electric heavy trucks", "Freight / powertrain", "#section_3")
_carrier(df_FT_trk_lgt, "electrical", "ft-truck-lgt-bev", "Electric light commercial vehicles", "Freight / powertrain", "#section_3")
_carrier(df_FT_trn, "electrical", "ft-train-electric", "Freight rail electrification", "Freight / powertrain", "#section_3")
_carrier(df_FT_nav, "liquid-diesel", "ft-nav-fossil", "Fossil diesel in domestic navigation", "Freight / powertrain", "#section_3")
# --- Load factors & energy intensity ---
try:
_co0 = float(occupancy_PM_car.iloc[:, 0].mean())
_co1 = float(occupancy_PM_car.iloc[:, -1].mean())
except Exception:
_co0, _co1 = 1.2, float(occu_trgt_PM_car)
_add("car-occupancy", "Car occupancy (carpooling)", "Passenger / load factor",
round(_co0, 2), round(_co1, 2), unit="persons/car",
notebook=_NB + "#section_2", reference="nW-BE \u00a72.3.5")
_add("car-energy", "Car energy use per km", "Passenger / efficiency",
100, round(redu_fuel_PM_car * 100), unit="% of 2019",
notebook=_NB + "#section_2", reference="speed limits, eco-driving, smaller cars")
_add("ft-truck-payload", "Heavy-truck load factor", "Freight / load factor",
100, round(pyld_trgt_FT_trk_hvy * 100), unit="% of 2019",
notebook=_NB + "#section_3", reference="nW-BE \u00a73.3.5")
_add("train-efficiency", "Train energy use per pkm", "Passenger / efficiency",
100, round(redu_fuel_PM_trn_cnv * 100), unit="% of 2019",
notebook=_NB + "#section_2", reference="rolling-stock efficiency")
# --- Plots (simple stacked bars, data straight from the notebook) ---
_pm_plot = ["car", "bus&coach", "train-conventional", "train-high speed", "tram&metro",
"bicycle", "two-wheeler", "pedestrian", "plane-intra EU", "plane-extra EU"]
_pm_lbl = {"car": "Car", "bus&coach": "Bus & coach", "train-conventional": "Conv. train",
"train-high speed": "HS train", "tram&metro": "Tram & metro", "bicycle": "Bicycle",
"two-wheeler": "Two-wheeler", "pedestrian": "Walking",
"plane-intra EU": "Plane intra-EU", "plane-extra EU": "Plane extra-EU"}
_ft_plot = ["truck-heavy duty", "truck-light commercial", "train", "navigation-inland",
"navigation-coastal", "plane-intra EU", "plane-extra EU"]
_plots = {
"modalShare": {"type": "stackedBar", "x": [str(Y0), str(Y1)], "yTitle": "% of passenger-km",
"series": [{"name": _pm_lbl[m],
"y": [round(float(df_PM.loc[(m, "% of total"), Y0]), 1),
round(float(df_PM.loc[(m, "% of total"), Y1]), 1)]} for m in _pm_plot]},
"freightModalShare": {"type": "stackedBar", "x": [str(Y0), str(Y1)], "yTitle": "% of tonne-km",
"series": [{"name": m,
"y": [round(float(df_FT.loc[(m, "% of total"), Y0]), 1),
round(float(df_FT.loc[(m, "% of total"), Y1]), 1)]} for m in _ft_plot]},
}
write_hypotheses_js("transport", _H, plots=_plots, title="Mobility & transport")
# Pin transport energy_totals overrides so CI can compare notebooks vs nW_BE.py.
_ov_path = make_energy_totals_overrides(
energy_totals_overrides_from_transport(df_PM_TWh_all, df_FT_TWh_all),
replace_sources=["transport"],
comment="generated from nW_BE demand-model notebooks; CI compares nW_BE.py to this file",
)
print(f"[energy_totals] wrote transport rows to {_ov_path}")
# Short summary table (fills the "short list of sufficiency assumptions").
import pandas as _pd
_pd.DataFrame([{ "id": h["id"], "hypothesis": h["name"], "category": h["category"],
"2019": h["refValue"], "2050": h["targetValue"], "unit": h["unit"] }
for h in _H])
7. Upstream Levers for the Interactive Workshop
Section 6 above exports the elementary hypotheses shown as read-only cards on the
public website. This section exports the upstream levers used by the interactive
workshop (website/workshop/), where participants set those assumptions themselves
before the négaWatt value is revealed.
Only genuine model degrees of freedom are exposed — the quantities this notebook actually takes as inputs — so that a group's answers always describe a consistent scenario and the reveal can be exact. Every lever is expressed in a unit a non-specialist can picture (km per day, people per car, tonnes per truck) rather than in the model's internal units.
The cell also cross-checks the model against the figures quoted in the prose above (the 1,22 fleet-average car occupancy of section 2.1, and the two modal-shift allocations): if the text and the code ever drift apart, the assertions fail here rather than silently feeding a wrong number into a workshop.
Question wording, factual anchors and the written justifications are not here —
they live in website/workshop/content/inland-mobility.yaml. See
docs/workshop_module.md for the full design.
# ===== Workshop export: build website/data/levers_transport.js =====
# The levers themselves live one module per topic, under workshop_levers/, so
# that inland mobility and international mobility can be written and changed
# independently instead of competing for this cell. Each module reads the frames
# computed above and adds no assumptions of its own; it also cross-checks the
# model against the figures quoted in the prose, and declares which of its
# numbers are the scenario's own choice so the workshop cannot leak them.
#
# See docs/workshop_module.md, and website/workshop/content/<topic>.yaml for the
# question wording, the facts and the written justifications.
from workshop_levers import export_topics, topic_modules
_lev_path = export_topics("transport", globals(), title="Mobility & transport")
import pandas as _pd
_pd.DataFrame([{"topic": _m.TOPIC, "module": _m.__name__.split(".")[-1],
"levers": len(_m.build(globals())["levers"])}
for _m in topic_modules("transport")])