ebm 0.99.5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ebm/__init__.py +0 -0
- ebm/__main__.py +152 -0
- ebm/__version__.py +1 -0
- ebm/cmd/__init__.py +0 -0
- ebm/cmd/calibrate.py +83 -0
- ebm/cmd/calibrate_excel_com_io.py +128 -0
- ebm/cmd/heating_systems_by_year.py +18 -0
- ebm/cmd/helpers.py +134 -0
- ebm/cmd/initialize.py +167 -0
- ebm/cmd/migrate.py +92 -0
- ebm/cmd/pipeline.py +227 -0
- ebm/cmd/prepare_main.py +174 -0
- ebm/cmd/result_handler.py +272 -0
- ebm/cmd/run_calculation.py +221 -0
- ebm/data/area.csv +92 -0
- ebm/data/area_new_residential_buildings.csv +3 -0
- ebm/data/area_per_person.csv +12 -0
- ebm/data/building_code_parameters.csv +9 -0
- ebm/data/energy_need_behaviour_factor.csv +6 -0
- ebm/data/energy_need_improvements.csv +7 -0
- ebm/data/energy_need_original_condition.csv +534 -0
- ebm/data/heating_system_efficiencies.csv +13 -0
- ebm/data/heating_system_forecast.csv +9 -0
- ebm/data/heating_system_initial_shares.csv +1113 -0
- ebm/data/holiday_home_energy_consumption.csv +24 -0
- ebm/data/holiday_home_stock.csv +25 -0
- ebm/data/improvement_building_upgrade.csv +9 -0
- ebm/data/new_buildings_residential.csv +32 -0
- ebm/data/population_forecast.csv +51 -0
- ebm/data/s_curve.csv +40 -0
- ebm/energy_consumption.py +307 -0
- ebm/extractors.py +115 -0
- ebm/heating_system_forecast.py +472 -0
- ebm/holiday_home_energy.py +341 -0
- ebm/migrations.py +224 -0
- ebm/model/__init__.py +0 -0
- ebm/model/area.py +403 -0
- ebm/model/bema.py +149 -0
- ebm/model/building_category.py +150 -0
- ebm/model/building_condition.py +78 -0
- ebm/model/calibrate_energy_requirements.py +84 -0
- ebm/model/calibrate_heating_systems.py +180 -0
- ebm/model/column_operations.py +157 -0
- ebm/model/construction.py +827 -0
- ebm/model/data_classes.py +223 -0
- ebm/model/database_manager.py +410 -0
- ebm/model/dataframemodels.py +115 -0
- ebm/model/defaults.py +30 -0
- ebm/model/energy_need.py +6 -0
- ebm/model/energy_need_filter.py +182 -0
- ebm/model/energy_purpose.py +115 -0
- ebm/model/energy_requirement.py +353 -0
- ebm/model/energy_use.py +202 -0
- ebm/model/enums.py +8 -0
- ebm/model/exceptions.py +4 -0
- ebm/model/file_handler.py +388 -0
- ebm/model/filter_scurve_params.py +83 -0
- ebm/model/filter_tek.py +152 -0
- ebm/model/heat_pump.py +53 -0
- ebm/model/heating_systems.py +20 -0
- ebm/model/heating_systems_parameter.py +17 -0
- ebm/model/heating_systems_projection.py +3 -0
- ebm/model/heating_systems_share.py +28 -0
- ebm/model/scurve.py +224 -0
- ebm/model/tek.py +1 -0
- ebm/s_curve.py +515 -0
- ebm/services/__init__.py +0 -0
- ebm/services/calibration_writer.py +262 -0
- ebm/services/console.py +106 -0
- ebm/services/excel_loader.py +66 -0
- ebm/services/files.py +38 -0
- ebm/services/spreadsheet.py +289 -0
- ebm/temp_calc.py +99 -0
- ebm/validators.py +565 -0
- ebm-0.99.5.dist-info/METADATA +212 -0
- ebm-0.99.5.dist-info/RECORD +80 -0
- ebm-0.99.5.dist-info/WHEEL +5 -0
- ebm-0.99.5.dist-info/entry_points.txt +3 -0
- ebm-0.99.5.dist-info/licenses/LICENSE +21 -0
- ebm-0.99.5.dist-info/top_level.txt +1 -0
@@ -0,0 +1,289 @@
|
|
1
|
+
import dataclasses
|
2
|
+
import itertools
|
3
|
+
import math
|
4
|
+
import pathlib
|
5
|
+
import typing
|
6
|
+
import string
|
7
|
+
from dataclasses import dataclass
|
8
|
+
|
9
|
+
import numpy as np
|
10
|
+
from loguru import logger
|
11
|
+
from openpyxl import load_workbook
|
12
|
+
from openpyxl.cell import Cell
|
13
|
+
from openpyxl.formatting.rule import FormulaRule
|
14
|
+
from openpyxl.styles import Font, PatternFill
|
15
|
+
from openpyxl.utils.cell import cols_from_range, coordinate_to_tuple, get_column_letter
|
16
|
+
from openpyxl.workbook import Workbook
|
17
|
+
from openpyxl.worksheet.errors import IgnoredError
|
18
|
+
|
19
|
+
|
20
|
+
@dataclass
|
21
|
+
class SpreadsheetCell:
|
22
|
+
"""
|
23
|
+
A class to represent a cell in a spreadsheet.
|
24
|
+
|
25
|
+
Attributes
|
26
|
+
----------
|
27
|
+
column : int
|
28
|
+
The column number of the cell.
|
29
|
+
row : int
|
30
|
+
The row number of the cell.
|
31
|
+
value : object
|
32
|
+
The value contained in the cell.
|
33
|
+
|
34
|
+
Methods
|
35
|
+
-------
|
36
|
+
spreadsheet_cell() -> str
|
37
|
+
Returns the cell's address in A1 notation.
|
38
|
+
replace(**kwargs) -> 'SpreadsheetCell'
|
39
|
+
Returns a new SpreadsheetCell with updated attributes.
|
40
|
+
first_row(cell_range) -> tuple
|
41
|
+
Returns the first row of cells in the given range.
|
42
|
+
first_column(cell_range) -> tuple
|
43
|
+
Returns the first column of cells in the given range.
|
44
|
+
submatrix(cell_range) -> tuple
|
45
|
+
Returns the submatrix excluding the first row and column.
|
46
|
+
"""
|
47
|
+
|
48
|
+
column: int
|
49
|
+
row: int
|
50
|
+
value: object
|
51
|
+
|
52
|
+
def spreadsheet_cell(self) -> str:
|
53
|
+
"""
|
54
|
+
Returns the cell's address in A1 notation.
|
55
|
+
|
56
|
+
Returns
|
57
|
+
-------
|
58
|
+
str
|
59
|
+
The cell's address in A1 notation.
|
60
|
+
"""
|
61
|
+
return f'{get_column_letter(self.column)}{self.row}'
|
62
|
+
|
63
|
+
def replace(self, **kwargs) -> 'SpreadsheetCell':
|
64
|
+
"""
|
65
|
+
Returns a new SpreadsheetCell with updated attributes.
|
66
|
+
|
67
|
+
Parameters
|
68
|
+
----------
|
69
|
+
**kwargs : dict
|
70
|
+
The attributes to update.
|
71
|
+
|
72
|
+
Returns
|
73
|
+
-------
|
74
|
+
SpreadsheetCell
|
75
|
+
A new SpreadsheetCell with updated attributes.
|
76
|
+
"""
|
77
|
+
return dataclasses.replace(self, **kwargs)
|
78
|
+
|
79
|
+
@classmethod
|
80
|
+
def first_row(cls, cell_range):
|
81
|
+
"""
|
82
|
+
Returns the first row of cells in the given range.
|
83
|
+
|
84
|
+
Parameters
|
85
|
+
----------
|
86
|
+
cell_range : str
|
87
|
+
The range of cells in A1 notation.
|
88
|
+
|
89
|
+
Returns
|
90
|
+
-------
|
91
|
+
tuple
|
92
|
+
A tuple of SpreadsheetCell objects representing the first row.
|
93
|
+
"""
|
94
|
+
table = list(cols_from_range(cell_range))
|
95
|
+
first_row = [coordinate_to_tuple(rows[0]) for rows in table]
|
96
|
+
return tuple(SpreadsheetCell(column=cell[1], row=cell[0], value=None) for cell in first_row)
|
97
|
+
|
98
|
+
@classmethod
|
99
|
+
def first_column(cls, cell_range):
|
100
|
+
"""
|
101
|
+
Returns the first column of cells in the given range.
|
102
|
+
|
103
|
+
Parameters
|
104
|
+
----------
|
105
|
+
cell_range : str
|
106
|
+
The range of cells in A1 notation.
|
107
|
+
|
108
|
+
Returns
|
109
|
+
-------
|
110
|
+
tuple
|
111
|
+
A tuple of SpreadsheetCell objects representing the first column.
|
112
|
+
"""
|
113
|
+
table = list(cols_from_range(cell_range))
|
114
|
+
first_column = [coordinate_to_tuple(cell) for cell in table[0]]
|
115
|
+
return tuple(SpreadsheetCell(column=cell[1], row=cell[0], value=None) for cell in first_column)
|
116
|
+
|
117
|
+
@classmethod
|
118
|
+
def submatrix(cls, cell_range):
|
119
|
+
"""
|
120
|
+
Returns the submatrix excluding the first row and column.
|
121
|
+
|
122
|
+
Parameters
|
123
|
+
----------
|
124
|
+
cell_range : str
|
125
|
+
The range of cells in A1 notation.
|
126
|
+
|
127
|
+
Returns
|
128
|
+
-------
|
129
|
+
tuple
|
130
|
+
A tuple of SpreadsheetCell objects representing the submatrix.
|
131
|
+
|
132
|
+
Examples
|
133
|
+
--------
|
134
|
+
the_range = "A1:C3"
|
135
|
+
submatrix = SpreadsheetCell.submatrix(the_range)
|
136
|
+
for cell in submatrix:
|
137
|
+
print(cell.spreadsheet_cell(), cell.column, cell.row)
|
138
|
+
|
139
|
+
B2 2 2
|
140
|
+
C2 3 2
|
141
|
+
B3 2 3
|
142
|
+
C3 3 3
|
143
|
+
"""
|
144
|
+
table = list(cols_from_range(cell_range))
|
145
|
+
first_column = [coordinate_to_tuple(cell) for cell in table[0]]
|
146
|
+
first_row = [coordinate_to_tuple(cols[0]) for cols in table]
|
147
|
+
box = []
|
148
|
+
for row in table:
|
149
|
+
for column in row:
|
150
|
+
row_idx, column_idx = coordinate_to_tuple(column)
|
151
|
+
if (row_idx, column_idx) not in first_column and (row_idx, column_idx) not in first_row:
|
152
|
+
box.append(SpreadsheetCell(column=column_idx, row=row_idx, value=None))
|
153
|
+
|
154
|
+
return tuple(sorted(box, key=lambda k: (k.row, k.column)))
|
155
|
+
|
156
|
+
|
157
|
+
def iter_cells(first_column: str = 'E', left_padding: str = '') -> typing.Generator[str, None, None]:
|
158
|
+
""" Returns spreadsheet column names from A up to ZZ
|
159
|
+
Parameters:
|
160
|
+
- first_column Letter of the first column to return. Default (E)
|
161
|
+
- left_padding Padding added in front of single letter columns. Default empty
|
162
|
+
Returns:
|
163
|
+
- Generator supplying a column name
|
164
|
+
"""
|
165
|
+
if not first_column:
|
166
|
+
first_index = 0
|
167
|
+
elif first_column.upper() not in string.ascii_uppercase:
|
168
|
+
raise ValueError(f'Expected first_column {first_column} in {string.ascii_uppercase}')
|
169
|
+
elif len(first_column) != 1:
|
170
|
+
raise ValueError(f'Expected first_column of length 1 was: {len(first_column)}')
|
171
|
+
else:
|
172
|
+
first_index = string.ascii_uppercase.index(first_column.upper())
|
173
|
+
for cell in string.ascii_uppercase[first_index:]:
|
174
|
+
yield f'{left_padding}{cell}'
|
175
|
+
for a, b in itertools.product(string.ascii_uppercase, repeat=2):
|
176
|
+
yield a+b
|
177
|
+
|
178
|
+
|
179
|
+
def detect_format_from_values(col_name, col_values, model):
|
180
|
+
cell_format = ''
|
181
|
+
if np.issubdtype(model[col_name].dtype, np.floating):
|
182
|
+
cell_format = '#,##0.00'
|
183
|
+
if col_values.max() > 1000.0:
|
184
|
+
cell_format = '# ##0'
|
185
|
+
elif 1.0 >= col_values.mean() >= -1.0:
|
186
|
+
cell_format = '0.00%'
|
187
|
+
elif np.issubdtype(model[col_name].dtype, np.integer):
|
188
|
+
cell_format = '#,##0'
|
189
|
+
|
190
|
+
return cell_format
|
191
|
+
|
192
|
+
|
193
|
+
def find_max_column_width(col: typing.Tuple[Cell]):
|
194
|
+
max_length = 5
|
195
|
+
for cell in col:
|
196
|
+
try:
|
197
|
+
if cell.value is not None:
|
198
|
+
cell_length = len(str(cell.value)) + 1
|
199
|
+
if cell.data_type == 'n':
|
200
|
+
thousands = math.floor(math.log(1_000_000_0000, 1000))
|
201
|
+
cell_length = max(len(str(cell.value).split('.')[0]) + thousands, 2)
|
202
|
+
if cell_length > max_length:
|
203
|
+
max_length = cell_length
|
204
|
+
except (AttributeError, KeyError, IgnoredError, ValueError) as ex:
|
205
|
+
logger.debug(f'Got error f{cell.column_letter}')
|
206
|
+
logger.error(ex)
|
207
|
+
pass
|
208
|
+
return max_length
|
209
|
+
|
210
|
+
|
211
|
+
def add_top_row_filter(workbook_file: pathlib.Path|str|None=None, workbook: Workbook| None=None, sheet_names: list[str] | None=None):
|
212
|
+
if not workbook_file and not workbook_file:
|
213
|
+
raise ValueError('add_top_row_filter require either workbook_file or workbook')
|
214
|
+
|
215
|
+
wb = workbook if workbook else load_workbook(workbook_file)
|
216
|
+
|
217
|
+
sheet_names = wb.sheetnames if not sheet_names else sheet_names
|
218
|
+
for worksheet in sheet_names:
|
219
|
+
ws = wb[worksheet]
|
220
|
+
top_row = f'A1:{get_column_letter(ws.max_column)}{1}'
|
221
|
+
ws.auto_filter.ref = top_row
|
222
|
+
|
223
|
+
if not workbook and workbook_file:
|
224
|
+
wb.save(workbook_file)
|
225
|
+
|
226
|
+
|
227
|
+
def make_pretty(workbook_name: pathlib.Path|str):
|
228
|
+
wb = load_workbook(workbook_name)
|
229
|
+
|
230
|
+
header_font = Font(name='Source Sans Pro', size=11, bold=True, color="ffffff")
|
231
|
+
body_font = Font(name='Source Sans Pro', size=11, bold=False, color="000000")
|
232
|
+
|
233
|
+
for s in wb.sheetnames:
|
234
|
+
ws = wb[s]
|
235
|
+
# Freeze the top row
|
236
|
+
ws.freeze_panes = ws['A2']
|
237
|
+
|
238
|
+
# Define the fill color
|
239
|
+
header_fill = PatternFill(start_color='c8102e', end_color='c8102e', fill_type='solid')
|
240
|
+
odd_fill = PatternFill(start_color='ffd8de', end_color='ffd8de', fill_type='solid')
|
241
|
+
even_fill = PatternFill(start_color='ffebee', end_color='ffebee', fill_type='solid')
|
242
|
+
|
243
|
+
# Apply the fill color to the header row
|
244
|
+
for cell in ws[1]:
|
245
|
+
cell.fill = header_fill
|
246
|
+
cell.font = header_font
|
247
|
+
for row_number, row in enumerate(ws.rows):
|
248
|
+
if row_number == 0:
|
249
|
+
continue
|
250
|
+
for column_number, cell in enumerate(row):
|
251
|
+
cell.font = body_font
|
252
|
+
even_rule = FormulaRule(formula=['MOD(ROW(),2)=0'], fill=even_fill)
|
253
|
+
odd_rule = FormulaRule(formula=['MOD(ROW(),2)=1'], fill=odd_fill)
|
254
|
+
worksheet_range = f'A2:{get_column_letter(ws.max_column)}{ws.max_row}'
|
255
|
+
# logger.error(worksheet_range)
|
256
|
+
ws.conditional_formatting.add(worksheet_range, odd_rule)
|
257
|
+
ws.conditional_formatting.add(worksheet_range, even_rule)
|
258
|
+
|
259
|
+
for col in ws.iter_cols(min_col=0):
|
260
|
+
adjusted_width = find_max_column_width(col)
|
261
|
+
ws.column_dimensions[col[0].column_letter].width = adjusted_width + 1.5
|
262
|
+
# Skipping first row, assuming it is a header for now.
|
263
|
+
first_column_value = col[0].value
|
264
|
+
values = [int(r.value) for r in col[1:] if r.value and r.data_type == 'n']
|
265
|
+
if values:
|
266
|
+
max_min = max(values), min(values)
|
267
|
+
number_format = ''
|
268
|
+
if max_min[0] > 1000:
|
269
|
+
number_format = r'_-* #,##0_-;\-* #,##0_-;_-* "-"??_-;_-@_-'
|
270
|
+
elif max_min[0] <=1.0 and max_min[1] >= 0:
|
271
|
+
# number_format = '0%'
|
272
|
+
number_format = '0.000'
|
273
|
+
elif max_min[0] <=1.0 and max_min[1] >=-1.0:
|
274
|
+
number_format = '0.000'
|
275
|
+
|
276
|
+
if number_format:
|
277
|
+
for row_number, cell in enumerate(col):
|
278
|
+
if row_number < 1:
|
279
|
+
if cell.value == 'year':
|
280
|
+
break
|
281
|
+
continue
|
282
|
+
if True or cell.value:
|
283
|
+
if True or cell.data_type == 'n':
|
284
|
+
cell.number_format = number_format
|
285
|
+
else:
|
286
|
+
cell.number_format = "@"
|
287
|
+
|
288
|
+
|
289
|
+
wb.save(workbook_name)
|
ebm/temp_calc.py
ADDED
@@ -0,0 +1,99 @@
|
|
1
|
+
import os
|
2
|
+
from typing import Optional
|
3
|
+
|
4
|
+
import pandas as pd
|
5
|
+
|
6
|
+
from ebm import extractors
|
7
|
+
from ebm.cmd.result_handler import transform_to_sorted_heating_systems
|
8
|
+
from ebm.model.data_classes import YearRange
|
9
|
+
from ebm.model.database_manager import DatabaseManager
|
10
|
+
from ebm.model import heating_systems_parameter as h_s_param
|
11
|
+
from ebm.model import energy_need as e_n
|
12
|
+
from ebm.model import energy_use as e_u
|
13
|
+
|
14
|
+
from ebm.model.file_handler import FileHandler
|
15
|
+
from ebm.s_curve import calculate_s_curves
|
16
|
+
|
17
|
+
|
18
|
+
def calculate_energy_use_wide(ebm_input):
|
19
|
+
fh = FileHandler(directory=ebm_input)
|
20
|
+
database_manager = DatabaseManager(file_handler=fh)
|
21
|
+
years = YearRange(2020, 2050)
|
22
|
+
|
23
|
+
heating_systems_projection = extractors.extract_heating_systems_forecast(years, database_manager) # 📍
|
24
|
+
heating_systems_parameter = h_s_param.heating_systems_parameter_from_projection(heating_systems_projection) # 📌
|
25
|
+
|
26
|
+
building_code_parameters = database_manager.file_handler.get_building_code() # 📍
|
27
|
+
scurve_parameters = database_manager.get_scurve_params() # 📍
|
28
|
+
|
29
|
+
s_curves_by_condition = calculate_s_curves(scurve_parameters, building_code_parameters, years) # 📌
|
30
|
+
area_parameters = database_manager.get_area_parameters() # 📍
|
31
|
+
area_forecast = extractors.extract_area_forecast(years, s_curves_by_condition, building_code_parameters,
|
32
|
+
area_parameters, database_manager) # 📍
|
33
|
+
energy_need_kwh_m2 = extractors.extract_energy_need(years, database_manager) # 📍
|
34
|
+
total_energy_need = e_n.transform_total_energy_need(energy_need_kwh_m2, area_forecast) # 📌
|
35
|
+
|
36
|
+
energy_use_kwh = e_u.building_group_energy_use_kwh(heating_systems_parameter, total_energy_need) # 📌
|
37
|
+
energy_use_gwh_by_building_group = e_u.energy_use_gwh_by_building_group(energy_use_kwh)
|
38
|
+
energy_use_holiday_homes = extractors.extract_energy_use_holiday_homes(database_manager) # 📍
|
39
|
+
energy_use_wide = transform_to_sorted_heating_systems(energy_use_gwh_by_building_group, energy_use_holiday_homes,
|
40
|
+
building_column='building_group')
|
41
|
+
|
42
|
+
return energy_use_wide
|
43
|
+
|
44
|
+
|
45
|
+
def calculate_area_forecast(input_directory: Optional[str] = None, file_handler: Optional[FileHandler] = None,
|
46
|
+
database_manager: Optional[DatabaseManager] = None,
|
47
|
+
years: Optional[YearRange] = None,
|
48
|
+
scurve_parameters: Optional[pd.DataFrame] = None,
|
49
|
+
area_parameters: Optional[pd.DataFrame] = None,
|
50
|
+
building_code_parameters: Optional[pd.DataFrame] = None,
|
51
|
+
s_curves_by_condition: Optional[pd.DataFrame] = None,
|
52
|
+
) -> pd.DataFrame:
|
53
|
+
input_dir = os.environ.get('EBM_INPUT_DIRECTORY', 'input') if input_directory is None else input_directory
|
54
|
+
|
55
|
+
fh = file_handler
|
56
|
+
if file_handler is None:
|
57
|
+
fh = FileHandler(directory=input_dir)
|
58
|
+
|
59
|
+
dm = database_manager
|
60
|
+
if database_manager is None:
|
61
|
+
dm = DatabaseManager(file_handler=fh)
|
62
|
+
|
63
|
+
years = years if years is not None else YearRange(
|
64
|
+
int(os.environ.get('EBM_START_YEAR', 2020)),
|
65
|
+
int(os.environ.get('EBM_END_YEAR', 2050)))
|
66
|
+
|
67
|
+
scurve_parameters = dm.get_scurve_params() if scurve_parameters is None else scurve_parameters
|
68
|
+
building_code_parameters = dm.file_handler.get_building_code() if building_code_parameters is None else building_code_parameters
|
69
|
+
area_parameters = dm.get_area_parameters() if area_parameters is None else area_parameters
|
70
|
+
|
71
|
+
area_parameters['year'] = years.start
|
72
|
+
|
73
|
+
if not s_curves_by_condition:
|
74
|
+
s_curves_by_condition = calculate_s_curves(scurve_parameters, building_code_parameters, years) # 📌
|
75
|
+
|
76
|
+
df = extractors.extract_area_forecast(years, s_curves_by_condition, building_code_parameters, area_parameters, dm) # 📍
|
77
|
+
|
78
|
+
return df.set_index(['building_category', 'building_code', 'building_condition', 'year'])
|
79
|
+
|
80
|
+
|
81
|
+
def calculate_energy_need(input_directory: Optional[str] = None, file_handler: Optional[FileHandler] = None,
|
82
|
+
database_manager: Optional[DatabaseManager] = None,
|
83
|
+
years: Optional[YearRange] = None) -> pd.DataFrame:
|
84
|
+
dm = database_manager
|
85
|
+
input_dir = os.environ.get('EBM_INPUT_DIRECTORY', 'input') if input_directory is None else input_directory
|
86
|
+
|
87
|
+
years = years if years is not None else YearRange(
|
88
|
+
int(os.environ.get('EBM_START_YEAR', 2020)),
|
89
|
+
int(os.environ.get('EBM_END_YEAR', 2050)))
|
90
|
+
|
91
|
+
fh = file_handler
|
92
|
+
if file_handler is None:
|
93
|
+
fh = FileHandler(directory=input_dir)
|
94
|
+
|
95
|
+
if database_manager is None:
|
96
|
+
dm = DatabaseManager(file_handler=fh)
|
97
|
+
|
98
|
+
energy_need = extractors.extract_energy_need(years, dm) # 📍
|
99
|
+
return energy_need
|