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
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
|
+
|
ebm/cmd/prepare_main.py
ADDED
@@ -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
|