lodging 0.0.12 → 0.0.13

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. data/.document +5 -0
  2. data/.gitignore +26 -0
  3. data/Gemfile +9 -0
  4. data/LICENSE-PREAMBLE +52 -0
  5. data/Rakefile +8 -0
  6. data/VERSION +1 -0
  7. data/certification_changelog.markdown +11 -0
  8. data/dot.rvmrc +9 -0
  9. data/features/lodging_committees.feature +356 -171
  10. data/features/lodging_impacts.feature +74 -0
  11. data/features/support/db/fixtures/census_divisions.csv +2 -0
  12. data/features/support/db/fixtures/climate_divisions.csv +2 -0
  13. data/features/support/db/fixtures/commercial_building_energy_consumption_survey_responses.csv +11 -0
  14. data/features/support/db/fixtures/countries.csv +4 -0
  15. data/features/support/db/fixtures/egrid_countries.csv +2 -0
  16. data/features/support/db/fixtures/egrid_regions.csv +2 -0
  17. data/features/support/db/fixtures/egrid_subregions.csv +3 -0
  18. data/features/support/db/fixtures/fuel_years.csv +2 -0
  19. data/features/support/db/fixtures/fuels.csv +4 -0
  20. data/features/support/db/fixtures/lodging_classes.csv +4 -0
  21. data/features/support/db/fixtures/lodging_properties.csv +5 -0
  22. data/features/support/db/fixtures/states.csv +2 -0
  23. data/features/support/db/fixtures/zip_codes.csv +4 -0
  24. data/features/support/env.rb +18 -1
  25. data/features/support/lodging_property.rb +26 -0
  26. data/{lib/test_support → features/support}/lodging_record.rb +1 -4
  27. data/lib/lodging.rb +1 -0
  28. data/lib/lodging/cbecs.rb +31 -0
  29. data/lib/lodging/characterization.rb +19 -3
  30. data/lib/lodging/data.rb +22 -8
  31. data/lib/lodging/impact_model.rb +544 -0
  32. data/lib/lodging/relationships.rb +3 -1
  33. data/lib/lodging/version.rb +5 -0
  34. data/lodging.gemspec +31 -0
  35. data/todo.txt +5 -0
  36. metadata +76 -119
  37. data/features/lodging_emissions.feature +0 -53
  38. data/lib/lodging/carbon_model.rb +0 -265
  39. data/lib/lodging/fallback.rb +0 -10
