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.
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.5.dist-info/METADATA +212 -0
  76. ebm-0.99.5.dist-info/RECORD +80 -0
  77. ebm-0.99.5.dist-info/WHEEL +5 -0
  78. ebm-0.99.5.dist-info/entry_points.txt +3 -0
  79. ebm-0.99.5.dist-info/licenses/LICENSE +21 -0
  80. ebm-0.99.5.dist-info/top_level.txt +1 -0
@@ -0,0 +1,262 @@
1
+ import typing
2
+ from datetime import datetime
3
+ import os
4
+
5
+ from loguru import logger
6
+ import pandas as pd
7
+
8
+ from ebm.model import building_category
9
+ from ebm.model.heating_systems import HeatingSystems
10
+ from ebm.model.energy_purpose import EnergyPurpose
11
+ from ebm.services.excel_loader import access_excel_sheet
12
+ from ebm.services.spreadsheet import SpreadsheetCell
13
+
14
+ KEY_ERROR_COLOR = 0xC0C0C0
15
+ COLOR_AUTO = -4105
16
+
17
+
18
+ class ComCalibrationReader:
19
+ filename: str
20
+ sheet_name: str
21
+ df: pd.DataFrame
22
+
23
+ def __init__(self, workbook_name=None, sheet_name=None):
24
+ wb, sh = os.environ.get('EBM_CALIBRATION_SHEET', '!').split('!')
25
+ self.workbook_name = wb if workbook_name is None else workbook_name
26
+ self.sheet_name = sh if sheet_name is None else sheet_name
27
+
28
+ def extract(self) -> typing.Tuple:
29
+ sheet = access_excel_sheet(self.workbook_name, self.sheet_name)
30
+ used_range = sheet.UsedRange
31
+
32
+ values = used_range.Value
33
+ logger.debug(f'Found {len(values)} rows in {sheet}')
34
+
35
+ return used_range.Value
36
+
37
+ def transform(self, com_calibration_table: typing.Tuple) -> pd.DataFrame:
38
+ def replace_building_category(row):
39
+ unknown_heating_systems = set()
40
+ for factor_name in row[2].split(' and '):
41
+ try:
42
+ bc = building_category.from_norsk(row[0])
43
+ except ValueError as value_error:
44
+ if row[0].lower() in ('yrksebygg', 'yrkesbygg'):
45
+ bc = building_category.NON_RESIDENTIAL
46
+ elif row[0].lower() == 'bolig':
47
+ bc = building_category.RESIDENTIAL
48
+ erq = 'energy_requirement' if row[1].lower() == 'energibehov' else 'energy_consumption'
49
+ variabel = factor_name
50
+ extra = None
51
+ if erq == 'energy_requirement':
52
+ variabel = EnergyPurpose(factor_name) if factor_name.lower() != 'elspesifikt' else EnergyPurpose.ELECTRICAL_EQUIPMENT
53
+ else:
54
+ variabel = factor_name
55
+ extra = row[-1]
56
+ if not extra or extra.strip() in ('?', ''):
57
+ extra = HeatingSystems.ELECTRICITY
58
+
59
+ if variabel not in [h for h in HeatingSystems]:
60
+ unknown_heating_systems.add(variabel)
61
+ if extra not in [h for h in HeatingSystems]:
62
+ unknown_heating_systems.add(extra)
63
+ yield bc, erq, variabel, row[3], extra
64
+ if len(unknown_heating_systems) > 0:
65
+ unknowns = ','.join([f'"{hs}"' for hs in unknown_heating_systems])
66
+ msg = f'Unknown heating systems {unknowns}'
67
+ raise ValueError(msg)
68
+
69
+
70
+ def handle_rows(rows):
71
+ for row in rows:
72
+ yield from replace_building_category(row)
73
+
74
+ logger.debug(f'Transform {self.sheet_name}')
75
+ data = com_calibration_table[1:]
76
+
77
+ data = list(handle_rows([r for r in data if r[1].lower().replace('_','').replace(' ','') in ('energibehov',
78
+ 'heatingsystem')]))
79
+
80
+ df = pd.DataFrame(data, columns=['building_category', 'group', 'variable', 'heating_rv_factor', 'extra'])
81
+
82
+ return df
83
+
84
+
85
+ class ExcelComCalibrationResultWriter:
86
+ """
87
+ A class to handle the extraction, transformation, and loading of a pd.Dataframe to a open Excel spreadsheet.
88
+ The Dataframe is expected to have two columns in the index. The first index column is used to match the
89
+ first row of the Excel range. The index second column is used to match the first column in the Excel range. The
90
+ rest of the range is filled out by the last column in the dataframe.
91
+
92
+ Excel range: A1:D4
93
+
94
+ XXXXXX | index1 | index2 | index3
95
+ indexA value1 value4 value7
96
+ indexB value2 value5 value8
97
+ indexC value3 value6 value8
98
+
99
+ Dataframe:
100
+
101
+ clumn1, colmn2, colmn3, colmn4
102
+ index1, indexA, valueA, value1
103
+ index1, indexB, valueB, value2
104
+ index1, indexC, valueC, value3
105
+ index2, indexA, valueD, value4
106
+ index2, indexB, valueE, value5
107
+ index2, indexC, valueF, value6
108
+ index3, indexA, valueG, value7
109
+ index3, indexB, valueH, value8
110
+ index3, indexC, valueI, value9
111
+
112
+
113
+ Attributes
114
+ ----------
115
+ workbook : str
116
+ The name of the workbook.
117
+ sheet : str
118
+ The name of the sheet.
119
+ df : pd.DataFrame
120
+ The DataFrame containing the data.
121
+ cells_to_update : typing.List[SpreadsheetCell]
122
+ List of cells to update.
123
+ rows : typing.List[SpreadsheetCell]
124
+ List of row header cells.
125
+ columns : typing.List[SpreadsheetCell]
126
+ List of column header cells.
127
+
128
+ Methods
129
+ -------
130
+ extract() -> typing.Tuple[typing.List[SpreadsheetCell], typing.List[SpreadsheetCell]]
131
+ Extracts the target cells and initializes row and column headers.
132
+ transform(df) -> typing.Iterable[SpreadsheetCell]
133
+ Transforms the DataFrame into a list of SpreadsheetCell objects to update.
134
+ load()
135
+ Loads the updated values into the Excel sheet.
136
+ """
137
+ workbook: str
138
+ sheet: str
139
+ target_cells: str
140
+ df: pd.DataFrame
141
+ cells_to_update: typing.List[SpreadsheetCell]
142
+ rows: typing.List[SpreadsheetCell]
143
+ columns: typing.List[SpreadsheetCell]
144
+
145
+ def __init__(self,
146
+ excel_filename=None,
147
+ workbook='Kalibreringsark.xlsx',
148
+ sheet='Ut',
149
+ target_cells=None):
150
+ """
151
+ Initializes the HeatingSystemsDistributionWriter with empty lists for cells to update, rows, and columns.
152
+
153
+ Parameters
154
+ ----------
155
+ excel_filename : str, optional
156
+ Name of the target spreadsheet. If there is no ! and sheet name in excel_filename, the parameter sheet is
157
+ used instead
158
+ workbook : str, optinal
159
+ Optional name of the spreadsheet to used for reading and writing. (default is 'Kalibreringsark.xlsx')
160
+ sheet : str, optional
161
+ Optional name of the sheet used for reading and writing. (default is 'Ut')
162
+ target_cells : str, optional
163
+ A range of cells that contain the data to update from the dataframe
164
+
165
+
166
+ """
167
+
168
+ self.workbook, self.sheet = os.environ.get('EBM_CALIBRATION_OUT', f'{workbook}!{sheet}').split('!')
169
+
170
+ self.workbook = workbook
171
+ self.sheet = sheet
172
+ self.target_cells = target_cells
173
+ if not target_cells:
174
+ self.target_cells = target_cells = os.environ.get('EBM_CALIBRATION_ENERGY_HEATING_SYSTEMS_DISTRIBUTION')
175
+
176
+ if excel_filename:
177
+ if '!' in excel_filename:
178
+ self.workbook, self.sheet = excel_filename.split('!')
179
+ else:
180
+ self.workbook = excel_filename
181
+ self.cells_to_update = []
182
+ self.rows = []
183
+ self.columns = []
184
+
185
+ def extract(self) -> typing.Tuple[
186
+ typing.Dict[int, SpreadsheetCell],
187
+ typing.Dict[int, SpreadsheetCell],
188
+ typing.Iterable[SpreadsheetCell]]:
189
+ """
190
+ Extracts the target cells and initializes row and column headers.
191
+
192
+ Returns
193
+ -------
194
+ typing.Tuple[
195
+ typing.Dict[int, SpreadsheetCell],
196
+ typing.Dict[int, SpreadsheetCell],
197
+ typing.Iterable[SpreadsheetCell]]
198
+
199
+ A tuple containing lists of row, column header cells and cells to update.
200
+ """
201
+ # Create an instance of the Excel application
202
+ sheet = access_excel_sheet(self.workbook, self.sheet)
203
+
204
+ # Make index of columns and rows
205
+ first_row = SpreadsheetCell.first_row(self.target_cells)
206
+ self.columns = {cell.column: cell.replace(value=sheet.Cells(cell.row, cell.column).Value) for cell in first_row[1:]}
207
+
208
+ first_column = SpreadsheetCell.first_column(self.target_cells)
209
+ self.rows = {cell.row: cell.replace(value=sheet.Cells(cell.row, cell.column).Value) for cell in first_column[1:]}
210
+
211
+ # Initialize value cells
212
+ self.values = SpreadsheetCell.submatrix(self.target_cells)
213
+
214
+ return self.rows, self.columns, self.values
215
+
216
+ def transform(self, df: pd.DataFrame) -> typing.Iterable[SpreadsheetCell]:
217
+ """
218
+ Transforms the DataFrame into a list of SpreadsheetCell objects to update.
219
+
220
+ Parameters
221
+ ----------
222
+ df : pd.DataFrame
223
+ The DataFrame containing the data.
224
+
225
+ Returns
226
+ -------
227
+ typing.Iterable[SpreadsheetCell]
228
+ An iterable of SpreadsheetCell objects to update.
229
+ """
230
+ self.cells_to_update = []
231
+ for cell in self.values:
232
+ try:
233
+ row_header = self.columns[cell.column].value
234
+ column_header = self.rows[cell.row].value
235
+ if row_header not in df.index:
236
+ raise KeyError(f'"{row_header}" not found')
237
+ elif (row_header, column_header) not in df.index:
238
+ raise KeyError(f'"{column_header}" for "{row_header}" not found')
239
+ column_name = 'heating_system_share' if 'heating_system_share' in df.columns else df.columns[-1]
240
+ value = df.loc[(row_header, column_header), column_name]
241
+ except KeyError as ex:
242
+ logger.warning(f'KeyError {str(ex)} while loading data for cell {cell.spreadsheet_cell()}')
243
+ value = f'KeyError {str(ex)}'
244
+ self.cells_to_update.append(SpreadsheetCell(row=cell.row, column=cell.column, value=value))
245
+
246
+ return self.cells_to_update
247
+
248
+ def load(self):
249
+ """
250
+ Loads the updated values into the Excel sheet defined in obj.workbook and obj.sheet.
251
+ """
252
+ sheet = access_excel_sheet(self.workbook, self.sheet)
253
+
254
+ # Update cells
255
+ for cell_to_update in self.cells_to_update:
256
+ cell = sheet.Cells(cell_to_update.row, cell_to_update.column)
257
+ if isinstance(cell_to_update.value, str) and cell_to_update.value.startswith('KeyError'):
258
+ cell.Value = 0
259
+ cell.Font.Color = KEY_ERROR_COLOR
260
+ else:
261
+ sheet.Cells(cell_to_update.row, cell_to_update.column).Value = cell_to_update.value
262
+ cell.Font.ColorIndex = COLOR_AUTO
@@ -0,0 +1,106 @@
1
+ """
2
+ Convert a pandas.DataFrame object into a rich.Table object for stylized printing in Python.
3
+ From: https://gist.github.com/avi-perl/83e77d069d97edbdde188a4f41a015c4
4
+
5
+ Also available as pypi package rich-tools
6
+ """
7
+ import contextlib
8
+ from datetime import datetime
9
+ from typing import Optional
10
+
11
+ import pandas as pd
12
+ from rich import box
13
+ from rich.console import Console
14
+ from rich.errors import NotRenderableError
15
+ from rich.table import Table
16
+
17
+ from ebm.services.spreadsheet import iter_cells
18
+
19
+ console = Console()
20
+
21
+
22
+ def rich_display_dataframe(df, title="Dataframe") -> None:
23
+ """Display dataframe as table using rich library.
24
+ Args:
25
+ df (pd.DataFrame): dataframe to display
26
+ title (str, optional): title of the table. Defaults to "Dataframe".
27
+ Raises:
28
+ NotRenderableError: if dataframe cannot be rendered
29
+ Returns:
30
+ rich.table.Table: rich table
31
+ """
32
+ from rich import print
33
+ from rich.table import Table
34
+
35
+ # ensure dataframe contains only string values
36
+ df = df.astype(str)
37
+
38
+ table = Table(title=title)
39
+ table.add_column('year')
40
+ for col in df.columns:
41
+ table.add_column(col)
42
+ for row, (year, cell,) in zip(df.values, enumerate(iter_cells(first_column='E', left_padding=' '), 2010), ):
43
+ with contextlib.suppress(NotRenderableError):
44
+ the_row = [f'{year}{cell}', *row]
45
+ table.add_row(*the_row)
46
+ print(table)
47
+
48
+
49
+ def df_to_table(
50
+ pandas_dataframe: pd.DataFrame,
51
+ rich_table: Table,
52
+ show_index: bool = True,
53
+ index_name: Optional[str] = None,
54
+ ) -> Table:
55
+ """Convert a pandas.DataFrame obj into a rich.Table obj.
56
+ Args:
57
+ pandas_dataframe (DataFrame): A Pandas DataFrame to be converted to a rich Table.
58
+ rich_table (Table): A rich Table that should be populated by the DataFrame values.
59
+ show_index (bool): Add a column with a row count to the table. Defaults to True.
60
+ index_name (str, optional): The column name to give to the index column. Defaults to None, showing no value.
61
+ Returns:
62
+ Table: The rich Table instance passed, populated with the DataFrame values."""
63
+
64
+ if show_index:
65
+ index_name = str(index_name) if index_name else ""
66
+ rich_table.add_column(index_name)
67
+
68
+ for column in pandas_dataframe.columns:
69
+ rich_table.add_column(str(column))
70
+
71
+ for index, value_list in enumerate(pandas_dataframe.values.tolist()):
72
+ row = [str(index)] if show_index else []
73
+ row += [str(x) for x in value_list]
74
+ rich_table.add_row(*row)
75
+
76
+ return rich_table
77
+
78
+
79
+ if __name__ == "__main__":
80
+ sample_data = {
81
+ "Date": [
82
+ datetime(year=2019, month=12, day=20),
83
+ datetime(year=2018, month=5, day=25),
84
+ datetime(year=2017, month=12, day=15),
85
+ ],
86
+ "Title": [
87
+ "Star Wars: The Rise of Skywalker",
88
+ "[red]Solo[/red]: A Star Wars Story",
89
+ "Star Wars Ep. VIII: The Last Jedi",
90
+ ],
91
+ "Production Budget": ["$275,000,000", "$275,000,000", "$262,000,000"],
92
+ "Box Office": ["$375,126,118", "$393,151,347", "$1,332,539,889"],
93
+ }
94
+ df = pd.DataFrame(sample_data)
95
+
96
+ # Initiate a Table instance to be modified
97
+ table = Table(show_header=True, header_style="bold magenta")
98
+
99
+ # Modify the table instance to have the data from the DataFrame
100
+ table = df_to_table(df, table)
101
+
102
+ # Update the style of the table
103
+ table.row_styles = ["none", "dim"]
104
+ table.box = box.SIMPLE_HEAD
105
+
106
+ console.print(table)
@@ -0,0 +1,66 @@
1
+ import logging
2
+
3
+ import win32com.client
4
+ from loguru import logger
5
+ from win32com.universal import com_error
6
+
7
+
8
+ def access_excel_sheet(workbook_name: str, sheet_name: str) -> win32com.client.CDispatch:
9
+ """
10
+ Opens the specified sheet in the specified workbook using COM.
11
+
12
+ Parameters
13
+ ----------
14
+ workbook_name : str
15
+ The name of the workbook.
16
+ sheet_name : str
17
+ The name of the sheet.
18
+
19
+ Returns
20
+ -------
21
+ win32com.client.CDispatch
22
+ The specified sheet object.
23
+ """
24
+ logging.debug(f'Opening sheet {sheet_name} in {workbook_name}')
25
+ workbooks = []
26
+ try:
27
+ excel = win32com.client.Dispatch("Excel.Application")
28
+ # Get the currently open workbooks
29
+ workbooks = excel.Workbooks
30
+ except AttributeError as attr_err:
31
+ logger.exception(attr_err)
32
+ msg = f'Got an AttributeError while opening {workbook_name} !{sheet_name}. Is the spreadsheet busy?'
33
+ raise IOError(msg)
34
+ # raise attr_err
35
+
36
+ for workbook in workbooks:
37
+ logger.debug(f"Found open Workbook: {workbook.Name}")
38
+ logger.debug(f'Using {workbook_name} {sheet_name}')
39
+ # Access a specific workbook by name
40
+ try:
41
+ if workbook_name not in [n.Name for n in workbooks]:
42
+ ex_msg = f'No open workbook named: \'{workbook_name}\''
43
+ raise IOError(ex_msg)
44
+ workbook = workbooks[workbook_name]
45
+ except com_error as ex:
46
+ logger.error(f'Error opening {workbook_name}')
47
+ if not workbooks:
48
+ logger.error('No open workbooks')
49
+ else:
50
+ logger.info('Open workbooks: ')
51
+ for wb in workbooks:
52
+ logger.info(f'Found workbook {wb.Name}')
53
+ raise ex
54
+ # Now you can interact with the workbook, for example, read a cell value
55
+ sheet = []
56
+ try:
57
+ if sheet_name not in [n.Name for n in workbook.Sheets]:
58
+ ex_msg = f'{workbook_name} exists and is open, but there is no sheet named: \'{sheet_name}\''
59
+ raise IOError(ex_msg)
60
+ sheet = workbook.Sheets(sheet_name)
61
+ except com_error as ex:
62
+ logger.error(f'Error opening {sheet_name}')
63
+ for s in workbook.Sheets:
64
+ logger.error(f'Found sheet {s.Name}')
65
+ raise ex
66
+ return sheet
ebm/services/files.py ADDED
@@ -0,0 +1,38 @@
1
+ import os
2
+ import pathlib
3
+
4
+ from loguru import logger
5
+
6
+ def make_unique_path(path: pathlib.Path):
7
+ path = pathlib.Path(path)
8
+ counter = 1
9
+ new_path = path
10
+
11
+ while new_path.exists():
12
+ new_path = path.with_stem(f"{path.stem}_{counter}")
13
+ counter += 1
14
+
15
+ return new_path
16
+
17
+
18
+ def file_is_writable(output_file: pathlib.Path) -> bool:
19
+ if not output_file.is_file():
20
+ # If the parent directory is writable we should be good to go
21
+ return os.access(output_file.parent, os.W_OK)
22
+
23
+ access = os.access(output_file, os.W_OK)
24
+ if not access:
25
+ logger.error(f'Permission denied: {output_file}. The file is not writable.')
26
+ return False
27
+
28
+ # It is not enough to check that the file is writable in Windows. We must also check that it is possible to open
29
+ # the file
30
+ try:
31
+ with output_file.open('a'):
32
+ pass
33
+ except PermissionError as ex:
34
+ # Unable to open a file that is reported as writable by the operating system. In that case it is a good chance
35
+ # that the file is already open. Error log our assumption and return False
36
+ logger.error(str(ex) + '. Is the file already open?')
37
+ return False
38
+ return True