diff --git a/CHANGELOG.md b/CHANGELOG.md index 0caec890..d2b47c7d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,9 @@ and this project tries to adhere to [Semantic Versioning](https://semver.org/spe - Adapt municipality label font size according to zoom level - update mapengine to v1.4.1 +### Changed +- pre results can be shown before simulation has finished + ### Fixed - duplicate loading of JS modules due to missing module support in django staticfile storage - settlement 200m layer is coupled to settlement layer (de)-activation diff --git a/Makefile b/Makefile index e7bb51f0..540dca2b 100644 --- a/Makefile +++ b/Makefile @@ -41,6 +41,9 @@ check_distill_coordinates: local_env_file: python merge_local_dotenvs_in_dotenv.py +celery: + redis-server --port 6379 & celery -A config.celery worker -l INFO + update_vendor_assets: # Note: call this command from the same folder your Makefile is located # Note: this run only update minor versions. diff --git a/digiplan/map/calculations.py b/digiplan/map/calculations.py index e461c393..29cc7208 100644 --- a/digiplan/map/calculations.py +++ b/digiplan/map/calculations.py @@ -3,9 +3,7 @@ from typing import Optional import pandas as pd -from django.conf import settings from django.utils.translation import gettext_lazy as _ -from django_oemof.models import Simulation from django_oemof.results import get_results from oemof.tabular.postprocessing import calculations, core, helper @@ -112,32 +110,34 @@ def capacities_per_municipality() -> pd.DataFrame: return datapackage.get_capacities_from_datapackage() -def capacities_per_municipality_2045(simulation_id: int) -> pd.DataFrame: - """Calculate capacities from 2045 scenario per municipality.""" - results = get_results( - simulation_id, - { - "capacities": Capacities, - }, +def capacities_per_municipality_2045(parameters: dict) -> pd.DataFrame: + """Calculate capacities from 2045 scenario per municipality in MW.""" + shares = calculate_potential_shares(parameters) + potential_capacities = datapackage.get_potential_values() # in MW + + # Use wind profile for selected wind year + potential_capacities = potential_capacities.drop( + columns=[ + column for column in potential_capacities if column.startswith("wind") and column != parameters["wind_year"] + ], ) - renewables = results["capacities"][ - results["capacities"].index.get_level_values(0).isin(config.SIMULATION_RENEWABLES) - ] - mapping = { - "ABW-solar-pv_ground": "pv_ground", - "ABW-solar-pv_rooftop": "pv_roof", - "ABW-wind-onshore": "wind", - "ABW-hydro-ror": "hydro", - "ABW-biomass": "biomass", - } - renewables.index = renewables.index.droplevel(1).map(mapping) - renewables = renewables.reindex(["wind", "pv_roof", "pv_ground", "hydro"]) + potential_capacities = potential_capacities.rename(columns={parameters["wind_year"]: "wind"}) + + # Apply shares from user selection + potential_capacities = potential_capacities * shares + + # Aggregate pv ground profiles + pv_ground_columns = ["pv_soil_quality_low", "pv_soil_quality_medium", "pv_permanent_crops"] + potential_capacities["pv_ground"] = potential_capacities[pv_ground_columns].sum(axis=1) + potential_capacities = potential_capacities.drop(pv_ground_columns, axis=1) + + # Set biomass potential to zero + potential_capacities["bioenergy"] = 0 + + # Correct order (for charts) + potential_capacities = potential_capacities[["wind", "pv_roof", "pv_ground", "hydro", "bioenergy"]] - parameters = Simulation.objects.get(pk=simulation_id).parameters - renewables = renewables * calculate_potential_shares(parameters) - renewables["bioenergy"] = 0.0 - renewables["st"] = 0.0 - return renewables.astype(float) + return potential_capacities def energies_per_municipality() -> pd.DataFrame: @@ -155,32 +155,13 @@ def energies_per_municipality() -> pd.DataFrame: return capacities * full_load_hours.values / 1e3 -def energies_per_municipality_2045(simulation_id: int) -> pd.DataFrame: - """Calculate energies from 2045 scenario per municipality.""" - results = get_results( - simulation_id, - { - "electricity_production": electricity_production, - }, - ) - renewables = results["electricity_production"][ - results["electricity_production"].index.get_level_values(0).isin(config.SIMULATION_RENEWABLES) - ] - mapping = { - "ABW-solar-pv_ground": "pv_ground", - "ABW-solar-pv_rooftop": "pv_roof", - "ABW-wind-onshore": "wind", - "ABW-hydro-ror": "hydro", - "ABW-biomass": "biomass", - } - renewables.index = renewables.index.droplevel([1, 2]).map(mapping) - renewables = renewables.reindex(["wind", "pv_roof", "pv_ground", "hydro"]) - - parameters = Simulation.objects.get(pk=simulation_id).parameters - renewables = renewables * calculate_potential_shares(parameters) - renewables["bioenergy"] = 0.0 - renewables["st"] = 0.0 - return renewables.astype(float) +def energies_per_municipality_2045(parameters: dict) -> pd.DataFrame: + """Calculate energies from 2045 scenario per municipality in MWh.""" + capacities = capacities_per_municipality_2045(parameters) # in MW + full_load_hours = datapackage.get_full_load_hours(year=2045).drop("st").rename({"ror": "hydro"}) + full_load_hours = full_load_hours.reindex(index=["wind", "pv_roof", "pv_ground", "hydro", "bioenergy"]) + energies = capacities * full_load_hours.values + return energies.fillna(0.0) def energy_shares_per_municipality() -> pd.DataFrame: @@ -238,7 +219,7 @@ def electricity_demand_per_municipality(year: int = 2022) -> pd.DataFrame: return demands_per_sector.astype(float) * 1e-3 -def energy_shares_2045_per_municipality(simulation_id: int) -> pd.DataFrame: +def energy_shares_2045_per_municipality(parameters: dict) -> pd.DataFrame: """ Calculate energy shares of renewables from electric demand per municipality in 2045. @@ -247,13 +228,13 @@ def energy_shares_2045_per_municipality(simulation_id: int) -> pd.DataFrame: pd.DataFrame Energy share per municipality (index) and technology (column) """ - energies = energies_per_municipality_2045(simulation_id).mul(1e-3) - demands = electricity_demand_per_municipality_2045(simulation_id).sum(axis=1) + energies = energies_per_municipality_2045(parameters).mul(1e-3) + demands = electricity_demand_per_municipality_2045(parameters).sum(axis=1) energy_shares = energies.div(demands, axis=0) return energy_shares.astype(float).mul(1e2) -def energy_shares_2045_region(simulation_id: int) -> pd.DataFrame: +def energy_shares_2045_region(parameters: dict) -> pd.DataFrame: """ Calculate energy shares of renewables from electric demand for region in 2045. @@ -265,15 +246,15 @@ def energy_shares_2045_region(simulation_id: int) -> pd.DataFrame: pd.DataFrame Energy share per municipality (index) and technology (column) """ - energies = energies_per_municipality_2045(simulation_id) - demands = electricity_demand_per_municipality_2045(simulation_id).sum(axis=1).mul(1e3) + energies = energies_per_municipality_2045(parameters) + demands = electricity_demand_per_municipality_2045(parameters).sum(axis=1).mul(1e3) demand_share = demands / demands.sum() energy_shares = energies.div(demands, axis=0).mul(demand_share, axis=0).sum(axis=0) return energy_shares.astype(float).mul(1e2) -def electricity_demand_per_municipality_2045(simulation_id: int) -> pd.DataFrame: +def electricity_demand_per_municipality_2045(user_settings: dict) -> pd.DataFrame: """ Calculate electricity demand per sector per municipality in GWh in 2045. @@ -282,33 +263,12 @@ def electricity_demand_per_municipality_2045(simulation_id: int) -> pd.DataFrame pd.DataFrame Electricity demand per municipality (index) and sector (column) """ - results = get_results( - simulation_id, - { - "electricity_demand": electricity_demand, - }, - ) - demand = results["electricity_demand"][ - results["electricity_demand"].index.get_level_values(1).isin(config.SIMULATION_DEMANDS) - ] - demand = demand.droplevel([0, 2]) - demands_per_sector = datapackage.get_power_demand() - mappings = { - "hh": "ABW-electricity-demand_hh", - "cts": "ABW-electricity-demand_cts", - "ind": "ABW-electricity-demand_ind", - } - demand = demand.reindex(mappings.values()) - sector_shares = pd.DataFrame( - {sector: demands_per_sector[sector]["2022"] / demands_per_sector[sector]["2022"].sum() for sector in mappings}, - ) - demand = sector_shares * demand.values - demand.columns = demand.columns.map(lambda column: config.SIMULATION_DEMANDS[mappings[column]]) - demand = demand * 1e-3 - return demand.astype(float) + demand = electricity_demand_per_municipality(year=2022) + shares = [int(user_settings[key]) / 100 for key in ("s_v_3", "s_v_4", "s_v_5")] + return demand.iloc[:] * shares -def heat_demand_per_municipality() -> pd.DataFrame: +def heat_demand_per_municipality(year: int) -> pd.DataFrame: """ Calculate heat demand per sector per municipality in GWh. @@ -319,7 +279,7 @@ def heat_demand_per_municipality() -> pd.DataFrame: """ demands_raw = datapackage.get_summed_heat_demand_per_municipality() demands_per_sector = pd.concat( - [distributions["cen"]["2022"] + distributions["dec"]["2022"] for distributions in demands_raw.values()], + [distributions["cen"][str(year)] + distributions["dec"][str(year)] for distributions in demands_raw.values()], axis=1, ) demands_per_sector.columns = [ @@ -330,7 +290,7 @@ def heat_demand_per_municipality() -> pd.DataFrame: return demands_per_sector.astype(float) * 1e-3 -def heat_demand_per_municipality_2045(simulation_id: int) -> pd.DataFrame: +def heat_demand_per_municipality_2045(user_settings: dict) -> pd.DataFrame: """ Calculate heat demand per sector per municipality in GWh in 2045. @@ -339,29 +299,9 @@ def heat_demand_per_municipality_2045(simulation_id: int) -> pd.DataFrame: pd.DataFrame Heat demand per municipality (index) and sector (column) """ - results = get_results( - simulation_id, - { - "heat_demand": heat_demand, - }, - ) - demand = results["heat_demand"] - demand.index = demand.index.map(lambda ind: f"heat-demand-{ind[1].split('_')[2]}") - demand = demand.groupby(level=0).sum() - demands_per_sector = datapackage.get_heat_demand() - mappings = { - "hh": "heat-demand-hh", - "cts": "heat-demand-cts", - "ind": "heat-demand-ind", - } - demand = demand.reindex(mappings.values()) - sector_shares = pd.DataFrame( - {sector: demands_per_sector[sector]["2022"] / demands_per_sector[sector]["2022"].sum() for sector in mappings}, - ) - demand = sector_shares * demand.values - demand.columns = demand.columns.map(lambda column: config.SIMULATION_DEMANDS[mappings[column]]) - demand = demand * 1e-3 - return demand.astype(float) + demand = heat_demand_per_municipality(year=2022) + shares = [int(user_settings[key]) / 100 for key in ("w_v_3", "w_v_4", "w_v_5")] + return demand.iloc[:] * shares def ghg_reduction(simulation_id: int) -> pd.Series: @@ -456,9 +396,9 @@ def electricity_from_from_biomass(simulation_id: int) -> pd.Series: return biomass.sum() -def wind_turbines_per_municipality_2045(simulation_id: int) -> pd.DataFrame: +def wind_turbines_per_municipality_2045(parameters: dict) -> pd.DataFrame: """Calculate number of wind turbines from 2045 scenario per municipality.""" - capacities = capacities_per_municipality_2045(simulation_id) + capacities = capacities_per_municipality_2045(parameters) return capacities["wind"] / config.TECHNOLOGY_DATA["nominal_power_per_unit"]["wind"] @@ -512,61 +452,29 @@ def electricity_heat_demand(simulation_id: int) -> pd.Series: return electricity_for_heat_sum -def calculate_potential_shares(parameters: dict) -> pd.DataFrame: +def calculate_potential_shares(parameters: dict) -> dict[str, float]: """Calculate potential shares depending on user settings.""" - # DISAGGREGATION - # Wind - wind_areas = pd.read_csv( - settings.DIGIPIPE_DIR.path("scalars").path("potentialarea_wind_area_stats_muns.csv"), - index_col=0, - ) - if parameters["s_w_3"]: - wind_area_per_mun = wind_areas["stp_2018_vreg"] - elif parameters["s_w_4_1"]: - wind_area_per_mun = wind_areas["stp_2027_vr"] - elif parameters["s_w_4_2"]: - wind_area_per_mun = wind_areas["stp_2027_repowering"] - elif parameters["s_w_5"]: - wind_area_per_mun = ( - wind_areas["stp_2027_search_area_open_area"] * parameters["s_w_5_1"] / 100 - + wind_areas["stp_2027_search_area_forest_area"] * parameters["s_w_5_2"] / 100 + shares = {} + if "wind_year" in parameters: + wind_year = parameters["wind_year"] + share = 1 + if wind_year == "wind_2024": + share = float(parameters["s_w_6"]) / float(config.ENERGY_SETTINGS_PANEL["s_w_6"]["max"]) + if wind_year == "wind_2027": + share = float(parameters["s_w_7"]) / 100 + shares["wind"] = share + if "s_pv_ff_3" in parameters: + shares.update( + { + "pv_soil_quality_low": int(parameters["s_pv_ff_3"]) / 100, + "pv_soil_quality_medium": int(parameters["s_pv_ff_4"]) / 100, + "pv_permanent_crops": int(parameters["s_pv_ff_5"]) / 100, + }, ) - else: - msg = "No wind switch set" - raise KeyError(msg) - wind_share_per_mun = wind_area_per_mun / wind_area_per_mun.sum() - - # PV ground - pv_ground_areas = pd.read_csv( - settings.DIGIPIPE_DIR.path("scalars").path("potentialarea_pv_ground_area_stats_muns.csv", index_col=0), - ) - pv_ground_area_per_mun = ( - pv_ground_areas["agriculture_lfa-off_region"] * parameters["s_pv_ff_3"] / 100 - + pv_ground_areas["road_railway_region"] * parameters["s_pv_ff_4"] / 100 - ) - pv_ground_share_per_mun = pv_ground_area_per_mun / pv_ground_area_per_mun.sum() - - # PV roof - pv_roof_areas = pd.read_csv( - settings.DIGIPIPE_DIR.path("scalars").path("potentialarea_pv_roof_area_stats_muns.csv"), - index_col=0, - ) - pv_roof_area_per_mun = pv_roof_areas["installable_power_total"] - pv_roof_share_per_mun = pv_roof_area_per_mun / pv_roof_area_per_mun.sum() - - # Hydro - hydro_areas = pd.read_csv( - settings.DIGIPIPE_DIR.path("scalars").path("bnetza_mastr_hydro_stats_muns.csv"), - index_col=0, - ) - hydro_area_per_mun = hydro_areas["capacity_net"] - hydro_share_per_mun = hydro_area_per_mun / hydro_area_per_mun.sum() - - shares = pd.concat( - [wind_share_per_mun, pv_roof_share_per_mun, pv_ground_share_per_mun, hydro_share_per_mun], - axis=1, - ) - shares.columns = ["wind", "pv_roof", "pv_ground", "hydro"] + if "s_pv_d_3" in parameters: + shares["pv_roof"] = int(parameters["s_pv_d_3"]) / 100 + if "s_h_1" in parameters: + shares["hydro"] = int(parameters["s_h_1"]) / 100 return shares diff --git a/digiplan/map/charts.py b/digiplan/map/charts.py index 9b8d964b..bf140ffd 100644 --- a/digiplan/map/charts.py +++ b/digiplan/map/charts.py @@ -2,7 +2,7 @@ import json import pathlib -from typing import Any, Optional +from typing import Any, Optional, Union import pandas as pd from django.utils.translation import gettext_lazy as _ @@ -101,19 +101,35 @@ def get_chart_data(self) -> None: return +class PreResultsChart(Chart): + """For charts based on user settings.""" + + def __init__(self, user_settings: dict) -> None: + """ + Init Chart. + + Parameters + ---------- + user_settings: dict + User settings coming from map + """ + self.user_settings = user_settings + super().__init__() + + class SimulationChart(Chart): """For charts based on simulations.""" - def __init__(self, simulation_id: int) -> None: + def __init__(self, user_settings: dict) -> None: """ - Init Detailed Overview Chart. + Init Chart. Parameters ---------- - simulation_id: any - id of used Simulation + user_settings: dict + User settings coming from map """ - self.simulation_id = simulation_id + self.simulation_id = user_settings["simulation_id"] super().__init__() @@ -373,7 +389,7 @@ def get_chart_options(self) -> dict: return chart_options -class Capacity2045RegionChart(SimulationChart): +class Capacity2045RegionChart(PreResultsChart): """Chart for regional capacities in 2045.""" lookup = "capacity" @@ -381,7 +397,7 @@ class Capacity2045RegionChart(SimulationChart): def get_chart_data(self) -> list: """Calculate capacities for whole region.""" status_quo_data = calculations.capacities_per_municipality().sum().round(1) - future_data = calculations.capacities_per_municipality_2045(self.simulation_id).sum().astype(float).round(1) + future_data = calculations.capacities_per_municipality_2045(self.user_settings).sum().astype(float).round(1) return list(zip(status_quo_data, future_data)) def get_chart_options(self) -> dict: @@ -415,7 +431,7 @@ def get_chart_options(self) -> dict: return chart_options -class CapacitySquare2045RegionChart(SimulationChart): +class CapacitySquare2045RegionChart(PreResultsChart): """Chart for regional capacities in 2045.""" lookup = "capacity" @@ -431,7 +447,7 @@ def get_chart_data(self) -> list: ) future_data = ( calculations.calculate_square_for_value( - pd.DataFrame(calculations.capacities_per_municipality_2045(self.simulation_id).sum()).transpose(), + pd.DataFrame(calculations.capacities_per_municipality_2045(self.user_settings).sum()).transpose(), ) .sum() .astype(float) @@ -465,7 +481,7 @@ def get_chart_options(self) -> dict: return chart_options -class Energy2045RegionChart(SimulationChart): +class Energy2045RegionChart(PreResultsChart): """Chart for regional energy.""" lookup = "capacity" @@ -473,7 +489,7 @@ class Energy2045RegionChart(SimulationChart): def get_chart_data(self) -> None: """Calculate capacities for whole region.""" status_quo_data = calculations.energies_per_municipality().sum().round(1) - future_data = calculations.energies_per_municipality_2045(self.simulation_id).sum().astype(float) * 1e-3 + future_data = calculations.energies_per_municipality_2045(self.user_settings).sum().astype(float) * 1e-3 future_data = future_data.round(1) return list(zip(status_quo_data, future_data)) @@ -503,7 +519,7 @@ def get_chart_options(self) -> dict: return chart_options -class EnergyShare2045RegionChart(SimulationChart): +class EnergyShare2045RegionChart(PreResultsChart): """Chart for regional energy shares.""" lookup = "capacity" @@ -511,7 +527,7 @@ class EnergyShare2045RegionChart(SimulationChart): def get_chart_data(self) -> None: """Calculate RES energy shares for whole region.""" status_quo_data = calculations.energy_shares_region().round(1) - future_data = calculations.energy_shares_2045_region(self.simulation_id).round(1) + future_data = calculations.energy_shares_2045_region(self.user_settings).round(1) return list(zip(status_quo_data, future_data)) def get_chart_options(self) -> dict: @@ -545,7 +561,7 @@ def get_chart_options(self) -> dict: return chart_options -class EnergyCapita2045RegionChart(SimulationChart): +class EnergyCapita2045RegionChart(PreResultsChart): """Chart for regional energy.""" lookup = "capacity" @@ -561,7 +577,7 @@ def get_chart_data(self) -> None: future_data = ( ( calculations.calculate_capita_for_value( - pd.DataFrame(calculations.energies_per_municipality_2045(self.simulation_id).sum()).transpose(), + pd.DataFrame(calculations.energies_per_municipality_2045(self.user_settings).sum()).transpose(), ).sum() ) .astype(float) @@ -601,7 +617,7 @@ def get_chart_options(self) -> dict: return chart_options -class EnergySquare2045RegionChart(SimulationChart): +class EnergySquare2045RegionChart(PreResultsChart): """Chart for regional energy shares per square meter.""" lookup = "capacity" @@ -617,7 +633,7 @@ def get_chart_data(self) -> None: future_data = ( ( calculations.calculate_square_for_value( - pd.DataFrame(calculations.energies_per_municipality_2045(self.simulation_id).sum()).transpose(), + pd.DataFrame(calculations.energies_per_municipality_2045(self.user_settings).sum()).transpose(), ).sum() ) .astype(float) @@ -651,7 +667,7 @@ def get_chart_options(self) -> dict: return chart_options -class WindTurbines2045RegionChart(SimulationChart): +class WindTurbines2045RegionChart(PreResultsChart): """Chart for regional wind turbines in 2045.""" lookup = "wind_turbines" @@ -659,7 +675,7 @@ class WindTurbines2045RegionChart(SimulationChart): def get_chart_data(self) -> list[int]: """Calculate population for whole region.""" status_quo_data = models.WindTurbine.quantity_per_municipality().sum() - future_data = calculations.wind_turbines_per_municipality_2045(self.simulation_id).sum() + future_data = calculations.wind_turbines_per_municipality_2045(self.user_settings).sum() return [int(status_quo_data), int(future_data)] def get_chart_options(self) -> dict: @@ -695,7 +711,7 @@ def get_chart_options(self) -> dict: return chart_options -class WindTurbinesSquare2045RegionChart(SimulationChart): +class WindTurbinesSquare2045RegionChart(PreResultsChart): """Chart for regional wind turbines per square meter in 2045.""" lookup = "wind_turbines" @@ -712,7 +728,7 @@ def get_chart_data(self) -> list[float]: future_data = ( calculations.calculate_square_for_value( pd.DataFrame( - {"turbines": calculations.wind_turbines_per_municipality_2045(self.simulation_id).sum()}, + {"turbines": calculations.wind_turbines_per_municipality_2045(self.user_settings).sum()}, index=[1], ), ) @@ -747,7 +763,7 @@ def get_chart_options(self) -> dict: return chart_options -class ElectricityDemand2045RegionChart(SimulationChart): +class ElectricityDemand2045RegionChart(PreResultsChart): """Chart for regional electricity demand.""" lookup = "electricity_demand" @@ -756,7 +772,7 @@ def get_chart_data(self) -> None: """Calculate capacities for whole region.""" status_quo_data = calculations.electricity_demand_per_municipality().sum().round(1) future_data = ( - calculations.electricity_demand_per_municipality_2045(self.simulation_id).sum().astype(float).round(1) + calculations.electricity_demand_per_municipality_2045(self.user_settings).sum().astype(float).round(1) ) return list(zip(status_quo_data, future_data)) @@ -791,7 +807,7 @@ def get_chart_options(self) -> dict: return chart_options -class ElectricityDemandCapita2045RegionChart(SimulationChart): +class ElectricityDemandCapita2045RegionChart(PreResultsChart): """Chart for regional electricity demand per population in 2045.""" lookup = "electricity_demand" @@ -808,7 +824,7 @@ def get_chart_data(self) -> pd.DataFrame: ( calculations.calculate_capita_for_value( pd.DataFrame( - calculations.electricity_demand_per_municipality_2045(self.simulation_id).sum(), + calculations.electricity_demand_per_municipality_2045(self.user_settings).sum(), ).transpose(), ).sum() * 1e6 @@ -834,7 +850,7 @@ class HeatDemandRegionChart(Chart): def get_chart_data(self) -> None: """Calculate capacities for whole region.""" - return calculations.heat_demand_per_municipality().sum().round(1) + return calculations.heat_demand_per_municipality(year=2022).sum().round(1) def get_chart_options(self) -> dict: """Overwrite title and unit.""" @@ -844,15 +860,15 @@ def get_chart_options(self) -> dict: return chart_options -class HeatDemand2045RegionChart(SimulationChart): +class HeatDemand2045RegionChart(PreResultsChart): """Chart for regional heat demand in 2045.""" lookup = "heat_demand" def get_chart_data(self) -> None: """Calculate capacities for whole region.""" - status_quo_data = calculations.heat_demand_per_municipality().sum().round(1) - future_data = calculations.heat_demand_per_municipality_2045(self.simulation_id).sum().astype(float).round(1) + status_quo_data = calculations.heat_demand_per_municipality(year=2022).sum().round(1) + future_data = calculations.heat_demand_per_municipality_2045(self.user_settings).sum().astype(float).round(1) return list(zip(status_quo_data, future_data)) def get_chart_options(self) -> dict: @@ -873,7 +889,7 @@ def get_chart_data(self) -> None: """Calculate capacities for whole region.""" return ( calculations.calculate_capita_for_value( - pd.DataFrame(calculations.heat_demand_per_municipality().sum()).transpose(), + pd.DataFrame(calculations.heat_demand_per_municipality(year=2022).sum()).transpose(), ).sum() * 1e6 ).round(1) @@ -886,7 +902,7 @@ def get_chart_options(self) -> dict: return chart_options -class HeatDemandCapita2045RegionChart(SimulationChart): +class HeatDemandCapita2045RegionChart(PreResultsChart): """Chart for regional heat demand per population in 2045.""" lookup = "heat_demand" @@ -895,7 +911,7 @@ def get_chart_data(self) -> pd.DataFrame: """Calculate capacities for whole region.""" status_quo_data = ( calculations.calculate_capita_for_value( - pd.DataFrame(calculations.heat_demand_per_municipality().sum()).transpose(), + pd.DataFrame(calculations.heat_demand_per_municipality(year=2022).sum()).transpose(), ).sum() * 1e6 ).round(1) @@ -903,7 +919,7 @@ def get_chart_data(self) -> pd.DataFrame: ( calculations.calculate_capita_for_value( pd.DataFrame( - calculations.heat_demand_per_municipality_2045(self.simulation_id).sum(), + calculations.heat_demand_per_municipality_2045(self.user_settings).sum(), ).transpose(), ).sum() * 1e6 @@ -958,7 +974,7 @@ def get_chart_options(self) -> dict: return chart_options -CHARTS: dict[str, type[Chart]] = { +CHARTS: dict[str, Union[type[PreResultsChart], type[SimulationChart]]] = { "detailed_overview": DetailedOverviewChart, "ghg_reduction": GHGReductionChart, "electricity_overview": ElectricityOverviewChart, @@ -997,6 +1013,13 @@ def get_chart_options(self) -> dict: "batteries_capacity_statusquo_region": BatteriesCapacityRegionChart, } +PRE_RESULTS = ( + "electricity_demand_2045_region", + "electricity_demand_capita_2045_region", + "heat_demand_2045_region", + "heat_demand_capita_2045_region", +) + def create_chart(lookup: str, chart_data: Optional[Any] = None) -> dict: """ diff --git a/digiplan/map/choropleths.py b/digiplan/map/choropleths.py index aaa1932d..1e002b4d 100644 --- a/digiplan/map/choropleths.py +++ b/digiplan/map/choropleths.py @@ -84,7 +84,7 @@ def get_values_per_feature(self) -> dict[int, float]: # noqa: D102 class EnergyShare2045Choropleth(Choropleth): # noqa: D101 def get_values_per_feature(self) -> dict[int, float]: # noqa: D102 - return calculations.energy_shares_2045_per_municipality(self.map_state["simulation_id"]).sum(axis=1).to_dict() + return calculations.energy_shares_2045_per_municipality(self.map_state).sum(axis=1).to_dict() class EnergyChoropleth(Choropleth): # noqa: D101 @@ -94,7 +94,7 @@ def get_values_per_feature(self) -> dict[int, float]: # noqa: D102 class Energy2045Choropleth(Choropleth): # noqa: D101 def get_values_per_feature(self) -> dict[int, float]: # noqa: D102 - energies = calculations.energies_per_municipality_2045(self.map_state["simulation_id"]).sum(axis=1) * 1e-3 + energies = calculations.energies_per_municipality_2045(self.map_state).sum(axis=1) * 1e-3 return energies.to_dict() @@ -107,7 +107,7 @@ def get_values_per_feature(self) -> dict[int, float]: # noqa: D102 class EnergyCapita2045Choropleth(Choropleth): # noqa: D101 def get_values_per_feature(self) -> dict[int, float]: # noqa: D102 - energies = calculations.energies_per_municipality_2045(self.map_state["simulation_id"]) + energies = calculations.energies_per_municipality_2045(self.map_state) energies_per_capita = calculations.calculate_capita_for_value(energies) return energies_per_capita.sum(axis=1).to_dict() @@ -121,7 +121,7 @@ def get_values_per_feature(self) -> dict[int, float]: # noqa: D102 class EnergySquare2045Choropleth(Choropleth): # noqa: D101 def get_values_per_feature(self) -> dict[int, float]: # noqa: D102 - energies = calculations.energies_per_municipality_2045(self.map_state["simulation_id"]) + energies = calculations.energies_per_municipality_2045(self.map_state) energies_per_square = calculations.calculate_square_for_value(energies) return energies_per_square.sum(axis=1).to_dict() @@ -134,13 +134,13 @@ def get_values_per_feature(self) -> pd.DataFrame: # noqa: D102 class Capacity2045Choropleth(Choropleth): # noqa: D101 def get_values_per_feature(self) -> pd.DataFrame: # noqa: D102 - capacities = calculations.capacities_per_municipality_2045(self.map_state["simulation_id"]).sum(axis=1) + capacities = calculations.capacities_per_municipality_2045(self.map_state).sum(axis=1) return capacities.to_dict() class CapacitySquare2045Choropleth(Choropleth): # noqa: D101 def get_values_per_feature(self) -> dict[int, float]: # noqa: D102 - capacities = calculations.capacities_per_municipality_2045(self.map_state["simulation_id"]) + capacities = calculations.capacities_per_municipality_2045(self.map_state) capacities_per_square = calculations.calculate_square_for_value(capacities) return capacities_per_square.sum(axis=1).to_dict() @@ -181,7 +181,7 @@ def get_values_per_feature(self) -> dict[int, float]: # noqa: D102 class WindTurbines2045Choropleth(Choropleth): # noqa: D101 def get_values_per_feature(self) -> dict[int, float]: # noqa: D102 - return calculations.wind_turbines_per_municipality_2045(simulation_id=self.map_state["simulation_id"]).to_dict() + return calculations.wind_turbines_per_municipality_2045(self.map_state).to_dict() class WindTurbinesSquareChoropleth(Choropleth): # noqa: D101 @@ -193,7 +193,7 @@ def get_values_per_feature(self) -> dict[int, float]: # noqa: D102 class WindTurbinesSquare2045Choropleth(Choropleth): # noqa: D101 def get_values_per_feature(self) -> dict[int, float]: # noqa: D102 - wind_turbines = calculations.wind_turbines_per_municipality_2045(self.map_state["simulation_id"]) + wind_turbines = calculations.wind_turbines_per_municipality_2045(self.map_state) wind_turbines_square = calculations.calculate_square_for_value(wind_turbines) return wind_turbines_square.to_dict() @@ -205,9 +205,7 @@ def get_values_per_feature(self) -> dict[int, float]: # noqa: D102 class ElectricityDemand2045Choropleth(Choropleth): # noqa: D101 def get_values_per_feature(self) -> dict[int, float]: # noqa: D102 - return ( - calculations.electricity_demand_per_municipality_2045(self.map_state["simulation_id"]).sum(axis=1).to_dict() - ) + return calculations.electricity_demand_per_municipality_2045(self.map_state).sum(axis=1).to_dict() class ElectricityDemandCapitaChoropleth(Choropleth): # noqa: D101 @@ -223,7 +221,7 @@ class ElectricityDemandCapita2045Choropleth(Choropleth): # noqa: D101 def get_values_per_feature(self) -> dict[int, float]: # noqa: D102 capita_demand = ( calculations.calculate_capita_for_value( - calculations.electricity_demand_per_municipality_2045(self.map_state["simulation_id"]), + calculations.electricity_demand_per_municipality_2045(self.map_state), ).sum(axis=1) * 1e6 ) @@ -232,18 +230,19 @@ def get_values_per_feature(self) -> dict[int, float]: # noqa: D102 class HeatDemandChoropleth(Choropleth): # noqa: D101 def get_values_per_feature(self) -> dict[int, float]: # noqa: D102 - return calculations.heat_demand_per_municipality().sum(axis=1).to_dict() + return calculations.heat_demand_per_municipality(year=2022).sum(axis=1).to_dict() class HeatDemand2045Choropleth(Choropleth): # noqa: D101 def get_values_per_feature(self) -> dict[int, float]: # noqa: D102 - return calculations.heat_demand_per_municipality_2045(self.map_state["simulation_id"]).sum(axis=1).to_dict() + return calculations.heat_demand_per_municipality_2045(self.map_state).sum(axis=1).to_dict() class HeatDemandCapitaChoropleth(Choropleth): # noqa: D101 def get_values_per_feature(self) -> dict[int, float]: # noqa: D102 capita_demand = ( - calculations.calculate_capita_for_value(calculations.heat_demand_per_municipality().sum(axis=1)) * 1e6 + calculations.calculate_capita_for_value(calculations.heat_demand_per_municipality(year=2022).sum(axis=1)) + * 1e6 ) return capita_demand.to_dict() @@ -252,7 +251,7 @@ class HeatDemandCapita2045Choropleth(Choropleth): # noqa: D101 def get_values_per_feature(self) -> dict[int, float]: # noqa: D102 capita_demand = ( calculations.calculate_capita_for_value( - calculations.heat_demand_per_municipality_2045(self.map_state["simulation_id"]), + calculations.heat_demand_per_municipality_2045(self.map_state), ).sum(axis=1) * 1e6 ) diff --git a/digiplan/map/config.py b/digiplan/map/config.py index 28cc2c20..bfe24081 100644 --- a/digiplan/map/config.py +++ b/digiplan/map/config.py @@ -120,7 +120,7 @@ def get_slider_per_sector() -> dict: STORE_COLD_INIT = { "version": __version__, "slider_marks": get_slider_marks(), - "potentials": datapackage.get_potential_values(), + "potentials": datapackage.get_potential_values().sum().to_dict(), "slider_per_sector": get_slider_per_sector(), "allowedSwitches": ["wind_distance"], "detailTab": {"showPotentialLayers": True}, diff --git a/digiplan/map/datapackage.py b/digiplan/map/datapackage.py index 690d5d6e..9ece6852 100644 --- a/digiplan/map/datapackage.py +++ b/digiplan/map/datapackage.py @@ -29,7 +29,7 @@ def get_data_from_sources(sources: Union[Source, list[Source]]) -> pd.DataFrame: for source_file, columns in source_files.items(): source_path = Path(DIGIPIPE_DIR, "scalars", source_file) dfs.append(pd.read_csv(source_path, usecols=columns)) - return pd.concat(dfs) + return pd.concat(dfs, axis=1) def get_employment() -> pd.DataFrame: @@ -164,7 +164,7 @@ def get_thermal_efficiency(component: str) -> float: @cache_memoize(timeout=None) -def get_potential_values() -> dict: +def get_potential_values() -> pd.DataFrame: """ Calculate max_values for sliders. @@ -180,20 +180,19 @@ def get_potential_values() -> dict: "pv_permanent_crops": "pv_ground_elevated", "pv_roof": "pv_roof", } - power_density = json.load(Path.open(Path(settings.DIGIPIPE_DIR, "scalars/technology_data.json")))["power_density"] - - potentials = {} - for key in areas: - if key.startswith("wind"): - potentials[key] = areas[key] * power_density["wind"] - if key.startswith("pv"): - potentials[key] = areas[key] * power_density[pv_density[key]] - return potentials + densities = [ + power_density["wind"] if technology.startswith("wind") else power_density[pv_density[technology]] + for technology in areas + ] + hydro = get_data_from_sources(Source("bnetza_mastr_hydro_stats_muns.csv", "capacity_net")) + potential_values = areas * densities + potential_values["hydro"] = hydro + return potential_values @cache_memoize(timeout=None) -def get_potential_areas(technology: Optional[str] = None) -> dict: +def get_potential_areas(technology: Optional[str] = None) -> pd.DataFrame: """ Return potential areas. @@ -216,23 +215,20 @@ def get_potential_areas(technology: Optional[str] = None) -> dict: "pv_roof": Source("potentialarea_pv_roof_area_stats_muns.csv", "roof_area_pv_potential_sqkm"), } + column_mapping = {source.column: new_column for new_column, source in sources.items()} + column_mapping["wind_2027"] = "wind_2027" + # Add wind for 2027 directly from model data, as it is not included in datapackage - areas = { - "wind_2027": area - if (area := models.Municipality.objects.all().values("area").aggregate(models.Sum("area"))["area__sum"]) - else 0, - } + wind_2027 = pd.DataFrame(models.Municipality.objects.all().values("id", "area")).set_index("id") + wind_2027.columns = ["wind_2027"] if technology is not None: if technology == "wind_2027": - return areas["wind_2027"] + return wind_2027 sources = {technology: sources[technology]} - data = get_data_from_sources(sources.values()) - data = data.sum() - for index in data.index: - # Add extracted data to areas and map source columns to keys accordingly - areas[next(key for key, source in sources.items() if source.column == index)] = data[index] - + areas = get_data_from_sources(sources.values()) + areas = pd.concat([areas, wind_2027], axis=1) + areas.columns = [column_mapping[column] for column in areas.columns] if technology is not None: return areas[technology] return areas diff --git a/digiplan/map/forms.py b/digiplan/map/forms.py index 73c4cfd9..9786ca5b 100644 --- a/digiplan/map/forms.py +++ b/digiplan/map/forms.py @@ -4,7 +4,15 @@ from itertools import count from typing import TYPE_CHECKING -from django.forms import BooleanField, Form, IntegerField, TextInput, renderers +from django.forms import ( + BooleanField, + CharField, + FloatField, + Form, + HiddenInput, + TextInput, + renderers, +) from django.shortcuts import reverse from django.utils.safestring import mark_safe @@ -60,7 +68,9 @@ def __init__(self, layer: legend.LegendLayer, *args, **kwargs) -> None: # noqa: class PanelForm(TemplateForm): # noqa: D101 def __init__(self, parameters, additional_parameters=None, **kwargs) -> None: # noqa: D107, ANN001 super().__init__(**kwargs) - self.fields = {item["name"]: item["field"] for item in self.generate_fields(parameters, additional_parameters)} + self.fields.update( + {item["name"]: item["field"] for item in self.generate_fields(parameters, additional_parameters)}, + ) def get_field_attrs(self, name: str, parameters: dict) -> dict: # noqa: ARG002 """Set up field attributes from parameters.""" @@ -89,7 +99,7 @@ def generate_fields(self, parameters: dict, additional_parameters: dict | None = charts.merge_dicts(parameters, additional_parameters) for name, item in parameters.items(): if item["type"] == "slider": - field = IntegerField( + field = FloatField( label=item["label"], widget=TextInput(attrs=self.get_field_attrs(name, item)), help_text=item["tooltip"], @@ -114,21 +124,27 @@ def generate_fields(self, parameters: dict, additional_parameters: dict | None = class EnergyPanelForm(PanelForm): # noqa: D101 template_name = "forms/panel_energy.html" + wind_year = CharField(initial="wind_2024", max_length=9, widget=HiddenInput) + def __init__(self, parameters, additional_parameters=None, **kwargs) -> None: # noqa: ANN001 """Overwrite init function to add initial key results for detail panels.""" super().__init__(parameters, additional_parameters, **kwargs) - for technology in ("wind_2018", "wind_2024", "wind_2027", "pv_ground", "pv_roof"): + key_results = {} + for wind_year in ("wind_2018", "wind_2024", "wind_2027"): # get initial slider values for wind and pv: - key_results = menu.detail_key_results( - technology, - id_s_w_6=parameters["s_w_6"]["start"], - id_s_w_7=parameters["s_w_7"]["start"], - id_s_pv_ff_3=parameters["s_pv_ff_3"]["start"], - id_s_pv_ff_4=parameters["s_pv_ff_4"]["start"], - id_s_pv_ff_5=parameters["s_pv_ff_5"]["start"], - id_s_pv_d_3=parameters["s_pv_d_3"]["start"], + key_results[wind_year] = menu.detail_key_results( + wind_year=wind_year, + s_w_6=parameters["s_w_6"]["start"], + s_w_7=parameters["s_w_7"]["start"], ) - for key, value in key_results.items(): + key_results["pv_ground"] = menu.detail_key_results( + s_pv_ff_3=parameters["s_pv_ff_3"]["start"], + s_pv_ff_4=parameters["s_pv_ff_4"]["start"], + s_pv_ff_5=parameters["s_pv_ff_5"]["start"], + ) + key_results["pv_roof"] = menu.detail_key_results(s_pv_d_3=parameters["s_pv_d_3"]["start"]) + for technology, key_result in key_results.items(): + for key, value in key_result.items(): self.extra_content[f"{technology}_key_result_{key}"] = value def get_field_attrs(self, name: str, parameters: dict) -> dict: diff --git a/digiplan/map/menu.py b/digiplan/map/menu.py index 73cecbba..8ef25934 100644 --- a/digiplan/map/menu.py +++ b/digiplan/map/menu.py @@ -1,49 +1,41 @@ """Add calculations for menu items.""" -from . import config, datapackage +from . import calculations, config, datapackage -def detail_key_results(technology: str, **kwargs: dict) -> dict: +def detail_key_results(**kwargs: dict) -> dict: """Calculate detail key results for given technology.""" - areas = datapackage.get_potential_areas() - potential_capacities = datapackage.get_potential_values() # in MW + shares = calculations.calculate_potential_shares(kwargs) + areas = datapackage.get_potential_areas().sum() + potential_capacities = datapackage.get_potential_values().sum() # in MW full_load_hours = datapackage.get_full_load_hours(2045) nominal_power_per_unit = config.TECHNOLOGY_DATA["nominal_power_per_unit"]["wind"] - if technology.startswith("wind"): - percentage = 1 - if technology == "wind_2024": - percentage = float(kwargs["id_s_w_6"]) / float(config.ENERGY_SETTINGS_PANEL["s_w_6"]["max"]) - if technology == "wind_2027": - percentage = float(kwargs["id_s_w_7"]) / 100 + if "wind_year" in kwargs: + wind_year = kwargs["wind_year"] + share = shares["wind"] return { - "area": areas[technology] * 100 * percentage, - "turbines": potential_capacities[technology] / nominal_power_per_unit * percentage, - "energy": potential_capacities[technology] * full_load_hours["wind"] * percentage * 1e-6, - } - if technology == "pv_ground": - percentages = { - "pv_soil_quality_low": int(kwargs["id_s_pv_ff_3"]) / 100, - "pv_soil_quality_medium": int(kwargs["id_s_pv_ff_4"]) / 100, - "pv_permanent_crops": int(kwargs["id_s_pv_ff_5"]) / 100, + "area": areas[wind_year] * 100 * share, + "turbines": potential_capacities[wind_year] / nominal_power_per_unit * share, + "energy": potential_capacities[wind_year] * full_load_hours["wind"] * share * 1e-6, } + if "s_pv_ff_3" in kwargs: flh_mapping = { "pv_soil_quality_low": "pv_ground", "pv_soil_quality_medium": "pv_ground_vertical_bifacial", "pv_permanent_crops": "pv_ground_elevated", } return { - "area": sum(areas[pv_type] * 100 * percentages[pv_type] for pv_type in percentages), + "area": sum(areas[pv_type] * 100 * shares[pv_type] for pv_type in shares), "energy": sum( - potential_capacities[pv_type] * full_load_hours[flh_mapping[pv_type]] * percentages[pv_type] - for pv_type in percentages + potential_capacities[pv_type] * full_load_hours[flh_mapping[pv_type]] * shares[pv_type] + for pv_type in shares ) * 1e-6, } - if technology == "pv_roof": - percentage = int(kwargs["id_s_pv_d_3"]) / 100 + if "s_pv_d_3" in kwargs: return { - "area": areas[technology] * 100 * percentage, - "energy": potential_capacities[technology] * full_load_hours[technology] * percentage * 1e-6, + "area": areas["pv_roof"] * 100 * shares["pv_roof"], + "energy": potential_capacities["pv_roof"] * full_load_hours["pv_roof"] * shares["pv_roof"] * 1e-6, } - raise KeyError(f"Unknown technology '{technology}'.") + raise KeyError(f"Unknown parameters ({kwargs}).") diff --git a/digiplan/map/migrations/0045_preresults.py b/digiplan/map/migrations/0045_preresults.py new file mode 100644 index 00000000..9e744ca7 --- /dev/null +++ b/digiplan/map/migrations/0045_preresults.py @@ -0,0 +1,21 @@ +# Generated by Django 4.2.11 on 2024-04-12 11:54 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("map", "0044_merge_20240318_1859"), + ] + + operations = [ + migrations.CreateModel( + name="PreResults", + fields=[ + ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("scenario", models.CharField(max_length=255)), + ("parameters", models.JSONField()), + ], + ), + ] diff --git a/digiplan/map/migrations/0046_merge_0045_preresults_0045_regionboundaries.py b/digiplan/map/migrations/0046_merge_0045_preresults_0045_regionboundaries.py new file mode 100644 index 00000000..4e0d7091 --- /dev/null +++ b/digiplan/map/migrations/0046_merge_0045_preresults_0045_regionboundaries.py @@ -0,0 +1,13 @@ +# Generated by Django 4.2.11 on 2024-04-23 15:29 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("map", "0045_preresults"), + ("map", "0045_regionboundaries"), + ] + + operations = [] diff --git a/digiplan/map/migrations/0047_delete_preresults.py b/digiplan/map/migrations/0047_delete_preresults.py new file mode 100644 index 00000000..3f9971cb --- /dev/null +++ b/digiplan/map/migrations/0047_delete_preresults.py @@ -0,0 +1,16 @@ +# Generated by Django 4.2.11 on 2024-04-26 12:10 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("map", "0046_merge_0045_preresults_0045_regionboundaries"), + ] + + operations = [ + migrations.DeleteModel( + name="PreResults", + ), + ] diff --git a/digiplan/map/popups.py b/digiplan/map/popups.py index 41e4fab3..e51327ac 100644 --- a/digiplan/map/popups.py +++ b/digiplan/map/popups.py @@ -228,7 +228,7 @@ class Capacity2045Popup(RegionPopup): title = "Installierte Leistung EE" def get_detailed_data(self) -> pd.DataFrame: # noqa: D102 - return calculations.capacities_per_municipality_2045(self.map_state["simulation_id"]) + return calculations.capacities_per_municipality_2045(self.map_state) def get_chart_options(self) -> dict: """Overwrite title and unit.""" @@ -270,7 +270,7 @@ class CapacitySquare2045Popup(RegionPopup): def get_detailed_data(self) -> pd.DataFrame: # noqa: D102 return calculations.calculate_square_for_value( - calculations.capacities_per_municipality_2045(self.map_state["simulation_id"]), + calculations.capacities_per_municipality_2045(self.map_state), ) def get_chart_options(self) -> dict: @@ -316,7 +316,7 @@ class Energy2045Popup(RegionPopup): title = _("Gewonnene Energie aus EE") def get_detailed_data(self) -> pd.DataFrame: # noqa: D102 - return calculations.energies_per_municipality_2045(self.map_state["simulation_id"]) + return calculations.energies_per_municipality_2045(self.map_state) def get_chart_options(self) -> dict: """Overwrite title and unit.""" @@ -357,7 +357,7 @@ class EnergyShare2045Popup(RegionPopup): title = _("Anteil Energie aus EE") def get_detailed_data(self) -> pd.DataFrame: # noqa: D102 - return calculations.energy_shares_2045_per_municipality(self.map_state["simulation_id"]) + return calculations.energy_shares_2045_per_municipality(self.map_state) def get_chart_options(self) -> dict: """Overwrite title and unit.""" @@ -399,7 +399,7 @@ class EnergyCapita2045Popup(RegionPopup): def get_detailed_data(self) -> pd.DataFrame: # noqa: D102 return calculations.calculate_capita_for_value( - calculations.energies_per_municipality_2045(self.map_state["simulation_id"]), + calculations.energies_per_municipality_2045(self.map_state), ) def get_chart_options(self) -> dict: @@ -445,7 +445,7 @@ class EnergySquare2045Popup(RegionPopup): def get_detailed_data(self) -> pd.DataFrame: # noqa: D102 return calculations.calculate_square_for_value( - calculations.energies_per_municipality_2045(self.map_state["simulation_id"]), + calculations.energies_per_municipality_2045(self.map_state), ) def get_chart_options(self) -> dict: @@ -585,7 +585,7 @@ class NumberWindturbines2045Popup(RegionPopup): def get_detailed_data(self) -> pd.DataFrame: """Return quantity of wind turbines per municipality (index).""" - return calculations.wind_turbines_per_municipality_2045(self.map_state["simulation_id"]) + return calculations.wind_turbines_per_municipality_2045(self.map_state) def get_region_value(self) -> float: """Return aggregated data of all municipalities and technologies.""" @@ -641,7 +641,7 @@ class NumberWindturbinesSquare2045Popup(RegionPopup): def get_detailed_data(self) -> pd.DataFrame: """Return quantity of wind turbines per municipality (index).""" - wind_turbines = calculations.wind_turbines_per_municipality_2045(self.map_state["simulation_id"]) + wind_turbines = calculations.wind_turbines_per_municipality_2045(self.map_state) return calculations.calculate_square_for_value(wind_turbines) def get_region_value(self) -> float: @@ -697,7 +697,7 @@ class ElectricityDemand2045Popup(RegionPopup): title = _("Strombedarf") def get_detailed_data(self) -> pd.DataFrame: # noqa: D102 - return calculations.electricity_demand_per_municipality_2045(self.map_state["simulation_id"]) + return calculations.electricity_demand_per_municipality_2045(self.map_state) def get_chart_data(self) -> Iterable: """Create capacity chart data for SQ and future scenario.""" @@ -744,7 +744,7 @@ class ElectricityDemandCapita2045Popup(RegionPopup): def get_detailed_data(self) -> pd.DataFrame: # noqa: D102 return calculations.calculate_capita_for_value( - calculations.electricity_demand_per_municipality_2045(self.map_state["simulation_id"]), + calculations.electricity_demand_per_municipality_2045(self.map_state), ) def get_region_value(self) -> float: # noqa: D102 @@ -777,7 +777,7 @@ class HeatDemandPopup(RegionPopup): title = _("Wärmebedarf") def get_detailed_data(self) -> pd.DataFrame: # noqa: D102 - return calculations.heat_demand_per_municipality().round(1) + return calculations.heat_demand_per_municipality(year=2022).round(1) def get_chart_options(self) -> dict: """Overwrite title and unit.""" @@ -794,11 +794,11 @@ class HeatDemand2045Popup(RegionPopup): title = _("Wärmebedarf") def get_detailed_data(self) -> pd.DataFrame: # noqa: D102 - return calculations.heat_demand_per_municipality_2045(self.map_state["simulation_id"]) + return calculations.heat_demand_per_municipality_2045(self.map_state) def get_chart_data(self) -> Iterable: """Create capacity chart data for SQ and future scenario.""" - status_quo_data = calculations.heat_demand_per_municipality().loc[self.selected_id].round(1) + status_quo_data = calculations.heat_demand_per_municipality(year=2022).loc[self.selected_id].round(1) future_data = super().get_chart_data().round(1) return list(zip(status_quo_data, future_data)) @@ -818,7 +818,9 @@ class HeatDemandCapitaPopup(RegionPopup): title = _("Wärmebedarf je EinwohnerIn") def get_detailed_data(self) -> pd.DataFrame: # noqa: D102 - return (calculations.calculate_capita_for_value(calculations.heat_demand_per_municipality()) * 1e6).round(1) + return ( + calculations.calculate_capita_for_value(calculations.heat_demand_per_municipality(year=2022)) * 1e6 + ).round(1) def get_region_value(self) -> float: # noqa: D102 return self.detailed_data.sum(axis=1).mean() @@ -839,7 +841,7 @@ class HeatDemandCapita2045Popup(RegionPopup): def get_detailed_data(self) -> pd.DataFrame: # noqa: D102 return calculations.calculate_capita_for_value( - calculations.heat_demand_per_municipality_2045(self.map_state["simulation_id"]), + calculations.heat_demand_per_municipality_2045(self.map_state), ).mul(1e6) def get_region_value(self) -> float: # noqa: D102 @@ -849,7 +851,7 @@ def get_chart_data(self) -> Iterable: """Create capacity chart data for SQ and future scenario.""" status_quo_data = ( calculations.calculate_capita_for_value( - calculations.heat_demand_per_municipality(), + calculations.heat_demand_per_municipality(year=2022), ) .loc[self.selected_id] .mul(1e6) diff --git a/digiplan/map/views.py b/digiplan/map/views.py index e393e621..3bf5c794 100644 --- a/digiplan/map/views.py +++ b/digiplan/map/views.py @@ -3,6 +3,7 @@ As map app is SPA, this module contains main view and various API points. """ +import json from django.conf import settings from django.http import HttpRequest, response @@ -190,11 +191,9 @@ def get_charts(request: HttpRequest) -> response.JsonResponse: `div_id` is used in frontend to detect chart container. """ lookups = request.GET.getlist("charts[]") - simulation_id = None - if "map_state[simulation_id]" in request.GET.dict(): - simulation_id = int(request.GET.dict()["map_state[simulation_id]"]) + map_state = json.loads(request.GET.get("map_state", "{}")) return response.JsonResponse( - {lookup: charts.CHARTS[lookup](simulation_id=simulation_id).render() for lookup in lookups}, + {lookup: charts.CHARTS[lookup](user_settings=map_state).render() for lookup in lookups}, ) @@ -205,4 +204,8 @@ class DetailKeyResultsView(TemplateView): def get_context_data(self, **kwargs) -> dict: # noqa: ARG002 """Get detail key results for requested technology.""" - return {f"key_result_{key}": value for key, value in menu.detail_key_results(**self.request.GET.dict()).items()} + # Cut off leading "id_" from form field id + parameters = { + key[3:] if key.startswith("id_") else key: value for key, value in self.request.GET.dict().items() + } + return {f"key_result_{key}": value for key, value in menu.detail_key_results(**parameters).items()} diff --git a/digiplan/static/js/event-topics.js b/digiplan/static/js/event-topics.js index 8fc67d02..1702dccc 100644 --- a/digiplan/static/js/event-topics.js +++ b/digiplan/static/js/event-topics.js @@ -35,6 +35,7 @@ const eventTopics = { SETTINGS_CHANGED: "SETTINGS_CHANGED", SIMULATION_STARTED: "SIMULATION_STARTED", SIMULATION_FINISHED: "SIMULATION_FINISHED", + PRE_RESULTS_READY: "PRE_RESULTS_READY", MAP_VIEW_SELECTED: "MAP_VIEW_SELECTED", CHART_VIEW_SELECTED: "CHART_VIEW_SELECTED", diff --git a/digiplan/static/js/menu.js b/digiplan/static/js/menu.js index 9fce4d22..4a4a7490 100644 --- a/digiplan/static/js/menu.js +++ b/digiplan/static/js/menu.js @@ -102,10 +102,6 @@ function toggleMenuButtons(tabIndex) { if (tabIndex === 0) { menuPreviousBtn.hidden = true; } - // TODO: Currently step 5 shall be inactive, to activate it again, remove next if-statement - if (tabIndex >= menuTabs.length - 2) { - menuNextBtn.disabled = true; - } if (tabIndex >= menuTabs.length - 1) { menuNextBtn.hidden = true; } diff --git a/digiplan/static/js/results.js b/digiplan/static/js/results.js index be89f748..6975f927 100644 --- a/digiplan/static/js/results.js +++ b/digiplan/static/js/results.js @@ -1,198 +1,210 @@ -import {statusquoDropdown, futureDropdown} from "./elements.js"; +import { statusquoDropdown, futureDropdown } from "./elements.js"; const imageResults = document.getElementById("info_tooltip_results"); -const simulation_spinner = document.getElementById("simulation_spinner"); -const chartViewTab = document.getElementById("chart-view-tab"); -const mapViewTab = document.getElementById("map-view-tab"); const resultSimNote = document.getElementById("result_simnote"); const SIMULATION_CHECK_TIME = 5000; +const PRE_RESULTS = [ + "energy_share_2045", + "energy_2045", + "energy_capita_2045", + "energy_square_2045", + "capacity_2045", + "capacity_square_2045", + "wind_turbines_2045", + "wind_turbines_square_2045", + "electricity_demand_2045", + "electricity_demand_capita_2045", + "heat_demand_2045", + "heat_demand_capita_2045", +]; const resultCharts = { - "electricity_overview": "electricity_overview_chart", - "electricity_autarky": "electricity_autarky_chart", - "ghg_reduction": "ghg_reduction_chart", - "heat_centralized": "heat_centralized_chart", - "heat_decentralized": "heat_decentralized_chart", + electricity_overview: "electricity_overview_chart", + electricity_autarky: "electricity_autarky_chart", + ghg_reduction: "ghg_reduction_chart", + heat_centralized: "heat_centralized_chart", + heat_decentralized: "heat_decentralized_chart", }; // Setup // Disable settings form submit -$('#settings').submit(false); +$("#settings").submit(false); statusquoDropdown.addEventListener("change", function () { - if (statusquoDropdown.value === "") { - deactivateChoropleth(); - PubSub.publish(eventTopics.CHOROPLETH_DEACTIVATED); - } else { - PubSub.publish(mapEvent.CHOROPLETH_SELECTED, statusquoDropdown.value); - } - imageResults.title = statusquoDropdown.options[statusquoDropdown.selectedIndex].title; + if (statusquoDropdown.value === "") { + deactivateChoropleth(); + PubSub.publish(eventTopics.CHOROPLETH_DEACTIVATED); + } else { + PubSub.publish(mapEvent.CHOROPLETH_SELECTED, statusquoDropdown.value); + } + imageResults.title = + statusquoDropdown.options[statusquoDropdown.selectedIndex].title; }); futureDropdown.addEventListener("change", function () { - if (futureDropdown.value === "") { - deactivateChoropleth(); - PubSub.publish(eventTopics.CHOROPLETH_DEACTIVATED); - } else { - PubSub.publish(mapEvent.CHOROPLETH_SELECTED, futureDropdown.value); - } - imageResults.title = futureDropdown.options[futureDropdown.selectedIndex].title; + if (futureDropdown.value === "") { + deactivateChoropleth(); + PubSub.publish(eventTopics.CHOROPLETH_DEACTIVATED); + } else { + PubSub.publish(mapEvent.CHOROPLETH_SELECTED, futureDropdown.value); + } + imageResults.title = + futureDropdown.options[futureDropdown.selectedIndex].title; }); - // Subscriptions PubSub.subscribe(eventTopics.MENU_RESULTS_SELECTED, simulate); -PubSub.subscribe(eventTopics.MENU_RESULTS_SELECTED, showSimulationSpinner); +PubSub.subscribe(eventTopics.MENU_RESULTS_SELECTED, storePreResults); +PubSub.subscribe(eventTopics.MENU_RESULTS_SELECTED, disableResultButtons); +PubSub.subscribe(eventTopics.MENU_RESULTS_SELECTED, hideRegionChart); +PubSub.subscribe(eventTopics.MENU_RESULTS_SELECTED, resetResultDropdown); PubSub.subscribe(eventTopics.SIMULATION_STARTED, checkResultsPeriodically); -PubSub.subscribe(eventTopics.SIMULATION_STARTED, hideResultButtons); -PubSub.subscribe(eventTopics.SIMULATION_STARTED, hideRegionChart); -PubSub.subscribe(eventTopics.SIMULATION_STARTED, resetResultDropdown); -PubSub.subscribe(eventTopics.SIMULATION_FINISHED, showResultButtons); +PubSub.subscribe(eventTopics.SIMULATION_FINISHED, enableFutureResults); PubSub.subscribe(eventTopics.SIMULATION_FINISHED, showResults); -PubSub.subscribe(eventTopics.SIMULATION_FINISHED, hideSimulationSpinner); PubSub.subscribe(eventTopics.SIMULATION_FINISHED, showResultCharts); PubSub.subscribe(mapEvent.CHOROPLETH_SELECTED, showRegionChart); PubSub.subscribe(eventTopics.CHOROPLETH_DEACTIVATED, hideRegionChart); - // Subscriber Functions function simulate(msg) { - const settings = document.getElementById("settings"); - const formData = new FormData(settings); // jshint ignore:line - if (store.cold.task_id != null) { - $.ajax({ - url: "/oemof/terminate", - type: "POST", - data: {task_id: store.cold.task_id}, - success: function () { - store.cold.task_id = null; - } - }); - } + const settings = document.getElementById("settings"); + const formData = new FormData(settings); // jshint ignore:line + if (store.cold.task_id != null) { $.ajax({ - url: "/oemof/simulate", - type: "POST", - processData: false, - contentType: false, - data: formData, - success: function (json) { - store.cold.task_id = json.task_id; - PubSub.publish(eventTopics.SIMULATION_STARTED); - }, + url: "/oemof/terminate", + type: "POST", + data: { task_id: store.cold.task_id }, + success: function () { + store.cold.task_id = null; + }, }); - return logMessage(msg); + } + $.ajax({ + url: "/oemof/simulate", + type: "POST", + processData: false, + contentType: false, + data: formData, + success: function (json) { + store.cold.task_id = json.task_id; + PubSub.publish(eventTopics.SIMULATION_STARTED); + }, + }); + return logMessage(msg); +} + +function storePreResults(msg) { + const settings = document.getElementById("settings"); + const formData = new FormData(settings); // jshint ignore:line + const userSettings = Object.fromEntries(formData.entries()); + Object.assign(map_store.cold.state, userSettings); + return logMessage(msg); } function checkResultsPeriodically(msg) { - setTimeout(checkResults, SIMULATION_CHECK_TIME); - return logMessage(msg); + setTimeout(checkResults, SIMULATION_CHECK_TIME); + return logMessage(msg); } function checkResults() { - $.ajax({ - url: "/oemof/simulate", - type: "GET", - data: {task_id: store.cold.task_id}, - success: function (json) { - if (json.simulation_id == null) { - setTimeout(checkResults, SIMULATION_CHECK_TIME); - } else { - store.cold.task_id = null; - map_store.cold.state.simulation_id = json.simulation_id; - PubSub.publish(eventTopics.SIMULATION_FINISHED); - } - }, - error: function (json) { - store.cold.task_id = null; - map_store.cold.state.simulation_id = null; - PubSub.publish(eventTopics.SIMULATION_FINISHED); - } - }); + $.ajax({ + url: "/oemof/simulate", + type: "GET", + data: { task_id: store.cold.task_id }, + success: function (json) { + if (json.simulation_id == null) { + setTimeout(checkResults, SIMULATION_CHECK_TIME); + } else { + store.cold.task_id = null; + map_store.cold.state.simulation_id = json.simulation_id; + PubSub.publish(eventTopics.SIMULATION_FINISHED); + } + }, + error: function (json) { + store.cold.task_id = null; + map_store.cold.state.simulation_id = null; + PubSub.publish(eventTopics.SIMULATION_FINISHED); + }, + }); } function showResults(msg, simulation_id) { - $.ajax({ - url: "/visualization", - type: "GET", - data: { - simulation_ids: simulation_id, - visualization: "total_system_costs", - }, - success: function (json) { - console.log(json); - }, - }); - return logMessage(msg); -} - -function showSimulationSpinner(msg) { - simulation_spinner.hidden = false; - return logMessage(msg); -} - -function hideSimulationSpinner(msg) { - simulation_spinner.hidden = true; - return logMessage(msg); + $.ajax({ + url: "/visualization", + type: "GET", + data: { + simulation_ids: simulation_id, + visualization: "total_system_costs", + }, + success: function (json) { + console.log(json); + }, + }); + return logMessage(msg); } -function showResultButtons(msg) { - chartViewTab.classList.remove("disabled"); - mapViewTab.classList.remove("disabled"); - futureDropdown.disabled = false; - return logMessage(msg); +function enableFutureResults(msg) { + resultSimNote.innerText = ""; + const options = futureDropdown.querySelectorAll("option"); + for (const option of options) { + option.disabled = false; + } + return logMessage(msg); } -function hideResultButtons(msg) { - chartViewTab.classList.add("disabled"); - mapViewTab.classList.add("disabled"); - futureDropdown.disabled = true; - return logMessage(msg); +function disableResultButtons(msg) { + resultSimNote.innerText = "Berechnung läuft ..."; + const options = futureDropdown.querySelectorAll("option"); + for (const option of options) { + if (!PRE_RESULTS.includes(option.value)) { + option.disabled = true; + } + } + return logMessage(msg); } function showRegionChart(msg, lookup) { - const region_lookup = `${lookup}_region`; - let charts = {}; - if (region_lookup.includes("2045")) { - charts[region_lookup] = "region_chart_2045"; - } else { - charts[region_lookup] = "region_chart_statusquo"; - } - showCharts(charts); - return logMessage(msg); + const region_lookup = `${lookup}_region`; + let charts = {}; + if (region_lookup.includes("2045")) { + charts[region_lookup] = "region_chart_2045"; + } else { + charts[region_lookup] = "region_chart_statusquo"; + } + showCharts(charts); + return logMessage(msg); } function hideRegionChart(msg) { - clearChart("region_chart_statusquo"); - clearChart("region_chart_2045"); - resultSimNote.innerText = "Berechnung läuft ..."; - return logMessage(msg); + clearChart("region_chart_statusquo"); + clearChart("region_chart_2045"); + return logMessage(msg); } function showResultCharts(msg) { - showCharts(resultCharts); - resultSimNote.innerText = ""; - return logMessage(msg); + showCharts(resultCharts); + return logMessage(msg); } function resetResultDropdown(msg) { - futureDropdown.selectedIndex = 0; - return logMessage(msg); + futureDropdown.selectedIndex = 0; + return logMessage(msg); } function showCharts(charts = {}) { - $.ajax({ - url: "/charts", - type: "GET", - data: { - "charts": Object.keys(charts), - "map_state": map_store.cold.state - }, - success: function (chart_options) { - for (const chart in charts) { - createChart(charts[chart], chart_options[chart]); - } - }, - }); + $.ajax({ + url: "/charts", + type: "GET", + data: { + charts: Object.keys(charts), + map_state: JSON.stringify(map_store.cold.state), + }, + success: function (chart_options) { + for (const chart in charts) { + createChart(charts[chart], chart_options[chart]); + } + }, + }); } diff --git a/digiplan/static/js/sliders.js b/digiplan/static/js/sliders.js index a147285e..c9162870 100644 --- a/digiplan/static/js/sliders.js +++ b/digiplan/static/js/sliders.js @@ -119,6 +119,7 @@ PubSub.subscribe( PubSub.subscribe(eventTopics.PV_CONTROL_ACTIVATED, showPVLayers); PubSub.subscribe(eventTopics.PV_CONTROL_ACTIVATED, highlightPVMapControls); PubSub.subscribe(eventTopics.PV_ROOF_CONTROL_ACTIVATED, showPVRoofLayers); +PubSub.subscribe(eventTopics.WIND_CONTROL_ACTIVATED, updateWindSelection); PubSub.subscribe(eventTopics.WIND_CONTROL_ACTIVATED, showWindLayers); // Subscriber Functions @@ -404,6 +405,23 @@ function showWindLayers(msg) { return logMessage(msg); } +function updateWindSelection(msg) { + const windInput = document.getElementById("id_wind_year"); + const currentWindTab = document + .getElementById("windTab") + .getElementsByClassName("active")[0].id; + if (currentWindTab === "windPastTab") { + windInput.value = "wind_2018"; + } else if (currentWindTab === "windPresentTab") { + windInput.value = "wind_2024"; + } else if (currentWindTab === "windFutureTab") { + windInput.value = "wind_2027"; + } else { + throw Error(`Unknown wind tab '${currentWindTab}' found.`); + } + return logMessage(msg); +} + export function hidePotentialLayers(msg) { for (const layer of potentialPVLayers .concat(potentialPVRoofLayers) @@ -424,22 +442,21 @@ function highlightPVMapControls(msg) { function adaptDetailKeyResults(msg, data) { const slider_id = data.input[0].id; - let technology; + let wind_year; let target; let url_data = {}; if (slider_id === "id_s_w_6") { - technology = "wind_2024"; + wind_year = "wind_2024"; url_data.id_s_w_6 = data.from; target = "wind_key_results_2024"; } else if (slider_id === "id_s_w_7") { - technology = "wind_2027"; + wind_year = "wind_2027"; url_data.id_s_w_7 = data.from; target = "wind_key_results_2027"; } else if ( ["id_s_pv_ff_3", "id_s_pv_ff_4", "id_s_pv_ff_5"].includes(slider_id) ) { - technology = "pv_ground"; url_data.id_s_pv_ff_3 = $("#id_s_pv_ff_3").data("ionRangeSlider").result.from; url_data.id_s_pv_ff_4 = @@ -448,7 +465,6 @@ function adaptDetailKeyResults(msg, data) { $("#id_s_pv_ff_5").data("ionRangeSlider").result.from; target = "pv_ground_key_results"; } else if (slider_id === "id_s_pv_d_3") { - technology = "pv_roof"; url_data.id_s_pv_d_3 = data.from; target = "pv_roof_key_results"; } else { @@ -456,7 +472,10 @@ function adaptDetailKeyResults(msg, data) { } const query = new URLSearchParams(url_data).toString(); - let url = `/detail_key_results?technology=${technology}&${query}`; + let url = `/detail_key_results?${query}`; + if (wind_year !== undefined) { + url += "&wind_year=" + wind_year; + } fetch(url) .then((response) => { // Check if the response is successful diff --git a/digiplan/templates/forms/panel_energy.html b/digiplan/templates/forms/panel_energy.html index 588c807c..fb2f4566 100644 --- a/digiplan/templates/forms/panel_energy.html +++ b/digiplan/templates/forms/panel_energy.html @@ -48,6 +48,7 @@