@@ -0,0 +1,74 @@
1
+ Feature: Lodging Emissions Calculations
2
+ The lodging model should generate correct emission calculations
3
+
4
+ Background:
5
+ Given a Lodging
6
+
7
+ Scenario: Calculations starting from nothing
8
+ Given a lodging has nothing
9
+ When impacts are calculated
10
+ Then the amount of "carbon" should be within "0.005" of "28.62"
11
+
12
+ Scenario Outline: Calculations starting from date
13
+ Given it has "date" of "<date>"
14
+ And it has "timeframe" of "<timeframe>"
15
+ When impacts are calculated
16
+ Then the amount of "carbon" should be within "0.005" of "<carbon>"
17
+ Examples:
18
+ | date | timeframe | carbon |
19
+ | 2011-01-15 | 2011-01-01/2012-01-01 | 28.62 |
20
+ | 2012-01-15 | 2011-01-01/2012-01-01 | 0.0 |
21
+
22
+ Scenario: Calculations starting from rooms and duration
23
+ Given it has "rooms" of "2"
24
+ And it has "duration" of "172800"
25
+ When impacts are calculated
26
+ Then the amount of "carbon" should be within "0.005" of "114.47"
27
+
28
+ Scenario Outline: Calculations from fuzzy inference based on country degree days
29
+ Given it has "rooms" of "2"
30
+ And it has "duration" of "172800"
31
+ And it has "country.iso_3166_code" of "<country>"
32
+ And it has "state.postal_abbreviation" of "<state>"
33
+ When impacts are calculated
34
+ Then the amount of "carbon" should be within "0.005" of "<carbon>"
35
+ Examples:
36
+ | country | state | carbon |
37
+ | VI | | 114.47 |
38
+ | GB | | 189.56 |
39
+ | US | | 131.28 |
40
+ | | CA | 131.28 |
41
+
42
+ Scenario Outline: Calculations from fuzzy inference
43
+ Given it has "rooms" of "2"
44
+ And it has "duration" of "172800"
45
+ And it has "property.northstar_id" of "<id>"
46
+ And it has "zip_code.name" of "<zip>"
47
+ And it has "city" of "<city>"
48
+ And it has "state.postal_abbreviation" of "<state>"
49
+ When impacts are calculated
50
+ Then the amount of "carbon" should be within "0.005" of "<carbon>"
51
+ Examples:
52
+ | id | zip | city | state | carbon | notes |
53
+ | 1 | 94122 | | | 86.85 | dd from climate divizion; fuzzy from property attributes |
54
+ | 1 | | San Francisco | CA | 122.33 | dd from country; fuzzy from property attributes |
55
+ | 2 | 94133 | | | 94.24 | dd from country; fuzzy from property attributes |
56
+ | 2 | | San Francisco | CA | 94.24 | dd from country; fuzzy from property attributes |
57
+ | 3 | 94014 | | | 128.13 | dd from country; fuzzy from property attributes |
58
+
59
+ Scenario: Calculations from all user inputs
60
+ Given it has "rooms" of "2"
61
+ And it has "duration" of "172800"
62
+ And it has "country.iso_3166_code" of "GB"
63
+ And it has "heating_degree_days" of "2700"
64
+ And it has "cooling_degree_days" of "100"
65
+ And it has "property_rooms" of "100"
66
+ And it has "floors" of "3"
67
+ And it has "construction_year" of "1993"
68
+ And it has "ac_coverage" of "0.5"
69
+ And it has "refrigerator_coverage" of "0.6"
70
+ And it has "hot_tubs" of "6"
71
+ And it has "indoor_pools" of "5"
72
+ And it has "outdoor_pools" of "5"
73
+ When impacts are calculated
74
+ Then the amount of "carbon" should be within "0.005" of "208.83"
@@ -0,0 +1,2 @@
1
+ number,census_region_number
2
+ 9,4
@@ -0,0 +1,2 @@
1
+ name,heating_degree_days,cooling_degree_days,state_postal_abbreviation
2
+ CA4,1350,150,CA
@@ -0,0 +1,11 @@
1
+ id,heating_degree_days,cooling_degree_days,lodging_rooms,floors,construction_year,percent_cooled,natural_gas_per_room_night,fuel_oil_per_room_night,electricity_per_room_night,district_heat_per_room_night,weighting
2
+ 1,5500,400,10,1,1967,1,0,1.9,12,0,1700
3
+ 2,1500,1200,35,2,1988,0.6,0,0,14,0,420
4
+ 3,200,2700,100,4,2000,1,1.2,0,34,0,125
5
+ 4,2300,900,2,1,1992,1,0,0,23,0,1770
6
+ 5,2300,700,1000,25,1986,1,11,0.015,120,0,135
7
+ 6,3800,200,7,4,1881,0,0,0,9.2,0,1925
8
+ 7,800,1700,125,3,2002,1,1.9,0,27,0,240
9
+ 8,3300,400,10,2,1920,0,0,6.1,11,0,1925
10
+ 9,1000,1700,14,2,1950,1,9.7,0,83,180,365
11
+ 10,2400,800,450,25,1929,1,0.74,0.00092,41,97,120
@@ -0,0 +1,4 @@
1
+ iso_3166_code,iso_3166_alpha_3_code,heating_degree_days,cooling_degree_days,electricity_emission_factor,electricity_loss_factor,lodging_occupancy_rate,lodging_natural_gas_intensity,lodging_fuel_oil_intensity,lodging_electricity_intensity,lodging_district_heat_intensity
2
+ US,USA,2200,880,0.59000,0.06025,0.6,2.0,0.4,33.9,1.8
3
+ GB,GBR,2800,60,0.9,0.1,0.5,3.0,0.5,60.0,0.0
4
+ VI,VIR
@@ -0,0 +1,2 @@
1
+ name,generation,imports,consumption
2
+ US,400000,3100,379000
@@ -0,0 +1,2 @@
1
+ name,loss_factor
2
+ W,0.05
@@ -0,0 +1,3 @@
1
+ abbreviation,egrid_region_name,net_generation,electricity_emission_factor
2
+ CAMX,W,1000,0.31
3
+ RMPA,W,1000,0.87
@@ -0,0 +1,2 @@
1
+ name,fuel_name,year,energy_content,energy_content_units,co2_emission_factor,co2_emission_factor_units
2
+ Pipeline Natural Gas 2008,Pipeline Natural Gas,2008,38,megajoules_per_cubic_metre,1.9,kilograms_per_cubic_metre
@@ -0,0 +1,4 @@
1
+ name,energy_content,co2_emission_factor
2
+ Residual Fuel Oil No. 6,42,3.0
3
+ District Heat,1,0.077
4
+ Pipeline Natural Gas
@@ -0,0 +1,4 @@
1
+ name
2
+ Hotel
3
+ Inn
4
+ Motel
@@ -0,0 +1,5 @@
1
+ northstar_id,name,city,locality,postcode,country_iso_3166_alpha_3_code,lodging_rooms,floors,construction_year,ac_coverage,lodging_class_name,fridge_coverage,mini_bar_coverage,hot_tubs,pools_indoor,pools_outdoor
2
+ 1,Courtyard by Marriott,San Francisco,California,94122,USA,100,3,1993,0.5,Motel,0.5,0.6,6,6,6
3
+ 2,Hilton San Francisco,San Francisco,California,94133,USA,50,2,2002,1,Hotel,0.5,,0,0,0
4
+ 3,Pacific Inn,Daly City,California,94014,USA,25,,,,Inn,,0.5,1,1,1
5
+ 4,,,,,,,,,,,0.6,0.5,5,5,5
@@ -0,0 +1,2 @@
1
+ postal_abbreviation,name,census_division_number
2
+ CA,California,9
@@ -0,0 +1,4 @@
1
+ name,state_postal_abbreviation,description,egrid_subregion_abbreviation,climate_division_name
2
+ 94122,CA,San Francisco,CAMX,CA4
3
+ 94133,CA,San Francisco
4
+ 94014,CA,Daly City
@@ -3,6 +3,23 @@ Bundler.setup
3
3
 
4
4
  require 'cucumber'
5
5
  require 'cucumber/formatter/unicode'
6
+ require 'rspec'
7
+ require 'rspec/expectations'
8
+ require 'cucumber/rspec/doubles'
9
+
10
+ require 'data_miner'
11
+ DataMiner.logger = Logger.new(nil)
6
12
 
7
13
  require 'sniff'
8
- Sniff.init File.join(File.dirname(__FILE__), '..', '..'), :earth => [:hospitality, :fuel, :locality], :cucumber => true
14
+
15
+ # need to require this before initializing Sniff for LodgingProperty fixture to be loaded
16
+ require File.expand_path('../lodging_property', __FILE__)
17
+
18
+ Sniff.init File.join(File.dirname(__FILE__), '..', '..'),
19
+ :adapter => 'mysql2',
20
+ :database => 'test_lodging',
21
+ :username => 'root',
22
+ :password => 'password',
23
+ :earth => [:hospitality, :fuel, :locality],
24
+ :cucumber => true,
25
+ :logger => 'log/test_log.txt'
@@ -0,0 +1,26 @@
1
+ # LodgingProperty is defined in CM1 rather than Earth, so we need to define it here for testing
2
+ class LodgingProperty < ActiveRecord::Base
3
+ col :northstar_id
4
+ col :name
5
+ col :city
6
+ col :locality # state / province / etc.
7
+ col :postcode # zip code / postal code / etc.
8
+ col :country_iso_3166_alpha_3_code
9
+ col :chain_name
10
+ col :lodging_rooms, :type => :integer
11
+ col :floors, :type => :integer
12
+ col :construction_year, :type => :integer
13
+ col :lodging_class_name
14
+ col :ac_coverage, :type => :float
15
+ col :mini_bar_coverage, :type => :float
16
+ col :fridge_coverage, :type => :float
17
+ col :hot_tubs, :type => :float # float b/c fallback needs to be a float
18
+ col :pools_indoor, :type => :float # float b/c fallback needs to be a float
19
+ col :pools_outdoor, :type => :float # float b/c fallback needs to be a float
20
+
21
+ # based on LodgingProperty as of 2012-02-21
22
+ falls_back_on :fridge_coverage => 0.6,
23
+ :hot_tubs => 0.3,
24
+ :pools_indoor => 0.3,
25
+ :pools_outdoor => 0.6
26
+ end
@@ -1,9 +1,6 @@
1
- require 'active_record'
2
- require 'falls_back_on'
3
1
  require 'lodging'
4
- require 'sniff'
5
2
 
6
3
  class LodgingRecord < ActiveRecord::Base
7
- include Sniff::Emitter
4
+ include BrighterPlanet::Emitter
8
5
  include BrighterPlanet::Lodging
9
6
  end
@@ -3,5 +3,6 @@ require 'emitter'
3
3
  module BrighterPlanet
4
4
  module Lodging
5
5
  extend BrighterPlanet::Emitter
6
+ scope 'The lodging emission estimate is the anthropogenic emissions from lodging room energy use. It includes CO2 emissions from direct fuel combustion and indirect fuel combustion to generate purchased electricity.'
6
7
  end
7
8
  end
@@ -0,0 +1,31 @@
1
+ require 'fuzzy_infer'
2
+
3
+ class CommercialBuildingEnergyConsumptionSurveyResponse < ActiveRecord::Base
4
+ fuzzy_infer :target => [:natural_gas_per_room_night, :fuel_oil_per_room_night, :electricity_per_room_night, :district_heat_per_room_night], # list of columns that this model is designed to infer
5
+ :basis => [:heating_degree_days, :cooling_degree_days, :lodging_rooms, :floors, :construction_year, :percent_cooled], # list of columns that are believed to affect energy use (aka MU)
6
+ :sigma => "(STDDEV_SAMP(:column)/5)+(ABS(AVG(:column)-:value)/3)", # empirically determined formula (SQL!) that captures the desired sample size once all the weights are compiled, across the full range of possible mu values
7
+ :membership => :energy_use_membership, # name of instance method to be called on kernel
8
+ :weight => :weighting # (optional) a pre-existing row weighting, if any, provided by the dataset authors
9
+
10
+ # empirically determined formula that minimizes variance between real and predicted energy use
11
+ # SQL! - :heating_degree_days_n_w will be replaced with, for example, `tmp_table_9301293.hdd_normalized_weight`
12
+ def energy_use_membership(basis)
13
+ keys = basis.keys
14
+
15
+ unless keys.include?(:heating_degree_days) and keys.include?(:cooling_degree_days)
16
+ raise ArgumentError, "[lodging] Must provide at least :heating_degree_days and :cooling_degree_days"
17
+ end
18
+
19
+ formula = ['(POW(:heating_degree_days_n_w, 0.8) + POW(:cooling_degree_days_n_w, 0.8))']
20
+ if keys.include?(:lodging_rooms) and keys.include?(:floors)
21
+ formula << '(POW(:lodging_rooms_n_w, 0.8) + POW(:floors_n_w, 0.8))'
22
+ elsif keys.include? :lodging_rooms
23
+ formula << 'POW(:lodging_rooms_n_w, 0.8)'
24
+ elsif keys.include? :floors
25
+ formula << 'POW(:floors_n_w, 0.8)'
26
+ end
27
+ formula << 'POW(:percent_cooled_n_w, 0.8)' if keys.include?(:percent_cooled)
28
+ formula << 'POW(:construction_year_n_w, 0.8)' if keys.include?(:construction_year)
29
+ formula.join(' * ')
30
+ end
31
+ end
@@ -3,11 +3,27 @@ module BrighterPlanet
3
3
  module Characterization
4
4
  def self.included(base)
5
5
  base.characterize do
6
- has :lodging_class
6
+ has :date
7
+ has :rooms
8
+ has :duration, :measures => :time
7
9
  has :zip_code
10
+ has :city
8
11
  has :state
9
- has :rooms
10
- has :duration
12
+ has :country
13
+ has :lodging_class
14
+ has :heating_degree_days
15
+ has :cooling_degree_days
16
+ has :property do |prop|
17
+ prop.name
18
+ end
19
+ has :property_rooms
20
+ has :floors
21
+ has :construction_year
22
+ has :ac_coverage
23
+ has :refrigerator_coverage
24
+ has :hot_tubs
25
+ has :outdoor_pools
26
+ has :indoor_pools
11
27
  end
12
28
  end
13
29
  end
@@ -2,16 +2,30 @@ module BrighterPlanet
2
2
  module Lodging
3
3
  module Data
4
4
  def self.included(base)
5
+ base.col :date, :type => :date
6
+ base.col :rooms, :type => :integer
7
+ base.col :duration, :type => :integer
8
+ base.col :zip_code_name
9
+ base.col :city
10
+ base.col :state_postal_abbreviation
11
+ base.col :country_iso_3166_code
12
+ base.col :lodging_class_name
13
+ base.col :heating_degree_days, :type => :float
14
+ base.col :cooling_degree_days, :type => :float
15
+ base.col :property_northstar_id
16
+ base.col :property_rooms, :type => :integer
17
+ base.col :floors, :type => :integer
18
+ base.col :construction_year, :type => :integer
19
+ base.col :ac_coverage, :type => :float
20
+ base.col :refrigerator_coverage, :type => :float
21
+ base.col :hot_tubs, :type => :integer
22
+ base.col :outdoor_pools, :type => :integer
23
+ base.col :indoor_pools, :type => :integer
24
+
5
25
  base.data_miner do
6
- schema do
7
- string 'lodging_class_name'
8
- string 'zip_code_name'
9
- string 'state_postal_abbreviation'
10
- integer 'rooms'
11
- integer 'duration'
26
+ process 'pull orphans' do
27
+ Fuel.run_data_miner!
12
28
  end
