• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

SEED-platform / seed / #6522

pending completion
#6522

push

coveralls-python

web-flow
Update derived column migration to prevent conflicting column names and prevent duplicate column names (#3728)

* update derived column migration to create unique names for derived columns

* prevent derived column names that are duplicated with column names

* disable create/save on error

* update and add test

15680 of 22591 relevant lines covered (69.41%)

0.69 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

22.9
/seed/analysis_pipelines/co2.py
1
# !/usr/bin/env python
2
# encoding: utf-8
3
"""
1✔
4
:copyright (c) 2014 - 2022, The Regents of the University of California, through Lawrence Berkeley National Laboratory (subject to receipt of any required approvals from the U.S. Department of Energy) and contributors. All rights reserved.
5
:author
6
"""
7
import datetime
1✔
8
import logging
1✔
9

10
from celery import chain, shared_task
1✔
11

12
from seed.analysis_pipelines.pipeline import (
1✔
13
    AnalysisPipeline,
14
    AnalysisPipelineException,
15
    analysis_pipeline_task,
16
    task_create_analysis_property_views
17
)
18
from seed.analysis_pipelines.utils import (
1✔
19
    SimpleMeterReading,
20
    get_days_in_reading
21
)
22
from seed.models import (
1✔
23
    Analysis,
24
    AnalysisMessage,
25
    AnalysisPropertyView,
26
    Column,
27
    Meter,
28
    MeterReading,
29
    PropertyView
30
)
31

32
logger = logging.getLogger(__name__)
1✔
33

34
ERROR_INVALID_METER_READINGS = 0
1✔
35
ERROR_NO_VALID_PROPERTIES = 1
1✔
36
WARNING_SOME_INVALID_PROPERTIES = 2
1✔
37
ERROR_NO_REGION_CODE = 3
1✔
38
ERROR_INVALID_REGION_CODE = 4
1✔
39

40
CO2_ANALYSIS_MESSAGES = {
1✔
41
    ERROR_INVALID_METER_READINGS: 'Property view skipped (no linked electricity meters with readings).',
42
    ERROR_NO_VALID_PROPERTIES: 'Analysis found no valid properties.',
43
    WARNING_SOME_INVALID_PROPERTIES: 'Some properties failed to validate.',
44
    ERROR_NO_REGION_CODE: 'Property is missing eGRID Subregion Code.',
45
    ERROR_INVALID_REGION_CODE: 'Could not find C02 rate for provided eGRID subregion code.'
46
}
47

48
VALID_METERS = [Meter.ELECTRICITY_GRID, Meter.ELECTRICITY_UNKNOWN]
1✔
49
TIME_PERIOD = datetime.timedelta(days=365)
1✔
50

51
# These factors represent how much CO2e is emitted per MWh of electricity used
52
# in a specific year and eGRID Subregion
53
#
54
# Sources:
55
#  https://github.com/NREL/openstudio-common-measures-gem/pull/80/files#diff-9b55886a63bf3970a5d1c55effeb291a3107e00091715e63448ac1983ef89559
56
#  https://github.com/NREL/openstudio-common-measures-gem/pull/80/files#diff-fd04e84984194976089ec4d90f103e6d68e641a596e2446447d05626057b38ad
57
EARLIEST_CO2_RATE = 2007
1✔
58
CO2_RATES = {
1✔
59
    EARLIEST_CO2_RATE: {
60
        'AZNM': 570.57,  'CAMX': 309.98,  'ERCT': 570.18,  'FRCC': 555.85,  'MROE': 771.83,  'MROW': 785.61,  # noqa: E241
61
        'NEWE': 378.35,  'NWPP': 391.53,  'NYCW': 320.35,  'NYLI': 646.1,   'NYUP': 311.42,  'RFCE': 483.05,  # noqa: E241
62
        'RFCM': 753,     'RFCW': 707.43,  'RMPA': 868.69,  'SPNO': 820.02,  'SPSO': 739.88,  'SRMV': 457.13,  # noqa: E241
63
        'SRMW': 811.26,  'SRSO': 681.88,  'SRTV': 702.55,  'SRVC': 510.09                                     # noqa: E241
64
    }, 2009: {
65
        'AZNM': 542.65,  'CAMX': 299.85,  'ERCT': 537.91,  'FRCC': 535.87,  'MROE': 725.84,  'MROW': 742.75,  # noqa: E241
66
        'NEWE': 333,     'NWPP': 373.41,  'NYCW': 277.56,  'NYLI': 613.98,  'NYUP': 226.91,  'RFCE': 432.02,  # noqa: E241
67
        'RFCM': 756.78,  'RFCW': 693.29,  'RMPA': 831.45,  'SPNO': 827.71,  'SPSO': 728.44,  'SRMV': 456.28,  # noqa: E241
68
        'SRMW': 797.77,  'SRSO': 604.33,  'SRTV': 618.99,  'SRVC': 472.42                                     # noqa: E241
69
    }, 2010: {
70
        'AZNM': 536.44,  'CAMX': 278.12,  'ERCT': 554.58,  'FRCC': 545.01,  'MROE': 734.6,   'MROW': 700.71,  # noqa: E241
71
        'NEWE': 329.97,  'NWPP': 384.1,   'NYCW': 282.88,  'NYLI': 608.15,  'NYUP': 248.69,  'RFCE': 456.69,  # noqa: E241
72
        'RFCM': 742.99,  'RFCW': 685.47,  'RMPA': 864.49,  'SPNO': 820.27,  'SPSO': 719.95,  'SRMV': 468.73,  # noqa: E241
73
        'SRMW': 825.57,  'SRSO': 617.24,  'SRTV': 633.32,  'SRVC': 489.58                                     # noqa: E241
74
    }, 2012: {
75
        'AZNM': 525.13,  'CAMX': 296.01,  'ERCT': 520.26,  'FRCC': 512.39,  'MROE': 694.31,  'MROW': 649.98,  # noqa: E241
76
        'NEWE': 291.49,  'NWPP': 303.5,   'NYCW': 316.58,  'NYLI': 546.88,  'NYUP': 186.08,  'RFCE': 391.23,  # noqa: E241
77
        'RFCM': 715.32,  'RFCW': 628.8,   'RMPA': 830.73,  'SPNO': 784.78,  'SPSO': 700.8,   'SRMV': 479.19,  # noqa: E241
78
        'SRMW': 779.87,  'SRSO': 523.48,  'SRTV': 609.49,  'SRVC': 425.34                                     # noqa: E241
79
    }, 2014: {
80
        'AZNM': 399.02,  'CAMX': 258.72,  'ERCT': 520.64,  'FRCC': 490.13,  'MROE': 760.31,  'MROW': 623.84,  # noqa: E241
81
        'NEWE': 261.56,  'NWPP': 414.24,  'NYCW': 302.46,  'NYLI': 546.16,  'NYUP': 166.72,  'RFCE': 378.43,  # noqa: E241
82
        'RFCM': 699.58,  'RFCW': 630.76,  'RMPA': 793.36,  'SPNO': 719.46,  'SPSO': 673.36,  'SRMV': 465.8,   # noqa: E241
83
        'SRMW': 809.86,  'SRSO': 521.86,  'SRTV': 610.15,  'SRVC': 391.27                                     # noqa: E241
84
    }, 2016: {
85
        'AZNM': 475.74,  'CAMX': 240.3,   'ERCT': 459.88,  'FRCC': 460.92,  'MROE': 761.56,  'MROW': 565.71,  # noqa: E241
86
        'NEWE': 255.65,  'NWPP': 297.23,  'NYCW': 288.91,  'NYLI': 537.85,  'NYUP': 134.21,  'RFCE': 345.62,  # noqa: E241
87
        'RFCM': 579.98,  'RFCW': 567.54,  'RMPA': 624.37,  'SPNO': 644.95,  'SPSO': 569.08,  'SRMV': 381.92,  # noqa: E241
88
        'SRMW': 735.79,  'SRSO': 496.64,  'SRTV': 540.86,  'SRVC': 367.39                                     # noqa: E241
89
    }, 2018: {
90
        'AZNM': 465.99,  'CAMX': 226.15,  'ERCT': 424.51,  'FRCC': 424.54,  'MROE': 766.26,  'MROW': 566.51,  # noqa: E241
91
        'NEWE': 239.25,  'NWPP': 291.77,  'NYCW': 271.09,  'NYLI': 541.07,  'NYUP': 115.14,  'RFCE': 326.51,  # noqa: E241
92
        'RFCM': 599.16,  'RFCW': 532.42,  'RMPA': 581.36,  'SPNO': 531.32,  'SPSO': 531.84,  'SRMV': 389.27,  # noqa: E241
93
        'SRMW': 760.42,  'SRSO': 468.68,  'SRTV': 470.79,  'SRVC': 339                                        # noqa: E241
94
    }, 2019: {
95
        'AZNM': 433.95,  'CAMX': 206.46,  'ERCT': 395.63,  'FRCC': 392.07,  'MROE': 685.96,  'MROW': 501.78,  # noqa: E241
96
        'NEWE': 223.95,  'NWPP': 326.46,  'NYCW': 251.72,  'NYLI': 552.78,  'NYUP': 105.69,  'RFCE': 316.76,  # noqa: E241
97
        'RFCM': 542.84,  'RFCW': 487.24,  'RMPA': 567.12,  'SPNO': 488.69,  'SPSO': 456.55,  'SRMV': 367.15,  # noqa: E241
98
        'SRMW': 723.76,  'SRSO': 441.7,   'SRTV': 433.36,  'SRVC': 307.99                                     # noqa: E241
99
    }, 2020: {
100
        'AZNM': 350.6,   'CAMX': 211.4,   'ERCT': 342.1,   'FRCC': 366.2,   'MROE': 535.3,   'MROW': 381.9,   # noqa: E241
101
        'NEWE': 134.4,   'NWPP': 175.7,                    'NYLI': 235.9,   'NYUP': 180.1,   'RFCE': 274.5,   # noqa: E241
102
        'RFCM': 609.5,   'RFCW': 485,     'RMPA': 514.4,   'SPNO': 457.5,   'SPSO': 285.6,   'SRMV': 419,     # noqa: E241
103
        'SRMW': 606,     'SRSO': 332.3,   'SRTV': 513.9,   'SRVC': 291.6                                      # noqa: E241
104
    }, 2022: {
105
        'AZNM': 410,     'CAMX': 215.4,   'ERCT': 340.2,   'FRCC': 375.2,   'MROE': 526.5,   'MROW': 392.6,   # noqa: E241
106
        'NEWE': 146.3,   'NWPP': 224,                      'NYLI': 248.9,   'NYUP': 198.2,   'RFCE': 282.1,   # noqa: E241
107
        'RFCM': 629.8,   'RFCW': 500.5,   'RMPA': 571.8,   'SPNO': 443.5,   'SPSO': 276,     'SRMV': 409.5,   # noqa: E241
108
        'SRMW': 632.1,   'SRSO': 320.5,   'SRTV': 550.4,   'SRVC': 301.1                                      # noqa: E241
109
    }, 2024: {
110
        'AZNM': 402.4,   'CAMX': 197.2,   'ERCT': 300.5,   'FRCC': 379.2,   'MROE': 529.5,   'MROW': 361.7,   # noqa: E241
111
        'NEWE': 131.1,   'NWPP': 183.8,                    'NYLI': 182.1,   'NYUP': 146.9,   'RFCE': 250.7,   # noqa: E241
112
        'RFCM': 519.3,   'RFCW': 460.4,   'RMPA': 567.2,   'SPNO': 246.9,   'SPSO': 160.2,   'SRMV': 357,     # noqa: E241
113
        'SRMW': 547.2,   'SRSO': 325.5,   'SRTV': 484.2,   'SRVC': 299.7                                      # noqa: E241
114
    }, 2026: {
115
        'AZNM': 387.2,   'CAMX': 178.9,   'ERCT': 300.5,   'FRCC': 397.2,   'MROE': 523.4,   'MROW': 335.3,   # noqa: E241
116
        'NEWE': 106.9,   'NWPP': 169,                      'NYLI': 162.7,   'NYUP': 128.3,   'RFCE': 247,     # noqa: E241
117
        'RFCM': 523.9,   'RFCW': 471.6,   'RMPA': 573.1,   'SPNO': 233.7,   'SPSO': 142.5,   'SRMV': 347.7,   # noqa: E241
118
        'SRMW': 537.9,   'SRSO': 345.6,   'SRTV': 509.9,   'SRVC': 280.6                                      # noqa: E241
119
    }, 2028: {
120
        'AZNM': 333,     'CAMX': 158.6,   'ERCT': 270.1,   'FRCC': 353.1,   'MROE': 477.6,   'MROW': 309.3,   # noqa: E241
121
        'NEWE': 95.4,    'NWPP': 146.9,                    'NYLI': 138.5,   'NYUP': 105.7,   'RFCE': 235.8,   # noqa: E241
122
        'RFCM': 476.8,   'RFCW': 457.5,   'RMPA': 532,     'SPNO': 226.4,   'SPSO': 133.2,   'SRMV': 327.9,   # noqa: E241
123
        'SRMW': 472.1,   'SRSO': 262,     'SRTV': 500.2,   'SRVC': 257.6                                      # noqa: E241
124
    }, 2030: {
125
        'AZNM': 299.5,   'CAMX': 149.3,   'ERCT': 206.7,   'FRCC': 287.3,   'MROE': 347.1,   'MROW': 195.6,   # noqa: E241
126
        'NEWE': 80.3,    'NWPP': 136.2,                    'NYLI': 116.5,   'NYUP': 87.8,    'RFCE': 206.9,   # noqa: E241
127
        'RFCM': 390.9,   'RFCW': 411.4,   'RMPA': 438,     'SPNO': 181.1,   'SPSO': 118.9,   'SRMV': 280.3,   # noqa: E241
128
        'SRMW': 362.9,   'SRSO': 221.4,   'SRTV': 405.5,   'SRVC': 210.6                                      # noqa: E241
129
    }, 2032: {
130
        'AZNM': 286.9,   'CAMX': 146.7,   'ERCT': 168,     'FRCC': 271.6,   'MROE': 302.6,   'MROW': 161.3,   # noqa: E241
131
        'NEWE': 81,      'NWPP': 136.7,                    'NYLI': 113.9,   'NYUP': 87.4,    'RFCE': 204.9,   # noqa: E241
132
        'RFCM': 371.6,   'RFCW': 385.9,   'RMPA': 413.1,   'SPNO': 161.3,   'SPSO': 86.6,    'SRMV': 268.4,   # noqa: E241
133
        'SRMW': 379.6,   'SRSO': 205.2,   'SRTV': 377,     'SRVC': 202.1                                      # noqa: E241
134
    }, 2034: {
135
        'AZNM': 277.9,   'CAMX': 139.5,   'ERCT': 157.1,   'FRCC': 275.5,   'MROE': 163.2,   'MROW': 144.8,   # noqa: E241
136
        'NEWE': 69.3,    'NWPP': 135,                      'NYLI': 98.8,    'NYUP': 74.7,    'RFCE': 198.1,   # noqa: E241
137
        'RFCM': 354.9,   'RFCW': 356.7,   'RMPA': 341,     'SPNO': 160.6,   'SPSO': 81.2,    'SRMV': 263.3,   # noqa: E241
138
        'SRMW': 363.1,   'SRSO': 206.9,   'SRTV': 352.8,   'SRVC': 190.1                                      # noqa: E241
139
    }, 2036: {
140
        'AZNM': 241.6,   'CAMX': 129.3,   'ERCT': 155.4,   'FRCC': 266.9,   'MROE': 157.4,   'MROW': 138.4,   # noqa: E241
141
        'NEWE': 71.9,    'NWPP': 134.1,                    'NYLI': 106.9,   'NYUP': 81.7,    'RFCE': 202.1,   # noqa: E241
142
        'RFCM': 335.9,   'RFCW': 329.1,   'RMPA': 287.9,   'SPNO': 155.4,   'SPSO': 71.8,    'SRMV': 252.2,   # noqa: E241
143
        'SRMW': 345,     'SRSO': 206.2,   'SRTV': 324.9,   'SRVC': 187.2                                      # noqa: E241
144
    }, 2038: {
145
        'AZNM': 207.3,   'CAMX': 111.7,   'ERCT': 140.1,   'FRCC': 258.4,   'MROE': 168.7,   'MROW': 137.6,   # noqa: E241
146
        'NEWE': 66.3,    'NWPP': 123.3,                    'NYLI': 100.7,   'NYUP': 80.1,    'RFCE': 205.4,   # noqa: E241
147
        'RFCM': 316.9,   'RFCW': 310,     'RMPA': 286,     'SPNO': 155.5,   'SPSO': 64.1,    'SRMV': 247,     # noqa: E241
148
        'SRMW': 332.9,   'SRSO': 187.1,   'SRTV': 323.4,   'SRVC': 184.1                                      # noqa: E241
149
    }, 2040: {
150
        'AZNM': 178.4,   'CAMX': 100.9,   'ERCT': 145.9,   'FRCC': 250.3,   'MROE': 159.9,   'MROW': 140.8,   # noqa: E241
151
        'NEWE': 66.5,    'NWPP': 110.7,                    'NYLI': 93.9,    'NYUP': 74.1,    'RFCE': 198,     # noqa: E241
152
        'RFCM': 289.3,   'RFCW': 281.8,   'RMPA': 284.8,   'SPNO': 153.6,   'SPSO': 59.7,    'SRMV': 239.8,   # noqa: E241
153
        'SRMW': 281.7,   'SRSO': 183.6,   'SRTV': 307.2,   'SRVC': 178.4                                      # noqa: E241
154
    }, 2042: {
155
        'AZNM': 180.9,   'CAMX': 93.9,    'ERCT': 127,     'FRCC': 227.1,   'MROE': 152.3,   'MROW': 120.6,   # noqa: E241
156
        'NEWE': 57.4,    'NWPP': 107.2,                    'NYLI': 89,      'NYUP': 75.9,    'RFCE': 192,     # noqa: E241
157
        'RFCM': 285.2,   'RFCW': 254.2,   'RMPA': 245.3,   'SPNO': 151.3,   'SPSO': 55.5,    'SRMV': 222.1,   # noqa: E241
158
        'SRMW': 243.9,   'SRSO': 160.5,   'SRTV': 279,     'SRVC': 155.4                                      # noqa: E241
159
    }, 2044: {
160
        'AZNM': 142.9,   'CAMX': 81.3,    'ERCT': 119.7,   'FRCC': 191.6,   'MROE': 121.2,   'MROW': 99.9,    # noqa: E241
161
        'NEWE': 59,      'NWPP': 102.5,                    'NYLI': 86.9,    'NYUP': 77.7,    'RFCE': 186.7,   # noqa: E241
162
        'RFCM': 287.9,   'RFCW': 237.1,   'RMPA': 238.4,   'SPNO': 116.6,   'SPSO': 46.7,    'SRMV': 205.6,   # noqa: E241
163
        'SRMW': 209.3,   'SRSO': 143.8,   'SRTV': 259.1,   'SRVC': 147.8                                      # noqa: E241
164
    }, 2046: {
165
        'AZNM': 135.6,   'CAMX': 76.2,    'ERCT': 104.8,   'FRCC': 191.8,   'MROE': 120.4,   'MROW': 97.8,    # noqa: E241
166
        'NEWE': 58.4,    'NWPP': 90.8,                     'NYLI': 85.1,    'NYUP': 76.5,    'RFCE': 190.6,   # noqa: E241
167
        'RFCM': 225.6,   'RFCW': 210.3,   'RMPA': 181,     'SPNO': 121,     'SPSO': 48.5,    'SRMV': 233.5,   # noqa: E241
168
        'SRMW': 185.6,   'SRSO': 122.1,   'SRTV': 228.4,   'SRVC': 127.1                                      # noqa: E241
169
    }, 2048: {
170
        'AZNM': 131.8,   'CAMX': 74.7,    'ERCT': 102.4,   'FRCC': 188.8,   'MROE': 134.6,   'MROW': 87.6,    # noqa: E241
171
        'NEWE': 57.1,    'NWPP': 86,                       'NYLI': 91.1,    'NYUP': 81.5,    'RFCE': 169.8,   # noqa: E241
172
        'RFCM': 201.6,   'RFCW': 212.4,   'RMPA': 161.5,   'SPNO': 129,     'SPSO': 57.5,    'SRMV': 232.6,   # noqa: E241
173
        'SRMW': 184.5,   'SRSO': 125.7,   'SRTV': 207.8,   'SRVC': 120.7                                      # noqa: E241
174
    }, 2050: {
175
        'AZNM': 125.7,   'CAMX': 68.8,    'ERCT': 87.7,    'FRCC': 178,     'MROE': 100.1,   'MROW': 78.9,    # noqa: E241
176
        'NEWE': 58,      'NWPP': 66,                       'NYLI': 89.1,    'NYUP': 81.4,    'RFCE': 161.9,   # noqa: E241
177
        'RFCM': 155.1,   'RFCW': 193,     'RMPA': 155.6,   'SPNO': 131.2,   'SPSO': 55.4,    'SRMV': 215.5,   # noqa: E241
178
        'SRMW': 173.4,   'SRSO': 111.8,   'SRTV': 175.4,   'SRVC': 97.7                                       # noqa: E241
179
    }
180
}
181

182

183
# currently only getting closest prior year
184
def _get_co2_rate(year, region_code):
1✔
185
    while year >= EARLIEST_CO2_RATE:
×
186
        if year in CO2_RATES and region_code in CO2_RATES[year]:
×
187
            return CO2_RATES[year][region_code]
×
188
        year -= 1
×
189
    return None
×
190

191

192
def _get_valid_meters(property_view_ids):
1✔
193
    """Performs basic validation of the properties for running CO2 analysis and returns any errors.
194

195
    :param analysis: property_view_ids
196
    :returns: dictionary[id:str], dictionary of property_view_ids to error message
197
    """
198
    invalid_meter = []
×
199
    meter_readings_by_property_view = {}
×
200
    property_views = PropertyView.objects.filter(id__in=property_view_ids)
×
201
    for property_view in property_views:
×
202

203
        # get the most recent electric meter reading's end_time
204
        try:
×
205
            end_time = MeterReading.objects.filter(
×
206
                meter__property=property_view.property,
207
                meter__type__in=VALID_METERS
208
            ).order_by('end_time').last().end_time
209
        except Exception:
×
210
            invalid_meter.append(property_view.id)
×
211
            continue
×
212

213
        # get all readings that started AND ended between end_time and a year prior
214
        property_meter_readings = [
×
215
            SimpleMeterReading(reading.start_time, reading.end_time, reading.reading)
216
            for reading in MeterReading.objects.filter(
217
                meter__property=property_view.property,
218
                meter__type__in=VALID_METERS,
219
                end_time__lte=end_time,
220
                start_time__gte=end_time - TIME_PERIOD
221
            ).order_by('start_time')
222
        ]
223

224
        meter_readings_by_property_view[property_view.id] = property_meter_readings
×
225

226
    errors_by_property_view_id = {}
×
227
    for pid in invalid_meter:
×
228
        if pid not in errors_by_property_view_id:
×
229
            errors_by_property_view_id[pid] = []
×
230
        errors_by_property_view_id[pid].append(CO2_ANALYSIS_MESSAGES[ERROR_INVALID_METER_READINGS])
×
231

232
    return meter_readings_by_property_view, errors_by_property_view_id
×
233

234

235
def _calculate_co2(meter_readings, region_code):
1✔
236
    """Calculate CO2 emissions for the meter readings. Raises an exception if it's
237
    unable to calculate the emissions (e.g., unable to find eGRID region code for
238
    a year)
239

240
    :param meter_readings: List[SimpleMeterReading | MeterReading], the `.reading`
241
        value must be in kBtu!
242
        Assumes the time span of meter_readings is less than or equal to TIME_PERIOD,
243
        i.e., that they are supposed to be representative of a year.
244
    :region_code: str, an eGRID Subregion Code
245
    :return: dict
246
    """
247
    total_reading = 0
×
248
    total_average = 0
×
249
    days_affected_by_readings = set()
×
250
    for meter_reading in meter_readings:
×
251
        reading_mwh = meter_reading.reading / 3.412 / 1000  # convert from kBtu to MWh
×
252
        total_reading += reading_mwh
×
253
        year = meter_reading.start_time.year
×
254
        rate = _get_co2_rate(year, region_code)
×
255
        if rate is None:
×
256
            raise Exception(f'Failed to find CO2 rate for {region_code} in {year}')
×
257
        total_average += (reading_mwh * rate)
×
258
        for day in get_days_in_reading(meter_reading):
×
259
            days_affected_by_readings.add(day)
×
260
    total_seconds_covered = len(days_affected_by_readings) * datetime.timedelta(days=1).total_seconds()
×
261
    fraction_of_time_covered = total_seconds_covered / TIME_PERIOD.total_seconds()
×
262
    return {
×
263
        'average_annual_kgco2e': round(total_average),
264
        'total_annual_electricity_mwh': round(total_reading, 2),
265
        'annual_coverage_percent': int(fraction_of_time_covered * 100),
266
    }
267

268

269
class CO2Pipeline(AnalysisPipeline):
1✔
270

271
    def _prepare_analysis(self, property_view_ids, start_analysis=True):
1✔
272
        # current implemtation will *always* start the analysis immediately
273

274
        meter_readings_by_property_view, errors_by_property_view_id = _get_valid_meters(property_view_ids)
×
275
        if not meter_readings_by_property_view:
×
276
            AnalysisMessage.log_and_create(
×
277
                logger=logger,
278
                type_=AnalysisMessage.ERROR,
279
                analysis_id=self._analysis_id,
280
                analysis_property_view_id=None,
281
                user_message=CO2_ANALYSIS_MESSAGES[ERROR_NO_VALID_PROPERTIES],
282
                debug_message=''
283
            )
284
            analysis = Analysis.objects.get(id=self._analysis_id)
×
285
            analysis.status = Analysis.FAILED
×
286
            analysis.save()
×
287
            raise AnalysisPipelineException(CO2_ANALYSIS_MESSAGES[ERROR_NO_VALID_PROPERTIES])
×
288

289
        if errors_by_property_view_id:
×
290
            AnalysisMessage.log_and_create(
×
291
                logger=logger,
292
                type_=AnalysisMessage.WARNING,
293
                analysis_id=self._analysis_id,
294
                analysis_property_view_id=None,
295
                user_message=CO2_ANALYSIS_MESSAGES[WARNING_SOME_INVALID_PROPERTIES],
296
                debug_message=''
297
            )
298

299
        progress_data = self.get_progress_data()
×
300
        progress_data.total = 3
×
301
        progress_data.save()
×
302

303
        chain(
×
304
            task_create_analysis_property_views.si(self._analysis_id, property_view_ids),
305
            _finish_preparation.s(meter_readings_by_property_view, errors_by_property_view_id, self._analysis_id),
306
            _run_analysis.s(self._analysis_id)
307
        ).apply_async()
308

309
    def _start_analysis(self):
1✔
310
        return None
×
311

312

313
@shared_task(bind=True)
1✔
314
@analysis_pipeline_task(Analysis.CREATING)
1✔
315
def _finish_preparation(self, analysis_view_ids_by_property_view_id, meter_readings_by_property_view, errors_by_property_view_id, analysis_id):
1✔
316
    pipeline = CO2Pipeline(analysis_id)
×
317
    pipeline.set_analysis_status_to_ready('Ready to run Average Annual CO2 analysis')
×
318

319
    # attach errors to respective analysis_property_views
320
    if errors_by_property_view_id:
×
321
        for pid in errors_by_property_view_id:
×
322
            analysis_view_id = analysis_view_ids_by_property_view_id[pid]
×
323
            AnalysisMessage.log_and_create(
×
324
                logger=logger,
325
                type_=AnalysisMessage.ERROR,
326
                analysis_id=analysis_id,
327
                analysis_property_view_id=analysis_view_id,
328
                user_message="  ".join(errors_by_property_view_id[pid]),
329
                debug_message=''
330
            )
331

332
    # replace property_view id with analysis_property_view id in meter lookup
333
    meter_readings_by_analysis_property_view = {}
×
334
    for property_view in meter_readings_by_property_view:
×
335
        analysis_view_id = analysis_view_ids_by_property_view_id[property_view]
×
336
        meter_readings_by_analysis_property_view[analysis_view_id] = meter_readings_by_property_view[property_view]
×
337

338
    return meter_readings_by_analysis_property_view
×
339

340

341
@shared_task(bind=True)
1✔
342
@analysis_pipeline_task(Analysis.READY)
1✔
343
def _run_analysis(self, meter_readings_by_analysis_property_view, analysis_id):
1✔
344
    pipeline = CO2Pipeline(analysis_id)
×
345
    progress_data = pipeline.set_analysis_status_to_running()
×
346
    progress_data.step('Calculating Average Annual CO2')
×
347
    analysis = Analysis.objects.get(id=analysis_id)
×
348

349
    # make sure we have the extra data columns we need, don't set the
350
    # displayname and description if the column already exists because
351
    # the user might have changed them which would re-create new columns
352
    # here.
353
    column, created = Column.objects.get_or_create(
×
354
        is_extra_data=True,
355
        column_name='analysis_co2',
356
        organization=analysis.organization,
357
        table_name='PropertyState',
358
    )
359
    if created:
×
360
        column.display_name = 'Average Annual CO2 (kgCO2e)',
×
361
        column.column_description = 'Average Annual CO2 (kgCO2e)',
×
362
        column.save()
×
363

364
    column, created = Column.objects.get_or_create(
×
365
        is_extra_data=True,
366
        column_name='analysis_co2_coverage',
367
        organization=analysis.organization,
368
        table_name='PropertyState',
369
    )
370
    if created:
×
371
        column.display_name = 'Average Annual CO2 Coverage (% of the year)'
×
372
        column.column_description = 'Average Annual CO2 Coverage (% of the year)'
×
373
        column.save()
×
374

375
    # fix the meter readings dict b/c celery messes with it when serializing
376
    meter_readings_by_analysis_property_view = {
×
377
        int(key): [SimpleMeterReading(*serialized_reading) for serialized_reading in serialized_readings]
378
        for key, serialized_readings in meter_readings_by_analysis_property_view.items()
379
    }
380
    analysis_property_view_ids = list(meter_readings_by_analysis_property_view.keys())
×
381

382
    # prefetching property and cycle b/c .get_property_views() uses them (this is not "clean" but whatever)
383
    analysis_property_views = (
×
384
        AnalysisPropertyView.objects.filter(id__in=analysis_property_view_ids)
385
        .prefetch_related('property', 'cycle', 'property_state')
386
    )
387
    property_views_by_apv_id = AnalysisPropertyView.get_property_views(analysis_property_views)
×
388

389
    # should we save data to the property?
390
    save_co2_results = analysis.configuration.get('save_co2_results', False)
×
391

392
    # create and save emissions for each property view
393
    for analysis_property_view in analysis_property_views:
×
394
        meter_readings = meter_readings_by_analysis_property_view[analysis_property_view.id]
×
395
        property_view = property_views_by_apv_id[analysis_property_view.id]
×
396

397
        # get the region code
398
        egrid_subregion_code = property_view.state.egrid_subregion_code
×
399
        if not egrid_subregion_code:
×
400
            AnalysisMessage.log_and_create(
×
401
                logger=logger,
402
                type_=AnalysisMessage.ERROR,
403
                analysis_id=analysis_id,
404
                analysis_property_view_id=analysis_property_view.id,
405
                user_message=CO2_ANALYSIS_MESSAGES[ERROR_NO_REGION_CODE],
406
                debug_message=''
407
            )
408
            continue
×
409

410
        # get the C02 rate
411
        try:
×
412
            co2 = _calculate_co2(meter_readings, egrid_subregion_code)
×
413
        except Exception as e:
×
414
            AnalysisMessage.log_and_create(
×
415
                logger=logger,
416
                type_=AnalysisMessage.ERROR,
417
                analysis_id=analysis_id,
418
                analysis_property_view_id=analysis_property_view.id,
419
                user_message=CO2_ANALYSIS_MESSAGES[ERROR_INVALID_REGION_CODE],
420
                debug_message='Failed to calculate CO2',
421
                exception=e
422
            )
423
            continue
×
424

425
        # save the results
426
        analysis_property_view.parsed_results = {
×
427
            'Average Annual CO2 (kgCO2e)': co2['average_annual_kgco2e'],
428
            'Annual Coverage %': co2['annual_coverage_percent'],
429
            'Total Annual Meter Reading (MWh)': co2['total_annual_electricity_mwh'],
430
            'Total GHG Emissions Intensity (kgCO2e/ft\u00b2/year)': co2['average_annual_kgco2e'] / property_view.state.gross_floor_area.magnitude
431
        }
432
        analysis_property_view.save()
×
433
        if save_co2_results:
×
434
            # Convert the analysis results which reports in kgCO2e to MtCO2e which is the canonical database field units
435
            property_view.state.total_ghg_emissions = co2['average_annual_kgco2e'] / 1000
×
436
            property_view.state.total_ghg_emissions_intensity = co2['average_annual_kgco2e'] / property_view.state.gross_floor_area.magnitude
×
437
            property_view.state.save()
×
438

439
    # all done!
440
    pipeline.set_analysis_status_to_completed()
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc