diff --git a/cea/optimization_new/building.py b/cea/optimization_new/building.py index 67e643425..6c3ec7ed1 100644 --- a/cea/optimization_new/building.py +++ b/cea/optimization_new/building.py @@ -35,6 +35,7 @@ def __init__(self, identifier, demands_file_path): self._demand_flow = EnergyFlow() self._footprint = None self._location = None + self._initial_connectivity_state = 'stand_alone' self._stand_alone_supply_system_code = None self._stand_alone_supply_system_composition = {'primary': [], 'secondary': [], 'tertiary': []} @@ -77,6 +78,18 @@ def location(self, new_location): raise ValueError("Please only assign a point to the building location. " "Try using the load_building_location method for assigning building locations.") + @property + def initial_connectivity_state(self): + return self._initial_connectivity_state + + @initial_connectivity_state.setter + def initial_connectivity_state(self, new_base_connectivity): + if new_base_connectivity in ['stand_alone', 'network'] \ + or (new_base_connectivity.startswith('N') and new_base_connectivity[1:].isdigit()): + self._initial_connectivity_state = new_base_connectivity + else: + raise ValueError("Please only assign 'stand_alone' or 'network_i' to the base connectivity of the building.") + def load_demand_profile(self, energy_system_type='DH'): """ Load the buildings relevant demand profile, i.e. 'QC_sys_kWh' for the district heating optimisation & @@ -142,15 +155,65 @@ def load_base_supply_system(self, file_locator, energy_system_type='DH'): raise ValueError(f"'{energy_system_type}' is not a valid energy system type. No appropriate " f"'assemblies'-supply system database could therefore be loaded.") - # fetch the system composition (primary, secondary & tertiary components) for the building + # fetch the supply system composition for the building or it's associated district energy system + self.fetch_supply_system_composition() + + return + + def fetch_supply_system_composition(self): + """ + Identify if the building is connected to a district heating or cooling network or has a stand-alone system. + Depending on the case, fetch the system composition (primary, secondary & tertiary components) for the building + or complete the system composition for the district energy system the building is connected to. + + To establish a default stand-alone supply system in the event of a building's disconnection from a district + energy system, it's assumed that the supply system composition of the respective networks is directly applied to + the building's stand-alone supply system. + """ + # register if the building is connected to a district heating or cooling network or has a stand-alone system system_details = Building._supply_system_database[Building._supply_system_database['code'] == self._stand_alone_supply_system_code] + energy_system_scale = system_details['scale'].values[0].replace(" ", "").lower() + + if energy_system_scale in ['', '-', 'building']: + self.initial_connectivity_state = 'stand_alone' + elif energy_system_scale == 'district': + self.initial_connectivity_state = 'network' + + # fetch the system composition (primary, secondary & tertiary components) + # ... for the stand-alone supply system for category in ['primary', 'secondary', 'tertiary']: category_components = system_details[category + '_components'].values[0].replace(" ", "") + if category_components == '-': self._stand_alone_supply_system_composition[category] = [] else: self._stand_alone_supply_system_composition[category] = category_components.split(',') + + # ... or for the network supply system + if self.initial_connectivity_state == 'network': + + if system_details['code'].values[0] not in SupplySystemStructure.initial_network_supply_systems.keys(): + network_id = f'N{len(SupplySystemStructure.initial_network_supply_systems) + 1001}' + SupplySystemStructure.initial_network_supply_systems[system_details['code'].values[0]] = network_id + SupplySystemStructure.initial_network_supply_systems_composition[network_id] = {'primary': [], + 'secondary': [], + 'tertiary': []} + + for category in ['primary', 'secondary', 'tertiary']: + category_components = system_details[category + '_components'].values[0].replace(" ", "") + + if category_components == '-': + SupplySystemStructure.initial_network_supply_systems_composition[network_id][category] = [] + else: + SupplySystemStructure.initial_network_supply_systems_composition[network_id][category] = \ + category_components.split(',') + + else: + network_id = SupplySystemStructure.initial_network_supply_systems[system_details['code'].values[0]] + + self.initial_connectivity_state = network_id + return def calculate_supply_system(self, available_potentials): diff --git a/cea/optimization_new/containerclasses/supplySystemStructure.py b/cea/optimization_new/containerclasses/supplySystemStructure.py index fad93a2b4..a18fe8fbc 100644 --- a/cea/optimization_new/containerclasses/supplySystemStructure.py +++ b/cea/optimization_new/containerclasses/supplySystemStructure.py @@ -43,6 +43,8 @@ class SupplySystemStructure(object): _releasable_grid_based_energy_carriers = [] _active_component_classes = [] _full_component_activation_order = () + initial_network_supply_systems = dict() + initial_network_supply_systems_composition = {'N1001': {'primary': [], 'secondary': [], 'tertiary': []}} def __init__(self, max_supply_flow=EnergyFlow(), available_potentials=None, user_component_selection=None): self.maximum_supply = max_supply_flow diff --git a/cea/optimization_new/districtEnergySystem.py b/cea/optimization_new/districtEnergySystem.py index d036e367d..6ed6dc98b 100644 --- a/cea/optimization_new/districtEnergySystem.py +++ b/cea/optimization_new/districtEnergySystem.py @@ -143,7 +143,7 @@ def evaluate_energy_system(connectivity_vector, district_buildings, energy_poten return non_dominated_systems, process_memory, optimization_tracker - def evaluate(self, optimization_tracker=None, return_full_des=False): + def evaluate(self, optimization_tracker=None, return_full_des=False, component_selection=None): """ Evaluate the possible district energy system configurations (based on buildings, potentials and connectivity) by: @@ -169,7 +169,12 @@ def evaluate(self, optimization_tracker=None, return_full_des=False): self.aggregate_demand() self.distribute_potentials() - # optimise supply systems for each network + # apply user-designated supply systems for each network ... + if component_selection: + self.apply_designated_supply_systems(component_selection) + return + + # ... or find the optimal supply systems for each network self.generate_optimal_supply_systems() # aggregate objective functions for all subsystems across the entire district energy system @@ -314,6 +319,30 @@ def distribute_potentials(self): return self.distributed_potentials + def apply_designated_supply_systems(self, designated_supply_system_components): + """ + Apply designated supply systems to the district energy system. + + :param dict designated_supply_system_components: component selection for the supply systems for each of the + networks in the district energy system. + """ + for network in self.networks: + # use the SupplySystemStructure methods to dimension each of the system's designated components + system_structure = SupplySystemStructure(max_supply_flow=self.subsystem_demands[network.identifier], + available_potentials=self.distributed_potentials[network.identifier], + user_component_selection= + designated_supply_system_components[network.identifier]) + system_structure.build() + + # create a SupplySystem-instance and operate the system to meet the yearly demand profile. + designated_supply_system = SupplySystem(system_structure, + system_structure.capacity_indicators, + self.subsystem_demands[network.identifier]) + designated_supply_system.evaluate() + self.supply_systems[network.identifier] = designated_supply_system + + return self.supply_systems + def generate_optimal_supply_systems(self): """ Build and calculate operation for supply systems of each of the subsystems of the district energy system: diff --git a/cea/optimization_new/domain.py b/cea/optimization_new/domain.py index 86b89779e..953676ea3 100644 --- a/cea/optimization_new/domain.py +++ b/cea/optimization_new/domain.py @@ -57,6 +57,7 @@ def __init__(self, config, locator): self.weather = self._load_weather(locator) self.buildings = [] self.energy_potentials = [] + self.initial_energy_system = None self.optimal_energy_systems = [] self._initialise_domain_descriptor_classes() @@ -139,11 +140,12 @@ def optimize_domain(self): print("\nInitializing domain:") self._initialize_energy_system_descriptor_classes() - # Calculate base-case supply systems for all buildings - print("\nCalculating operation of buildings' base-supply systems...") + # Calculate base-case supply systems for all buildings (i.e. initial state as per the input editor) + print("\nCalculating operation of districts' initial supply systems...") building_energy_potentials = Building.distribute_building_potentials(self.energy_potentials, self.buildings) [building.calculate_supply_system(building_energy_potentials[building.identifier]) for building in self.buildings] + self.model_initial_energy_system() # Optimise district energy systems print("Starting optimisation of district energy systems (i.e. networks + supply systems)...") @@ -226,6 +228,25 @@ def optimize_domain(self): return self.optimal_energy_systems + def model_initial_energy_system(self): + """ + Model the energy system currently installed in the domain, i.e.: + - reconstruct a network linking all buildings that are currently connected to a district energy system + - determine the supply system for each network and each of the stand-alone buildings + """ + # Determine buildings connected to a district energy system + connection_list = [Connection(0, building.identifier) if building.initial_connectivity_state == 'stand_alone' + else Connection(int(building.initial_connectivity_state[1:]) - 1000, building.identifier) + for building in self.buildings] + + # Create a network of connected buildings + self.initial_energy_system = DistrictEnergySystem(ConnectivityVector(connection_list), self.buildings, + self.energy_potentials) + self.initial_energy_system.evaluate(component_selection= + SupplySystemStructure.initial_network_supply_systems_composition) + + return self.initial_energy_system + def _select_final_optimal_systems(self, last_population, min_final_selection_size): """ Each solution in the last population of the 'outer' optimisation algorithm consists of: @@ -268,15 +289,14 @@ def generate_result_files(self): of .xlsx-files to summarise the most important information about the near-pareto-optimal district energy systems and their corresponding supply systems. """ + # save the current energy system's network layouts and supply systems + self._write_network_layouts_to_geojson(self.initial_energy_system, 'current_DES') + self._write_supply_systems_to_csv(self.initial_energy_system, 'current_DES') + # save the results for near-pareto-optimal district energy systems one-by-one for des in self.optimal_energy_systems: # first save network layouts - for ntw_ind, network in enumerate(des.networks): - network_layout_file = self.locator.get_new_optimization_optimal_network_layout_file(des.identifier, - network.identifier) - network_layout = pd.concat([network.network_nodes, network.network_edges]).drop(['coordinates'], axis=1) - network_layout = network_layout.to_crs(get_geographic_coordinate_system()) - network_layout.to_file(network_layout_file, driver='GeoJSON') + self._write_network_layouts_to_geojson(des) # then save all information about the selected supply systems self._write_supply_systems_to_csv(des) @@ -287,13 +307,41 @@ def generate_result_files(self): return - def _write_supply_systems_to_csv(self, district_energy_system): + def _write_network_layouts_to_geojson(self, district_energy_system, system_name=None): + """ + Writes the network layout of a given district energy system into a geojson file. + + :param network: selected network + :type network: Network-class object + :param system_name: name of the district energy system + :type system_name: str + """ + if not system_name: + system_name = district_energy_system.identifier + + for ntw_ind, network in enumerate(district_energy_system.networks): + network_layout_file = self.locator.get_new_optimization_optimal_network_layout_file(system_name, + network.identifier) + network_layout = pd.concat([network.network_nodes, network.network_edges]).drop(['coordinates'], axis=1) + network_layout = network_layout.to_crs(get_geographic_coordinate_system()) + network_layout.to_file(network_layout_file, driver='GeoJSON') + + return + + def _write_supply_systems_to_csv(self, district_energy_system, system_name=None): """ - Writes information on supply systems of subsystems of a near-pareto-optimal district energy system into - csv files. Information on each of the supply systems is written in a separate file. + Writes information on supply systems of subsystems of a given district energy system into csv files. Information + on each of the supply systems is written in a separate file. + + :param district_energy_system: selected district energy system + :type district_energy_system: DistrictEnergySystem-class object + :param system_name: name of the district energy system + :type system_name: str """ # Create general values - des_id = district_energy_system.identifier + if not system_name: + system_name = district_energy_system.identifier + supply_system_summary = {'Supply_System': [], 'Heat_Emissions_kWh': [], 'System_Energy_Demand_kWh': [], @@ -308,7 +356,7 @@ def _write_supply_systems_to_csv(self, district_energy_system): supply_system = building.stand_alone_supply_system # Summarise structure of the supply system & print to file - building_file = self.locator.get_new_optimization_optimal_supply_system_file(des_id, supply_system_id) + building_file = self.locator.get_new_optimization_optimal_supply_system_file(system_name, supply_system_id) Domain._write_system_structure(building_file, supply_system) # Calculate supply system fitness-values and add them to the summary of all supply systems @@ -318,7 +366,7 @@ def _write_supply_systems_to_csv(self, district_energy_system): # FOR NETWORKS for network_id, supply_system in district_energy_system.supply_systems.items(): # Summarise structure of the supply system & print to file - network_file = self.locator.get_new_optimization_optimal_supply_system_file(des_id, network_id) + network_file = self.locator.get_new_optimization_optimal_supply_system_file(system_name, network_id) Domain._write_system_structure(network_file, supply_system) # Calculate supply system fitness-values and add them to the summary of all supply systems @@ -332,7 +380,7 @@ def _write_supply_systems_to_csv(self, district_energy_system): supply_system_summary['GHG_Emissions_kgCO2'] += [sum(supply_system_summary['GHG_Emissions_kgCO2'])] supply_system_summary['Cost_USD'] += [sum(supply_system_summary['Cost_USD'])] - summary_file = self.locator.get_new_optimization_optimal_supply_systems_summary_file(des_id) + summary_file = self.locator.get_new_optimization_optimal_supply_systems_summary_file(system_name) pd.DataFrame(supply_system_summary).to_csv(summary_file, index=False) return