13
-
14
- process :run_data_miner_on_belongs_to_associations
15
29
  end
16
30
  end
17
31
  end
@@ -0,0 +1,544 @@
1
+ # Copyright © 2010 Brighter Planet.
2
+ # See LICENSE for details.
3
+ # Contact Brighter Planet for dual-license arrangements.
4
+
5
+ ## Lodging impact model
6
+ # This model is used by the [Brighter Planet](http://brighterplanet.com) [CM1 web service](http://carbon.brighterplanet.com) to calculate the impacts of a lodging (e.g. a hotel stay), such as energy use, greenhouse gas emissions, and water use.
7
+
8
+ ##### Timeframe
9
+ # The model calculates impacts that occured during a particular time period (`timeframe`).
10
+ # For example if the `timeframe` is February 2010, a lodging that occurred (`date`) on February 15, 2010 will have impacts, but a lodging that occurred on January 31, 2010 will have zero impacts.
11
+ #
12
+ # The default `timeframe` is the current calendar year.
13
+
14
+ ##### Calculations
15
+ # The final impacts are the result of the calculations below. These are performed in reverse order, starting with the last calculation listed and finishing with the greenhouse gas emissions calculation.
16
+ #
17
+ # Each calculation listing shows:
18
+ #
19
+ # * value returned (*units of measurement*)
20
+ # * description of the value
21
+ # * calculation methods, listed from most to least preferred
22
+ #
23
+ # Some methods use `values` returned by prior calculations. If any of these `values` are unknown the method is skipped.
24
+ # If all the methods for a calculation are skipped, the value the calculation would return is unknown.
25
+
26
+ ##### Standard compliance
27
+ # When compliance with a particular standard is requested, all methods that do not comply with that standard are ignored.
28
+ # Thus any `values` a method needs will have been calculated using a compliant method or will be unknown.
29
+ # To see which standards a method complies with, look at the `:complies =>` section of the code in the right column.
30
+ #
31
+ # Client input complies with all standards.
32
+
33
+ ##### Collaboration
34
+ # Contributions to this impact model are actively encouraged and warmly welcomed. This library includes a comprehensive test suite to ensure that your changes do not cause regressions. All changes should include test coverage for new functionality. Please see [sniff](https://github.com/brighterplanet/sniff#readme), our emitter testing framework, for more information.
35
+ require 'lodging/cbecs'
36
+
37
+ module BrighterPlanet
38
+ module Lodging
39
+ module ImpactModel
40
+ def self.included(base)
41
+ base.decide :impact, :with => :characteristics do
42
+ # * * *
43
+
44
+ #### Carbon (*kg CO<sub>2</sub>e*)
45
+ # *The lodging's total anthropogenic greenhouse gas emissions during `timeframe`.*
46
+ committee :carbon do
47
+ # Multiply `natural gas use` (*m<sup>3</sup>*) by natural gas' emission factor (*kg CO<sub>2</sub> / m<sup>3</sup>*) to give *kg CO<sub>2</sub>.*
48
+ # Multiply `fuel oil use` (*l*) by fuel oil's emission factor (*kg CO<sub>2</sub> / l*) to give *kg CO<sub>2</sub>.*
49
+ # Multiply `electricity use` (*kWh*) by `electricity emission factor` (*kg CO<sub>2</sub> / kWh*) to give *kg CO<sub>2</sub>.*
50
+ # Multiply `district heat use` (*MJ*) by district heat's emission factor` (*kg CO<sub>2</sub> / MJ*) to give *kg CO<sub>2</sub>.*
51
+ # Sum to give *kg CO<sub>2</sub>e.*
52
+ quorum 'from natural gas use, fuel oil use, electricity use, district heat use, and electricity emission factor', :needs => [:natural_gas_use, :fuel_oil_use, :electricity_use, :district_heat_use, :electricity_emission_factor],
53
+ :complies => [:ghg_protocol_scope_3, :iso, :tcr] do |characteristics|
54
+ characteristics[:natural_gas_use] * Fuel.find_by_name('Pipeline Natural Gas').co2_emission_factor +
55
+ characteristics[:fuel_oil_use] * Fuel.find_by_name('Residual Fuel Oil No. 6').co2_emission_factor +
56
+ characteristics[:electricity_use] * characteristics[:electricity_emission_factor] +
57
+ characteristics[:district_heat_use] * Fuel.find_by_name('District Heat').co2_emission_factor
58
+ end
59
+ end
60
+
61
+ #### Electricity emission factor (*kg CO<sub>2</sub>e / kWh*)
62
+ # *A greenhouse gas emission factor for electricity used by the lodging.*
63
+ committee :electricity_emission_factor do
64
+ # Multiply the `eGRID subregion` electricity emission factors by 1 minus the the subregion's eGRID region loss factor (to account for transmission and distribution losses) to give *kg CO<sub>2</sub>e / kWh*.
65
+ quorum 'from eGRID subregion', :needs => :egrid_subregion,
66
+ :complies => [:ghg_protocol_scope_3, :iso, :tcr] do |characteristics|
67
+ characteristics[:egrid_subregion].electricity_emission_factor / (1 - characteristics[:egrid_subregion].egrid_region.loss_factor)
68
+ end
69
+
70
+ # Otherwise look up the `country` electricity emission factor (*kg CO<sub>2</sub>e / kWh*).
71
+ quorum 'from country', :needs => :country,
72
+ :complies => [:ghg_protocol_scope_3, :iso, :tcr] do |characteristics|
73
+ if (emission_factor = characteristics[:country].electricity_emission_factor).present? and (loss_factor = characteristics[:country].electricity_loss_factor).present?
74
+ emission_factor / (1 - loss_factor)
75
+ end
76
+ end
77
+
78
+ # Otherwise use a global average electricity emission factor (*kg CO<sub>2</sub>e / kWh*).
79
+ quorum 'default',
80
+ :complies => [:ghg_protocol_scope_3, :iso, :tcr] do
81
+ Country.fallback.electricity_emission_factor / (1 - Country.fallback.electricity_loss_factor)
82
+ end
83
+ end
84
+
85
+ #### Natural gas use (*m<sup>3</sup>*)
86
+ # *The lodging's natural gas use during `timeframe`.*
87
+ committee :natural_gas_use do
88
+ # Multiply `room nights` (*room-nights*) by `natural gas intensity` (*m<sup>3</sup> / occupied room-night*) to give *m<sup>3</sup>*.
89
+ quorum 'from adjusted fuel intensities and room nights', :needs => [:adjusted_fuel_intensities, :room_nights],
90
+ :complies => [:ghg_protocol_scope_3, :iso, :tcr] do |characteristics|
91
+ characteristics[:room_nights] * characteristics[:adjusted_fuel_intensities][:natural_gas]
92
+ end
93
+ end
94
+
95
+ #### Fuel oil use (*l*)
96
+ # *The lodging's fuel oil use during `timeframe`.*
97
+ committee :fuel_oil_use do
98
+ # Multiply `room nights` (*room-nights*) by `fuel oil intensity` (*l / occupied room-night*) to give *l*.
99
+ quorum 'from adjusted fuel intensities and room nights', :needs => [:adjusted_fuel_intensities, :room_nights],
100
+ :complies => [:ghg_protocol_scope_3, :iso, :tcr] do |characteristics|
101
+ characteristics[:room_nights] * characteristics[:adjusted_fuel_intensities][:fuel_oil]
102
+ end
103
+ end
104
+
105
+ #### Electricity use (*kWh*)
106
+ # *The lodging's electricity use during `timeframe`.*
107
+ committee :electricity_use do
108
+ # Multiply `room nights` (*room-nights*) by `electricity intensity` (*kWh / occupied room-night*) to give *kWh*.
109
+ quorum 'from adjusted fuel intensities and room nights', :needs => [:adjusted_fuel_intensities, :room_nights],
110
+ :complies => [:ghg_protocol_scope_3, :iso, :tcr] do |characteristics|
111
+ characteristics[:room_nights] * characteristics[:adjusted_fuel_intensities][:electricity]
112
+ end
113
+ end
114
+
115
+ #### District heat use (*MJ*)
116
+ # *The lodging's district heat use during `timeframe`.*
117
+ committee :district_heat_use do
118
+ # Multiply `room nights` (*room-nights*) by `district heat intensity` (*MJ / occupied room-night*) to give *MJ*.
119
+ quorum 'from adjusted fuel intensities and room nights', :needs => [:adjusted_fuel_intensities, :room_nights],
120
+ :complies => [:ghg_protocol_scope_3, :iso, :tcr] do |characteristics|
121
+ characteristics[:room_nights] * characteristics[:adjusted_fuel_intensities][:district_heat]
122
+ end
123
+ end
124
+
125
+ #### Adjusted fuel intensities (*various*)
126
+ # *The lodging's use per occupied room night of a variety of fuels, adjusted by number of pools, mini-fridges, etc.*
127
+ #
128
+ # - Natural gas intensity: *m<sup>3</sup> / occupied room-night*
129
+ # - Fuel oil intensity: *l / occupied room-night*
130
+ # - Electricity intensity: *kWh / occupied room-night*
131
+ # - District heat intensity: *MJ / occupied room-night*
132
+ committee :adjusted_fuel_intensities do
133
+ # Adjust `fuel intensities` based on any amenity adjustments:
134
+ quorum 'from fuel intensities and amenity adjustments',
135
+ :needs => :fuel_intensities, :appreciates => [:indoor_pool_adjustment, :outdoor_pool_adjustment, :refrigerator_adjustment, :hot_tub_adjustment],
136
+ :complies => [:ghg_protocol_scope_3, :iso, :tcr] do |characteristics|
137
+ gas = Fuel.find_by_name('Pipeline Natural Gas')
138
+ oil = Fuel.find_by_name('Residual Fuel Oil No. 6')
139
+
140
+ # Duplicate the current intensities so we don't overwrite them.
141
+ intensities = characteristics[:fuel_intensities].dup
142
+
143
+ # TODO combine pool adjustments - so if we're below average in indoor but above in outdoor we don't accidentally subtract, get less than zero, discard some energy use, and then add energy back for outdoor pools
144
+
145
+ # Cycle through each adjustment...
146
+ characteristics.each do |characteristic, value|
147
+ unless characteristic == :fuel_intensities
148
+ value.each do |fuel, adjustment|
149
+ if fuel == :pool_energy
150
+ # If the adjustment relates to pool energy, convert natural gas and fuel oil inensities from physical units to energy units.
151
+ gas_energy = intensities[:natural_gas] * gas.energy_content
152
+ oil_energy = intensities[:fuel_oil] * oil.energy_content
153
+
154
+ # Apply the adjustment to natural gas intensity.
155
+ # If adjusted natural gas intensity reaches zero, apply any remaining adjustment to fuel oil intensity.
156
+ # If adjusted fuel oil intensity also reaches zero, discard any remaining adjustment.
157
+ if (gas_energy += adjustment) < 0.0
158
+ if (oil_energy += gas_energy) < 0.0
159
+ oil_energy = 0.0
160
+ end
161
+ gas_energy = 0
162
+ end
163
+
164
+ # Convert adjusted natural gas and fuel oil intensities from energy units back to physical units.
165
+ intensities[:natural_gas] = gas_energy / gas.energy_content
166
+ intensities[:fuel_oil] = oil_energy / oil.energy_content
167
+ # Otherwise apply the adjustment to the appropriate fuel intensity. If adjusted fuel intensity reaches zero, discard any remaining adjustment.
168
+ elsif (intensities[fuel] += adjustment) < 0.0
169
+ intensities[fuel] = 0.0
170
+ end
171
+ end
172
+ end
173
+ end
174
+ intensities
175
+ end
176
+ end
177
+
178
+ #### Outdoor pools adjustment (*MJ / occupied room-night*)
179
+ # *Adjusts the natural gas intensity based on the number of outdoor pools.*
180
+ committee :outdoor_pool_adjustment do
181
+ # Assume outdoor pool energy intensity of 329,917 *BTU / night* per [Energy Star](http://www.energystar.gov/ia/business/evaluate_performance/swimming_pool_tech_desc.pdf).
182
+ # Calculate the difference between `outdoor pools` and average outdoor pools.
183
+ # Multiply the difference by outdoor pool energy intensity (*MJ / night*) and divide by `property rooms` and `occupancy rate` to give *MJ / occupied room-night*.
184
+ quorum 'from outdoor pools, property rooms, and occupancy rate', :needs => [:outdoor_pools, :property_rooms, :occupancy_rate],
185
+ :complies => [:ghg_protocol_scope_3, :iso, :tcr] do |characteristics|
186
+ difference = characteristics[:outdoor_pools] - LodgingProperty.fallback.pools_outdoor
187
+ { :pool_energy => difference * 329_917.btus.to(:megajoules) / characteristics[:property_rooms] / characteristics[:occupancy_rate] }
188
+ end
189
+ end
190
+
191
+ #### Indoor pools adjustment (*MJ / occupied room-night*)
192
+ # *Adjusts the natural gas intensity based on the number of indoor pools.*
193
+ committee :indoor_pool_adjustment do
194
+ # Assume indoor pool energy intensity of 2,770,942 *BTU / night* per [Energy Star](http://www.energystar.gov/ia/business/evaluate_performance/swimming_pool_tech_desc.pdf).
195
+ # Calculate the difference between `indoor pools` and average indoor pools.
196
+ # Multiply the difference by indoor pool energy intensity (*MJ / night*) and divide by `property rooms` and `occupancy rate` to give *MJ / occupied room-night*.
197
+ quorum 'from indoor pools, property rooms, and occupancy rate', :needs => [:indoor_pools, :property_rooms, :occupancy_rate],
198
+ :complies => [:ghg_protocol_scope_3, :iso, :tcr] do |characteristics|
199
+ difference = characteristics[:indoor_pools] - LodgingProperty.fallback.pools_indoor
200
+ { :pool_energy => difference * 2_770_942.btus.to(:megajoules) / characteristics[:property_rooms] / characteristics[:occupancy_rate] }
201
+ end
202
+ end
203
+
204
+ #### Hot tub adjustment (*kWh / occupied room-night*)
205
+ # *Adjusts the electricity intensity based on the number of hot tubs.*
206
+ committee :hot_tub_adjustment do
207
+ # Calculate the difference between `hot tubs` and average hot tubs.
208
+ # Assume hot tub electricity intensity of 6.3 *kWh / night* per [LBL residential energy data sourcebook, p128](http://enduse.lbl.gov/info/LBNL-40297.pdf).
209
+ # Multiply the difference by the hot tub electricity intensity (*kWh / night*) and divide by `property rooms` and `occupancy rate` to give *kWh / occupied room-night*.
210
+ quorum 'from hot tubs, property rooms, and occupancy rate', :needs => [:hot_tubs, :property_rooms, :occupancy_rate],
211
+ :complies => [:ghg_protocol_scope_3, :iso, :tcr] do |characteristics|
212
+ difference = characteristics[:hot_tubs] - LodgingProperty.fallback.hot_tubs
213
+ { :electricity => difference * 6.3 / characteristics[:property_rooms] / characteristics[:occupancy_rate] }
214
+ end
215
+ end
216
+
217
+ #### Refrigerator adjustment (*kWh / occupied room-night*)
218
+ # *Adjusts the electricity intensity based on refrigerator coverage.*
219
+ committee :refrigerator_adjustment do
220
+ # Calculate the difference between `refrigerator coverage` (*refrigerators / room*) and average refrigerator coverage (*refrigerators / room*).
221
+ # Assume an auto-defrost compact fridge electricity intensity of 1.18 *kWh / refrigerator night* per [Energy Star](http://www.energystar.gov/ia/business/bulk_purchasing/bpsavings_calc/Bulk_Purchasing_CompactRefrig_Sav_Calc.xls).
222
+ # Multiply the difference (*refrigerators / room*) by the refrigerator electricity intensity (*kWh / refrigerator night*) and divide by `occupancy rate` to give *kWh / occupied room-night*.
223
+ quorum 'from refrigerator coverage and occupancy rate', :needs => [:refrigerator_coverage, :occupancy_rate],
224
+ :complies => [:ghg_protocol_scope_3, :iso, :tcr] do |characteristics|
225
+ difference = characteristics[:refrigerator_coverage] - LodgingProperty.fallback.fridge_coverage
226
+ { :electricity => (difference * 1.18 / characteristics[:occupancy_rate]) }
227
+ end
228
+ end
229
+
230
+ #### Fuel intensities (*various*)
231
+ # *The lodging's use per occupied room night of a variety of fuels.*
232
+ #
233
+ # - Natural gas intensity: *m<sup>3</sup> / occupied room-night*
234
+ # - Fuel oil intensity: *l / occupied room-night*
235
+ # - Electricity intensity: *kWh / occupied room-night*
236
+ # - District heat intensity: *MJ / occupied room-night*
237
+ committee :fuel_intensities do
238
+ # If we know `heating degree days` and `cooling degree days`, calculate fuel intensities from [CBECS 2003](http://data.brighterplanet.com/commercial_building_energy_consumption_survey_responses) data using fuzzy inference and adjust the inferred values based on `occupancy rate`.
239
+ quorum 'from degree days, occupancy rate, and user inputs', :needs => [:heating_degree_days, :cooling_degree_days, :occupancy_rate], :appreciates => [:property_rooms, :floors, :construction_year, :ac_coverage],
240
+ :complies => [:ghg_protocol_scope_3, :iso, :tcr] do |characteristics|
241
+ inputs = characteristics.to_hash.inject({}) do |memo, (characteristic, value)|
242
+ case characteristic
243
+ when :property_rooms
244
+ memo[:lodging_rooms] = value
245
+ when :ac_coverage
246
+ memo[:percent_cooled] = value
247
+ when :occupancy_rate
248
+ # Don't include `occupancy rate` in inputs to fuzzy inference
249
+ else
250
+ memo[characteristic] = value
251
+ end
252
+ memo
253
+ end
254
+
255
+ kernel = CommercialBuildingEnergyConsumptionSurveyResponse.new(inputs)
256
+ n, f, e, d = kernel.fuzzy_infer(:natural_gas_per_room_night, :fuel_oil_per_room_night, :electricity_per_room_night, :district_heat_per_room_night)
257
+ {
258
+ :natural_gas => n / characteristics[:occupancy_rate],
259
+ :fuel_oil => f / characteristics[:occupancy_rate],
260
+ :electricity => e / characteristics[:occupancy_rate],
261
+ :district_heat => d / characteristics[:occupancy_rate]
262
+ }
263
+ end
264
+
265
+ # Otherwise use global averages.
266
+ quorum 'default',
267
+ :complies => [:ghg_protocol_scope_3, :iso, :tcr] do |characteristics|
268
+ intensities = {
269
+ :natural_gas => Country.fallback.lodging_natural_gas_intensity,
270
+ :fuel_oil => Country.fallback.lodging_fuel_oil_intensity,
271
+ :electricity => Country.fallback.lodging_electricity_intensity,
272
+ :district_heat => Country.fallback.lodging_district_heat_intensity,
273
+ }
274
+ end
275
+ end
276
+
277
+ #### Occupancy rate
278
+ # *The percent of the proprety's rooms that are occupied on an average night.*
279
+ committee :occupancy_rate do
280
+ # Look up the `country` average lodging occupancy rate.
281
+ quorum 'from country', :needs => :country,
282
+ :complies => [:ghg_protocol_scope_3, :iso, :tcr] do |characteristics|
283
+ characteristics[:country].lodging_occupancy_rate
284
+ end
285
+
286
+ # Otherwise use a global average.
287
+ quorum 'default', :complies => [:ghg_protocol_scope_3, :iso, :tcr] do
288
+ Country.fallback.lodging_occupancy_rate
289
+ end
290
+ end
291
+
292
+ #### Outdoor pools
293
+ # *The number of outdoor pools.*
294
+ committee :outdoor_pools do
295
+ # Use client input, if available.
296
+
297
+ # Otherwise look up the `property` number of outdoor pools, but set a ceiling of 5 outdoor pools.
298
+ quorum 'from property', :needs => :property,
299
+ :complies => [:ghg_protocol_scope_3, :iso, :tcr] do |characteristics|
300
+ unless characteristics[:property].pools_outdoor.nil?
301
+ [characteristics[:property].pools_outdoor.to_f, 5].min
302
+ end
303
+ end
304
+ end
305
+
306
+ #### Indoor pools
307
+ # *The number of indoor pools.*
308
+ committee :indoor_pools do
309
+ # Use client input, if available.
310
+
311
+ # Otherwise look up the `property` number of indoor pools, but set a ceiling of 5 indoor pools.
312
+ quorum 'from property', :needs => :property,
313
+ :complies => [:ghg_protocol_scope_3, :iso, :tcr] do |characteristics|
314
+ unless characteristics[:property].pools_indoor.nil?
315
+ [characteristics[:property].pools_indoor.to_f, 5].min
316
+ end
317
+ end
318
+ end
319
+
320
+ #### Hot tubs
321
+ # *The number of hot tubs.*
322
+ committee :hot_tubs do
323
+ # Use client input, if available.
324
+
325
+ # Otherwise look up the `property` number of hot tubs.
326
+ quorum 'from property', :needs => :property,
327
+ :complies => [:ghg_protocol_scope_3, :iso, :tcr] do |characteristics|
328
+ characteristics[:property].hot_tubs
329
+ end
330
+ end
331
+
332
+ #### Refrigerator coverage
333
+ # *The percentage of property rooms that have refrigerators.*
334
+ committee :refrigerator_coverage do
335
+ # Use client input, if available.
336
+ # Otherwise take whichever is greater of the `property` refrigerator coverage and mini bar coverage.
337
+ quorum 'from property', :needs => :property,
338
+ :complies => [:ghg_protocol_scope_3, :iso, :tcr] do |characteristics|
339
+ if characteristics[:property].mini_bar_coverage || characteristics[:property].fridge_coverage
340
+ [characteristics[:property].mini_bar_coverage.to_f, characteristics[:property].fridge_coverage.to_f].max
341
+ end
342
+ end
343
+ end
344
+
345
+ #### A/C coverage
346
+ # *The percentage of property rooms that are air conditioned.*
347
+ committee :ac_coverage do
348
+ # Use client input, if available.
349
+
350
+ # Otherwise look up the `property` A/C coverage.
351
+ quorum 'from property', :needs => :property,
352
+ :complies => [:ghg_protocol_scope_3, :iso, :tcr] do |characteristics|
353
+ characteristics[:property].ac_coverage
354
+ end
355
+ end
356
+
357
+ #### Construction year
358
+ # *The year the property was built.*
359
+ committee :construction_year do
360
+ # Use client input, if available.
361
+
362
+ # Otherwise look up the year the `property` was built.
363
+ quorum 'from property', :needs => :property,
364
+ :complies => [:ghg_protocol_scope_3, :iso, :tcr] do |characteristics|
365
+ characteristics[:property].construction_year
366
+ end
367
+ end
368
+
369
+ #### Floors
370
+ # *The number of floors.*
371
+ committee :floors do
372
+ # Use client input, if available.
373
+
374
+ # Otherwise look up the `property` number of floors.
375
+ quorum 'from property', :needs => :property,
376
+ :complies => [:ghg_protocol_scope_3, :iso, :tcr] do |characteristics|
377
+ characteristics[:property].floors
378
+ end
379
+ end
380
+
381
+ #### Property rooms
382
+ # *The number of guest rooms in the property.*
383
+ committee :property_rooms do
384
+ # Use client input, if available.
385
+
386
+ # Otherwise look up the `property` number of rooms.
387
+ quorum 'from property', :needs => :property,
388
+ :complies => [:ghg_protocol_scope_3, :iso, :tcr] do |characteristics|
389
+ characteristics[:property].lodging_rooms
390
+ end
391
+ end
392
+
393
+ #### Property
394
+ # *The property where the stay occurred.*
395
+ #
396
+ # Use client input, if available
397
+
398
+ #### Heating degree days
399
+ # *The average number of annual heating degree days (base 18°C) at the lodging's location.*
400
+ committee :heating_degree_days do
401
+ # Use client input, if available
402
+
403
+ # Otherwise look up the `climate division` heating degree days.
404
+ quorum 'from climate division', :needs => :climate_division,
405
+ :complies => [:ghg_protocol_scope_3, :iso, :tcr] do |characteristics|
406
+ characteristics[:climate_division].heating_degree_days
407
+ end
408
+
409
+ # Otherwise look up the `country` heating degree days.
410
+ quorum 'from country', :needs => :country,
411
+ :complies => [:ghg_protocol_scope_3, :iso, :tcr] do |characteristics|
412
+ characteristics[:country].heating_degree_days
413
+ end
414
+ end
415
+
416
+ #### Cooling degree days
417
+ # *The average number of annual cooling degree days (base 18°C) at the lodging's location.*
418
+ committee :cooling_degree_days do
419
+ # Use client input, if available
420
+
421
+ # Otherwise look up the `climate division` cooling degree days.
422
+ quorum 'from climate division', :needs => :climate_division,
423
+ :complies => [:ghg_protocol_scope_3, :iso, :tcr] do |characteristics|
424
+ characteristics[:climate_division].cooling_degree_days
425
+ end
426
+
427
+ # Otherwise look up the `country` cooling degree days.
428
+ quorum 'from country', :needs => :country,
429
+ :complies => [:ghg_protocol_scope_3, :iso, :tcr] do |characteristics|
430
+ characteristics[:country].cooling_degree_days
431
+ end
432
+ end
433
+
434
+ #### eGRID subregion
435
+ # *The [eGRID subregion](http://data.brighterplanet.com/egrid_subregions).*
436
+ committee :egrid_subregion do
437
+ # Look up the `zip code` eGRID subregion.
438
+ quorum 'from zip code', :needs => :zip_code,
439
+ :complies => [:ghg_protocol_scope_3, :iso, :tcr] do |characteristics|
440
+ characteristics[:zip_code].egrid_subregion
441
+ end
442
+ end
443
+
444
+ #### Country
445
+ # *The [country](http://data.brighterplanet.com/countries).*
446
+ committee :country do
447
+ # Use client input, if available.
448
+
449
+ # Otherwise if state is defined then the country is the United States.
450
+ quorum 'from state', :needs => :state,
451
+ :complies => [:ghg_protocol_scope_3, :iso, :tcr] do |characteristics|
452
+ Country.united_states
453
+ end
454
+ end
455
+
456
+ #### State
457
+ # *The [US state](http://data.brighterplanet.com/states).*
458
+ committee :state do
459
+ # Use client input, if available.
460
+
461
+ # Otherwise look up the `zip code` state.
462
+ quorum 'from zip code', :needs => :zip_code,
463
+ :complies => [:ghg_protocol_scope_3, :iso, :tcr] do |characteristics|
464
+ characteristics[:zip_code].state
465
+ end
466
+ end
467
+
468
+ #### City
469
+ # *The city.*
470
+ committee :city do
471
+ # Use client input, if available.
472
+
473
+ # Otherwise look up the `zip code` description.
474
+ quorum 'from zip code', :needs => :zip_code,
475
+ :complies => [:ghg_protocol_scope_3, :iso, :tcr] do |characteristics|
476
+ characteristics[:zip_code].description
477
+ end
478
+ end
479
+
480
+ #### Climate division
481
+ # *The [US climate division](http://data.brighterplanet.com/climate_divisions).*
482
+ committee :climate_division do
483
+ # Look up the `zip code` climate division.
484
+ quorum 'from zip code', :needs => :zip_code,
485
+ :complies => [:ghg_protocol_scope_3, :iso, :tcr] do |characteristics|
486
+ characteristics[:zip_code].climate_division
487
+ end
488
+ end
489
+
490
+ #### Zip code
491
+ # *The [US zip code](http://data.brighterplanet.com/zip_codes).*
492
+ #
493
+ # Use client input, if available.
494
+
495
+ #### Room nights (*room-nights*)
496
+ # *The stay's room-nights that occurred during `timeframe`.*
497
+ committee :room_nights do
498
+ # If `date` falls within `timeframe`, divide `duration` (*seconds*) by 86,400 (*seconds / night*) and multiply by `rooms` to give *room-nights*.
499
+ # Otherwise `room nights` is zero.
500
+ quorum 'from rooms, duration, date, and timeframe', :needs => [:rooms, :duration, :date],
501
+ :complies => [:ghg_protocol_scope_3, :iso, :tcr] do |characteristics, timeframe|
502
+ date = characteristics[:date].is_a?(Date) ? characteristics[:date] : Date.parse(characteristics[:date].to_s)
503
+ timeframe.include?(date) ? characteristics[:duration] / 86400.0 * characteristics[:rooms] : 0
504
+ end
505
+ end
506
+
507
+ #### Duration (*seconds*)
508
+ # *The stay's duration.*
509
+ committee :duration do
510
+ # Use client input, if available.
511
+
512
+ # Otherwise use 86400 *seconds* (1 night).
513
+ quorum 'default' do
514
+ 86400.0
515
+ end
516
+ end
517
+
518
+ #### Rooms
519
+ # *The number of rooms used.*
520
+ committee :rooms do
521
+ # Use client input, if available.
522
+
523
+ # Otherwise use 1 *room*.
524
+ quorum 'default' do
525
+ 1
526
+ end
527
+ end
528
+
529
+ #### Date (*date*)
530
+ # *The day the stay occurred.*
531
+ committee :date do
532
+ # Use client input, if available.
533
+
534
+ # Otherwise use the first day of `timeframe`.
535
+ quorum 'from timeframe',
536
+ :complies => [:ghg_protocol_scope_3, :iso] do |characteristics, timeframe|
537
+ timeframe.from
538
+ end
539
+ end
540
+ end
541
+ end
542
+ end
543
+ end
544
+ end