ebm 0.99.3__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.
Files changed (80) hide show
  1. ebm/__init__.py +0 -0
  2. ebm/__main__.py +152 -0
  3. ebm/__version__.py +1 -0
  4. ebm/cmd/__init__.py +0 -0
  5. ebm/cmd/calibrate.py +83 -0
  6. ebm/cmd/calibrate_excel_com_io.py +128 -0
  7. ebm/cmd/heating_systems_by_year.py +18 -0
  8. ebm/cmd/helpers.py +134 -0
  9. ebm/cmd/initialize.py +167 -0
  10. ebm/cmd/migrate.py +92 -0
  11. ebm/cmd/pipeline.py +227 -0
  12. ebm/cmd/prepare_main.py +174 -0
  13. ebm/cmd/result_handler.py +272 -0
  14. ebm/cmd/run_calculation.py +221 -0
  15. ebm/data/area.csv +92 -0
  16. ebm/data/area_new_residential_buildings.csv +3 -0
  17. ebm/data/area_per_person.csv +12 -0
  18. ebm/data/building_code_parameters.csv +9 -0
  19. ebm/data/energy_need_behaviour_factor.csv +6 -0
  20. ebm/data/energy_need_improvements.csv +7 -0
  21. ebm/data/energy_need_original_condition.csv +534 -0
  22. ebm/data/heating_system_efficiencies.csv +13 -0
  23. ebm/data/heating_system_forecast.csv +9 -0
  24. ebm/data/heating_system_initial_shares.csv +1113 -0
  25. ebm/data/holiday_home_energy_consumption.csv +24 -0
  26. ebm/data/holiday_home_stock.csv +25 -0
  27. ebm/data/improvement_building_upgrade.csv +9 -0
  28. ebm/data/new_buildings_residential.csv +32 -0
  29. ebm/data/population_forecast.csv +51 -0
  30. ebm/data/s_curve.csv +40 -0
  31. ebm/energy_consumption.py +307 -0
  32. ebm/extractors.py +115 -0
  33. ebm/heating_system_forecast.py +472 -0
  34. ebm/holiday_home_energy.py +341 -0
  35. ebm/migrations.py +224 -0
  36. ebm/model/__init__.py +0 -0
  37. ebm/model/area.py +403 -0
  38. ebm/model/bema.py +149 -0
  39. ebm/model/building_category.py +150 -0
  40. ebm/model/building_condition.py +78 -0
  41. ebm/model/calibrate_energy_requirements.py +84 -0
  42. ebm/model/calibrate_heating_systems.py +180 -0
  43. ebm/model/column_operations.py +157 -0
  44. ebm/model/construction.py +827 -0
  45. ebm/model/data_classes.py +223 -0
  46. ebm/model/database_manager.py +410 -0
  47. ebm/model/dataframemodels.py +115 -0
  48. ebm/model/defaults.py +30 -0
  49. ebm/model/energy_need.py +6 -0
  50. ebm/model/energy_need_filter.py +182 -0
  51. ebm/model/energy_purpose.py +115 -0
  52. ebm/model/energy_requirement.py +353 -0
  53. ebm/model/energy_use.py +202 -0
  54. ebm/model/enums.py +8 -0
  55. ebm/model/exceptions.py +4 -0
  56. ebm/model/file_handler.py +388 -0
  57. ebm/model/filter_scurve_params.py +83 -0
  58. ebm/model/filter_tek.py +152 -0
  59. ebm/model/heat_pump.py +53 -0
  60. ebm/model/heating_systems.py +20 -0
  61. ebm/model/heating_systems_parameter.py +17 -0
  62. ebm/model/heating_systems_projection.py +3 -0
  63. ebm/model/heating_systems_share.py +28 -0
  64. ebm/model/scurve.py +224 -0
  65. ebm/model/tek.py +1 -0
  66. ebm/s_curve.py +515 -0
  67. ebm/services/__init__.py +0 -0
  68. ebm/services/calibration_writer.py +262 -0
  69. ebm/services/console.py +106 -0
  70. ebm/services/excel_loader.py +66 -0
  71. ebm/services/files.py +38 -0
  72. ebm/services/spreadsheet.py +289 -0
  73. ebm/temp_calc.py +99 -0
  74. ebm/validators.py +565 -0
  75. ebm-0.99.3.dist-info/METADATA +217 -0
  76. ebm-0.99.3.dist-info/RECORD +80 -0
  77. ebm-0.99.3.dist-info/WHEEL +5 -0
  78. ebm-0.99.3.dist-info/entry_points.txt +3 -0
  79. ebm-0.99.3.dist-info/licenses/LICENSE +21 -0
  80. ebm-0.99.3.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