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
ebm/cmd/initialize.py ADDED
@@ -0,0 +1,167 @@
1
+ """Module for setting up input, output and managing default data"""
2
+ import argparse
3
+ import os
4
+ import pathlib
5
+ import shutil
6
+ import typing
7
+
8
+ from loguru import logger
9
+
10
+ from ebm.model.file_handler import FileHandler
11
+ from ebm.__version__ import version
12
+ DEFAULT_INPUT = pathlib.Path(f'X:\\NAS\\Data\\ebm\\default-input-{".".join(version.split(".")[:2])}\\')
13
+
14
+
15
+ def create_input(file_handler: FileHandler,
16
+ source_directory: typing.Optional[pathlib.Path]=None) -> bool:
17
+ """
18
+ Create any input file missing in file_handler.input_directory using the default data source.
19
+
20
+ Parameters
21
+ ----------
22
+ source_directory :
23
+ file_handler : FileHandler
24
+
25
+ Returns
26
+ -------
27
+ bool
28
+ """
29
+
30
+ source = file_handler.default_data_directory()
31
+
32
+ if source_directory:
33
+ if not source_directory.is_dir():
34
+ raise NotADirectoryError(f'{source_directory} is not a directory')
35
+
36
+ source_fh = FileHandler(directory=source_directory)
37
+ missing_files = source_fh.check_for_missing_files()
38
+ if len(missing_files) > 0:
39
+ msg = f'File not found {missing_files[0]}'
40
+ raise FileNotFoundError(msg)
41
+ source = source_directory
42
+
43
+ file_handler.create_missing_input_files(source_directory=source)
44
+
45
+ return True
46
+
47
+
48
+ def copy_available_calibration_files(file_handler: FileHandler, source_directory: pathlib.Path):
49
+ """
50
+
51
+ Copies calibration file from source to file_handler
52
+
53
+ Parameters
54
+ ----------
55
+ source_directory : pathlib.Path
56
+ file_handler : FileHandler
57
+
58
+ Returns
59
+ -------
60
+ None
61
+
62
+ """
63
+
64
+ logger.debug(f'Copy calibration files from {source_directory}')
65
+ for calibration_file in [source_directory / FileHandler.CALIBRATE_ENERGY_REQUIREMENT,
66
+ source_directory / FileHandler.CALIBRATE_ENERGY_CONSUMPTION]:
67
+ if calibration_file.is_file():
68
+ logger.debug(f'Creating calibration file {file_handler.input_directory / calibration_file.name}')
69
+ shutil.copy(calibration_file, file_handler.input_directory)
70
+
71
+
72
+ def create_output_directory(output_directory: typing.Optional[pathlib.Path]=None,
73
+ filename: typing.Optional[pathlib.Path]=None) -> pathlib.Path:
74
+ """
75
+ Creates the output directory if it does not exist. If a filename is supplied its parent will be created.
76
+
77
+ Parameters
78
+ ----------
79
+ output_directory : pathlib.Path, optional
80
+ The path to the output directory.
81
+ filename : pathlib.Path, optional
82
+ The name of a file in a directory expected to exist.
83
+ Raises
84
+ -------
85
+ IOError
86
+ The output_directory exists, but it is a file.
87
+ ValueError
88
+ output_directory and filename is empty
89
+ Returns
90
+ -------
91
+ pathlib.Path
92
+ The directory
93
+ """
94
+ if not output_directory and not filename:
95
+ raise ValueError('Both output_directory and filename cannot be None')
96
+ if output_directory and output_directory.is_file():
97
+ raise IOError(f'{output_directory} is a file')
98
+
99
+ if output_directory:
100
+ if output_directory.is_dir():
101
+ return output_directory
102
+ logger.debug(f'Creating output directory {output_directory}')
103
+ output_directory.mkdir(exist_ok=True)
104
+ return output_directory
105
+ elif filename and not filename.is_file():
106
+ logger.debug(f'Creating output directory {filename.parent}')
107
+ filename.parent.mkdir(exist_ok=True)
108
+ return filename.parent
109
+
110
+
111
+ def init(file_handler: FileHandler, source_directory: pathlib.Path|None = None) -> pathlib.Path:
112
+ """
113
+ Initialize file_handler with input data from ebm.data or DEFAULT_INPUT_OVERRIDE.
114
+ Create output directory in current working directory if missing
115
+
116
+ Parameters
117
+ ----------
118
+ file_handler : FileHandler
119
+ source_directory : pathlib.Path, optional
120
+ Where location of input data
121
+
122
+ Returns
123
+ -------
124
+ pathlib.Path
125
+ """
126
+ if source_directory is None:
127
+ default_input_override = pathlib.Path(os.environ.get('EBM_DEFAULT_INPUT', DEFAULT_INPUT))
128
+ if default_input_override.is_dir():
129
+ logger.debug(f'{default_input_override=} exists')
130
+ source_directory = default_input_override
131
+ else:
132
+ logger.info(f'{default_input_override=} does not exist.')
133
+ source_directory = file_handler.default_data_directory()
134
+ elif not source_directory.is_dir():
135
+ raise NotADirectoryError(f'{source_directory} is not a directory')
136
+
137
+ logger.info(f'Copy input from {source_directory}')
138
+ create_input(file_handler, source_directory=source_directory)
139
+ copy_available_calibration_files(file_handler, source_directory=source_directory)
140
+ create_output_directory(pathlib.Path('output'))
141
+ return file_handler.input_directory
142
+
143
+
144
+ def main() -> int:
145
+ """
146
+ Run module using command line arguments. Currently create_input.
147
+
148
+ Returns
149
+ -------
150
+ int
151
+
152
+ See Also
153
+ --------
154
+ create_input
155
+ """
156
+ ap = argparse.ArgumentParser()
157
+ ap.add_argument('input', nargs='?', type=pathlib.Path, default='input')
158
+ ap.add_argument('source',nargs='?', type=pathlib.Path, default=FileHandler.default_data_directory())
159
+
160
+ arguments = ap.parse_args()
161
+
162
+ init(FileHandler(directory=arguments.input), source_directory=arguments.source)
163
+ # create_input(FileHandler(directory=arguments.input), source_directory=arguments.source)
164
+ return 0
165
+
166
+ if __name__ == '__main__':
167
+ main()
ebm/cmd/migrate.py ADDED
@@ -0,0 +1,92 @@
1
+ import argparse
2
+ import os
3
+ import pathlib
4
+ import sys
5
+ import typing
6
+
7
+ from loguru import logger
8
+ from ebm.cmd.helpers import load_environment_from_dotenv, configure_loglevel
9
+ from ebm.migrations import migrate_input_directory, translate_heating_system_efficiencies
10
+ from ebm.model.file_handler import FileHandler
11
+
12
+
13
+ def migrate_directories(directories: typing.Iterable[pathlib.Path|str]) -> None:
14
+ """
15
+ Migrates and validates a list of input directories.
16
+
17
+ Parameters
18
+ ----------
19
+ directories : Iterable[pathlib.Path or str]
20
+ A list of directory paths to be migrated and validated.
21
+
22
+ Returns
23
+ -------
24
+ None
25
+ """
26
+
27
+ for directory in map(pathlib.Path, directories):
28
+ migrate_directory(directory)
29
+
30
+
31
+ def migrate_directory(directory: pathlib.Path):
32
+ """
33
+ Migrates and validates a single input directory.
34
+
35
+ Applies the `translate_heating_system_efficiencies` migration and validates
36
+ the input files using `FileHandler`.
37
+
38
+ Parameters
39
+ ----------
40
+ directory : pathlib.Path
41
+ The path to the directory to be migrated and validated.
42
+
43
+ Returns
44
+ -------
45
+ None
46
+ """
47
+
48
+ logger.debug(f'Open {directory} {type(directory)}')
49
+ migrate_input_directory(directory=directory, migration=translate_heating_system_efficiencies)
50
+ FileHandler(directory=directory).validate_input_files()
51
+
52
+
53
+ def main() -> None:
54
+ """
55
+ Main entry point for the migration script.
56
+
57
+ Loads environment variables, configures logging, creates missing input files,
58
+ and performs migration and validation on a set of predefined directories.
59
+
60
+ Returns
61
+ -------
62
+ None
63
+ """
64
+
65
+ load_environment_from_dotenv()
66
+ configure_loglevel(log_format=os.environ.get('LOG_FORMAT', None))
67
+
68
+ logger.debug(f'Starting {sys.executable} {__file__}')
69
+
70
+ parser = argparse.ArgumentParser(description="Migrate and validate EBM input directories.")
71
+ parser.add_argument("directories",
72
+ nargs="*",
73
+ help="List of input directories to migrate. If omitted, uses EBM_INPUT_DIRECTORY environment variable.")
74
+
75
+ arguments = parser.parse_args()
76
+
77
+ directories = arguments.directories or os.environ.get('EBM_INPUT_DIRECTORY', '').split(os.pathsep)
78
+ if not directories or directories == ['']:
79
+ logger.error('No input directories provided via CLI or EBM_INPUT_DIRECTORY.')
80
+ sys.exit(1)
81
+
82
+ #new_fh = FileHandler(directory='t3192_input')
83
+ #new_fh.create_missing_input_files()
84
+
85
+ #migration_directories = ['../Energibruksmodell/tests/ebm/data/kalibrert', 't3192_input', 't2734_input',
86
+ # '../Energibruksmodell/ebm/data']
87
+
88
+ migrate_directories(directories)
89
+
90
+
91
+ if __name__ == '__main__':
92
+ main()
ebm/cmd/pipeline.py ADDED
@@ -0,0 +1,227 @@
1
+ import os
2
+ import pathlib
3
+
4
+ import pandas as pd
5
+
6
+ from loguru import logger
7
+
8
+ from ebm import extractors
9
+ from ebm.cmd.helpers import load_environment_from_dotenv, configure_loglevel, configure_json_log
10
+ from ebm.cmd.result_handler import transform_model_to_horizontal, transform_to_sorted_heating_systems
11
+ from ebm.model import area as a_f, bema
12
+ from ebm.model.data_classes import YearRange
13
+ from ebm.model.database_manager import DatabaseManager
14
+ from ebm.model import energy_need as e_n
15
+ from ebm.model import energy_purpose as e_p
16
+ from ebm.model import energy_use as e_u
17
+ from ebm.model.file_handler import FileHandler
18
+ from ebm.model import heating_systems_parameter as h_s_param
19
+ from ebm.model import heat_pump as h_p
20
+ from ebm.model.heating_systems_share import transform_heating_systems_share_long, transform_heating_systems_share_wide
21
+ from ebm.s_curve import calculate_s_curves
22
+ from ebm.services.spreadsheet import make_pretty, add_top_row_filter
23
+
24
+
25
+ def main():
26
+ load_environment_from_dotenv()
27
+
28
+ configure_loglevel(os.environ.get('LOG_FORMAT', None))
29
+ configure_json_log()
30
+ input_path, output_path, years = load_config()
31
+
32
+ output_path.mkdir(exist_ok=True)
33
+
34
+ file_handler = FileHandler(directory=input_path)
35
+ database_manager = DatabaseManager(file_handler=file_handler)
36
+ list(export_energy_model_reports(years, database_manager, output_path))
37
+
38
+
39
+ def export_energy_model_reports(years: YearRange, database_manager: DatabaseManager, output_path: pathlib.Path):
40
+ logger.info('Area to area.xlsx')
41
+ logger.debug('Extract area')
42
+
43
+ scurve_parameters = database_manager.get_scurve_params() # 📍
44
+
45
+ area_parameters = database_manager.get_area_parameters() # 📍
46
+ area_parameters['year'] = years.start
47
+
48
+ building_code_parameters = database_manager.file_handler.get_building_code() # 📍
49
+
50
+ s_curves_by_condition = calculate_s_curves(scurve_parameters, building_code_parameters, years) # 📌
51
+ area_forecast = extractors.extract_area_forecast(years, s_curves_by_condition, building_code_parameters, area_parameters, database_manager) # 📍
52
+ energy_need_kwh_m2 = extractors.extract_energy_need(years, database_manager) # 📍
53
+ heating_systems_projection = extractors.extract_heating_systems_forecast(years, database_manager) # 📍
54
+ energy_use_holiday_homes = extractors.extract_energy_use_holiday_homes(database_manager) # 📍
55
+
56
+ total_energy_need = e_n.transform_total_energy_need(energy_need_kwh_m2, area_forecast) # 📌
57
+ heating_systems_parameter = h_s_param.heating_systems_parameter_from_projection(heating_systems_projection) # 📌
58
+ energy_use_kwh = e_u.building_group_energy_use_kwh(heating_systems_parameter, total_energy_need) # 📌
59
+
60
+ existing_area = a_f.filter_existing_area(area_forecast)
61
+
62
+ logger.debug('Transform fane 1 (wide)')
63
+ merged_building_code_and_condition = a_f.merge_building_code_and_condition(existing_area)
64
+ area_wide = transform_model_to_horizontal(merged_building_code_and_condition)
65
+ area_wide = area_wide.drop(columns=['building_code', 'building_condition']) #🏙️
66
+
67
+ logger.debug('Transform fane 2 (long')
68
+
69
+ area_by_year_category_building_code = existing_area.groupby(by='year,building_category,building_code'.split(','))[['m2']].sum()
70
+ area_by_year_category_building_code = area_by_year_category_building_code.rename(columns={'m2': 'area'})
71
+ area_by_year_category_building_code.insert(0, 'U', 'm2')
72
+ area_long = area_by_year_category_building_code.reset_index().sort_values(
73
+ by=['building_category', 'building_code', 'year'], key=bema.map_sort_order) #🏙️
74
+
75
+ logger.debug('Write file area.xlsx')
76
+
77
+ area_output = output_path / 'area.xlsx'
78
+
79
+ with pd.ExcelWriter(area_output, engine='xlsxwriter') as writer:
80
+ # Write wide first order matters
81
+ area_wide.to_excel(writer, sheet_name='wide', index=False) # 🏙️️💾
82
+ area_long.to_excel(writer, sheet_name='long', index=False) # 🏙️💾
83
+ logger.debug(f'Adding top row filter to {area_output}')
84
+ make_pretty(area_output)
85
+ add_top_row_filter(workbook_file=area_output, sheet_names=['long'])
86
+ yield area_output
87
+
88
+ logger.success(f'Wrote {area_output}')
89
+
90
+ logger.info('Energy use to energy_purpose')
91
+
92
+ logger.info('Heating_system_share')
93
+
94
+ logger.debug('Transform fane 2')
95
+ heating_systems_share = transform_heating_systems_share_long(heating_systems_projection)
96
+
97
+ logger.debug('Transform fane 1')
98
+ heating_systems_share_wide = transform_heating_systems_share_wide(heating_systems_share) # ♨️
99
+ heating_systems_share_long = heating_systems_share.rename(columns={ # ♨️
100
+ 'heating_system_share': 'Share',
101
+ 'heating_systems': 'Heating system'})
102
+
103
+ heating_systems_share_wide = heating_systems_share_wide.rename(columns={'heating_systems':'Heating technology'})
104
+
105
+ logger.debug('Write file heating_system_share.xlsx')
106
+ heating_system_share_file = output_path / 'heating_system_share.xlsx'
107
+ with pd.ExcelWriter(heating_system_share_file, engine='xlsxwriter') as writer:
108
+ # Write wide first order matters
109
+ heating_systems_share_wide.to_excel(writer, sheet_name='wide', merge_cells=False, index=False) # ♨️💾
110
+ heating_systems_share_long.to_excel(writer, sheet_name='long', merge_cells=False) # ♨️💾
111
+ make_pretty(heating_system_share_file)
112
+ logger.debug(f'Adding top row filter to {heating_system_share_file}')
113
+ add_top_row_filter(workbook_file=heating_system_share_file, sheet_names=['long'])
114
+ logger.success(f'Wrote {heating_system_share_file.name}')
115
+ yield heating_system_share_file
116
+
117
+ logger.info('heat_prod_hp')
118
+ logger.debug('Transform heating_system_parameters')
119
+
120
+ logger.debug('Transform to hp')
121
+ expanded_heating_systems_parameter = h_s_param.expand_heating_system_parameters(heating_systems_parameter)
122
+ air_air = h_p.air_source_heat_pump(expanded_heating_systems_parameter)
123
+ district_heating = h_p.district_heating_heat_pump(expanded_heating_systems_parameter)
124
+
125
+ production = h_p.heat_pump_production(total_energy_need, air_air, district_heating)
126
+ heat_prod_hp_wide = h_p.heat_prod_hp_wide(production) # 🧮
127
+
128
+ logger.debug('Write file heat_prod_hp.xlsx')
129
+ heat_prod_hp_file = output_path / 'heat_prod_hp.xlsx'
130
+
131
+ with pd.ExcelWriter(heat_prod_hp_file, engine='xlsxwriter') as writer:
132
+ heat_prod_hp_wide.to_excel(writer, sheet_name='wide', index=False) # 🧮💾
133
+ make_pretty(heat_prod_hp_file)
134
+ logger.success(f'Wrote {heat_prod_hp_file.name}')
135
+ yield heat_prod_hp_file
136
+
137
+ logger.info('Energy_use')
138
+
139
+ logger.debug('Transform energy_use_kwh')
140
+
141
+ logger.debug('Transform fane 1')
142
+ energy_use_gwh_by_building_group = e_u.energy_use_gwh_by_building_group(energy_use_kwh)
143
+
144
+ logger.debug('Transform fane 2')
145
+ logger.debug('Group by category, year, product')
146
+
147
+ column_order = ['year', 'building_category', 'building_code', 'energy_product', 'kwh']
148
+ energy_use_long = energy_use_kwh[column_order].groupby(
149
+ by=['building_category', 'building_code', 'energy_product', 'year']).sum() / 1_000_000
150
+ energy_use_long = energy_use_long.reset_index()[column_order].rename(columns={'kwh': 'energy_use'})
151
+ energy_use_long = energy_use_long.sort_values(
152
+ by=['building_category', 'building_code', 'year'], key=bema.map_sort_order) #🔌
153
+
154
+ logger.debug('Transform fane 1')
155
+ logger.debug('Group by group, product year')
156
+ energy_use_wide = transform_to_sorted_heating_systems(energy_use_gwh_by_building_group, energy_use_holiday_homes, #🔌
157
+ building_column='building_group')
158
+ logger.debug('Write file energy_use')
159
+ energy_use_file = output_path / 'energy_use.xlsx'
160
+ with pd.ExcelWriter(energy_use_file, engine='xlsxwriter') as writer:
161
+ # Write wide first order matters
162
+ energy_use_wide.to_excel(writer, sheet_name='wide', index=False) #🔌💾
163
+ energy_use_long.to_excel(writer, sheet_name='long', index=False) #🔌💾
164
+ make_pretty(energy_use_file)
165
+ logger.debug(f'Adding top row filter to {energy_use_file}')
166
+ add_top_row_filter(workbook_file=energy_use_file, sheet_names=['long'])
167
+ logger.success(f'Wrote {energy_use_file.name}')
168
+ yield energy_use_file
169
+
170
+ logger.debug('Transform fane 1')
171
+ energy_purpose_wide = e_p.group_energy_use_kwh_by_building_group_purpose_year_wide(energy_use_kwh=energy_use_kwh) # 🚿
172
+
173
+ logger.debug('Transform fane 2')
174
+ energy_purpose_long = e_p.group_energy_use_by_year_category_building_code_purpose(energy_use_kwh=energy_use_kwh) # 🚿
175
+
176
+ logger.debug('Write file energy_purpose.xlsx')
177
+ energy_purpose_output = output_path / 'energy_purpose.xlsx'
178
+ with pd.ExcelWriter(energy_purpose_output, engine='xlsxwriter') as writer:
179
+ # Write wide first order matters
180
+ energy_purpose_wide.to_excel(writer, sheet_name='wide', index=False) # 🚿 💾
181
+ energy_purpose_long.to_excel(writer, sheet_name='long', index=False) # 🚿💾
182
+ make_pretty(energy_purpose_output)
183
+ logger.debug(f'Adding top row filter to {energy_purpose_output}')
184
+ add_top_row_filter(workbook_file=energy_purpose_output, sheet_names=['long'])
185
+ logger.success(f'Wrote {energy_purpose_output.name}')
186
+ yield energy_purpose_output
187
+
188
+ area_change = a_f.transform_area_forecast_to_area_change(area_forecast=area_forecast, building_code_parameters=building_code_parameters)
189
+
190
+ logger.info('demolition_construction')
191
+ logger.debug('Transform demolition_construction')
192
+ demolition_construction_long = a_f.transform_demolition_construction(energy_use_kwh, area_change)
193
+ demolition_construction_long = demolition_construction_long.rename(columns={'m2': 'Area [m2]',
194
+ 'gwh': 'Energy use [GWh]'})
195
+ demolition_construction_long = demolition_construction_long.sort_values(
196
+ by=['building_category', 'building_code', 'year', 'demolition_construction'], key=bema.map_sort_order) # 🏗️
197
+
198
+ logger.debug('Write file demolition_construction.xlsx')
199
+ demolition_construction_file = output_path / 'demolition_construction.xlsx'
200
+ with pd.ExcelWriter(demolition_construction_file, engine='xlsxwriter') as writer:
201
+ demolition_construction_long.to_excel(writer, sheet_name='long', index=False) # 🏗️💾
202
+ make_pretty(demolition_construction_file)
203
+ logger.debug(f'Adding top row filter to {demolition_construction_file}')
204
+ add_top_row_filter(workbook_file=demolition_construction_file, sheet_names=['long'])
205
+ logger.success(f'Wrote {demolition_construction_file.name}')
206
+
207
+ yield demolition_construction_file
208
+
209
+
210
+ def load_config():
211
+ try:
212
+ start_year = int(os.environ.get('EBM_START_YEAR', 2020))
213
+ except ValueError:
214
+ start_year = 2020
215
+ try:
216
+ end_year = int(os.environ.get('EBM_END_YEAR', 2050))
217
+ except ValueError:
218
+ end_year = 2050
219
+ years = YearRange(start_year, end_year)
220
+ input_path = pathlib.Path(os.environ.get('EBM_INPUT_DIRECTORY', 'input'))
221
+ output_path = pathlib.Path(os.environ.get('EBM_OUTPUT_DIRECTORY', 'output'))
222
+ return input_path, output_path, years
223
+
224
+
225
+ if __name__ == '__main__':
226
+ main()
227
+
@@ -0,0 +1,174 @@
1
+ """Utility functions for __main__"""
2
+ import argparse
3
+ import os
4
+ import pathlib
5
+ import sys
6
+ import textwrap
7
+ import typing
8
+ from dataclasses import dataclass
9
+ from typing import List
10
+
11
+ from loguru import logger
12
+
13
+ from ebm.__version__ import version
14
+
15
+ from ebm.model.building_category import BuildingCategory
16
+ from ebm.model.building_condition import BuildingCondition
17
+ from ebm.model.data_classes import YearRange
18
+ from ebm.model.enums import ReturnCode
19
+ from ebm.services.files import file_is_writable
20
+
21
+ TEK = """PRE_TEK49
22
+ PRE_TEK49
23
+ TEK49
24
+ TEK69
25
+ TEK87
26
+ TEK97
27
+ TEK07
28
+ TEK10
29
+ TEK17
30
+ TEK21""".strip().split('\n')
31
+
32
+
33
+ @dataclass
34
+ class EbmConfig:
35
+ """Prototype for an argument or configuation class to be used by main. Its purpose is improved readability."""
36
+ default_input: str = None
37
+ calibration_year: int = 2023
38
+ years: YearRange = YearRange(2020, 2050)
39
+ output_directory: pathlib.Path = pathlib.Path('output')
40
+ input_directory: pathlib.Path = pathlib.Path('input')
41
+ force_overwrite: bool = False
42
+ open_after_writing: bool = False
43
+ csv_delimiter: str = ','
44
+ always_open: bool = False
45
+ write_to_disk: bool = False
46
+
47
+ #building_categories: typing.List[BuildingCategory]
48
+ #building_conditions = typing.List[BuildingCondition]
49
+ #building_code_filter: typing.List[str]
50
+
51
+
52
+ def make_arguments(program_name, default_path: pathlib.Path) -> argparse.Namespace:
53
+ """
54
+ Create and parse command-line arguments for the area forecast calculation.
55
+
56
+ Parameters
57
+ ----------
58
+ program_name : str
59
+ Name of this program
60
+ default_path : pathlib.Path
61
+ Default path for the output file.
62
+
63
+ Returns
64
+ -------
65
+ argparse.Namespace
66
+ Parsed command-line arguments.
67
+
68
+ Notes
69
+ -----
70
+ The function sets up an argument parser with various options including version, debug mode,
71
+ filename, force write, open file after writing, CSV delimiter, building categories,
72
+ creating default input, start year, and end year.
73
+ """
74
+
75
+ default_building_categories: List[str] = [str(b) for b in iter(BuildingCategory)]
76
+
77
+ arg_parser = argparse.ArgumentParser(prog=program_name,
78
+ description=f'Calculate EBM energy use {version}',
79
+ formatter_class=argparse.RawTextHelpFormatter
80
+ )
81
+ arg_parser.add_argument('--version', '-v', action='version', version=f'{program_name} {version}')
82
+ arg_parser.add_argument('--debug', action='store_true',
83
+ help='Run in debug mode. (Extra information written to stdout)')
84
+ arg_parser.add_argument('step', type=str, nargs='?',
85
+ choices=['area-forecast',
86
+ 'energy-requirements',
87
+ 'heating-systems',
88
+ 'energy-use'],
89
+ default='energy-use',
90
+ help="""
91
+ The calculation step you want to run. The steps are sequential. Any prerequisite to the chosen step will run
92
+ automatically.""")
93
+ arg_parser.add_argument('output_file', nargs='?', type=pathlib.Path, default=default_path,
94
+ help=textwrap.dedent(
95
+ f'''The location of the file you want to be written. default: {default_path}
96
+ If the file already exists the program will terminate without overwriting.
97
+ Use "-" to output to the console instead'''))
98
+ arg_parser.add_argument('--categories', '--building-categories', '-c',
99
+ nargs='*', type=str, default=default_building_categories,
100
+ help=textwrap.dedent(f"""
101
+ One or more of the following building categories:
102
+ {", ".join(default_building_categories)}.
103
+ The default is to use all categories."""
104
+ ))
105
+ arg_parser.add_argument('--input', '--input-directory', '-i',
106
+ nargs='?',
107
+ type=pathlib.Path,
108
+ default=pathlib.Path(os.environ.get('EBM_INPUT_DIRECTORY', 'input')),
109
+ help='path to the directory with input files')
110
+ arg_parser.add_argument('--force', '-f', action='store_true',
111
+ help='Write to <filename> even if it already exists')
112
+ arg_parser.add_argument('--open', '-o', action='store_true',
113
+ help='Open output file(s) automatically when finished writing. (Usually Excel)')
114
+ arg_parser.add_argument('--csv-delimiter', '--delimiter', '-e', type=str, default=',',
115
+ help='A single character to be used for separating columns when writing csv. ' +
116
+ 'Default: "," Special characters like ; should be quoted ";"')
117
+ arg_parser.add_argument('--create-input', action='store_true',
118
+ help='''
119
+ Create input directory containing all required files in the current working directory''')
120
+
121
+ arg_parser.add_argument('--migrate', action='store_true', help=argparse.SUPPRESS)
122
+
123
+ arg_parser.add_argument('--start-year', nargs='?', type=int,
124
+ default=os.environ.get('EBM_START_YEAR', 2020),
125
+ help=argparse.SUPPRESS)
126
+ arg_parser.add_argument('--end-year', nargs='?', type=int,
127
+ default=os.environ.get('EBM_END_YEAR', 2050),
128
+ help=argparse.SUPPRESS)
129
+
130
+ arg_parser.add_argument('--horizontal-years', '--horizontal', '--horisontal', action='store_true',
131
+ help='Show years horizontal (left to right)')
132
+
133
+ arguments = arg_parser.parse_args()
134
+ return arguments
135
+
136
+
137
+ def check_output_file_status(output_file: pathlib.Path,
138
+ force: bool=False,
139
+ default_path: pathlib.Path = None,
140
+ program_name: str='ebm') -> ReturnCode:
141
+ """
142
+ Checks if the output file exists and that it is writable. If force is true the output_file will be opened for
143
+ appending. A permission denied at that point is an indication that the file is already open by another process.
144
+
145
+ The following values may be returned
146
+ - 0 RETURN_CODE_OK
147
+ - 1 RETURN_CODE_FILE_NOT_ACCESSIBLE
148
+ - 2 RETURN_CODE_FILE_EXISTS
149
+
150
+ Parameters
151
+ ----------
152
+ output_file : pathlib.Path
153
+ force : bool
154
+ default_path : pathlib.Path
155
+ program_name : str
156
+
157
+ Returns
158
+ -------
159
+ ReturnCode
160
+ """
161
+ default_path = default_path if default_path else pathlib.Path('output/ebm_output.xlsx')
162
+
163
+ if output_file.is_file() and output_file != default_path and not force:
164
+ # If the file already exists and is not the default, display error and exit unless --force was used.
165
+ logger.error(f'{output_file}. already exists.')
166
+ print(f"""
167
+ You can overwrite the {output_file}. by using --force: {program_name} {' '.join(sys.argv[1:])} --force
168
+ """.strip(),
169
+ file=sys.stderr)
170
+ return ReturnCode.FILE_EXISTS
171
+ if output_file.name != '-' and not file_is_writable(output_file):
172
+ # logger.error(f'{output_file} is not writable')
173
+ return ReturnCode.FILE_NOT_ACCESSIBLE
174
+ return ReturnCode.OK