forked from trynthink/scout
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathrun.py
3158 lines (2924 loc) · 168 KB
/
run.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#!/usr/bin/env python3
import json
import numpy
import copy
from numpy.linalg import LinAlgError
from collections import OrderedDict
import gzip
import pickle
from os import getcwd, path, pathsep, sep, environ, walk, devnull
from ast import literal_eval
import math
from optparse import OptionParser
import subprocess
import sys
import warnings
class UsefulInputFiles(object):
"""Class of input files to be opened by this routine.
Attributes:
metadata = Baseline metadata including common min/max for year range.
meas_summary_data (string): High-level measure summary data.
meas_compete_data (string): Contributing microsegment data needed
for measure competition.
active_measures (string): Measures that are active for the analysis.
meas_engine_out (string): Measure output summaries.
htcl_totals (string): Heating/cooling energy totals by climate zone,
building type, and structure type.
"""
def __init__(self):
self.metadata = "metadata.json"
# UNCOMMENT WITH ISSUE 188
# self.metadata = "metadata_2017.json"
self.meas_summary_data = \
("supporting_data", "ecm_prep.json")
self.meas_compete_data = ("supporting_data", "ecm_competition_data")
self.active_measures = "run_setup.json"
self.meas_engine_out_ecms = ("results", "ecm_results.json")
self.meas_engine_out_agg = ("results", "agg_results.json")
self.htcl_totals = ("supporting_data", "stock_energy_tech_data",
"htcl_totals.json")
class UsefulVars(object):
"""Class of variables that are used globally across functions.
Attributes:
adopt_schemes (list): Possible consumer adoption scenarios.
retro_rate (float): Rate at which existing stock is retrofitted.
aeo_years (list) = Modeling time horizon.
discount_rate (float): General rate to use in discounting cash flows.
com_timeprefs (dict): Time preference premiums for commercial adopters.
out_break_czones (OrderedDict): Maps measure climate zone names to
the climate zone categories used in summarizing measure outputs.
out_break_bldgtypes (OrderedDict): Maps measure building type names to
the building sector categories used in summarizing measure outputs.
out_break_enduses (OrderedDict): Maps measure end use names to
the end use categories used in summarizing measure outputs.
"""
def __init__(self, base_dir, handyfiles):
self.adopt_schemes = ['Technical potential', 'Max adoption potential']
self.retro_rate = 0.01
# Load metadata including AEO year range
with open(path.join(base_dir, handyfiles.metadata), 'r') as aeo_yrs:
try:
aeo_yrs = json.load(aeo_yrs)
except ValueError as e:
raise ValueError(
"Error reading in '" +
handyfiles.metadata + "': " + str(e)) from None
# Set minimum AEO modeling year
aeo_min = aeo_yrs["min year"]
# Set maximum AEO modeling year
aeo_max = aeo_yrs["max year"]
# Derive time horizon from min/max years
self.aeo_years = [
str(i) for i in range(aeo_min, aeo_max + 1)]
self.discount_rate = 0.07
self.com_timeprefs = {
"rates": [10.0, 1.0, 0.45, 0.25, 0.15, 0.065, 0.0],
"distributions": {
"heating": {
key: [0.265, 0.226, 0.196, 0.192, 0.105, 0.013, 0.003]
for key in self.aeo_years},
"cooling": {
key: [0.264, 0.225, 0.193, 0.192, 0.106, 0.016, 0.004]
for key in self.aeo_years},
"water heating": {
key: [0.263, 0.249, 0.212, 0.169, 0.097, 0.006, 0.004]
for key in self.aeo_years},
"ventilation": {
key: [0.265, 0.226, 0.196, 0.192, 0.105, 0.013, 0.003]
for key in self.aeo_years},
"cooking": {
key: [0.261, 0.248, 0.214, 0.171, 0.097, 0.005, 0.004]
for key in self.aeo_years},
"lighting": {
key: [0.264, 0.225, 0.193, 0.193, 0.085, 0.013, 0.027]
for key in self.aeo_years},
"refrigeration": {
key: [0.262, 0.248, 0.213, 0.170, 0.097, 0.006, 0.004]
for key in self.aeo_years}}}
self.out_break_czones = OrderedDict([
('AIA CZ1', 'AIA_CZ1'), ('AIA CZ2', 'AIA_CZ2'),
('AIA CZ3', 'AIA_CZ3'), ('AIA CZ4', 'AIA_CZ4'),
('AIA CZ5', 'AIA_CZ5')])
self.out_break_bldgtypes = OrderedDict([
('Residential (New)', [
'new', 'single family home', 'multi family home',
'mobile home']),
('Residential (Existing)', [
'existing', 'single family home', 'multi family home',
'mobile home'],),
('Commercial (New)', [
'new', 'assembly', 'education', 'food sales',
'food service', 'health care', 'mercantile/service',
'lodging', 'large office', 'small office', 'warehouse',
'other']),
('Commercial (Existing)', [
'existing', 'assembly', 'education', 'food sales',
'food service', 'health care', 'mercantile/service',
'lodging', 'large office', 'small office', 'warehouse',
'other'])])
self.out_break_enduses = OrderedDict([
('Heating (Equip.)', ["heating", "secondary heating"]),
('Cooling (Equip.)', ["cooling"]),
('Envelope', ["heating", "secondary heating", "cooling"]),
('Ventilation', ["ventilation"]),
('Lighting', ["lighting"]),
('Water Heating', ["water heating"]),
('Refrigeration', ["refrigeration", "other"]),
('Computers and Electronics', [
"PCs", "non-PC office equipment", "TVs", "computers"]),
('Other', [
"cooking", "drying", "ceiling fan", "fans & pumps",
"MELs", "other"])])
class Measure(object):
"""Class representing individual efficiency measures.
Attributes:
**kwargs: Arbitrary keyword arguments used to fill measure attributes
from an input dictionary.
update_results (dict): Flags markets, savings, and financial metric
outputs that have yet to be finalized by the analysis engine.
markets (dict): Data grouped by adoption scheme on:
a) 'master_mseg': a measure's master market microsegments (stock,
energy, carbon, cost),
b) 'mseg_adjust': all microsegments that contribute to each master
microsegment (required later for measure competition).
c) 'mseg_out_break': master microsegment breakdowns by key
variables (e.g., climate zone, building type, end use, etc.)
savings (dict): Energy, carbon, and stock, energy, and carbon cost
savings for measure over baseline technology case.
portfolio_metrics (dict): Financial metrics relevant to assessing a
large portfolio of efficiency measures (e.g., CCE, CCC).
consumer_metrics (dict): Financial metrics relevant to the adoption
decisions of individual consumers (e.g., unit costs, IRR, payback).
"""
def __init__(self, handyvars, **kwargs):
# Read Measure object attributes from measures input JSON
for key, value in kwargs.items():
setattr(self, key, value)
self.savings, self.portfolio_metrics, self.consumer_metrics = (
{} for n in range(3))
self.update_results = {
"savings and portfolio metrics": {},
"consumer metrics": True}
# Convert any master market microsegment data formatted as lists to
# numpy arrays
self.convert_to_numpy(self.markets)
for adopt_scheme in handyvars.adopt_schemes:
# Initialize 'uncompeted' and 'competed' versions of
# Measure markets (initially, they are identical)
self.markets[adopt_scheme] = {
"uncompeted": copy.deepcopy(self.markets[adopt_scheme]),
"competed": copy.deepcopy(self.markets[adopt_scheme])}
self.update_results["savings and portfolio metrics"][
adopt_scheme] = {"uncompeted": True, "competed": True}
self.savings[adopt_scheme] = {
"uncompeted": {
"stock": {
"cost savings (total)": None,
"cost savings (annual)": None},
"energy": {
"savings (total)": None,
"savings (annual)": None,
"cost savings (total)": None,
"cost savings (annual)": None},
"carbon": {
"savings (total)": None,
"savings (annual)": None,
"cost savings (total)": None,
"cost savings (annual)": None}},
"competed": {
"stock": {
"cost savings (total)": None,
"cost savings (annual)": None},
"energy": {
"savings (total)": None,
"savings (annual)": None,
"cost savings (total)": None,
"cost savings (annual)": None},
"carbon": {
"savings (total)": None,
"savings (annual)": None,
"cost savings (total)": None,
"cost savings (annual)": None}}}
self.portfolio_metrics[adopt_scheme] = {
"uncompeted": {
"cce": None,
"cce (w/ carbon cost benefits)": None,
"ccc": None,
"ccc (w/ energy cost benefits)": None},
"competed": {
"cce": None,
"cce (w/ carbon cost benefits)": None,
"ccc": None,
"ccc (w/ energy cost benefits)": None}}
self.consumer_metrics = {
"unit cost": {
"stock cost": {
"residential": None,
"commercial": None
},
"energy cost": {
"residential": None,
"commercial": None
},
"carbon cost": {
"residential": None,
"commercial": None
}},
"irr (w/ energy costs)": None,
"irr (w/ energy and carbon costs)": None,
"payback (w/ energy costs)": None,
"payback (w/ energy and carbon costs)": None}
def convert_to_numpy(self, markets):
"""Convert terminal/leaf node lists in a dict to numpy arrays.
Args:
markets (dict): Input dict with possible lists at terminal/leaf
nodes.
"""
for k, i in markets.items():
if isinstance(i, dict):
self.convert_to_numpy(i)
else:
if isinstance(markets[k], list):
markets[k] = numpy.array(markets[k])
class Engine(object):
"""Class representing a collection of efficiency measures.
Attributes:
handyvars (object): Global variables useful across class methods.
measures (list): List of active measure objects to be analyzed.
output (OrderedDict): Summary results data for all active measures.
"""
def __init__(self, handyvars, measure_objects):
self.handyvars = handyvars
self.measures = measure_objects
self.output_ecms, self.output_all = (OrderedDict() for n in range(2))
self.output_all["All ECMs"] = OrderedDict([
("Markets and Savings (Overall)", OrderedDict())])
for adopt_scheme in self.handyvars.adopt_schemes:
self.output_all["All ECMs"]["Markets and Savings (Overall)"][
adopt_scheme] = OrderedDict()
for m in self.measures:
# Set measure climate zone, building sector, and end use
# output category names for use in filtering and/or breaking
# out results
czones, bldgtypes, end_uses = ([] for n in range(3))
# Find measure climate zone output categories
for cz in self.handyvars.out_break_czones.items():
if any([x in cz[1] for x in m.climate_zone]) and \
cz[0] not in czones:
czones.append(cz[0])
# Find measure building sector output categories
for bldg in self.handyvars.out_break_bldgtypes.items():
if any([x in bldg[1] for x in m.bldg_type]) and \
bldg[0] not in bldgtypes:
bldgtypes.append(bldg[0])
# Find measure end use output categories
for euse in self.handyvars.out_break_enduses.items():
# Find primary end use categories
if any([x in euse[1] for x in m.end_use["primary"]]) and \
euse[0] not in end_uses:
# * Note: classify special freezers ECM case as
# 'Refrigeration'; classify 'supply' side heating/cooling
# ECMs as 'Heating (Equip.)'/'Cooling (Equip.)' and
# 'demand' side heating/cooling ECMs as 'Envelope'
if (euse[0] == "Refrigeration" and
("refrigeration" in m.end_use["primary"] or
"freezers" in m.technology)) or (
euse[0] != "Refrigeration" and ((
euse[0] in ["Heating (Equip.)",
"Cooling (Equip.)"] and
"supply" in m.technology_type["primary"]) or (
euse[0] == "Envelope" and "demand" in
m.technology_type["primary"]) or (euse[0] not in [
"Heating (Equip.)", "Cooling (Equip.)",
"Envelope"]))):
end_uses.append(euse[0])
# Assign secondary heating/cooling microsegments that
# represent waste heat from lights to the 'Lighting' end use
# category
if m.end_use["secondary"] is not None and any([
x in m.end_use["secondary"] for x in [
"heating", "cooling"]]) and "Lighting" not in end_uses:
end_uses.append("Lighting")
# Set measure climate zone(s), building sector(s), and end use(s)
# as filter variables
self.output_ecms[m.name] = OrderedDict([
("Filter Variables", OrderedDict([
("Applicable Climate Zones", czones),
("Applicable Building Classes", bldgtypes),
("Applicable End Uses", end_uses)])),
("Markets and Savings (Overall)", OrderedDict()),
("Markets and Savings (by Category)", OrderedDict()),
("Financial Metrics", OrderedDict([
("Portfolio Level", OrderedDict()),
("Consumer Level", OrderedDict())]))])
for adopt_scheme in self.handyvars.adopt_schemes:
# Initialize measure overall markets and savings
self.output_ecms[m.name]["Markets and Savings (Overall)"][
adopt_scheme] = OrderedDict()
# Initialize measure markets and savings broken out by climate
# zone, building sector, and end use categories
self.output_ecms[m.name]["Markets and Savings (by Category)"][
adopt_scheme] = OrderedDict()
# Initialize measure financial metrics
self.output_ecms[m.name]["Financial Metrics"][
"Portfolio Level"][adopt_scheme] = OrderedDict()
def calc_savings_metrics(self, adopt_scheme, comp_scheme):
"""Calculate and update measure savings and financial metrics.
Notes:
Given information on measure master microsegments for
each projection year, determine capital, energy, and carbon
cost savings; energy and carbon savings; and the net present
value, internal rate of return, simple payback, cost of
conserved energy, and cost of conserved carbon for the measure.
Args:
adopt_scheme (string): Assumed consumer adoption scenario.
comp_scheme (string): Assumed measure competition scenario.
"""
# Find all active measures that require savings updates
measures_update = [m for m in self.measures if m.update_results[
"savings and portfolio metrics"][
adopt_scheme][comp_scheme] is True]
# Update measure savings and associated financial metrics
for m in measures_update:
# Initialize energy/energy cost savings, carbon/
# carbon cost savings, and dicts for financial metrics
scostsave_tot, scostsave, esave_tot, esave, ecostsave_tot, \
ecostsave, csave_tot, csave, ccostsave_tot, ccostsave, \
stock_unit_cost_res, stock_unit_cost_com, \
energy_unit_cost_res, energy_unit_cost_com, \
carb_unit_cost_res, carb_unit_cost_com, \
irr_e, irr_ec, payback_e, payback_ec, cce, cce_bens, ccc, \
ccc_bens, scost_meas_tot, ecost_meas_tot, ccost_meas_tot = ({
yr: None for yr in self.handyvars.aeo_years} for
n in range(27))
# Determine the total uncompeted measure/baseline capital
# cost and total number of applicable baseline stock units,
# used below to calculate incremental capital cost per unit
# stock for the measure in each year
# Total uncompeted baseline capital cost
stock_meas_cost_tot = {
yr: m.markets[adopt_scheme]["uncompeted"]["master_mseg"][
"cost"]["stock"]["total"]["efficient"][yr] for
yr in self.handyvars.aeo_years}
# Total uncompeted measure capital cost
stock_base_cost_tot = {
yr: m.markets[adopt_scheme]["uncompeted"]["master_mseg"][
"cost"]["stock"]["total"]["baseline"][yr] for
yr in self.handyvars.aeo_years}
# Total number of applicable stock units
nunits_tot = {
yr: m.markets[adopt_scheme]["uncompeted"]["master_mseg"][
"stock"]["total"]["all"][yr] for
yr in self.handyvars.aeo_years}
# Set measure master microsegments for the current adoption and
# competition schemes
markets = m.markets[adopt_scheme][comp_scheme]["master_mseg"]
# Calculate measure capital cost savings, energy/carbon savings,
# energy/carbon cost savings, and financial metrics for each
# projection year
for yr in self.handyvars.aeo_years:
# Calculate per unit baseline capital cost and incremental
# measure capital cost (used in financial metrics
# calculations below); set these values to zero for
# years in which total number of units is zero
if nunits_tot[yr] != 0:
# Per unit baseline capital cost
scostbase = \
stock_base_cost_tot[yr] / nunits_tot[yr]
# Per unit measure incremental capital cost
scostmeas_delt = \
(stock_base_cost_tot[yr] -
stock_meas_cost_tot[yr]) / nunits_tot[yr]
else:
scostbase, scost_save = (0 for n in range(2))
# Calculate total annual capital/energy/carbon costs for the
# measure. Total costs reflect the impact of all measure
# adoptions simulated up until and including the current year
scost_meas_tot[yr] = \
markets["cost"]["stock"]["total"]["efficient"][yr]
ecost_meas_tot[yr] = \
markets["cost"]["energy"]["total"]["efficient"][yr]
ccost_meas_tot[yr] = \
markets["cost"]["carbon"]["total"]["efficient"][yr]
# Calculate total annual energy/carbon and capital/energy/
# carbon cost savings for the measure vs. baseline. Total
# savings reflect the impact of all measure adoptions
# simulated up until and including the current year
esave_tot[yr] = \
markets["energy"]["total"]["baseline"][yr] - \
markets["energy"]["total"]["efficient"][yr]
csave_tot[yr] = \
markets["carbon"]["total"]["baseline"][yr] - \
markets["carbon"]["total"]["efficient"][yr]
scostsave_tot[yr] = \
markets["cost"]["stock"]["total"]["baseline"][yr] - \
markets["cost"]["stock"]["total"]["efficient"][yr]
ecostsave_tot[yr] = \
markets["cost"]["energy"]["total"]["baseline"][yr] - \
markets["cost"]["energy"]["total"]["efficient"][yr]
ccostsave_tot[yr] = \
markets["cost"]["carbon"]["total"]["baseline"][yr] - \
markets["cost"]["carbon"]["total"]["efficient"][yr]
# Calculate the annual energy/carbon and capital/energy/carbon
# cost savings for the measure vs. baseline. (Annual savings
# will later be used in measure competition routines). Annual
# savings reflect the impact of only the measure adoptions
# that are new in the current year
esave[yr] = \
markets["energy"]["competed"]["baseline"][yr] - \
markets["energy"]["competed"]["efficient"][yr]
csave[yr] = \
markets["carbon"]["competed"]["baseline"][yr] - \
markets["carbon"]["competed"]["efficient"][yr]
scostsave[yr] = markets[
"cost"]["stock"]["competed"]["baseline"][yr] - \
markets["cost"]["stock"]["competed"]["efficient"][yr]
ecostsave[yr] = markets[
"cost"]["energy"]["competed"]["baseline"][yr] - \
markets["cost"]["energy"]["competed"]["efficient"][yr]
ccostsave[yr] = markets[
"cost"]["carbon"]["competed"]["baseline"][yr] - \
markets["cost"]["carbon"]["competed"]["efficient"][yr]
# Set the lifetime of the baseline technology for comparison
# with measure lifetime
life_base = markets["lifetime"]["baseline"][yr]
# Ensure that baseline lifetime is at least 1 year
if type(life_base) == numpy.ndarray and any(life_base) < 1:
life_base[numpy.where(life_base) < 1] = 1
elif type(life_base) != numpy.ndarray and life_base < 1:
life_base = 1
# Set lifetime of the measure
life_meas = markets["lifetime"]["measure"]
# Ensure that measure lifetime is at least 1 year
if type(life_meas) == numpy.ndarray and any(life_meas) < 1:
life_meas[numpy.where(life_meas) < 1] = 1
elif type(life_meas) != numpy.ndarray and life_meas < 1:
life_meas = 1
# Calculate measure financial metrics
# Create short name for number of captured measure stock units
nunits_meas = markets["stock"]["total"]["measure"][yr]
# If the total baseline stock is zero or no measure units
# have been captured for a given year, set financial metrics
# to 999
if nunits_tot[yr] == 0 or (
type(nunits_meas) != numpy.ndarray and nunits_meas < 1 or
type(nunits_meas) == numpy.ndarray and all(
nunits_meas) < 1):
if yr == self.handyvars.aeo_years[0]:
stock_unit_cost_res[yr], energy_unit_cost_res[yr], \
carb_unit_cost_res[yr], stock_unit_cost_com[yr], \
energy_unit_cost_com[yr], carb_unit_cost_com[yr], \
irr_e[yr], irr_ec[yr], payback_e[yr], \
payback_ec[yr], cce[yr], cce_bens[yr], \
ccc[yr], ccc_bens[yr] = [999 for n in range(14)]
else:
yr_prev = str(int(yr) - 1)
stock_unit_cost_res[yr], energy_unit_cost_res[yr], \
carb_unit_cost_res[yr], stock_unit_cost_com[yr], \
energy_unit_cost_com[yr], carb_unit_cost_com[yr], \
irr_e[yr], irr_ec[yr], payback_e[yr], \
payback_ec[yr], cce[yr], cce_bens[yr], \
ccc[yr], ccc_bens[yr] = [x[yr_prev] for x in [
stock_unit_cost_res, energy_unit_cost_res,
carb_unit_cost_res, stock_unit_cost_com,
energy_unit_cost_com, carb_unit_cost_com,
irr_e, irr_ec, payback_e, payback_ec, cce,
cce_bens, ccc, ccc_bens]]
# Otherwise, check whether any financial metric calculation
# inputs that can be arrays are in fact arrays
elif any(type(x) == numpy.ndarray for x in [
scostmeas_delt, esave[yr], life_meas]):
# Make copies of the above stock, energy, carbon, and cost
# variables for possible further manipulation below before
# using as inputs to the "metric update" function
scostmeas_delt_tmp, esave_tmp, ecostsave_tmp, csave_tmp, \
ccostsave_tmp, life_meas_tmp, scost_meas_tmp, \
ecost_meas_tmp, ccost_meas_tmp = [
scostmeas_delt, esave_tot[yr], ecostsave_tot[yr],
csave_tot[yr], ccostsave_tot[yr], life_meas,
scost_meas_tot[yr], ecost_meas_tot[yr],
ccost_meas_tot[yr]]
# Ensure consistency in length of all "metric_update"
# inputs that can be arrays
# Determine the length that any array inputs to
# "metric_update" should consistently have
len_arr = next((len(item) for item in [
scostmeas_delt, esave_tot[yr], life_meas] if
type(item) == numpy.ndarray), None)
# Ensure all array inputs to "metric_update" are of the
# above length
# Check capital cost inputs
if type(scostmeas_delt_tmp) != numpy.ndarray:
scostmeas_delt_tmp = numpy.repeat(
scostmeas_delt_tmp, len_arr)
scost_meas_tmp = numpy.repeat(scost_meas_tmp, len_arr)
# Check energy/energy cost and carbon/cost savings inputs
if type(esave_tmp) != numpy.ndarray:
esave_tmp = numpy.repeat(esave_tmp, len_arr)
ecostsave_tmp = numpy.repeat(ecostsave_tmp, len_arr)
csave_tmp = numpy.repeat(csave_tmp, len_arr)
ccostsave_tmp = numpy.repeat(ccostsave_tmp, len_arr)
ecost_meas_tmp = numpy.repeat(ecost_meas_tmp, len_arr)
ccost_meas_tmp = numpy.repeat(ccost_meas_tmp, len_arr)
# Check measure lifetime input
if type(life_meas_tmp) != numpy.ndarray:
life_meas_tmp = numpy.repeat(life_meas_tmp, len_arr)
# Initialize numpy arrays for financial metrics outputs
stock_unit_cost_res[yr], energy_unit_cost_res[yr], \
carb_unit_cost_res[yr], stock_unit_cost_com[yr], \
energy_unit_cost_com[yr], carb_unit_cost_com[yr], \
irr_e[yr], irr_ec[yr], payback_e[yr], payback_ec[yr], \
cce[yr], cce_bens[yr], ccc[yr], ccc_bens[yr] = (
numpy.repeat(None, len(scostmeas_delt_tmp)) for
v in range(14))
# Run measure energy/carbon/cost savings and lifetime
# inputs through "metric_update" function to yield
# financial metric outputs. To handle inputs that are
# arrays, use a for loop to generate an output for each
# input array element one-by-one and append it to the
# appropriate output list. Note that lifetime float
# values are translated to integers, and all
# energy, carbon, and energy/carbon cost savings values
# are normalized by total applicable stock units
for x in range(0, len(scostmeas_delt_tmp)):
stock_unit_cost_res[yr][x], \
energy_unit_cost_res[yr][x], \
carb_unit_cost_res[yr][x], \
stock_unit_cost_com[yr][x], \
energy_unit_cost_com[yr][x], \
carb_unit_cost_com[yr][x], \
irr_e[yr][x], irr_ec[yr][x], \
payback_e[yr][x], payback_ec[yr][x], \
cce[yr][x], cce_bens[yr][x], ccc[yr][x], \
ccc_bens[yr][x] = self.metric_update(
m, int(round(life_base)),
int(round(life_meas_tmp[x])),
scostbase, scostmeas_delt_tmp[x],
esave_tmp[x] / nunits_tot[yr],
ecostsave_tmp[x] / nunits_tot[yr],
csave_tmp[x] / nunits_tot[yr],
ccostsave_tmp[x] / nunits_tot[yr],
scost_meas_tmp[x] / nunits_tot[yr],
ecost_meas_tmp[x] / nunits_tot[yr],
ccost_meas_tmp[x] / nunits_tot[yr])
else:
# Run measure energy/carbon/cost savings and lifetime
# inputs through "metric_update" function to yield
# financial metric outputs. Note that lifetime float
# values are translated to integers, and all
# energy, carbon, and energy/carbon cost savings values
# are normalized by total applicable stock units
stock_unit_cost_res[yr], energy_unit_cost_res[yr], \
carb_unit_cost_res[yr], stock_unit_cost_com[yr], \
energy_unit_cost_com[yr], carb_unit_cost_com[yr], \
irr_e[yr], irr_ec[yr], payback_e[yr], payback_ec[yr], \
cce[yr], cce_bens[yr], ccc[yr], ccc_bens[yr] = \
self.metric_update(
m, int(round(life_base)),
int(round(life_meas)), scostbase, scostmeas_delt,
esave_tot[yr] / nunits_tot[yr],
ecostsave_tot[yr] / nunits_tot[yr],
csave_tot[yr] / nunits_tot[yr],
ccostsave_tot[yr] / nunits_tot[yr],
scost_meas_tot[yr] / nunits_tot[yr],
ecost_meas_tot[yr] / nunits_tot[yr],
ccost_meas_tot[yr] / nunits_tot[yr])
# Record final measure savings figures and financial metrics
# Set measure savings dict to update
save = m.savings[adopt_scheme][comp_scheme]
# Update capital cost savings
save["stock"]["cost savings (total)"] = scostsave_tot
save["stock"]["cost savings (annual)"] = scostsave
# Update energy and energy cost savings
save["energy"]["savings (total)"] = esave_tot
save["energy"]["savings (annual)"] = esave
save["energy"]["cost savings (total)"] = ecostsave_tot
save["energy"]["cost savings (annual)"] = ecostsave
# Update carbon and carbon cost savings
save["carbon"]["savings (total)"] = csave_tot
save["carbon"]["savings (annual)"] = csave
save["carbon"]["cost savings (total)"] = ccostsave_tot
save["carbon"]["cost savings (annual)"] = ccostsave
# Set measure portfolio-level financial metrics dict to update
metrics_port = m.portfolio_metrics[adopt_scheme][comp_scheme]
# Update cost of conserved energy
metrics_port["cce"] = cce
metrics_port["cce (w/ carbon cost benefits)"] = cce_bens
# Update cost of conserved carbon
metrics_port["ccc"] = ccc
metrics_port["ccc (w/ energy cost benefits)"] = ccc_bens
# Set measure savings and portolio-level metrics for the current
# adoption and competition schemes to finalized status
m.update_results["savings and portfolio metrics"][
adopt_scheme][comp_scheme] = False
# Update measure consumer-level financial metrics if they have
# not already been finalized (these metrics remain constant across
# all consumer adoption and measure competition schemes)
if m.update_results["consumer metrics"] is True:
# Set measure consumer-level financial metrics dict to update
metrics_consumer = m.consumer_metrics
# Update unit capital and operating costs
metrics_consumer["unit cost"]["stock cost"]["residential"], \
metrics_consumer["unit cost"]["stock cost"][
"commercial"] = [stock_unit_cost_res, stock_unit_cost_com]
metrics_consumer["unit cost"]["energy cost"]["residential"], \
metrics_consumer["unit cost"]["energy cost"][
"commercial"] = [energy_unit_cost_res,
energy_unit_cost_com]
metrics_consumer["unit cost"]["carbon cost"]["residential"], \
metrics_consumer["unit cost"]["carbon cost"][
"commercial"] = [carb_unit_cost_res, carb_unit_cost_com]
# Update internal rate of return
metrics_consumer["irr (w/ energy costs)"] = irr_e
metrics_consumer["irr (w/ energy and carbon costs)"] = irr_ec
# Update payback period
metrics_consumer["payback (w/ energy costs)"] = payback_e
metrics_consumer["payback (w/ energy and carbon costs)"] = \
payback_ec
# Set measure consumer-level metrics to finalized status
m.update_results["consumer metrics"] = False
def metric_update(self, m, life_base, life_meas, scost_base,
scost_meas_delt, esave, ecostsave, csave, ccostsave,
scost_meas, ecost_meas, ccost_meas):
"""Calculate measure financial metrics for a given year.
Notes:
Calculate internal rate of return, simple payback, and cost of
conserved energy/carbon from cash flows and energy/carbon
savings across the measure lifetime. In the cash flows, represent
the benefits of longer lifetimes for lighting equipment ECMs over
comparable baseline technologies.
Args:
m (object): Measure object.
nunits (int): Total competed baseline units in a given year.
nunits_meas (int): Total competed units captured by measure in
given year.
life_base (float): Baseline technology lifetime.
life_meas (float): Measure lifetime.
scost_base (float): Per unit baseline capital cost in given year.
scost_meas_delt (float): Per unit incremental capital
cost for measure over baseline unit in given year.
esave (float): Per unit annual energy savings over measure
lifetime, starting in given year.
ecostsave (float): Per unit annual energy cost savings over
measure lifetime, starting in a given year.
csave (float): Per unit annual avoided carbon emissions over
measure lifetime, starting in given year.
ccostsave (float): Per unit annual carbon cost savings over
measure lifetime, starting in a given year.
scost_meas (float): Per unit measure capital cost in given year.
ecost_meas (float): Per unit measure energy cost in given year.
ccost_meas (float): Per unit measure carbon cost in given year.
Returns:
Consumer and portfolio-level financial metrics for the given
measure cost savings inputs.
"""
# Develop four initial cash flow scenarios over the measure life:
# 1) Cash flows considering capital costs only
# 2) Cash flows considering capital costs and energy costs
# 3) Cash flows considering capital costs and carbon costs
# 4) Cash flows considering capital, energy, and carbon costs
# For lighting equipment ECMs only: determine when over the course of
# the ECM lifetime (if at all) a cost gain is realized from an avoided
# purchase of the baseline lighting technology due to longer measure
# lifetime; store this information in a list of year indicators for
# subsequent use below. Example: an LED bulb lasts 30 years compared
# to a baseline bulb's 10 years, meaning 3 purchases of the baseline
# bulb would have occurred by the time the LED bulb has reached the
# end of its life.
added_stockcost_gain_yrs = []
if (life_meas > life_base) and ("lighting" in m.end_use[
"primary"]) and (m.measure_type == "full service") and (
m.technology_type["primary"] == "supply"):
for i in range(1, life_meas):
if i % life_base == 0:
added_stockcost_gain_yrs.append(i - 1)
# If the measure lifetime is less than 1 year, set it to 1 year
# (a minimum for measure lifetime to work in below calculations)
if life_meas < 1:
life_meas = 1
# Construct capital cost cash flows across measure life
# Initialize incremental and total capital cost cash flows with
# upfront incremental and total capital cost
cashflows_s_delt = numpy.array(scost_meas_delt)
cashflows_s_tot = numpy.array(scost_meas)
for life_yr in range(0, life_meas):
# Check whether an avoided cost of the baseline technology should
# be added for given year; if not, set this term to zero
if life_yr in added_stockcost_gain_yrs:
scost_life = scost_base
else:
scost_life = 0
# Add avoided capital costs as appropriate (e.g., for an LED
# lighting measure with a longer lifetime than the comparable
# baseline lighting technology)
cashflows_s_delt = numpy.append(cashflows_s_delt, scost_life)
cashflows_s_tot = numpy.append(cashflows_s_tot, scost_life)
# Construct complete incremental and total energy and carbon cash
# flows across measure lifetime. First term (reserved for initial
# investment) is zero
cashflows_e_delt, cashflows_c_delt = [
numpy.append(0, [x] * life_meas) for x in [ecostsave, ccostsave]]
cashflows_e_tot, cashflows_c_tot = [
numpy.append(0, [x] * life_meas) for x in [ecost_meas, ccost_meas]]
# Calculate net present values (NPVs) using the above cashflows
npv_s_delt, npv_e_delt, npv_c_delt = [
numpy.npv(self.handyvars.discount_rate, x) for x in [
cashflows_s_delt, cashflows_e_delt, cashflows_c_delt]]
# Develop arrays of energy and carbon savings across measure
# lifetime (for use in cost of conserved energy and carbon calcs).
# First term (reserved for initial investment figure) is zero, and
# each array is normalized by number of captured stock units
esave_array = numpy.append(0, [esave] * life_meas)
csave_array = numpy.append(0, [csave] * life_meas)
# Calculate Net Present Value and annuity equivalent Net Present Value
# of the above energy and carbon savings
npv_esave = numpy.npv(self.handyvars.discount_rate, esave_array)
npv_csave = numpy.npv(self.handyvars.discount_rate, csave_array)
# Calculate portfolio-level financial metrics
# Calculate cost of conserved energy w/ and w/o carbon cost savings
# benefits. Restrict denominator values less than or equal to zero
if npv_esave > 0:
cce = (-npv_s_delt / npv_esave)
cce_bens = (-(npv_s_delt + npv_c_delt) / npv_esave)
else:
cce, cce_bens = [999 for n in range(2)]
# Calculate cost of conserved carbon w/ and w/o energy cost savings
# benefits. Restrict denominator values less than or equal to zero
if npv_csave > 0:
ccc = (-npv_s_delt / (npv_csave * 1000000))
ccc_bens = (-(npv_s_delt + npv_e_delt) /
(npv_csave * 1000000))
else:
ccc, ccc_bens = [999 for n in range(2)]
# Calculate consumer-level financial metrics
# Only calculate consumer-level financial metrics once; do not
# recalculate if already finalized
if m.update_results["consumer metrics"] is True:
# Set unit capital and operating costs using the above
# cashflows for later use in measure competition calculations. For
# residential sector measures, unit costs are simply the unit-level
# capital and operating costs for the measure. For commerical
# sector measures, unit costs are translated to life cycle capital
# and operating costs across the measure lifetime using multiple
# discount rate levels that reflect various degrees of risk
# tolerance observed amongst commercial adopters. These discount
# rate levels are imported from commercial AEO demand module data.
# Populate unit costs for residential sector
# Check whether measure applies to residential sector
if any([x in ["single family home", "multi family home",
"mobile home"] for x in m.bldg_type]):
unit_cost_s_res, unit_cost_e_res, unit_cost_c_res = [
scost_meas, ecost_meas, ccost_meas]
# If measure does not apply to residential sector, set residential
# unit costs to 'None'
else:
unit_cost_s_res, unit_cost_e_res, unit_cost_c_res = (
None for n in range(3))
# Populate unit costs for commercial sector
# Check whether measure applies to commercial sector
if any([x not in ["single family home", "multi family home",
"mobile home"] for x in m.bldg_type]):
unit_cost_s_com, unit_cost_e_com, unit_cost_c_com = (
{} for n in range(3))
# Set unit cost values under 7 discount rate categories
try:
for ind, tps in enumerate(
self.handyvars.com_timeprefs["rates"]):
unit_cost_s_com["rate " + str(ind + 1)],\
unit_cost_e_com["rate " + str(ind + 1)],\
unit_cost_c_com["rate " + str(ind + 1)] = \
[numpy.npv(tps, x) for x in [
cashflows_s_tot, cashflows_e_tot,
cashflows_c_tot]]
if any([not math.isfinite(x) for x in [
unit_cost_s_com["rate " + str(ind + 1)],
unit_cost_e_com["rate " + str(ind + 1)],
unit_cost_c_com["rate " + str(ind + 1)]]]):
raise(ValueError)
except ValueError:
unit_cost_s_com, unit_cost_e_com, unit_cost_c_com = (
999 for n in range(3))
# If measure does not apply to commercial sector, set commercial
# unit costs to 'None'
else:
unit_cost_s_com, unit_cost_e_com, unit_cost_c_com = (
None for n in range(3))
# Calculate internal rate of return and simple payback for capital
# + energy and capital + energy + carbon cash flows. Use try/
# except to handle cases where IRR/payback cannot be calculated
# IRR and payback given capital + energy cash flows
try:
irr_e = numpy.irr(cashflows_s_delt + cashflows_e_delt)
if not math.isfinite(irr_e):
raise(ValueError)
except ValueError:
irr_e = 999
try:
payback_e = self.payback(cashflows_s_delt + cashflows_e_delt)
except (ValueError, LinAlgError):
payback_e = 999
# IRR and payback given capital + energy + carbon cash flows
try:
irr_ec = numpy.irr(
cashflows_s_delt + cashflows_e_delt + cashflows_c_delt)
if not math.isfinite(irr_ec):
raise(ValueError)
except ValueError:
irr_ec = 999
try:
payback_ec = \
self.payback(
cashflows_s_delt + cashflows_e_delt + cashflows_c_delt)
except (ValueError, LinAlgError):
payback_ec = 999
else:
unit_cost_s_res, unit_cost_e_res, unit_cost_c_res, \
unit_cost_s_com, unit_cost_e_com, unit_cost_c_com, \
irr_e, irr_ec, payback_e, payback_ec = (
None for n in range(10))
# Return all updated economic metrics
return unit_cost_s_res, unit_cost_e_res, unit_cost_c_res, \
unit_cost_s_com, unit_cost_e_com, unit_cost_c_com, irr_e, \
irr_ec, payback_e, payback_ec, cce, cce_bens, ccc, ccc_bens
def payback(self, cashflows):
"""Calculate simple payback period.
Notes:
Calculate the simple payback period given an input list of
cash flows, which may be uneven.
Args:
cashflows (list): Cash flows across measure lifetime.
Returns:
Simple payback period for the input cash flows.
"""
# Separate initial investment and subsequent cash flows
# from "cashflows" input; extend cashflows up until 100 years
# out to ensure calculation of all paybacks under 100 years
investment, cashflows = cashflows[0], list(
cashflows[1:]) + [cashflows[-1]] * (100 - len(cashflows[1:]))
# If initial investment is positive, payback = 0
if investment >= 0:
payback_val = 0
else:
# Find absolute value of initial investment to compare
# subsequent cash flows against
investment = abs(investment)
# Initialize cumulative cashflow and # years tracking
total, years, cumulative = 0, 0, []
# Add to years and cumulative cashflow trackers while cumulative
# cashflow < investment
for cashflow in cashflows:
total += cashflow
if total < investment:
years += 1
cumulative.append(total)
# If investment pays back within the measure lifetime,
# calculate this payback period in years
if years < len(cashflows):
a = years
# Case where payback period < 1 year
if (years - 1) < 0:
b = investment
c = cumulative[0]
# Case where payback period >= 1 year
else:
b = investment - cumulative[years - 1]
c = cumulative[years] - cumulative[years - 1]
payback_val = a + (b / c)
# If investment does not pay back within measure lifetime,
# set payback period to artifically high number
else:
payback_val = 999
# Return updated payback period value in years
return payback_val
def compete_measures(self, adopt_scheme, htcl_totals):
"""Compete and apportion total stock/energy/carbon/cost across measures.
Notes:
Adjust each competing measure's 'baseline' and 'efficient'
energy/carbon/cost to reflect either a) direct competition between
measures, or b) the indirect effects of measure competition.
Args:
adopt_scheme (string): Assumed consumer adoption scenario.
"""
# Establish list of key chains and supporting competition data for all
# stock/energy/carbon/cost microsegments that contribute to a measure's
# total stock/energy/carbon/cost microsegments, across active measures
mseg_keys, mkts_adj = ([] for n in range(2))
for x in self.measures:
mseg_keys.extend(x.markets[adopt_scheme]["competed"][
"mseg_adjust"]["contributing mseg keys and values"].keys())
mkts_adj.append(x.markets[adopt_scheme]["competed"]["mseg_adjust"])
# Establish list of unique key chains in mseg_keys list above,
# ensuring that all 'primary' microsegments (e.g., relating to direct
# equipment replacement) are ordered and updated before 'secondary'
# microsegments (e.g., relating to indirect effects of equipment
# replacement, such as reduced waste heat from changes in lighting)
msegs = sorted(numpy.unique(mseg_keys))
# Initialize a dict used to store data on overlaps between supply-side
# heating/cooling ECMs (e.g., HVAC equipment) and demand-side
# heating/cooling ECMs (e.g., envelope). If the current set of ECMs
# does not affect both supply-side and demand-side heating/cooling
# markets, this dict is set to None