# Copyright © 2010 Brighter Planet.
# See LICENSE for details.
# Contact Brighter Planet for dual-license arrangements.
require 'weighted_average'
require 'builder'
## Flight:carbon model
# This module is used by [Brighter Planet](http://brighterplanet.com)'s carbon emission [web service](http://carbon.brighterplanet.com) to estimate the **greenhouse gas emissions of passenger air travel**.
#
# The final estimate is the result of the **calculations** detailed below.
# These calculations are performed in reverse order, starting with the last calculation listed and finishing with the `emission` calculation.
#
# To accomodate varying input data, each calculation may have one or more **methods**. These are listed under each calculation in order from most to least preferred.
module BrighterPlanet
module Flight
module CarbonModel
def self.included(base)
base.extend FastTimestamp
base.decide :emission, :with => :characteristics do
### Emission
# This calculation returns the `emission` estimate in *kg CO2e*.
# The `emission` estimate is the passenger's share of the total flight emissions that occured during the `timeframe`.
committee :emission do
##### From fuel, emission factor, freight share, passengers, and multipliers
# This method:
#
# 1. Checks that the flight occured during the `timeframe`
# 2. Multiplies `fuel use` (*kg fuel*) by an `emission factor` (*kg CO2e / kg fuel*) and an `aviation multiplier` to give total flight emissions in *kg CO2e*
# 3. Multiplies by (1 - `freight share`) to take out emissions attributed to freight cargo and mail, leaving emissions attributed to passengers and their baggage
# 4. Divides by the number of `passengers` and multiplies by a `seat class multiplier` to give `emission` for the passenger
quorum 'from fuel, emission factor, freight share, passengers, multipliers, and date',
:needs => [:fuel, :emission_factor, :freight_share, :passengers, :seat_class_multiplier, :aviation_multiplier, :date], :complies => [:ghg_protocol, :iso, :tcr] do |characteristics, timeframe|
date = characteristics[:date].is_a?(Date) ?
characteristics[:date] :
Date.parse(characteristics[:date].to_s)
if timeframe.include? date
characteristics[:fuel] * characteristics[:emission_factor] * characteristics[:aviation_multiplier] * (1 - characteristics[:freight_share]) / characteristics[:passengers] * characteristics[:seat_class_multiplier]
# If the flight did not occur during the `timeframe`, `emission` is zero.
else
0
end
end
quorum 'default' do
raise "The emission committee's default quorum should never be called"
end
end
### Emission factor
# This calculation returns the `emission factor` in *kg CO2e / kg fuel*.
committee :emission_factor do
##### From fuel type
# This method looks up data on [fuel types](http://data.brighterplanet.com/fuel_types) and divides the `fuel type` `emission factor` (*kg CO2 / litre fuel*) by the `fuel type` `density` (*kg fuel / litre fuel*) to give *kg CO2e / kg fuel*.
quorum 'from fuel type', :needs => :fuel_type, :complies => [:ghg_protocol, :iso, :tcr] do |characteristics|
characteristics[:fuel_type].emission_factor.to_f / characteristics[:fuel_type].density.to_f
end
end
### Aviation multiplier
# This calculation returns the `aviation multiplier`, which approximates the extra climate impact of emissions high in the atmosphere.
committee :aviation_multiplier do
##### Default
# This method uses an `aviation multiplier` of **2.0** after [Kolmuss and Crimmins (2009)](http://sei-us.org/publications/id/13).
quorum 'default', :complies => [:ghg_protocol, :iso, :tcr] do
base.fallback.aviation_multiplier
end
end
### Fuel
# This calculation returns the flight's total `fuel` use in *kg fuel*.
committee :fuel do
##### From fuel per segment and segments per trip and trips
# This method multiplies the `fuel per segment` (*kg fuel*) by the `segments per trip` and the number of `trips` to give *kg fuel*.
quorum 'from fuel per segment and segments per trip and trips', :needs => [:fuel_per_segment, :segments_per_trip, :trips], :complies => [:ghg_protocol, :iso, :tcr] do |characteristics|
characteristics[:fuel_per_segment] * characteristics[:segments_per_trip].to_f * characteristics[:trips].to_f
end
end
### Fuel per segment
# This calculation returns the `fuel per segment` in *kg fuel*.
committee :fuel_per_segment do
##### From adjusted distance per segment and fuel use coefficients
# This method uses a third-order polynomial equation to calculate the fuel used per segment:
#
# (m3 * d^3 ) + (m2 * d^2 ) + (m1 * d) + endpoint fuel
#
# Where d is the `adjusted distance per segment` and m3, m2, m2, and endpoint fuel are the `fuel use coefficients`.
quorum 'from adjusted distance per segment and fuel use coefficients', :needs => [:adjusted_distance_per_segment, :fuel_use_coefficients], :complies => [:ghg_protocol, :iso, :tcr] do |characteristics|
characteristics[:fuel_use_coefficients].m3.to_f * characteristics[:adjusted_distance_per_segment].to_f ** 3 +
characteristics[:fuel_use_coefficients].m2.to_f * characteristics[:adjusted_distance_per_segment].to_f ** 2 +
characteristics[:fuel_use_coefficients].m1.to_f * characteristics[:adjusted_distance_per_segment].to_f +
characteristics[:fuel_use_coefficients].endpoint_fuel.to_f
end
end
### Adjusted distance per segment
# This calculation returns the `adjusted distance per segment` in *nautical miles*.
committee :adjusted_distance_per_segment do
##### From adjusted distance and segments per trip
# This method divides the `adjusted distance` (*nautical miles*) by `segments per trip` to give *nautical miles*.
quorum 'from adjusted distance and segments per trip', :needs => [:adjusted_distance, :segments_per_trip], :complies => [:ghg_protocol, :iso, :tcr] do |characteristics|
characteristics[:adjusted_distance] / characteristics[:segments_per_trip]
end
end
### Adjusted distance
# This calculation returns the `adjusted distance` in *nautical miles*.
# The `adjusted distance` accounts for factors that increase the actual distance traveled by real world flights.
committee :adjusted_distance do
##### From distance, route inefficiency factor, and dogleg factor
# This method multiplies `distance` (*nautical miles*) by a `route inefficiency factor` and a `dogleg factor` to give *nautical miles*.
quorum 'from distance, route inefficiency factor, and dogleg factor', :needs => [:distance, :route_inefficiency_factor, :dogleg_factor], :complies => [:ghg_protocol, :iso, :tcr] do |characteristics|
characteristics[:distance] * characteristics[:route_inefficiency_factor] * characteristics[:dogleg_factor]
end
end
### Distance
# This calculation returns the flight's base `distance` in *nautical miles*.
committee :distance do
##### From airports
# This first-tier method calculates the great circle distance between the `origin airport` and `destination airport` and converts from *km* to *nautical miles*.
quorum 'from airports', :needs => [:origin_airport, :destination_airport], :complies => [:ghg_protocol, :iso, :tcr] do |characteristics|
if characteristics[:origin_airport].latitude and
characteristics[:origin_airport].longitude and
characteristics[:destination_airport].latitude and
characteristics[:destination_airport].longitude
characteristics[:origin_airport].distance_to(characteristics[:destination_airport], :units => :kms).kilometres.to :nautical_miles
end
end
##### From distance estimate
# This second-tier method converts the `distance_estimate` in *km* to *nautical miles*.
quorum 'from distance estimate', :needs => :distance_estimate, :complies => [:ghg_protocol, :iso, :tcr] do |characteristics|
characteristics[:distance_estimate].kilometres.to :nautical_miles
end
##### From distance class
# This third-tier method looks up the [distance class](http://data.brighterplanet.com/flight_distance_classes)'s `distance` and converts from *km* to *nautical miles*.
quorum 'from distance class', :needs => :distance_class, :complies => [:ghg_protocol, :iso, :tcr] do |characteristics|
characteristics[:distance_class].distance.kilometres.to :nautical_miles
end
##### From cohort
# This fourth-tier method calculates the average `distance` of the `cohort` segments, weighted by their passengers, and converts from *km* to *nautical miles*.
quorum 'from cohort', :needs => :cohort do |characteristics| # cohort here will be some combo of origin, airline, and aircraft
distance = characteristics[:cohort].weighted_average(:distance, :weighted_by => :passengers).kilometres.to(:nautical_miles)
distance > 0 ? distance : nil
end
##### Default
# This default method calculates the average `distance` of [all segments in the T-100 database](http://data.brighterplanet.com/flight_segments), weighted by their passengers, and converts from *km* to *nautical miles*.
quorum 'default' do
FlightSegment.fallback.distance.kilometres.to :nautical_miles
end
end
### Route inefficiency factor
# This calculation returns the `route inefficiency factor`, a measure of how much farther real world flights travel than the great circle distance between their origin and destination.
# It accounts for factors like flight path routing around controlled airspace and circling while waiting for clearance to land.
committee :route_inefficiency_factor do
##### From country
# This first-tier method looks up the `route inefficiency factor` for the [country](http://data.brighterplanet.com/countries) in which the flight occurs.
quorum 'from country', :needs => :country, :complies => [:ghg_protocol, :iso, :tcr] do |characteristics|
characteristics[:country].andand.flight_route_inefficiency_factor
end
##### Default
# This default method uses a `route inefficiency factor` of **10%** based on [Kettunen et al. (2005)](http://www.atmseminar.org/seminarContent/seminar6/papers/p_055_MPM.pdf)
quorum 'default', :complies => [:ghg_protocol, :iso, :tcr] do
Country.fallback.flight_route_inefficiency_factor
end
end
### Dogleg factor
# This calculation returns the `dogleg factor`, a measure of how far out of the way the average layover is compared to a direct flight.
committee :dogleg_factor do
##### From segments per trip
# This method assumes that each layover increases the total flight distance by **25%**.
quorum 'from segments per trip', :needs => :segments_per_trip, :complies => [:ghg_protocol, :iso, :tcr] do |characteristics|
base.fallback.dogleg_factor ** (characteristics[:segments_per_trip] - 1)
end
end
### Distance estimate
# This implied calculation returns the client-input 'distance estimate' in *km*.
### Distance class
# This implied calculation returns the client-input [distance class](http://data.brighterplanet.com/distance_classes).
### Fuel use coefficients
# This calculation returns the `fuel use coefficients`, the coefficients of the third-order polynomial equation that describes aircraft fuel use.
committee :fuel_use_coefficients do
##### From aircraft
# This first-tier method looks up the [aircraft](http://data.brighterplanet.com/aircraft)'s `fuel use coefficients`.
quorum 'from aircraft', :needs => :aircraft, :complies => [:ghg_protocol, :iso, :tcr] do |characteristics|
aircraft = characteristics[:aircraft]
fuel_use = FuelUseEquation.new aircraft.m3, aircraft.m2, aircraft.m1, aircraft.endpoint_fuel
if fuel_use.empty?
nil
else
fuel_use
end
end
##### From aircraft class
# This second-tier method looks up the [aircraft class](http://data.brighterplanet.com/aircraft_classes)'s `fuel use coefficients`.
quorum 'from aircraft class', :needs => :aircraft_class, :complies => [:ghg_protocol, :iso, :tcr] do |characteristics|
aircraft_class = characteristics[:aircraft_class]
FuelUseEquation.new aircraft_class.m3, aircraft_class.m2, aircraft_class.m1, aircraft_class.endpoint_fuel
end
##### From cohort
# This third-tier method calculates the average `fuel use coefficients` of the `cohort` segments, weighted by their passengers.
quorum 'from cohort', :needs => :cohort, :complies => [:ghg_protocol, :iso, :tcr] do |characteristics|
flight_segments = characteristics[:cohort]
passengers = flight_segments.inject(0) do |passengers, flight_segment|
passengers + flight_segment.passengers
end
bts_codes = flight_segments.map(&:bts_aircraft_type_code).uniq
relevant_aircraft = Aircraft.find_all_by_bts_aircraft_type_code(bts_codes).inject({}) do |hsh, aircraft|
hsh[aircraft.bts_aircraft_type_code] = aircraft
hsh
end
flight_segment_aircraft = flight_segments.inject({}) do |hsh, flight_segment|
bts_code = flight_segment.bts_aircraft_type_code
key = flight_segment.row_hash
aircraft = relevant_aircraft[bts_code.to_s]
hsh[key] = aircraft
hsh
end
if flight_segment_aircraft.values.map(&:m3).any?
m3 = flight_segments.inject(0) do |m3, flight_segment|
aircraft = flight_segment_aircraft[flight_segment.row_hash]
aircraft_m3 = aircraft.andand.m3 || 0
m3 + (aircraft_m3 * flight_segment.passengers)
end
else
m3 = Aircraft.fallback.m3
end
if flight_segment_aircraft.values.map(&:m2).any?
m2 = flight_segments.inject(0) do |m2, flight_segment|
aircraft = flight_segment_aircraft[flight_segment.row_hash]
aircraft_m2 = aircraft.andand.m2 || 0
m2 + (aircraft_m2 * flight_segment.passengers)
end
else
m2 = Aircraft.fallback.m2
end
if flight_segment_aircraft.values.map(&:m1).any?
m1 = flight_segments.inject(0) do |m1, flight_segment|
aircraft = flight_segment_aircraft[flight_segment.row_hash]
aircraft_m1 = aircraft.andand.m1 || 0
m1 + (aircraft_m1 * flight_segment.passengers)
end
else
m1 = Aircraft.fallback.m1
end
if flight_segment_aircraft.values.map(&:endpoint_fuel).any?
endpoint_fuel = flight_segments.inject(0) do |endpoint_fuel, flight_segment|
aircraft = flight_segment_aircraft[flight_segment.row_hash]
aircraft_epfuel = aircraft.andand.endpoint_fuel || 0
endpoint_fuel + (aircraft_epfuel * flight_segment.passengers)
end
else
endpoint_fuel = Aircraft.fallback.endpoint_fuel
end
if [m3, m2, m1, endpoint_fuel, passengers].any?(&:nonzero?)
m3 = m3 / passengers
m2 = m2 / passengers
m1 = m1 / passengers
endpoint_fuel = endpoint_fuel / passengers
FuelUseEquation.new m3, m2, m1, endpoint_fuel
end
end
##### Default
# This default method calculates the average `fuel use coefficients` of [all segments in the T-100 database](http://data.brighterplanet.com/flight_segments), weighted by their passengers.
quorum 'default', :complies => [:ghg_protocol, :iso, :tcr] do
fallback = Aircraft.fallback
if fallback
FuelUseEquation.new fallback.m3, fallback.m2, fallback.m1, fallback.endpoint_fuel
end
end
end
### Fuel type
# This calculation returns the `fuel type`.
committee :fuel_type do
##### From client input
# This implied first-tier method uses the client-input [fuel type](http://data.brighterplanet.com/fuel_types).
##### Default
# This method assumes the flight uses **Jet Fuel**.
quorum 'default' do
FuelType.find_by_name 'Jet Fuel'
end
end
### Passengers
# This calculation returns the number of `passengers`.
committee :passengers do
##### From seats and load factor
# This method multiplies the number of `seats` by the `load factor`.
quorum 'from seats and load factor', :needs => [:seats, :load_factor], :complies => [:ghg_protocol, :iso, :tcr] do |characteristics|
(characteristics[:seats] * characteristics[:load_factor]).round
end
end
### Seats
# This calculation returns the number of `seats`.
committee :seats do
##### From aircraft
# This first-tier method looks up the [aircraft](http://data.brighterplanet.com/aircraft)'s average number of `seats`.
quorum 'from aircraft', :needs => :aircraft, :complies => [:ghg_protocol, :iso, :tcr] do |characteristics|
characteristics[:aircraft].seats
end
##### From seats estimate
# This second-tier method uses the input estimate of the number of `seats`.
quorum 'from seats estimate', :needs => :seats_estimate, :complies => [:ghg_protocol, :iso, :tcr] do |characteristics|
characteristics[:seats_estimate]
end
##### From cohort
# This third-tier method calculates the average number of `seats` of the `cohort` segments, weighted by their passengers.
quorum 'from cohort', :needs => :cohort, :complies => [:ghg_protocol, :iso, :tcr] do |characteristics|
seats = characteristics[:cohort].weighted_average :seats, :weighted_by => :passengers
if seats.nil? or seats.zero?
nil
else
seats
end
end
##### From aircraft class
# This fourth-tier method looks up the [aircraft class](http://data.brighterplanet.com/aircraft_classes)'s average number of `seats`.
quorum 'from aircraft class', :needs => :aircraft_class, :complies => [:ghg_protocol, :iso, :tcr] do |characteristics|
characteristics[:aircraft_class].seats
end
##### Default
# This default method calculates the average number of `seats` of [all segments in the T-100 database](http://data.brighterplanet.com/flight_segments), weighted by their passengers.
quorum 'default' do
FlightSegment.fallback.seats.to_f # need before_type_cast b/c seats is an integer but the fallback value is a float
end
end
### Seats estimate
# This implied calculation returns the client-input `seats estimate`.
### Load factor
# This calculation returns the `load factor`.
# The `load factor` is the portion of available seats that are occupied.
committee :load_factor do
##### From client input
# This implied first-tier method uses the client-input `load factor`.
##### From cohort
# This second-tier method calculates the average `load factor` of the `cohort` segments, weighted by their passengers.
quorum 'from cohort', :needs => :cohort, :complies => [:ghg_protocol, :iso, :tcr] do |characteristics|
characteristics[:cohort].weighted_average(:load_factor, :weighted_by => :passengers)
end
##### Default
# This default method calculates the average `load factor` of [all segments in the T-100 database](http://data.brighterplanet.com/flight_segments), weighted by their passengers.
quorum 'default', :complies => [:ghg_protocol, :iso, :tcr] do
FlightSegment.fallback.load_factor
end
end
### Freight share
# This calculation returns the `freight share`.
# The `freight share` is the percent of the total aircraft weight that is freight cargo and mail (as opposed to passengers and their baggage).
committee :freight_share do
##### From cohort
# This first-tier method calculates the average `freight share` of the `cohort` segments, weighted by their passengers
quorum 'from cohort', :needs => :cohort, :complies => [:ghg_protocol, :iso, :tcr] do |characteristics|
characteristics[:cohort].weighted_average(:freight_share, :weighted_by => :passengers)
end
##### Default
# This default method calculates the average `freight share` of [all segments in the T-100 database](http://data.brighterplanet.com/flight_segments), weighted by their passengers.
quorum 'default', :complies => [:ghg_protocol, :iso, :tcr] do
FlightSegment.fallback.freight_share
end
end
### Trips
# This calculation returns the number of `trips`.
# A one-way flight has one trip; a round-trip flight has two trips.
committee :trips do
##### From client input
# This implied first-tier method uses the client-input number of `trips`.
##### Default
# This default method calculates the average number of `trips` from the [U.S. National Household Travel Survey](http://www.bts.gov/publications/america_on_the_go/long_distance_transportation_patterns/html/table_07.html).
quorum 'default', :complies => [:ghg_protocol, :iso, :tcr] do
base.fallback.trips_before_type_cast # need before_type_cast b/c trips is an integer but fallback value is a float
end
end
### Seat class multiplier
# This calculation returns the `seat class multiplier`, which reflects the amount of cabin space occupied by the passenger's seat.
committee :seat_class_multiplier do
##### From seat class
# This first-tier method looks up the [seat class](http://data.brighterplanet.com/flight_seat_classes)'s `seat class multiplier`.
quorum 'from seat class', :needs => :seat_class, :complies => [:ghg_protocol, :iso, :tcr] do |characteristics|
characteristics[:seat_class].multiplier
end
##### Default
# This default method uses a `seat class multiplier` of **1**.
quorum 'default', :complies => [:ghg_protocol, :iso, :tcr] do
FlightSeatClass.fallback.multiplier
end
end
### Seat class
# This implied calculation returns the client-input [seat class](http://data.brighterplanet.com/seat_classes).
### Country
# This calculation returns the [country](http://data.brighterplanet.com/countries) in which a flight occurs.
committee :country do
##### From origin airport and destination airport
# This method checks that the flight's `origin airport` and `destination airport` are within the same country.
# If so, that country is the `country`.
quorum 'from origin airport and destination airport', :needs => [:origin_airport, :destination_airport], :complies => [:ghg_protocol, :iso, :tcr] do |characteristics|
if characteristics[:origin_airport].country == characteristics[:destination_airport].country
characteristics[:origin_airport].country
end
end
##### From client input
# This implied method uses the client-input [country](http://data.brighterplanet.com/countries).
end
### Cohort
# This calculation returns the `cohort`, which is a set of flight segment records in the [T-100 database](http://data.brighterplanet.com/flight_segments) that match certain client-input values.
committee :cohort do
##### From segments per trip and input
# This method:
#
# 1. Checks that the flight is direct
# 2. Takes the input values for `origin airport`, `destination airport`, `aircraft`, and `airline`
# 3. Selects all the records in the T-100 database that match the available input values
# 4. Drops the last input value (initially `airline`, then `aircraft`, etc.) if no records match all of the available input values
# 5. Repeats steps 3 and 4 until some records match or no input values remain
#
# If no records match any of the input values, or if the flight is indirect, then `cohort` is undefined.
quorum 'from segments per trip and input', :needs => :segments_per_trip, :appreciates => [:origin_airport, :destination_airport, :aircraft, :airline], :complies => [:ghg_protocol, :iso, :tcr] do |characteristics|
cohort = {}
if characteristics[:segments_per_trip] == 1
provided_characteristics = [:origin_airport, :destination_airport, :aircraft, :airline].
inject(ActiveSupport::OrderedHash.new) do |memo, characteristic_name|
memo[characteristic_name] = characteristics[characteristic_name]
memo
end
cohort = FlightSegment.strict_cohort provided_characteristics
end
if cohort.any?
cohort
else
nil
end
end
end
### Origin airport
# This implied calculation returns the client-input [origin airport](http://data.brighterplanet.com/airports).
### Destination airport
# This implied calculation returns the client-input [destination airport](http://data.brighterplanet.com/airports).
### Aircraft
# This implied calculation returns the client-input type of [aircraft](http://data.brighterplanet.com/aircraft).
### Aircraft class
# This implied calculation returns the client-input [aircraft_class](http://data.brighterplanet.com/aircraft_classes).
### Airline
# This implied calculation returns the client-input [airline](http://data.brighterplanet.com/airlines) operating the flight.
### Segments per trip
# This calculation returns the `segments per trip`.
# Direct flights have a single segment per trip. Indirect flights with one or more layovers have two or more segments per trip.
committee :segments_per_trip do
##### From client input
# This implied first-tier method uses the client-input `segments per trip`.
##### Default
# This default method calculates the average `segments per trip` from the [U.S. National Household Travel Survey](http://nhts.ornl.gov/).
quorum 'default', :complies => [:ghg_protocol, :iso, :tcr] do
base.fallback.segments_per_trip_before_type_cast # need before_type_cast b/c segments_per_trip is an integer but fallback value is a float
end
end
### Date
# This calculation returns the `date` on which the flight occured.
committee :date do
##### From client input
# This implied first-tier method uses the client-input value for `date`.
##### From timeframe
# This second-tier method assumes the flight occured on the first day of the `timeframe`.
quorum 'from timeframe', :complies => [:ghg_protocol, :iso, :tcr] do |characteristics, timeframe|
timeframe.from
end
end
end
end
class FuelUseEquation < Struct.new(:m3, :m2, :m1, :endpoint_fuel)
def empty?
m3.nil? and m2.nil? and m1.nil? and endpoint_fuel.nil?
end
def to_xml(options = {})
options[:indent] ||= 2
xml = options[:builder] ||= Builder::XmlMarkup.new(:indent => options[:indent])
xml.instruct! unless options[:skip_instruct]
xml.fuel_use_equation do |estimate_block|
estimate_block.endpoint_fuel endpoint_fuel, :type => 'float'
estimate_block.m1 m1, :type => 'float'
estimate_block.m2 m2, :type => 'float'
estimate_block.m3 m3, :type => 'float'
end
end
end
end
end
end