Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added computation of private jet emissions #23

Merged
merged 3 commits into from
Jan 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions examples/country.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ of {country.plural} that every day act to reduce their emissions.

Of these,
* {number_of_legs_less_300km} trips were less than 300 km[^4] and could have been replaced by
a limusine or first class train ticket, which would have emitted ~17x less CO2[^5].
a limusine or first class train ticket, which would have emitted ~{ratio_train_300km.claim}x less CO2[^5].
* {number_of_legs_more_300km} trips[^4] could have been replaced by
a first class ticket on a commercial aircraft, which would have emitted
~{ratio_commercial_300km}x less CO2[^1].
~{ratio_commercial_300km.claim}x less CO2[^6].

The use of private jets by billionaires and large companies alike is an insult
to {country.plural} and {country.possessive} companies alike that are doing everything they can
Expand All @@ -24,4 +24,5 @@ to reduce emissions.
[^2]: {citizen_years.source} - retrieved on {citizen_years.date}
[^3]: {number_of_private_jets.source} on {number_of_private_jets.date}
[^4]: {number_of_legs.source} - retrieved on {number_of_legs.date}
[^5]: 10x vs a commercial flight[^1] plus 7x vs a train, as per https://ourworldindata.org/travel-carbon-footprint (UK data, vary by country) - retrieved on 2024-01-20
[^5]: {ratio_train_300km.source} - retrieved on {ratio_train_300km.date}
[^6]: {ratio_commercial_300km.source} - retrieved on {ratio_commercial_300km.date}
133 changes: 114 additions & 19 deletions examples/country.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
use std::{collections::HashMap, error::Error};
use std::{collections::HashMap, error::Error, sync::Arc};

use clap::Parser;
use futures::{StreamExt, TryStreamExt};
use num_format::{Locale, ToFormattedString};
use simple_logger::SimpleLogger;

use flights::{emissions, load_aircrafts, load_private_jet_types, Class, Fact, Leg, Position};
use flights::{
emissions, leg_co2_kg, leg_co2_kg_per_person, load_aircraft_consumption, load_aircrafts,
load_private_jet_types, AircraftTypeConsumptions, Class, Fact, Leg, Position,
};
use time::Date;

fn render(context: &Context) -> Result<(), Box<dyn Error>> {
Expand All @@ -24,6 +27,21 @@ fn render(context: &Context) -> Result<(), Box<dyn Error>> {
Ok(())
}

#[derive(serde::Serialize)]
struct LegOut {
tail_number: String,
model: String,
start: String,
end: String,
duration: String,
from_lat: f64,
from_lon: f64,
to_lat: f64,
to_lon: f64,
commercial_emissions_kg: usize,
emissions_kg: usize,
}

#[derive(serde::Serialize)]
pub struct CountryContext {
pub name: String,
Expand All @@ -43,7 +61,8 @@ pub struct Context {
pub citizen_years: Fact<String>,
pub number_of_legs_less_300km: String,
pub number_of_legs_more_300km: String,
pub ratio_commercial_300km: String,
pub ratio_commercial_300km: Fact<usize>,
pub ratio_train_300km: Fact<usize>,
}

#[derive(clap::ValueEnum, Debug, Clone)]
Expand Down Expand Up @@ -217,6 +236,40 @@ async fn legs(
}
}

fn private_emissions(
legs: &HashMap<(Arc<str>, String), Vec<Leg>>,
consumptions: &AircraftTypeConsumptions,
filter: impl Fn(&&Leg) -> bool + Copy,
) -> f64 {
legs.iter()
.map(|((_, model), legs)| {
legs.iter()
.filter(filter)
.map(|leg| {
leg_co2_kg(
consumptions.get(model).expect(model).gph as f64,
leg.duration(),
) / 1000.0
})
.sum::<f64>()
})
.sum::<f64>()
}

fn commercial_emissions(
legs: &HashMap<(Arc<str>, String), Vec<Leg>>,
filter: impl Fn(&&Leg) -> bool + Copy,
) -> f64 {
legs.iter()
.map(|(_, legs)| {
legs.iter()
.filter(filter)
.map(|leg| emissions(leg.from().pos(), leg.to().pos(), Class::First) / 1000.0)
.sum::<f64>()
})
.sum::<f64>()
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
SimpleLogger::new()
Expand All @@ -243,6 +296,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
// load datasets to memory
let aircrafts = load_aircrafts(client.as_ref()).await?;
let types = load_private_jet_types()?;
let consumptions = load_aircraft_consumption()?;

let private_jets = aircrafts
.into_iter()
Expand All @@ -263,7 +317,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
let legs = private_jets.iter().map(|(_, aircraft)| async {
legs(from, to, &aircraft.icao_number, cli.location, client)
.await
.map(|legs| (aircraft.icao_number.clone(), legs))
.map(|legs| ((aircraft.icao_number.clone(), aircraft.model.clone()), legs))
});

let legs = futures::stream::iter(legs)
Expand All @@ -272,6 +326,32 @@ async fn main() -> Result<(), Box<dyn Error>> {
.try_collect::<HashMap<_, _>>()
.await?;

let mut wtr = csv::Writer::from_writer(vec![]);
for ((tail_number, model), legs) in legs.iter() {
for leg in legs {
wtr.serialize(LegOut {
tail_number: tail_number.to_string(),
model: model.to_string(),
start: leg.from().datetime().to_string(),
end: leg.to().datetime().to_string(),
duration: leg.duration().to_string(),
from_lat: leg.from().latitude(),
from_lon: leg.from().longitude(),
to_lat: leg.to().latitude(),
to_lon: leg.to().longitude(),
commercial_emissions_kg: emissions(leg.from().pos(), leg.to().pos(), Class::First)
as usize,
emissions_kg: leg_co2_kg(
consumptions.get(model).expect(model).gph as f64,
leg.duration(),
) as usize,
})
.unwrap()
}
}
let data_csv = wtr.into_inner().unwrap();
std::fs::write("data.csv", data_csv)?;

let number_of_private_jets = Fact {
claim: legs.iter().filter(|x| x.1.len() > 0).count().to_formatted_string(&Locale::en),
source: format!(
Expand All @@ -292,20 +372,12 @@ async fn main() -> Result<(), Box<dyn Error>> {
date: now.to_string(),
};

let commercial_emissions_tons = legs
.iter()
.map(|(_, legs)| {
legs.iter()
.map(|leg| emissions(leg.from().pos(), leg.to().pos(), Class::First) / 1000.0)
.sum::<f64>()
})
.sum::<f64>();
let commercial_to_private_ratio = 10.0;
let emissions_tons_value = commercial_emissions_tons * commercial_to_private_ratio;
let emissions_value_tons = private_emissions(&legs, &consumptions, |_| true);

let emissions_tons = Fact {
claim: (emissions_tons_value as usize).to_formatted_string(&Locale::en),
source: format!("Commercial flights would have emitted {commercial_emissions_tons:.1} tons of CO2e (based on [myclimate.org](https://www.myclimate.org/en/information/about-myclimate/downloads/flight-emission-calculator/) - retrieved on 2023-10-19). Private jets emit 5-14x times. 10x was used based on [transportenvironment.org](https://www.transportenvironment.org/discover/private-jets-can-the-super-rich-supercharge-zero-emission-aviation/)"),
date: "2023-10-05, from 2021-05-27".to_string(),
claim: (emissions_value_tons as usize).to_formatted_string(&Locale::en),
source: "See [methodology M-7](https://github.com/jorgecardleitao/private-jets/blob/main/methodology.md)".to_string(),
date: time::OffsetDateTime::now_utc().date().to_string(),
};

let short_legs = legs
Expand All @@ -317,9 +389,31 @@ async fn main() -> Result<(), Box<dyn Error>> {
.map(|(_, legs)| legs.iter().filter(|leg| leg.distance() >= 300.0).count())
.sum::<usize>();

let emissions_short_legs =
private_emissions(&legs, &consumptions, |leg| leg.distance() < 300.0);
let commercial_emissions_short = commercial_emissions(&legs, |leg| leg.distance() < 300.0);

let short_ratio = leg_co2_kg_per_person(emissions_short_legs) / commercial_emissions_short;
let ratio_train_300km = Fact {
claim: (short_ratio + 7.0) as usize,
source: format!("{}x in comparison to a commercial flight[^1][^6] plus 7x of a commercial flight in comparison to a train, as per https://ourworldindata.org/travel-carbon-footprint (UK data, vary by country) - retrieved on 2024-01-20", short_ratio as usize),
date: now.to_string()
};

// compute emissions for the >300km legs, so we can compare with emissions from commercial flights
let emissions_long_legs =
private_emissions(&legs, &consumptions, |leg| leg.distance() >= 300.0);
let commercial_emissions_long = commercial_emissions(&legs, |leg| leg.distance() >= 300.0);

let ratio_commercial_300km = Fact {
claim: (leg_co2_kg_per_person(emissions_long_legs) / commercial_emissions_long) as usize,
source: "Commercial flight emissions based on [myclimate.org](https://www.myclimate.org/en/information/about-myclimate/downloads/flight-emission-calculator/) - retrieved on 2023-10-19".to_string(),
date: now.to_string(),
};

let citizen_emissions_tons = cli.country.emissions();

let citizen_years = (emissions_tons_value / citizen_emissions_tons.claim) as usize;
let citizen_years = (emissions_value_tons / citizen_emissions_tons.claim) as usize;
let citizen_years = Fact {
claim: citizen_years.to_formatted_string(&Locale::en),
source: citizen_emissions_tons.source,
Expand All @@ -340,7 +434,8 @@ async fn main() -> Result<(), Box<dyn Error>> {
citizen_years,
number_of_legs_less_300km: short_legs.to_formatted_string(&Locale::en),
number_of_legs_more_300km: long_legs.to_formatted_string(&Locale::en),
ratio_commercial_300km: format!("{:.0}", commercial_to_private_ratio),
ratio_commercial_300km,
ratio_train_300km,
};

render(&context)?;
Expand Down
55 changes: 28 additions & 27 deletions examples/single_day.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ async fn flight_date(
aircrafts: &Aircrafts,
client: Option<&fs_azure::ContainerClient>,
) -> Result<Vec<Event>, Box<dyn Error>> {
let consumptions = load_aircraft_consumption()?;
let airports = airports_cached().await?;
let aircraft_owner = aircraft_owners
.get(tail_number)
Expand All @@ -94,43 +95,43 @@ async fn flight_date(

let aircraft = aircrafts
.get(tail_number)
.ok_or_else(|| Into::<Box<dyn Error>>::into("Aircraft ICAO number not found"))?;
.ok_or_else(|| Into::<Box<dyn Error>>::into("Aircraft transponder number"))?;
let icao = &aircraft.icao_number;
log::info!("ICAO number: {}", icao);
log::info!("transponder number: {}", icao);

let consumption = consumptions
.get(&aircraft.model)
.ok_or_else(|| Into::<Box<dyn Error>>::into("Consumption not found"))?;
log::info!("Consumption: {} [gallon/h]", consumption.gph);

let positions = positions(icao, date, client).await?;
let legs = legs(positions);

log::info!("Number of legs: {}", legs.len());

Ok(legs.into_iter().filter_map(|leg| {
let is_leg = matches!(leg.from(), Position::Grounded{..}) & matches!(leg.to(), Position::Grounded{..});
if !is_leg {
log::info!("{:?} -> {:?} skipped", leg.from(), leg.to());
}
is_leg.then_some((leg.from().clone(), leg.to().clone()))
}).map(|(from, to)| {
let emissions = emissions(from.pos(), to.pos(), Class::First);
Ok(legs.into_iter().map(|leg| {
let commercial_emissions_kg = Fact {
claim: emissions(leg.from().pos(), leg.to().pos(), Class::First) as usize,
source: "https://www.myclimate.org/en/information/about-myclimate/downloads/flight-emission-calculator/".to_string(),
date: "2023-10-19".to_string()
};
let emissions_kg = Fact {
claim: leg_co2_kg(consumption.gph as f64, leg.duration()) as usize,
source: "See [methodology M-7](https://github.com/jorgecardleitao/private-jets/blob/main/methodology.md)".to_string(),
date: time::OffsetDateTime::now_utc().date().to_string(),
};

Event {
tail_number: tail_number.to_string(),
owner: owner.clone(),
date: date.to_string(),
from_airport: closest(from.pos(), &airports).name.clone(),
to_airport: closest(to.pos(), &airports).name.clone(),
two_way: false,
commercial_emissions_kg: Fact {
claim: emissions as usize,
source: "https://www.myclimate.org/en/information/about-myclimate/downloads/flight-emission-calculator/".to_string(),
date: "2023-10-19".to_string()
},
emissions_kg: Fact {
claim: (emissions * 10.0) as usize,
source: "Private jets emit 5-14x times. 10x was used here https://www.transportenvironment.org/discover/private-jets-can-the-super-rich-supercharge-zero-emission-aviation/".to_string(),
date: "2023-10-05, from 2021-05-27".to_string(),
},
source: format!("https://globe.adsbexchange.com/?icao={icao}&showTrace={date}"),
source_date: date.to_string(),
owner: owner.clone(),
date: date.to_string(),
from_airport: closest(leg.from().pos(), &airports).name.clone(),
to_airport: closest(leg.to().pos(), &airports).name.clone(),
two_way: false,
commercial_emissions_kg,
emissions_kg,
source: format!("https://globe.adsbexchange.com/?icao={icao}&showTrace={date}"),
source_date: date.to_string(),
}
}).collect())
}
Expand Down
47 changes: 46 additions & 1 deletion methodology.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,52 @@ flight in first class.

Details are available in the source code, [src/emissions.rs](./src/emissions.rs).

### M-6: Identify aircraft owner in Denmark
### M-6: Consumption of private jet

This was performed by a human, and consisted:
* access websites of companies that sell private jets
* extract the consumption in gallons per hour (GPH) of each private jet model
* store it in a table with the jet's model, GPH, source and date of extraction, at [`./src/consumption.csv`](./src/consumption.csv).

### M-7: Emissions of a private jet over a leg

This was performed automatically by the program and consisted in performing the
following calculation:

```
leg emissions [kg CO2e] =
consumption [gallon/h]
x liters / gallons [L/gallon]
x liters to kg of jet fuel [L/kg]
x emissions per kg [kg CO2 / kg jet fuel]
x Radiative Forcing index [kg CO2e / kg CO2]
x Life-cycle emissions [kg CO2e / kg CO2e]
x leg time [h]
```

Where:

* `consumption` is obtained via the methodology `M-6` in this document.
* `liters / gallons = 3.78541 [L/gallon]`, as specified in [NIST's guide to SI](https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication811e2008.pdf)
* `liters to kg of jet fuel = 0.8 [kg/L]`, as [recommended by ICAO](https://data.icao.int/newDataPlus/content/docs/glossary.pdf)
* `emissions per kg = 3.16 [kg CO2 / kg jet fuel]`, as used on [ICAO Carbon Emissions Calculator Methodology, v12 from Sep. 2023](https://applications.icao.int/icec/Methodology%20ICAO%20Carbon%20Calculator_v12-2023.pdf)
* `Radiative Forcing index = 3 [kg CO2e / kg CO2]`, as concluded in [The contribution of global aviation to anthropogenic climate forcing for 2000 to 2018](https://www.sciencedirect.com/science/article/pii/S1352231020305689), from 2021.
* `Life-cycle emissions = 1.68 [kg CO2e / kg CO2e]`, [Life Cycle Greenhouse Gas Emissions from Alternative Jet Fuels v1.2](https://web.mit.edu/aeroastro/partner/reports/proj28/partner-proj28-2010-001.pdf) from 2010-06, accessed 2024-01-28.
* `leg time [h]` is obtained by computing duration of the leg, as identified via the methodology `M-4` in this document.

#### Per passager

```
leg emissions/person [kg CO2e/person] =
leg emissions [kg CO2e]
x occupancy [1/person]
```

where
* `leg emissions [kg CO2e]` is as computed above
* `occupancy = 0.23 [1/person] = 1/4.3 [1/person]` obtained from [Average number of passengers per flight who flew private worldwide from 2016 to 2019](https://www.statista.com/statistics/1171518/private-jet-per-flight/), where there were 4.3 passagers per flight in 2019, accessed 2024-01-28.

### M-8: Identify aircraft owner in Denmark

This was performed by a human, and consisted in extracting the ownership of the active
tail number from website https://www.danishaircraft.dk.
Expand Down
Loading
Loading