From 129aaf1e82b5f9e3bece718aa9a55d878cd65af0 Mon Sep 17 00:00:00 2001 From: Tom Bland Date: Wed, 21 Aug 2024 13:55:19 +0100 Subject: [PATCH 01/23] First pass at abstracting LCOE --- src/muse/objectives.py | 121 +++++++--------------------------------- src/muse/quantities.py | 122 ++++++++++++++++++++++++++++++++++++++++- 2 files changed, 138 insertions(+), 105 deletions(-) diff --git a/src/muse/objectives.py b/src/muse/objectives.py index 998437fdf..e1d33d7e5 100644 --- a/src/muse/objectives.py +++ b/src/muse/objectives.py @@ -572,116 +572,38 @@ def lifetime_levelized_cost_of_energy( Return: xr.DataArray with the LCOE calculated for the relevant technologies """ - from muse.commodities import is_enduse, is_fuel, is_material, is_pollutant - from muse.quantities import consumption + from muse.quantities import lifetime_levelized_cost_of_energy from muse.timeslices import QuantityType, convert_timeslice - # Filtering of the inputs - tech = agent.filter_input( - technologies[ - [ - "technical_life", - "interest_rate", - "cap_par", - "cap_exp", - "var_par", - "var_exp", - "fix_par", - "fix_exp", - "fixed_outputs", - "fixed_inputs", - "flexible_inputs", - "utilization_factor", - ] - ], + techs = agent.filter_input( + technologies, technology=search_space.replacement, year=agent.forecast_year, ).drop_vars("technology") - nyears = tech.technical_life.astype(int) - interest_rate = tech.interest_rate - cap_par = tech.cap_par - cap_exp = tech.cap_exp - var_par = tech.var_par - var_exp = tech.var_exp - fix_par = tech.fix_par - fix_exp = tech.fix_exp - fixed_outputs = tech.fixed_outputs - utilization_factor = tech.utilization_factor - # All years the simulation is running - # NOTE: see docstring about installation year - iyears = range( - agent.forecast_year, - max(agent.forecast_year + nyears.values.max(), agent.forecast_year), - ) - years = xr.DataArray(iyears, coords={"year": iyears}, dims="year") - - # Filters - environmentals = is_pollutant(technologies.comm_usage) - material = is_material(technologies.comm_usage) - products = is_enduse(technologies.comm_usage) - fuels = is_fuel(technologies.comm_usage) - # Capacity capacity = capacity_to_service_demand( agent, demand, search_space, technologies, market ) - - # Evolution of rates with time - rates = discount_factor( - years - agent.forecast_year + 1, - interest_rate, - years <= agent.forecast_year + nyears, - ) - production = capacity * fixed_outputs * utilization_factor + production = capacity * techs.fixed_outputs * techs.utilization_factor production = convert_timeslice(production, demand.timeslice, QuantityType.EXTENSIVE) - # raw costs --> make the NPV more negative - # Cost of installed capacity - installed_capacity_costs = convert_timeslice( - cap_par * (capacity**cap_exp), - demand.timeslice, - QuantityType.EXTENSIVE, - ) - # Cost related to environmental products - prices_environmental = agent.filter_input( - market.prices, commodity=environmentals, year=years.values - ).ffill("year") - environmental_costs = (production * prices_environmental * rates).sum( - ("commodity", "year") + iyears = range( + agent.forecast_year, + max( + agent.forecast_year + techs.technical_life.astype(int).values.max(), + agent.forecast_year, + ), ) + years = xr.DataArray(iyears, coords={"year": iyears}, dims="year") - # Fuel/energy costs - prices_fuel = agent.filter_input( - market.prices, commodity=fuels, year=years.values - ).ffill("year") - prices = agent.filter_input(market.prices, year=years.values).ffill("year") - fuel = consumption(technologies=tech, production=production, prices=prices) - fuel_costs = (fuel * prices_fuel * rates).sum(("commodity", "year")) - - # Cost related to material other than fuel/energy and environmentals - prices_material = agent.filter_input( - market.prices, commodity=material, year=years.values - ).ffill("year") - material_costs = (production * prices_material * rates).sum(("commodity", "year")) - - # Fixed and Variable costs - fixed_costs = convert_timeslice( - fix_par * (capacity**fix_exp), - demand.timeslice, - QuantityType.EXTENSIVE, - ) - variable_costs = (var_par * production.sel(commodity=products) ** var_exp).sum( - "commodity" + results = lifetime_levelized_cost_of_energy( + prices=market.prices, + technologies=techs, + capacity=capacity, + production=production, + years=years, + forecast_year=agent.forecast_year, ) - fixed_and_variable_costs = ((fixed_costs + variable_costs) * rates).sum("year") - denominator = production.where(production > 0.0, 1e-6) - results = ( - installed_capacity_costs - + fuel_costs - + environmental_costs - + material_costs - + fixed_and_variable_costs - ) / (denominator.sel(commodity=products).sum("commodity") * rates).sum("year") return results.where(np.isfinite(results)).fillna(0.0) @@ -732,7 +654,7 @@ def net_present_value( xr.DataArray with the NPV calculated for the relevant technologies """ from muse.commodities import is_enduse, is_fuel, is_material, is_pollutant - from muse.quantities import consumption + from muse.quantities import consumption, discount_factor from muse.timeslices import QuantityType, convert_timeslice # Filtering of the inputs @@ -888,11 +810,6 @@ def net_present_cost( ) -def discount_factor(years, interest_rate, mask=1.0): - """Calculate an array with the rate (aka discount factor) values over the years.""" - return mask / (1 + interest_rate) ** years - - @register_objective(name="EAC") def equivalent_annual_cost( agent: Agent, diff --git a/src/muse/quantities.py b/src/muse/quantities.py index d81d16477..13de03df3 100644 --- a/src/muse/quantities.py +++ b/src/muse/quantities.py @@ -310,6 +310,117 @@ def consumption( return consumption + flex * production +def lifetime_levelized_cost_of_energy( + prices: xr.DataArray, + technologies: xr.Dataset, + capacity, + production, + years, + forecast_year, +): + """Levelized cost of energy (LCOE) of technologies over their lifetime. + + It follows the `simplified LCOE` given by NREL. The LCOE is set to zero for those + timeslices where the production is zero, normally due to a zero utilisation + factor. + + Arguments: + agent: The agent of interest + demand: Demand for commodities + search_space: The search space space for replacement technologies + technologies: All the technologies + market: The market parameters + *args: Extra arguments (unused) + **kwargs: Extra keyword arguments (unused) + + Return: + xr.DataArray with the LCOE calculated for the relevant technologies + """ + from muse.commodities import is_enduse, is_fuel, is_material, is_pollutant + from muse.timeslices import QuantityType, convert_timeslice + from muse.utilities import filter_input + + techs = technologies[ + [ + "technical_life", + "interest_rate", + "cap_par", + "cap_exp", + "var_par", + "var_exp", + "fix_par", + "fix_exp", + "fixed_outputs", + "fixed_inputs", + "flexible_inputs", + "utilization_factor", + ] + ] + + # Filters + environmentals = is_pollutant(technologies.comm_usage) + material = is_material(technologies.comm_usage) + products = is_enduse(technologies.comm_usage) + fuels = is_fuel(technologies.comm_usage) + + # Evolution of rates with time + rates = discount_factor( + years=years - forecast_year + 1, + interest_rate=techs.interest_rate, + mask=years <= forecast_year + techs.technical_life.astype(int), + ) + + # Cost of installed capacity + installed_capacity_costs = convert_timeslice( + techs.cap_par * (capacity**techs.cap_exp), + prices.timeslice, + QuantityType.EXTENSIVE, + ) + + # Cost related to environmental products + prices_environmental = filter_input( + prices, commodity=environmentals, year=years.values, region=techs.region + ).ffill("year") + environmental_costs = (production * prices_environmental * rates).sum( + ("commodity", "year") + ) + + # Fuel/energy costs + prices_fuel = filter_input( + prices, commodity=fuels, year=years.values, region=techs.region + ).ffill("year") + prices = filter_input(prices, year=years.values, region=techs.region).ffill("year") + fuel = consumption(technologies=techs, production=production, prices=prices) + fuel_costs = (fuel * prices_fuel * rates).sum(("commodity", "year")) + + # Cost related to material other than fuel/energy and environmentals + prices_material = filter_input(prices, commodity=material, year=years.values).ffill( + "year" + ) + material_costs = (production * prices_material * rates).sum(("commodity", "year")) + + # Fixed and Variable costs + fixed_costs = convert_timeslice( + techs.fix_par * (capacity**techs.fix_exp), + prices.timeslice, + QuantityType.EXTENSIVE, + ) + variable_costs = ( + techs.var_par * production.sel(commodity=products) ** techs.var_exp + ).sum("commodity") + fixed_and_variable_costs = ((fixed_costs + variable_costs) * rates).sum("year") + denominator = production.where(production > 0.0, 1e-6) + results = ( + installed_capacity_costs + + fuel_costs + + environmental_costs + + material_costs + + fixed_and_variable_costs + ) / (denominator.sel(commodity=products).sum("commodity") * rates).sum("year") + + return results.where(np.isfinite(results)).fillna(0.0) + + def annual_levelized_cost_of_energy( prices: xr.DataArray, technologies: xr.Dataset, @@ -380,11 +491,11 @@ def annual_levelized_cost_of_energy( life = techs.technical_life.astype(int) + rates = techs.interest_rate / (1 - (1 + techs.interest_rate) ** (-life)) + annualized_capital_costs = ( convert_timeslice( - techs.cap_par - * techs.interest_rate - / (1 - (1 + techs.interest_rate) ** (-life)), + techs.cap_par * rates, prices.timeslice, QuantityType.EXTENSIVE, ) @@ -656,3 +767,8 @@ def group_assets(x: xr.DataArray) -> xr.DataArray: result = xr.zeros_like(maxprod) result[dict(commodity=commodity)] = result[dict(commodity=commodity)] + production return result + + +def discount_factor(years, interest_rate, mask=1.0): + """Calculate an array with the rate (aka discount factor) values over the years.""" + return mask / (1 + interest_rate) ** years From f0254aa120fec6acb17f2e9eb91939583070f873 Mon Sep 17 00:00:00 2001 From: Tom Bland Date: Wed, 21 Aug 2024 15:41:32 +0100 Subject: [PATCH 02/23] First pass at using LCOE function for MCA output --- src/muse/objectives.py | 15 +---- src/muse/outputs/mca.py | 137 ++++----------------------------------- src/muse/quantities.py | 36 +++++----- tests/test_objectives.py | 8 +-- 4 files changed, 37 insertions(+), 159 deletions(-) diff --git a/src/muse/objectives.py b/src/muse/objectives.py index e1d33d7e5..0485dcc82 100644 --- a/src/muse/objectives.py +++ b/src/muse/objectives.py @@ -580,6 +580,7 @@ def lifetime_levelized_cost_of_energy( technology=search_space.replacement, year=agent.forecast_year, ).drop_vars("technology") + prices = cast(xr.DataArray, agent.filter_input(market.prices)) capacity = capacity_to_service_demand( agent, demand, search_space, technologies, market @@ -587,22 +588,12 @@ def lifetime_levelized_cost_of_energy( production = capacity * techs.fixed_outputs * techs.utilization_factor production = convert_timeslice(production, demand.timeslice, QuantityType.EXTENSIVE) - iyears = range( - agent.forecast_year, - max( - agent.forecast_year + techs.technical_life.astype(int).values.max(), - agent.forecast_year, - ), - ) - years = xr.DataArray(iyears, coords={"year": iyears}, dims="year") - results = lifetime_levelized_cost_of_energy( - prices=market.prices, + prices=prices, technologies=techs, capacity=capacity, production=production, - years=years, - forecast_year=agent.forecast_year, + year=agent.forecast_year, ) return results.where(np.isfinite(results)).fillna(0.0) diff --git a/src/muse/outputs/mca.py b/src/muse/outputs/mca.py index c73b97ac1..f0726f2cc 100644 --- a/src/muse/outputs/mca.py +++ b/src/muse/outputs/mca.py @@ -884,9 +884,7 @@ def metric_lcoe( def sector_lcoe(sector: AbstractSector, market: xr.Dataset, **kwargs) -> pd.DataFrame: """Levelized cost of energy () of technologies over their lifetime.""" - from muse.commodities import is_enduse, is_fuel, is_material, is_pollutant - from muse.objectives import discount_factor - from muse.quantities import consumption + from muse.quantities import lifetime_levelized_cost_of_energy def capacity_to_service_demand(demand, technologies): from muse.timeslices import represent_hours @@ -926,138 +924,28 @@ def capacity_to_service_demand(demand, technologies): ] agent_market.loc[dict(commodity=excluded)] = 0 years = [output_year, agent.year] - agent_market["prices"] = agent.filter_input(market["prices"], year=years) - tech = agent.filter_input( - technologies[ - [ - "technical_life", - "interest_rate", - "cap_par", - "cap_exp", - "var_par", - "var_exp", - "fix_par", - "fix_exp", - "fixed_outputs", - "fixed_inputs", - "flexible_inputs", - "utilization_factor", - ] - ], + techs = agent.filter_input( + technologies, year=agent.year, - region=agent.region, - ) - nyears = tech.technical_life.astype(int) - interest_rate = tech.interest_rate - cap_par = tech.cap_par - cap_exp = tech.cap_exp - var_par = tech.var_par - var_exp = tech.var_exp - fix_par = tech.fix_par - fix_exp = tech.fix_exp - fixed_outputs = tech.fixed_outputs - utilization_factor = tech.utilization_factor - - # All years the simulation is running - # NOTE: see docstring about installation year - iyears = range( - agent.year, - max(agent.year + nyears.values.max(), agent.forecast_year), ) - years = xr.DataArray(iyears, coords={"year": iyears}, dims="year") - - prices = agent.filter_input(agent_market.prices, year=years.values) - # Filters - environmentals = is_pollutant(tech.comm_usage) - e = np.where(environmentals) - environmentals = environmentals[e].commodity.values - material = is_material(tech.comm_usage) - e = np.where(material) - material = material[e].commodity.values - products = is_enduse(tech.comm_usage) - e = np.where(products) - products = products[e].commodity.values - fuels = is_fuel(tech.comm_usage) - e = np.where(fuels) - fuels = fuels[e].commodity.values - # Capacity + prices = agent_market["prices"].sel(commodity=techs.commodity) demand = agent_market.consumption.sel(commodity=included) - capacity = capacity_to_service_demand(demand, tech) - - # Evolution of rates with time - rates = discount_factor( - years - agent.year + 1, interest_rate, years <= agent.year + nyears - ) - - production = capacity * fixed_outputs * utilization_factor + capacity = agent.filter_input(capacity_to_service_demand(demand, techs)) + production = capacity * techs.fixed_outputs * techs.utilization_factor production = convert_timeslice( production, demand.timeslice, QuantityType.EXTENSIVE, ) - # raw costs --> make the NPV more negative - # Cost of installed capacity - installed_capacity_costs = convert_timeslice( - cap_par * (capacity**cap_exp), - demand.timeslice, - QuantityType.EXTENSIVE, - ) - - # Cost related to environmental products - prices_environmental = agent.filter_input(prices, year=years.values).sel( - commodity=environmentals - ) - environmental_costs = ( - (production * prices_environmental * rates) - .sel(commodity=environmentals, year=years.values) - .sum(("commodity", "year")) # , "timeslice") - ) - - # Fuel/energy costs - prices_fuel = agent.filter_input(prices, year=years.values).sel( - commodity=fuels - ) - - fuel = consumption( - technologies=tech, - production=production.sel(region=tech.region), + result = lifetime_levelized_cost_of_energy( prices=prices, - ) - fuel_costs = (fuel * prices_fuel * rates).sum(("commodity", "year")) - - # Cost related to material other than fuel/energy and environmentals - prices_material = agent.filter_input(prices, year=years.values).sel( - commodity=material - ) - material_costs = (production * prices_material * rates).sum( - ("commodity", "year") - ) - - # Fixed and Variable costs - fixed_costs = convert_timeslice( - fix_par * (capacity**fix_exp), - demand.timeslice, - QuantityType.EXTENSIVE, - ) - variable_costs = ( - var_par * production.sel(commodity=products) ** var_exp - ).sum("commodity") - # assert set(fixed_costs.dims) == set(variable_costs.dims) - fixed_and_variable_costs = ((fixed_costs + variable_costs) * rates).sum( - "year" - ) - - result = ( - installed_capacity_costs - + fuel_costs - + environmental_costs - + material_costs - + fixed_and_variable_costs - ) / (production.sel(commodity=products).sum("commodity") * rates).sum( - "year" + technologies=techs, + capacity=capacity, + production=production, + year=agent.year, ) data_agent = result @@ -1090,8 +978,7 @@ def metric_eac( def sector_eac(sector: AbstractSector, market: xr.Dataset, **kwargs) -> pd.DataFrame: """Net Present Value of technologies over their lifetime.""" from muse.commodities import is_enduse, is_fuel, is_material, is_pollutant - from muse.objectives import discount_factor - from muse.quantities import consumption + from muse.quantities import consumption, discount_factor def capacity_to_service_demand(demand, technologies): from muse.timeslices import represent_hours diff --git a/src/muse/quantities.py b/src/muse/quantities.py index 13de03df3..65252c6ee 100644 --- a/src/muse/quantities.py +++ b/src/muse/quantities.py @@ -315,8 +315,7 @@ def lifetime_levelized_cost_of_energy( technologies: xr.Dataset, capacity, production, - years, - forecast_year, + year, ): """Levelized cost of energy (LCOE) of technologies over their lifetime. @@ -357,19 +356,27 @@ def lifetime_levelized_cost_of_energy( ] ] - # Filters - environmentals = is_pollutant(technologies.comm_usage) - material = is_material(technologies.comm_usage) - products = is_enduse(technologies.comm_usage) - fuels = is_fuel(technologies.comm_usage) + # Years + life = techs.technical_life.astype(int) + iyears = range( + year, + max(year + life.values.max(), year), + ) + years = xr.DataArray(iyears, coords={"year": iyears}, dims="year") # Evolution of rates with time rates = discount_factor( - years=years - forecast_year + 1, + years=years - year + 1, interest_rate=techs.interest_rate, - mask=years <= forecast_year + techs.technical_life.astype(int), + mask=years <= year + life, ) + # Filters + environmentals = is_pollutant(technologies.comm_usage) + material = is_material(technologies.comm_usage) + products = is_enduse(technologies.comm_usage) + fuels = is_fuel(technologies.comm_usage) + # Cost of installed capacity installed_capacity_costs = convert_timeslice( techs.cap_par * (capacity**techs.cap_exp), @@ -379,17 +386,14 @@ def lifetime_levelized_cost_of_energy( # Cost related to environmental products prices_environmental = filter_input( - prices, commodity=environmentals, year=years.values, region=techs.region + prices, commodity=environmentals, year=years.values ).ffill("year") environmental_costs = (production * prices_environmental * rates).sum( ("commodity", "year") ) # Fuel/energy costs - prices_fuel = filter_input( - prices, commodity=fuels, year=years.values, region=techs.region - ).ffill("year") - prices = filter_input(prices, year=years.values, region=techs.region).ffill("year") + prices_fuel = filter_input(prices, commodity=fuels, year=years.values).ffill("year") fuel = consumption(technologies=techs, production=production, prices=prices) fuel_costs = (fuel * prices_fuel * rates).sum(("commodity", "year")) @@ -410,7 +414,7 @@ def lifetime_levelized_cost_of_energy( ).sum("commodity") fixed_and_variable_costs = ((fixed_costs + variable_costs) * rates).sum("year") denominator = production.where(production > 0.0, 1e-6) - results = ( + result = ( installed_capacity_costs + fuel_costs + environmental_costs @@ -418,7 +422,7 @@ def lifetime_levelized_cost_of_energy( + fixed_and_variable_costs ) / (denominator.sel(commodity=products).sum("commodity") * rates).sum("year") - return results.where(np.isfinite(results)).fillna(0.0) + return result def annual_levelized_cost_of_energy( diff --git a/tests/test_objectives.py b/tests/test_objectives.py index 37b177673..5cfa28168 100644 --- a/tests/test_objectives.py +++ b/tests/test_objectives.py @@ -286,12 +286,8 @@ def test_net_present_value( """ import xarray from muse.commodities import is_enduse, is_fuel, is_material, is_pollutant - from muse.objectives import ( - capacity_to_service_demand, - discount_factor, - net_present_value, - ) - from muse.quantities import consumption + from muse.objectives import capacity_to_service_demand, net_present_value + from muse.quantities import consumption, discount_factor technologies.technical_life.loc[{"region": retro_agent.region}] = 10 From fa12549f61a49c8ee435d4ec6cf700f9ef31c013 Mon Sep 17 00:00:00 2001 From: Tom Bland Date: Wed, 21 Aug 2024 16:32:45 +0100 Subject: [PATCH 03/23] Abstract capacity_to_service_demand --- src/muse/objectives.py | 25 ++++++------------------- src/muse/outputs/mca.py | 39 +++++---------------------------------- src/muse/quantities.py | 23 +++++++++++++++++++---- 3 files changed, 30 insertions(+), 57 deletions(-) diff --git a/src/muse/objectives.py b/src/muse/objectives.py index 0485dcc82..9e8ed3737 100644 --- a/src/muse/objectives.py +++ b/src/muse/objectives.py @@ -260,23 +260,16 @@ def capacity_to_service_demand( **kwargs, ) -> xr.DataArray: """Minimum capacity required to fulfill the demand.""" - params = agent.filter_input( + from muse.quantities import capacity_to_service_demand + + techs = agent.filter_input( technologies[["utilization_factor", "fixed_outputs"]], year=agent.forecast_year, region=agent.region, technology=search_space.replacement, ).drop_vars("technology") hours = _represent_hours(market, search_space) - max_hours = hours.max() / hours.sum() - - commodity_output = params.fixed_outputs.sel(commodity=demand.commodity) - - max_demand = ( - demand.where(commodity_output > 0, 0) - / commodity_output.where(commodity_output > 0, 1) - ).max(("commodity", "timeslice")) - - return max_demand / params.utilization_factor / max_hours + return capacity_to_service_demand(demand=demand, technologies=techs, hours=hours) @register_objective @@ -361,7 +354,7 @@ def emission_cost( *args, **kwargs, ) -> xr.DataArray: - r"""Emission cost for each technology when fultfilling whole demand. + r"""Emission cost for each technology when fulfilling whole demand. Given the demand share :math:`D`, the emissions per amount produced :math:`E`, and the prices per emittant :math:`P`, then emissions costs :math:`C` are computed @@ -399,14 +392,8 @@ def capacity_in_use( **kwargs, ): from muse.commodities import is_enduse - from muse.timeslices import represent_hours - if "represent_hours" in market: - hours = market.represent_hours - elif "represent_hours" in search_space.coords: - hours = search_space.represent_hours - else: - hours = represent_hours(market.timeslice) + hours = _represent_hours(market, search_space) ufac = agent.filter_input( technologies.utilization_factor, diff --git a/src/muse/outputs/mca.py b/src/muse/outputs/mca.py index f0726f2cc..f813cc1f5 100644 --- a/src/muse/outputs/mca.py +++ b/src/muse/outputs/mca.py @@ -884,23 +884,10 @@ def metric_lcoe( def sector_lcoe(sector: AbstractSector, market: xr.Dataset, **kwargs) -> pd.DataFrame: """Levelized cost of energy () of technologies over their lifetime.""" - from muse.quantities import lifetime_levelized_cost_of_energy - - def capacity_to_service_demand(demand, technologies): - from muse.timeslices import represent_hours - - hours = represent_hours(demand.timeslice) - - max_hours = hours.max() / hours.sum() - - commodity_output = technologies.fixed_outputs.sel(commodity=demand.commodity) - - max_demand = ( - demand.where(commodity_output > 0, 0) - / commodity_output.where(commodity_output > 0, 1) - ).max(("commodity", "timeslice")) - - return max_demand / technologies.utilization_factor / max_hours + from muse.quantities import ( + lifetime_levelized_cost_of_energy, + capacity_to_service_demand, + ) # Filtering of the inputs data_sector: list[xr.DataArray] = [] @@ -978,23 +965,7 @@ def metric_eac( def sector_eac(sector: AbstractSector, market: xr.Dataset, **kwargs) -> pd.DataFrame: """Net Present Value of technologies over their lifetime.""" from muse.commodities import is_enduse, is_fuel, is_material, is_pollutant - from muse.quantities import consumption, discount_factor - - def capacity_to_service_demand(demand, technologies): - from muse.timeslices import represent_hours - - hours = represent_hours(demand.timeslice) - - max_hours = hours.max() / hours.sum() - - commodity_output = technologies.fixed_outputs.sel(commodity=demand.commodity) - - max_demand = ( - demand.where(commodity_output > 0, 0) - / commodity_output.where(commodity_output > 0, 1) - ).max(("commodity", "timeslice")) - - return max_demand / technologies.utilization_factor / max_hours + from muse.quantities import consumption, discount_factor, capacity_to_service_demand # Filtering of the inputs data_sector: list[xr.DataArray] = [] diff --git a/src/muse/quantities.py b/src/muse/quantities.py index 65252c6ee..a5947bf29 100644 --- a/src/muse/quantities.py +++ b/src/muse/quantities.py @@ -324,11 +324,7 @@ def lifetime_levelized_cost_of_energy( factor. Arguments: - agent: The agent of interest - demand: Demand for commodities - search_space: The search space space for replacement technologies technologies: All the technologies - market: The market parameters *args: Extra arguments (unused) **kwargs: Extra keyword arguments (unused) @@ -773,6 +769,25 @@ def group_assets(x: xr.DataArray) -> xr.DataArray: return result +def capacity_to_service_demand( + demand: xr.DataArray, + technologies: xr.Dataset, + hours=None, +) -> xr.DataArray: + """Minimum capacity required to fulfill the demand.""" + from muse.timeslices import represent_hours + + if hours is None: + hours = represent_hours(demand.timeslice) + max_hours = hours.max() / hours.sum() + commodity_output = technologies.fixed_outputs.sel(commodity=demand.commodity) + max_demand = ( + demand.where(commodity_output > 0, 0) + / commodity_output.where(commodity_output > 0, 1) + ).max(("commodity", "timeslice")) + return max_demand / technologies.utilization_factor / max_hours + + def discount_factor(years, interest_rate, mask=1.0): """Calculate an array with the rate (aka discount factor) values over the years.""" return mask / (1 + interest_rate) ** years From 618131e162eec833c3ae5d5fa27dacfb4cd5b98b Mon Sep 17 00:00:00 2001 From: Tom Bland Date: Wed, 21 Aug 2024 17:29:14 +0100 Subject: [PATCH 04/23] Move NPV, NPC, EAC and CRF code to quantities.py --- src/muse/objectives.py | 212 +++++++++++----------------------------- src/muse/outputs/mca.py | 141 +++----------------------- src/muse/quantities.py | 198 +++++++++++++++++++++++++++++++++++-- 3 files changed, 262 insertions(+), 289 deletions(-) diff --git a/src/muse/objectives.py b/src/muse/objectives.py index 9e8ed3737..532219cb5 100644 --- a/src/muse/objectives.py +++ b/src/muse/objectives.py @@ -502,35 +502,6 @@ def annual_levelized_cost_of_energy( return aLCOE(prices, techs).rename(technology="replacement").max("timeslice") -def capital_recovery_factor( - agent: Agent, search_space: xr.DataArray, technologies: xr.Dataset -) -> xr.DataArray: - """Capital recovery factor using interest rate and expected lifetime. - - The `capital recovery factor`_ is computed using the expression given by HOMER - Energy. - - .. _capital recovery factor: - https://www.homerenergy.com/products/pro/docs/3.15/capital_recovery_factor.html - - Arguments: - agent: The agent of interest - search_space: The search space space for replacement technologies - technologies: All the technologies - - Return: - xr.DataArray with the CRF calculated for the relevant technologies - """ - tech = agent.filter_input( - technologies[["technical_life", "interest_rate"]], - technology=search_space.replacement, - year=agent.forecast_year, - ).drop_vars("technology") - nyears = tech.technical_life.astype(int) - - return tech.interest_rate / (1 - (1 / (1 + tech.interest_rate) ** nyears)) - - @register_objective(name=["LCOE", "LLCOE"]) def lifetime_levelized_cost_of_energy( agent: Agent, @@ -631,136 +602,29 @@ def net_present_value( Return: xr.DataArray with the NPV calculated for the relevant technologies """ - from muse.commodities import is_enduse, is_fuel, is_material, is_pollutant - from muse.quantities import consumption, discount_factor + from muse.quantities import net_present_value from muse.timeslices import QuantityType, convert_timeslice - # Filtering of the inputs - tech = agent.filter_input( - technologies[ - [ - "technical_life", - "interest_rate", - "cap_par", - "cap_exp", - "var_par", - "var_exp", - "fix_par", - "fix_exp", - "fixed_outputs", - "fixed_inputs", - "flexible_inputs", - "utilization_factor", - ] - ], + techs = agent.filter_input( + technologies, technology=search_space.replacement, year=agent.forecast_year, ).drop_vars("technology") - nyears = tech.technical_life.astype(int) - interest_rate = tech.interest_rate - cap_par = tech.cap_par - cap_exp = tech.cap_exp - var_par = tech.var_par - var_exp = tech.var_exp - fix_par = tech.fix_par - fix_exp = tech.fix_exp - fixed_outputs = tech.fixed_outputs - utilization_factor = tech.utilization_factor - - # All years the simulation is running - # NOTE: see docstring about installation year - iyears = range( - agent.forecast_year, - max(agent.forecast_year + nyears.values.max(), agent.forecast_year + 1), - ) - years = xr.DataArray(iyears, coords={"year": iyears}, dims="year") - - # Filters - environmentals = is_pollutant(technologies.comm_usage) - material = is_material(technologies.comm_usage) - products = is_enduse(technologies.comm_usage) - fuels = is_fuel(technologies.comm_usage) - # Capacity + prices = cast(xr.DataArray, agent.filter_input(market.prices)) + capacity = capacity_to_service_demand( agent, demand, search_space, technologies, market ) - - # Evolution of rates with time - rates = discount_factor( - years - agent.forecast_year + 1, - interest_rate, - years <= agent.forecast_year + nyears, - ) - - # raw revenues --> Make the NPV more positive - # This production is the absolute maximum production, given the capacity - prices_non_env = agent.filter_input( - market.prices, commodity=products, year=years.values - ).ffill("year") - - production = capacity * fixed_outputs * utilization_factor + production = capacity * techs.fixed_outputs * techs.utilization_factor production = convert_timeslice(production, demand.timeslice, QuantityType.EXTENSIVE) - raw_revenues = (production * prices_non_env * rates).sum(("commodity", "year")) - - # raw costs --> make the NPV more negative - # Cost of installed capacity - installed_capacity_costs = convert_timeslice( - cap_par * (capacity**cap_exp), - demand.timeslice, - QuantityType.EXTENSIVE, - ) - - # Cost related to environmental products - prices_environmental = agent.filter_input( - market.prices, commodity=environmentals, year=years.values - ).ffill("year") - environmental_costs = (production * prices_environmental * rates).sum( - ("commodity", "year") - ) - - # Fuel/energy costs - prices_fuel = agent.filter_input( - market.prices, commodity=fuels, year=years.values - ).ffill("year") - prices = agent.filter_input(market.prices, year=years.values).ffill("year") - fuel = consumption(technologies=tech, production=production, prices=prices).sel( - commodity=fuels - ) - fuel_costs = (fuel * prices_fuel * rates).sum(("commodity", "year")) - - # Cost related to material other than fuel/energy and environmentals - prices_material = agent.filter_input( - market.prices, commodity=material, year=years.values - ).ffill("year") - material_costs = (production * prices_material * rates).sum(("commodity", "year")) - - # Fixed and Variable costs - fixed_costs = convert_timeslice( - fix_par * (capacity**fix_exp), - demand.timeslice, - QuantityType.EXTENSIVE, - ) - variable_costs = var_par * ( - (production.sel(commodity=products).sum("commodity")) ** var_exp - ) - assert set(fixed_costs.dims) == set(variable_costs.dims) - fixed_and_variable_costs = ((fixed_costs + variable_costs) * rates).sum("year") - - assert set(raw_revenues.dims) == set(installed_capacity_costs.dims) - assert set(raw_revenues.dims) == set(environmental_costs.dims) - assert set(raw_revenues.dims) == set(fuel_costs.dims) - assert set(raw_revenues.dims) == set(material_costs.dims) - assert set(raw_revenues.dims) == set(fixed_and_variable_costs.dims) - - results = raw_revenues - ( - +installed_capacity_costs - + environmental_costs - + material_costs - + fixed_and_variable_costs - + fuel_costs + results = net_present_value( + prices=prices, + technologies=techs, + capacity=capacity, + production=production, + year=agent.forecast_year, ) - return results @@ -783,9 +647,30 @@ def net_present_cost( .. seealso:: :py:func:`net_present_value`. """ - return -net_present_value( - agent, demand, search_space, technologies, market, *args, **kwargs + from muse.quantities import net_present_cost + from muse.timeslices import QuantityType, convert_timeslice + + techs = agent.filter_input( + technologies, + technology=search_space.replacement, + year=agent.forecast_year, + ).drop_vars("technology") + prices = cast(xr.DataArray, agent.filter_input(market.prices)) + + capacity = capacity_to_service_demand( + agent, demand, search_space, technologies, market ) + production = capacity * techs.fixed_outputs * techs.utilization_factor + production = convert_timeslice(production, demand.timeslice, QuantityType.EXTENSIVE) + + results = net_present_cost( + prices=prices, + technologies=techs, + capacity=capacity, + production=production, + year=agent.forecast_year, + ) + return results @register_objective(name="EAC") @@ -820,6 +705,27 @@ def equivalent_annual_cost( Return: xr.DataArray with the EAC calculated for the relevant technologies """ - npv = net_present_cost(agent, demand, search_space, technologies, market) - crf = capital_recovery_factor(agent, search_space, technologies) - return npv * crf + from muse.quantities import equivalent_annual_cost + from muse.timeslices import QuantityType, convert_timeslice + + techs = agent.filter_input( + technologies, + technology=search_space.replacement, + year=agent.forecast_year, + ).drop_vars("technology") + prices = cast(xr.DataArray, agent.filter_input(market.prices)) + + capacity = capacity_to_service_demand( + agent, demand, search_space, technologies, market + ) + production = capacity * techs.fixed_outputs * techs.utilization_factor + production = convert_timeslice(production, demand.timeslice, QuantityType.EXTENSIVE) + + results = equivalent_annual_cost( + prices=prices, + technologies=techs, + capacity=capacity, + production=production, + year=agent.forecast_year, + ) + return results diff --git a/src/muse/outputs/mca.py b/src/muse/outputs/mca.py index f813cc1f5..0e77cf7ef 100644 --- a/src/muse/outputs/mca.py +++ b/src/muse/outputs/mca.py @@ -964,8 +964,7 @@ def metric_eac( def sector_eac(sector: AbstractSector, market: xr.Dataset, **kwargs) -> pd.DataFrame: """Net Present Value of technologies over their lifetime.""" - from muse.commodities import is_enduse, is_fuel, is_material, is_pollutant - from muse.quantities import consumption, discount_factor, capacity_to_service_demand + from muse.quantities import equivalent_annual_cost, capacity_to_service_demand # Filtering of the inputs data_sector: list[xr.DataArray] = [] @@ -988,149 +987,31 @@ def sector_eac(sector: AbstractSector, market: xr.Dataset, **kwargs) -> pd.DataF i for i in agent_market["commodity"].values if i not in included ] agent_market.loc[dict(commodity=excluded)] = 0 - years = [output_year, agent.year] - agent_market["prices"] = agent.filter_input(market["prices"], year=years) - tech = agent.filter_input( - technologies[ - [ - "technical_life", - "interest_rate", - "cap_par", - "cap_exp", - "var_par", - "var_exp", - "fix_par", - "fix_exp", - "fixed_outputs", - "fixed_inputs", - "flexible_inputs", - "utilization_factor", - ] - ], + techs = agent.filter_input( + technologies, year=agent.year, - region=agent.region, - ) - nyears = tech.technical_life.astype(int) - interest_rate = tech.interest_rate - cap_par = tech.cap_par - cap_exp = tech.cap_exp - var_par = tech.var_par - var_exp = tech.var_exp - fix_par = tech.fix_par - fix_exp = tech.fix_exp - fixed_outputs = tech.fixed_outputs - utilization_factor = tech.utilization_factor - - # All years the simulation is running - # NOTE: see docstring about installation year - iyears = range( - agent.year, - max(agent.year + nyears.values.max(), agent.forecast_year), ) - years = xr.DataArray(iyears, coords={"year": iyears}, dims="year") - - prices = agent.filter_input(agent_market.prices, year=years.values) - # Filters - environmentals = is_pollutant(tech.comm_usage) - e = np.where(environmentals) - environmentals = environmentals[e].commodity.values - material = is_material(tech.comm_usage) - e = np.where(material) - material = material[e].commodity.values - products = is_enduse(tech.comm_usage) - e = np.where(products) - products = products[e].commodity.values - fuels = is_fuel(tech.comm_usage) - e = np.where(fuels) - fuels = fuels[e].commodity.values - # Capacity + prices = agent_market["prices"].sel(commodity=techs.commodity) demand = agent_market.consumption.sel(commodity=included) - capacity = capacity_to_service_demand(demand, tech) - - # Evolution of rates with time - rates = discount_factor( - years - agent.year + 1, interest_rate, years <= agent.year + nyears - ) - - production = capacity * fixed_outputs * utilization_factor + capacity = agent.filter_input(capacity_to_service_demand(demand, techs)) + production = capacity * techs.fixed_outputs * techs.utilization_factor production = convert_timeslice( production, demand.timeslice, QuantityType.EXTENSIVE, ) - # raw costs --> make the NPV more negative - # Cost of installed capacity - installed_capacity_costs = convert_timeslice( - cap_par * (capacity**cap_exp), - demand.timeslice, - QuantityType.EXTENSIVE, - ) - - # Cost related to environmental products - prices_environmental = agent.filter_input(prices, year=years.values).sel( - commodity=environmentals - ) - environmental_costs = ( - (production * prices_environmental * rates) - .sel(commodity=environmentals, year=years.values) - .sum(("commodity", "year")) - ) - - # Fuel/energy costs - prices_fuel = agent.filter_input(prices, year=years.values).sel( - commodity=fuels - ) - - fuel = consumption( - technologies=tech, - production=production.sel(region=tech.region), + result = equivalent_annual_cost( prices=prices, - ) - fuel_costs = (fuel * prices_fuel * rates).sum(("commodity", "year")) - - # Cost related to material other than fuel/energy and environmentals - prices_material = agent.filter_input(prices, year=years.values).sel( - commodity=material - ) # .ffill("year") - material_costs = (production * prices_material * rates).sum( - ("commodity", "year") - ) - - # Fixed and Variable costs - fixed_costs = convert_timeslice( - fix_par * (capacity**fix_exp), - demand.timeslice, - QuantityType.EXTENSIVE, - ) - variable_costs = ( - var_par * production.sel(commodity=products) ** var_exp - ).sum("commodity") - # assert set(fixed_costs.dims) == set(variable_costs.dims) - fixed_and_variable_costs = ((fixed_costs + variable_costs) * rates).sum( - "year" - ) - # raw revenues --> Make the NPV more positive - # This production is the absolute maximum production, - # given the capacity - raw_revenues = ( - (production * prices * rates) - .sel(commodity=products) - .sum(("commodity", "year")) + technologies=techs, + capacity=capacity, + production=production, + year=agent.year, ) - result = ( - installed_capacity_costs - + fuel_costs - + environmental_costs - + material_costs - + fixed_and_variable_costs - ) - raw_revenues - crf = interest_rate / (1 - (1 / (1 + interest_rate) ** nyears)) - result *= crf data_agent = result data_agent["agent"] = agent.name data_agent["category"] = agent.category diff --git a/src/muse/quantities.py b/src/muse/quantities.py index a5947bf29..0e8bab089 100644 --- a/src/muse/quantities.py +++ b/src/muse/quantities.py @@ -310,6 +310,174 @@ def consumption( return consumption + flex * production +def net_present_value(prices, technologies: xr.Dataset, capacity, production, year): + """Net present value (NPV) of the relevant technologies. + + The net present value of a Component is the present value of all the revenues that + a Component earns over its lifetime minus all the costs of installing and operating + it. Follows the definition of the `net present cost`_ given by HOMER Energy. + Metrics are calculated + .. _net present cost: + .. https://www.homerenergy.com/products/pro/docs/3.15/net_present_cost.html + + - energy commodities INPUTS are related to fuel costs + - environmental commodities OUTPUTS are related to environmental costs + - material and service commodities INPUTS are related to consumable costs + - fixed and variable costs are given as technodata inputs and depend on the + installed capacity and production (non-environmental), respectively + - capacity costs are given as technodata inputs and depend on the installed capacity + + Note: + Here, the installation year is always agent.forecast_year, + since objectives compute the + NPV for technologies to be installed in the current year. A more general NPV + computation (which would then live in quantities.py) would have to refer to + installation year of the technology. + + Arguments: + technologies: All the technologies + + Return: + xr.DataArray with the NPV calculated for the relevant technologies + """ + from muse.commodities import is_enduse, is_fuel, is_material, is_pollutant + from muse.timeslices import QuantityType, convert_timeslice + from muse.utilities import filter_input + + # Filtering of the inputs + techs = technologies[ + [ + "technical_life", + "interest_rate", + "cap_par", + "cap_exp", + "var_par", + "var_exp", + "fix_par", + "fix_exp", + "fixed_outputs", + "fixed_inputs", + "flexible_inputs", + "utilization_factor", + ] + ] + + # Years + life = techs.technical_life.astype(int) + iyears = range(year, max(year + life.values.max(), year + 1)) + years = xr.DataArray(iyears, coords={"year": iyears}, dims="year") + + # Evolution of rates with time + rates = discount_factor( + years - year + 1, + interest_rate=techs.interest_rate, + mask=years <= year + life, + ) + + # Filters + environmentals = is_pollutant(technologies.comm_usage) + material = is_material(technologies.comm_usage) + products = is_enduse(technologies.comm_usage) + fuels = is_fuel(technologies.comm_usage) + + # Revenue + prices_non_env = filter_input(prices, commodity=products, year=years.values).ffill( + "year" + ) + raw_revenues = (production * prices_non_env * rates).sum(("commodity", "year")) + + # Cost of installed capacity + installed_capacity_costs = convert_timeslice( + techs.cap_par * (capacity**techs.cap_exp), + prices.timeslice, + QuantityType.EXTENSIVE, + ) + + # Cost related to environmental products + prices_environmental = filter_input( + prices, commodity=environmentals, year=years.values + ).ffill("year") + environmental_costs = (production * prices_environmental * rates).sum( + ("commodity", "year") + ) + + # Fuel/energy costs + prices_fuel = filter_input(prices, commodity=fuels, year=years.values).ffill("year") + fuel = consumption(technologies=techs, production=production, prices=prices) + fuel_costs = (fuel * prices_fuel * rates).sum(("commodity", "year")) + + # Cost related to material other than fuel/energy and environmentals + prices_material = filter_input(prices, commodity=material, year=years.values).ffill( + "year" + ) + material_costs = (production * prices_material * rates).sum(("commodity", "year")) + + # Fixed and Variable costs + fixed_costs = convert_timeslice( + techs.fix_par * (capacity**techs.fix_exp), + prices.timeslice, + QuantityType.EXTENSIVE, + ) + variable_costs = techs.var_par * ( + (production.sel(commodity=products).sum("commodity")) ** techs.var_exp + ) + assert set(fixed_costs.dims) == set(variable_costs.dims) + fixed_and_variable_costs = ((fixed_costs + variable_costs) * rates).sum("year") + + assert set(raw_revenues.dims) == set(installed_capacity_costs.dims) + assert set(raw_revenues.dims) == set(environmental_costs.dims) + assert set(raw_revenues.dims) == set(fuel_costs.dims) + assert set(raw_revenues.dims) == set(material_costs.dims) + assert set(raw_revenues.dims) == set(fixed_and_variable_costs.dims) + + results = raw_revenues - ( + installed_capacity_costs + + fuel_costs + + environmental_costs + + material_costs + + fixed_and_variable_costs + ) + + return results + + +def net_present_cost(prices, technologies: xr.Dataset, capacity, production, year): + """Net present cost (NPC) of the relevant technologies. + + The net present cost of a Component is the present value of all the costs of + installing and operating the Component over the project lifetime, minus the present + value of all the revenues that it earns over the project lifetime. + + .. seealso:: + :py:func:`net_present_value`. + """ + return -net_present_value(prices, technologies, capacity, production, year) + + +def equivalent_annual_cost( + prices, technologies: xr.Dataset, capacity, production, year +): + """Equivalent annual costs (or annualized cost) of a technology. + + This is the cost that, if it were to occur equally in every year of the + project lifetime, would give the same net present cost as the actual cash + flow sequence associated with that component. The cost is computed using the + `annualized cost`_ expression given by HOMER Energy. + + .. _annualized cost: + https://www.homerenergy.com/products/pro/docs/3.15/annualized_cost.html + + Arguments: + technologies: All the technologies + + Return: + xr.DataArray with the EAC calculated for the relevant technologies + """ + npc = net_present_cost(prices, technologies, capacity, production, year) + crf = capital_recovery_factor(technologies) + return npc * crf + + def lifetime_levelized_cost_of_energy( prices: xr.DataArray, technologies: xr.Dataset, @@ -325,8 +493,6 @@ def lifetime_levelized_cost_of_energy( Arguments: technologies: All the technologies - *args: Extra arguments (unused) - **kwargs: Extra keyword arguments (unused) Return: xr.DataArray with the LCOE calculated for the relevant technologies @@ -354,10 +520,7 @@ def lifetime_levelized_cost_of_energy( # Years life = techs.technical_life.astype(int) - iyears = range( - year, - max(year + life.values.max(), year), - ) + iyears = range(year, max(year + life.values.max(), year)) years = xr.DataArray(iyears, coords={"year": iyears}, dims="year") # Evolution of rates with time @@ -788,6 +951,29 @@ def capacity_to_service_demand( return max_demand / technologies.utilization_factor / max_hours + + +def capital_recovery_factor(technologies: xr.Dataset) -> xr.DataArray: + """Capital recovery factor using interest rate and expected lifetime. + + The `capital recovery factor`_ is computed using the expression given by HOMER + Energy. + + .. _capital recovery factor: + https://www.homerenergy.com/products/pro/docs/3.15/capital_recovery_factor.html + + Arguments: + technologies: All the technologies + + Return: + xr.DataArray with the CRF calculated for the relevant technologies + """ + nyears = technologies.technical_life.astype(int) + return technologies.interest_rate / ( + 1 - (1 / (1 + technologies.interest_rate) ** nyears) + ) + + def discount_factor(years, interest_rate, mask=1.0): """Calculate an array with the rate (aka discount factor) values over the years.""" return mask / (1 + interest_rate) ** years From 753948e9792f44d912a64e327a6f15f4e14ff280 Mon Sep 17 00:00:00 2001 From: Tom Bland Date: Wed, 21 Aug 2024 17:30:13 +0100 Subject: [PATCH 05/23] Formatting --- src/muse/outputs/mca.py | 4 ++-- src/muse/quantities.py | 2 -- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/muse/outputs/mca.py b/src/muse/outputs/mca.py index 0e77cf7ef..6e214585a 100644 --- a/src/muse/outputs/mca.py +++ b/src/muse/outputs/mca.py @@ -885,8 +885,8 @@ def metric_lcoe( def sector_lcoe(sector: AbstractSector, market: xr.Dataset, **kwargs) -> pd.DataFrame: """Levelized cost of energy () of technologies over their lifetime.""" from muse.quantities import ( - lifetime_levelized_cost_of_energy, capacity_to_service_demand, + lifetime_levelized_cost_of_energy, ) # Filtering of the inputs @@ -964,7 +964,7 @@ def metric_eac( def sector_eac(sector: AbstractSector, market: xr.Dataset, **kwargs) -> pd.DataFrame: """Net Present Value of technologies over their lifetime.""" - from muse.quantities import equivalent_annual_cost, capacity_to_service_demand + from muse.quantities import capacity_to_service_demand, equivalent_annual_cost # Filtering of the inputs data_sector: list[xr.DataArray] = [] diff --git a/src/muse/quantities.py b/src/muse/quantities.py index 0e8bab089..2b120fd48 100644 --- a/src/muse/quantities.py +++ b/src/muse/quantities.py @@ -951,8 +951,6 @@ def capacity_to_service_demand( return max_demand / technologies.utilization_factor / max_hours - - def capital_recovery_factor(technologies: xr.Dataset) -> xr.DataArray: """Capital recovery factor using interest rate and expected lifetime. From 8d1b364bf4799f625589c67e0a7e28149d5583c6 Mon Sep 17 00:00:00 2001 From: Tom Bland Date: Thu, 22 Aug 2024 08:39:13 +0100 Subject: [PATCH 06/23] Create costs module --- src/muse/costs.py | 435 +++++++++++++++++++++++++++++++++++++++ src/muse/objectives.py | 126 ++++++------ src/muse/outputs/mca.py | 13 +- src/muse/quantities.py | 440 +--------------------------------------- 4 files changed, 506 insertions(+), 508 deletions(-) create mode 100644 src/muse/costs.py diff --git a/src/muse/costs.py b/src/muse/costs.py new file mode 100644 index 000000000..986b07f73 --- /dev/null +++ b/src/muse/costs.py @@ -0,0 +1,435 @@ +from typing import Optional, Union + +import numpy as np +import xarray as xr + +from muse.commodities import is_enduse, is_fuel, is_material, is_pollutant +from muse.quantities import consumption +from muse.timeslices import QuantityType, convert_timeslice +from muse.utilities import filter_input + + +def net_present_value(prices, technologies: xr.Dataset, capacity, production, year): + """Net present value (NPV) of the relevant technologies. + + The net present value of a Component is the present value of all the revenues that + a Component earns over its lifetime minus all the costs of installing and operating + it. Follows the definition of the `net present cost`_ given by HOMER Energy. + Metrics are calculated + .. _net present cost: + .. https://www.homerenergy.com/products/pro/docs/3.15/net_present_cost.html + + - energy commodities INPUTS are related to fuel costs + - environmental commodities OUTPUTS are related to environmental costs + - material and service commodities INPUTS are related to consumable costs + - fixed and variable costs are given as technodata inputs and depend on the + installed capacity and production (non-environmental), respectively + - capacity costs are given as technodata inputs and depend on the installed capacity + + Note: + Here, the installation year is always agent.forecast_year, + since objectives compute the + NPV for technologies to be installed in the current year. A more general NPV + computation (which would then live in quantities.py) would have to refer to + installation year of the technology. + + Arguments: + technologies: All the technologies + + Return: + xr.DataArray with the NPV calculated for the relevant technologies + """ + # Filtering of the inputs + techs = technologies[ + [ + "technical_life", + "interest_rate", + "cap_par", + "cap_exp", + "var_par", + "var_exp", + "fix_par", + "fix_exp", + "fixed_outputs", + "fixed_inputs", + "flexible_inputs", + "utilization_factor", + ] + ] + + # Years + life = techs.technical_life.astype(int) + iyears = range(year, max(year + life.values.max(), year + 1)) + years = xr.DataArray(iyears, coords={"year": iyears}, dims="year") + + # Evolution of rates with time + rates = discount_factor( + years - year + 1, + interest_rate=techs.interest_rate, + mask=years <= year + life, + ) + + # Filters + environmentals = is_pollutant(technologies.comm_usage) + material = is_material(technologies.comm_usage) + products = is_enduse(technologies.comm_usage) + fuels = is_fuel(technologies.comm_usage) + + # Revenue + prices_non_env = filter_input(prices, commodity=products, year=years.values).ffill( + "year" + ) + raw_revenues = (production * prices_non_env * rates).sum(("commodity", "year")) + + # Cost of installed capacity + installed_capacity_costs = convert_timeslice( + techs.cap_par * (capacity**techs.cap_exp), + prices.timeslice, + QuantityType.EXTENSIVE, + ) + + # Cost related to environmental products + prices_environmental = filter_input( + prices, commodity=environmentals, year=years.values + ).ffill("year") + environmental_costs = (production * prices_environmental * rates).sum( + ("commodity", "year") + ) + + # Fuel/energy costs + prices_fuel = filter_input(prices, commodity=fuels, year=years.values).ffill("year") + fuel = consumption(technologies=techs, production=production, prices=prices) + fuel_costs = (fuel * prices_fuel * rates).sum(("commodity", "year")) + + # Cost related to material other than fuel/energy and environmentals + prices_material = filter_input(prices, commodity=material, year=years.values).ffill( + "year" + ) + material_costs = (production * prices_material * rates).sum(("commodity", "year")) + + # Fixed and Variable costs + fixed_costs = convert_timeslice( + techs.fix_par * (capacity**techs.fix_exp), + prices.timeslice, + QuantityType.EXTENSIVE, + ) + variable_costs = techs.var_par * ( + (production.sel(commodity=products).sum("commodity")) ** techs.var_exp + ) + assert set(fixed_costs.dims) == set(variable_costs.dims) + fixed_and_variable_costs = ((fixed_costs + variable_costs) * rates).sum("year") + + assert set(raw_revenues.dims) == set(installed_capacity_costs.dims) + assert set(raw_revenues.dims) == set(environmental_costs.dims) + assert set(raw_revenues.dims) == set(fuel_costs.dims) + assert set(raw_revenues.dims) == set(material_costs.dims) + assert set(raw_revenues.dims) == set(fixed_and_variable_costs.dims) + + results = raw_revenues - ( + installed_capacity_costs + + fuel_costs + + environmental_costs + + material_costs + + fixed_and_variable_costs + ) + + return results + + +def net_present_cost(prices, technologies: xr.Dataset, capacity, production, year): + """Net present cost (NPC) of the relevant technologies. + + The net present cost of a Component is the present value of all the costs of + installing and operating the Component over the project lifetime, minus the present + value of all the revenues that it earns over the project lifetime. + + .. seealso:: + :py:func:`net_present_value`. + """ + return -net_present_value(prices, technologies, capacity, production, year) + + +def equivalent_annual_cost( + prices, technologies: xr.Dataset, capacity, production, year +): + """Equivalent annual costs (or annualized cost) of a technology. + + This is the cost that, if it were to occur equally in every year of the + project lifetime, would give the same net present cost as the actual cash + flow sequence associated with that component. The cost is computed using the + `annualized cost`_ expression given by HOMER Energy. + + .. _annualized cost: + https://www.homerenergy.com/products/pro/docs/3.15/annualized_cost.html + + Arguments: + technologies: All the technologies + + Return: + xr.DataArray with the EAC calculated for the relevant technologies + """ + npc = net_present_cost(prices, technologies, capacity, production, year) + crf = capital_recovery_factor(technologies) + return npc * crf + + +def lifetime_levelized_cost_of_energy( + prices: xr.DataArray, + technologies: xr.Dataset, + capacity, + production, + year, +): + """Levelized cost of energy (LCOE) of technologies over their lifetime. + + It follows the `simplified LCOE` given by NREL. The LCOE is set to zero for those + timeslices where the production is zero, normally due to a zero utilisation + factor. + + Arguments: + technologies: All the technologies + + Return: + xr.DataArray with the LCOE calculated for the relevant technologies + """ + techs = technologies[ + [ + "technical_life", + "interest_rate", + "cap_par", + "cap_exp", + "var_par", + "var_exp", + "fix_par", + "fix_exp", + "fixed_outputs", + "fixed_inputs", + "flexible_inputs", + "utilization_factor", + ] + ] + + # Years + life = techs.technical_life.astype(int) + iyears = range(year, max(year + life.values.max(), year)) + years = xr.DataArray(iyears, coords={"year": iyears}, dims="year") + + # Evolution of rates with time + rates = discount_factor( + years=years - year + 1, + interest_rate=techs.interest_rate, + mask=years <= year + life, + ) + + # Filters + environmentals = is_pollutant(technologies.comm_usage) + material = is_material(technologies.comm_usage) + products = is_enduse(technologies.comm_usage) + fuels = is_fuel(technologies.comm_usage) + + # Cost of installed capacity + installed_capacity_costs = convert_timeslice( + techs.cap_par * (capacity**techs.cap_exp), + prices.timeslice, + QuantityType.EXTENSIVE, + ) + + # Cost related to environmental products + prices_environmental = filter_input( + prices, commodity=environmentals, year=years.values + ).ffill("year") + environmental_costs = (production * prices_environmental * rates).sum( + ("commodity", "year") + ) + + # Fuel/energy costs + prices_fuel = filter_input(prices, commodity=fuels, year=years.values).ffill("year") + fuel = consumption(technologies=techs, production=production, prices=prices) + fuel_costs = (fuel * prices_fuel * rates).sum(("commodity", "year")) + + # Cost related to material other than fuel/energy and environmentals + prices_material = filter_input(prices, commodity=material, year=years.values).ffill( + "year" + ) + material_costs = (production * prices_material * rates).sum(("commodity", "year")) + + # Fixed and Variable costs + fixed_costs = convert_timeslice( + techs.fix_par * (capacity**techs.fix_exp), + prices.timeslice, + QuantityType.EXTENSIVE, + ) + variable_costs = ( + techs.var_par * production.sel(commodity=products) ** techs.var_exp + ).sum("commodity") + fixed_and_variable_costs = ((fixed_costs + variable_costs) * rates).sum("year") + denominator = production.where(production > 0.0, 1e-6) + result = ( + installed_capacity_costs + + fuel_costs + + environmental_costs + + material_costs + + fixed_and_variable_costs + ) / (denominator.sel(commodity=products).sum("commodity") * rates).sum("year") + + return result + + +def annual_levelized_cost_of_energy( + prices: xr.DataArray, + technologies: xr.Dataset, + interpolation: str = "linear", + fill_value: Union[int, str] = "extrapolate", + **filters, +) -> xr.DataArray: + """Undiscounted levelized cost of energy (LCOE) of technologies on each given year. + + It mostly follows the `simplified LCOE`_ given by NREL. In the argument description, + we use the following: + + * [h]: hour + * [y]: year + * [$]: unit of currency + * [E]: unit of energy + * [1]: dimensionless + + Arguments: + prices: [$/(Eh)] the price of all commodities, including consumables and fuels. + This dataarray contains at least timeslice and commodity dimensions. + + technologies: Describe the technologies, with at least the following parameters: + + * cap_par: [$/E] overnight capital cost + * interest_rate: [1] + * fix_par: [$/(Eh)] fixed costs of operation and maintenance costs + * var_par: [$/(Eh)] variable costs of operation and maintenance costs + * fixed_inputs: [1] == [(Eh)/(Eh)] ratio indicating the amount of commodity + consumed per units of energy created. + * fixed_outputs: [1] == [(Eh)/(Eh)] ration indicating the amount of + environmental pollutants produced per units of energy created. + + interpolation: interpolation method. + fill_value: Fill value for values outside the extrapolation range. + **filters: Anything by which prices can be filtered. + + Return: + The lifetime LCOE in [$/(Eh)] for each technology at each timeslice. + + .. _simplified LCOE: https://www.nrel.gov/analysis/tech-lcoe-documentation.html + """ + techs = technologies[ + [ + "technical_life", + "interest_rate", + "cap_par", + "var_par", + "fix_par", + "fixed_inputs", + "flexible_inputs", + "fixed_outputs", + "utilization_factor", + ] + ] + if "year" in techs.dims: + techs = techs.interp( + year=prices.year, method=interpolation, kwargs={"fill_value": fill_value} + ) + if filters is not None: + prices = prices.sel({k: v for k, v in filters.items() if k in prices.dims}) + techs = techs.sel({k: v for k, v in filters.items() if k in techs.dims}) + + assert {"timeslice", "commodity"}.issubset(prices.dims) + + life = techs.technical_life.astype(int) + + rates = techs.interest_rate / (1 - (1 + techs.interest_rate) ** (-life)) + + annualized_capital_costs = ( + convert_timeslice( + techs.cap_par * rates, + prices.timeslice, + QuantityType.EXTENSIVE, + ) + / techs.utilization_factor + ) + + o_and_e_costs = ( + convert_timeslice( + (techs.fix_par + techs.var_par), + prices.timeslice, + QuantityType.EXTENSIVE, + ) + / techs.utilization_factor + ) + + fuel_costs = (techs.fixed_inputs * prices).sum("commodity") + + fuel_costs += (techs.flexible_inputs * prices).sum("commodity") + if "region" in techs.dims: + env_costs = ( + (techs.fixed_outputs * prices) + .sel(region=techs.region) + .sel(commodity=is_pollutant(techs.comm_usage)) + .sum("commodity") + ) + else: + env_costs = ( + (techs.fixed_outputs * prices) + .sel(commodity=is_pollutant(techs.comm_usage)) + .sum("commodity") + ) + return annualized_capital_costs + o_and_e_costs + env_costs + fuel_costs + + +def supply_cost( + production: xr.DataArray, lcoe: xr.DataArray, asset_dim: Optional[str] = "asset" +) -> xr.DataArray: + """Supply cost given production and the levelized cost of energy. + + In practice, the supply cost is the weighted average LCOE over assets (`asset_dim`), + where the weights are the production. + + Arguments: + production: Amount of goods produced. In practice, production can be obtained + from the capacity for each asset via the method + `muse.quantities.production`. + lcoe: Levelized cost of energy for each good produced. In practice, it can be + obtained from market prices via + `muse.quantities.annual_levelized_cost_of_energy` or + `muse.quantities.lifetime_levelized_cost_of_energy`. + asset_dim: Name of the dimension(s) holding assets, processes or technologies. + """ + data = xr.Dataset(dict(production=production, prices=production * lcoe)) + if asset_dim is not None: + if "region" not in data.coords or len(data.region.dims) == 0: + data = data.sum(asset_dim) + else: + data = data.groupby("region").sum(asset_dim) + + return data.prices / data.production.where(np.abs(data.production) > 1e-15, np.inf) + + +def capital_recovery_factor(technologies: xr.Dataset) -> xr.DataArray: + """Capital recovery factor using interest rate and expected lifetime. + + The `capital recovery factor`_ is computed using the expression given by HOMER + Energy. + + .. _capital recovery factor: + https://www.homerenergy.com/products/pro/docs/3.15/capital_recovery_factor.html + + Arguments: + technologies: All the technologies + + Return: + xr.DataArray with the CRF calculated for the relevant technologies + """ + nyears = technologies.technical_life.astype(int) + return technologies.interest_rate / ( + 1 - (1 / (1 + technologies.interest_rate) ** nyears) + ) + + +def discount_factor(years, interest_rate, mask=1.0): + """Calculate an array with the rate (aka discount factor) values over the years.""" + return mask / (1 + interest_rate) ** years diff --git a/src/muse/objectives.py b/src/muse/objectives.py index 532219cb5..e5ac665cd 100644 --- a/src/muse/objectives.py +++ b/src/muse/objectives.py @@ -272,6 +272,60 @@ def capacity_to_service_demand( return capacity_to_service_demand(demand=demand, technologies=techs, hours=hours) +@register_objective +def capacity_in_use( + agent: Agent, + demand: xr.DataArray, + search_space: xr.DataArray, + technologies: xr.Dataset, + market: xr.Dataset, + *args, + **kwargs, +): + from muse.commodities import is_enduse + + hours = _represent_hours(market, search_space) + + ufac = agent.filter_input( + technologies.utilization_factor, + technology=search_space.replacement, + year=agent.forecast_year, + ).drop_vars("technology") + enduses = is_enduse(technologies.comm_usage.sel(commodity=demand.commodity)) + return ( + (demand.sel(commodity=enduses).sum("commodity") / hours).sum("timeslice") + * hours.sum() + / ufac + ) + + +@register_objective +def consumption( + agent: Agent, + demand: xr.DataArray, + search_space: xr.DataArray, + technologies: xr.Dataset, + market: xr.Dataset, + *args, + **kwargs, +) -> xr.DataArray: + """Commodity consumption when fulfilling the whole demand. + + Currently, the consumption is implemented for commodity_max == +infinity. + """ + from muse.quantities import consumption + + params = agent.filter_input( + technologies[["fixed_inputs", "flexible_inputs"]], + year=agent.forecast_year, + technology=search_space.replacement.values, + ) + prices = agent.filter_input(market.prices, year=agent.forecast_year) + demand = demand.where(search_space, 0).rename(replacement="technology") + result = consumption(technologies=params, prices=prices, production=demand) + return result.sum("commodity").rename(technology="replacement") + + @register_objective def fixed_costs( agent: Agent, @@ -381,60 +435,6 @@ def emission_cost( return total * (allemissions * prices).sum("commodity") -@register_objective -def capacity_in_use( - agent: Agent, - demand: xr.DataArray, - search_space: xr.DataArray, - technologies: xr.Dataset, - market: xr.Dataset, - *args, - **kwargs, -): - from muse.commodities import is_enduse - - hours = _represent_hours(market, search_space) - - ufac = agent.filter_input( - technologies.utilization_factor, - technology=search_space.replacement, - year=agent.forecast_year, - ).drop_vars("technology") - enduses = is_enduse(technologies.comm_usage.sel(commodity=demand.commodity)) - return ( - (demand.sel(commodity=enduses).sum("commodity") / hours).sum("timeslice") - * hours.sum() - / ufac - ) - - -@register_objective -def consumption( - agent: Agent, - demand: xr.DataArray, - search_space: xr.DataArray, - technologies: xr.Dataset, - market: xr.Dataset, - *args, - **kwargs, -) -> xr.DataArray: - """Commodity consumption when fulfilling the whole demand. - - Currently, the consumption is implemented for commodity_max == +infinity. - """ - from muse.quantities import consumption - - params = agent.filter_input( - technologies[["fixed_inputs", "flexible_inputs"]], - year=agent.forecast_year, - technology=search_space.replacement.values, - ) - prices = agent.filter_input(market.prices, year=agent.forecast_year) - demand = demand.where(search_space, 0).rename(replacement="technology") - result = consumption(technologies=params, prices=prices, production=demand) - return result.sum("commodity").rename(technology="replacement") - - @register_objective def fuel_consumption_cost( agent: Agent, @@ -494,7 +494,7 @@ def annual_levelized_cost_of_energy( Return: xr.DataArray with the LCOE calculated for the relevant technologies """ - from muse.quantities import annual_levelized_cost_of_energy as aLCOE + from muse.costs import annual_levelized_cost_of_energy as aLCOE techs = agent.filter_input(technologies, technology=search_space.replacement.values) assert isinstance(techs, xr.Dataset) @@ -530,7 +530,7 @@ def lifetime_levelized_cost_of_energy( Return: xr.DataArray with the LCOE calculated for the relevant technologies """ - from muse.quantities import lifetime_levelized_cost_of_energy + from muse.costs import lifetime_levelized_cost_of_energy as LCOE from muse.timeslices import QuantityType, convert_timeslice techs = agent.filter_input( @@ -546,7 +546,7 @@ def lifetime_levelized_cost_of_energy( production = capacity * techs.fixed_outputs * techs.utilization_factor production = convert_timeslice(production, demand.timeslice, QuantityType.EXTENSIVE) - results = lifetime_levelized_cost_of_energy( + results = LCOE( prices=prices, technologies=techs, capacity=capacity, @@ -602,7 +602,7 @@ def net_present_value( Return: xr.DataArray with the NPV calculated for the relevant technologies """ - from muse.quantities import net_present_value + from muse.costs import net_present_value as NPV from muse.timeslices import QuantityType, convert_timeslice techs = agent.filter_input( @@ -618,7 +618,7 @@ def net_present_value( production = capacity * techs.fixed_outputs * techs.utilization_factor production = convert_timeslice(production, demand.timeslice, QuantityType.EXTENSIVE) - results = net_present_value( + results = NPV( prices=prices, technologies=techs, capacity=capacity, @@ -647,7 +647,7 @@ def net_present_cost( .. seealso:: :py:func:`net_present_value`. """ - from muse.quantities import net_present_cost + from muse.costs import net_present_cost as NPC from muse.timeslices import QuantityType, convert_timeslice techs = agent.filter_input( @@ -663,7 +663,7 @@ def net_present_cost( production = capacity * techs.fixed_outputs * techs.utilization_factor production = convert_timeslice(production, demand.timeslice, QuantityType.EXTENSIVE) - results = net_present_cost( + results = NPC( prices=prices, technologies=techs, capacity=capacity, @@ -705,7 +705,7 @@ def equivalent_annual_cost( Return: xr.DataArray with the EAC calculated for the relevant technologies """ - from muse.quantities import equivalent_annual_cost + from muse.costs import equivalent_annual_cost as EAC from muse.timeslices import QuantityType, convert_timeslice techs = agent.filter_input( @@ -721,7 +721,7 @@ def equivalent_annual_cost( production = capacity * techs.fixed_outputs * techs.utilization_factor production = convert_timeslice(production, demand.timeslice, QuantityType.EXTENSIVE) - results = equivalent_annual_cost( + results = EAC( prices=prices, technologies=techs, capacity=capacity, diff --git a/src/muse/outputs/mca.py b/src/muse/outputs/mca.py index 6e214585a..adabf600f 100644 --- a/src/muse/outputs/mca.py +++ b/src/muse/outputs/mca.py @@ -884,10 +884,8 @@ def metric_lcoe( def sector_lcoe(sector: AbstractSector, market: xr.Dataset, **kwargs) -> pd.DataFrame: """Levelized cost of energy () of technologies over their lifetime.""" - from muse.quantities import ( - capacity_to_service_demand, - lifetime_levelized_cost_of_energy, - ) + from muse.costs import lifetime_levelized_cost_of_energy as LCOE + from muse.quantities import capacity_to_service_demand # Filtering of the inputs data_sector: list[xr.DataArray] = [] @@ -927,7 +925,7 @@ def sector_lcoe(sector: AbstractSector, market: xr.Dataset, **kwargs) -> pd.Data QuantityType.EXTENSIVE, ) - result = lifetime_levelized_cost_of_energy( + result = LCOE( prices=prices, technologies=techs, capacity=capacity, @@ -964,7 +962,8 @@ def metric_eac( def sector_eac(sector: AbstractSector, market: xr.Dataset, **kwargs) -> pd.DataFrame: """Net Present Value of technologies over their lifetime.""" - from muse.quantities import capacity_to_service_demand, equivalent_annual_cost + from muse.costs import equivalent_annual_cost as EAC + from muse.quantities import capacity_to_service_demand # Filtering of the inputs data_sector: list[xr.DataArray] = [] @@ -1004,7 +1003,7 @@ def sector_eac(sector: AbstractSector, market: xr.Dataset, **kwargs) -> pd.DataF QuantityType.EXTENSIVE, ) - result = equivalent_annual_cost( + result = EAC( prices=prices, technologies=techs, capacity=capacity, diff --git a/src/muse/quantities.py b/src/muse/quantities.py index 2b120fd48..6d232d571 100644 --- a/src/muse/quantities.py +++ b/src/muse/quantities.py @@ -310,389 +310,6 @@ def consumption( return consumption + flex * production -def net_present_value(prices, technologies: xr.Dataset, capacity, production, year): - """Net present value (NPV) of the relevant technologies. - - The net present value of a Component is the present value of all the revenues that - a Component earns over its lifetime minus all the costs of installing and operating - it. Follows the definition of the `net present cost`_ given by HOMER Energy. - Metrics are calculated - .. _net present cost: - .. https://www.homerenergy.com/products/pro/docs/3.15/net_present_cost.html - - - energy commodities INPUTS are related to fuel costs - - environmental commodities OUTPUTS are related to environmental costs - - material and service commodities INPUTS are related to consumable costs - - fixed and variable costs are given as technodata inputs and depend on the - installed capacity and production (non-environmental), respectively - - capacity costs are given as technodata inputs and depend on the installed capacity - - Note: - Here, the installation year is always agent.forecast_year, - since objectives compute the - NPV for technologies to be installed in the current year. A more general NPV - computation (which would then live in quantities.py) would have to refer to - installation year of the technology. - - Arguments: - technologies: All the technologies - - Return: - xr.DataArray with the NPV calculated for the relevant technologies - """ - from muse.commodities import is_enduse, is_fuel, is_material, is_pollutant - from muse.timeslices import QuantityType, convert_timeslice - from muse.utilities import filter_input - - # Filtering of the inputs - techs = technologies[ - [ - "technical_life", - "interest_rate", - "cap_par", - "cap_exp", - "var_par", - "var_exp", - "fix_par", - "fix_exp", - "fixed_outputs", - "fixed_inputs", - "flexible_inputs", - "utilization_factor", - ] - ] - - # Years - life = techs.technical_life.astype(int) - iyears = range(year, max(year + life.values.max(), year + 1)) - years = xr.DataArray(iyears, coords={"year": iyears}, dims="year") - - # Evolution of rates with time - rates = discount_factor( - years - year + 1, - interest_rate=techs.interest_rate, - mask=years <= year + life, - ) - - # Filters - environmentals = is_pollutant(technologies.comm_usage) - material = is_material(technologies.comm_usage) - products = is_enduse(technologies.comm_usage) - fuels = is_fuel(technologies.comm_usage) - - # Revenue - prices_non_env = filter_input(prices, commodity=products, year=years.values).ffill( - "year" - ) - raw_revenues = (production * prices_non_env * rates).sum(("commodity", "year")) - - # Cost of installed capacity - installed_capacity_costs = convert_timeslice( - techs.cap_par * (capacity**techs.cap_exp), - prices.timeslice, - QuantityType.EXTENSIVE, - ) - - # Cost related to environmental products - prices_environmental = filter_input( - prices, commodity=environmentals, year=years.values - ).ffill("year") - environmental_costs = (production * prices_environmental * rates).sum( - ("commodity", "year") - ) - - # Fuel/energy costs - prices_fuel = filter_input(prices, commodity=fuels, year=years.values).ffill("year") - fuel = consumption(technologies=techs, production=production, prices=prices) - fuel_costs = (fuel * prices_fuel * rates).sum(("commodity", "year")) - - # Cost related to material other than fuel/energy and environmentals - prices_material = filter_input(prices, commodity=material, year=years.values).ffill( - "year" - ) - material_costs = (production * prices_material * rates).sum(("commodity", "year")) - - # Fixed and Variable costs - fixed_costs = convert_timeslice( - techs.fix_par * (capacity**techs.fix_exp), - prices.timeslice, - QuantityType.EXTENSIVE, - ) - variable_costs = techs.var_par * ( - (production.sel(commodity=products).sum("commodity")) ** techs.var_exp - ) - assert set(fixed_costs.dims) == set(variable_costs.dims) - fixed_and_variable_costs = ((fixed_costs + variable_costs) * rates).sum("year") - - assert set(raw_revenues.dims) == set(installed_capacity_costs.dims) - assert set(raw_revenues.dims) == set(environmental_costs.dims) - assert set(raw_revenues.dims) == set(fuel_costs.dims) - assert set(raw_revenues.dims) == set(material_costs.dims) - assert set(raw_revenues.dims) == set(fixed_and_variable_costs.dims) - - results = raw_revenues - ( - installed_capacity_costs - + fuel_costs - + environmental_costs - + material_costs - + fixed_and_variable_costs - ) - - return results - - -def net_present_cost(prices, technologies: xr.Dataset, capacity, production, year): - """Net present cost (NPC) of the relevant technologies. - - The net present cost of a Component is the present value of all the costs of - installing and operating the Component over the project lifetime, minus the present - value of all the revenues that it earns over the project lifetime. - - .. seealso:: - :py:func:`net_present_value`. - """ - return -net_present_value(prices, technologies, capacity, production, year) - - -def equivalent_annual_cost( - prices, technologies: xr.Dataset, capacity, production, year -): - """Equivalent annual costs (or annualized cost) of a technology. - - This is the cost that, if it were to occur equally in every year of the - project lifetime, would give the same net present cost as the actual cash - flow sequence associated with that component. The cost is computed using the - `annualized cost`_ expression given by HOMER Energy. - - .. _annualized cost: - https://www.homerenergy.com/products/pro/docs/3.15/annualized_cost.html - - Arguments: - technologies: All the technologies - - Return: - xr.DataArray with the EAC calculated for the relevant technologies - """ - npc = net_present_cost(prices, technologies, capacity, production, year) - crf = capital_recovery_factor(technologies) - return npc * crf - - -def lifetime_levelized_cost_of_energy( - prices: xr.DataArray, - technologies: xr.Dataset, - capacity, - production, - year, -): - """Levelized cost of energy (LCOE) of technologies over their lifetime. - - It follows the `simplified LCOE` given by NREL. The LCOE is set to zero for those - timeslices where the production is zero, normally due to a zero utilisation - factor. - - Arguments: - technologies: All the technologies - - Return: - xr.DataArray with the LCOE calculated for the relevant technologies - """ - from muse.commodities import is_enduse, is_fuel, is_material, is_pollutant - from muse.timeslices import QuantityType, convert_timeslice - from muse.utilities import filter_input - - techs = technologies[ - [ - "technical_life", - "interest_rate", - "cap_par", - "cap_exp", - "var_par", - "var_exp", - "fix_par", - "fix_exp", - "fixed_outputs", - "fixed_inputs", - "flexible_inputs", - "utilization_factor", - ] - ] - - # Years - life = techs.technical_life.astype(int) - iyears = range(year, max(year + life.values.max(), year)) - years = xr.DataArray(iyears, coords={"year": iyears}, dims="year") - - # Evolution of rates with time - rates = discount_factor( - years=years - year + 1, - interest_rate=techs.interest_rate, - mask=years <= year + life, - ) - - # Filters - environmentals = is_pollutant(technologies.comm_usage) - material = is_material(technologies.comm_usage) - products = is_enduse(technologies.comm_usage) - fuels = is_fuel(technologies.comm_usage) - - # Cost of installed capacity - installed_capacity_costs = convert_timeslice( - techs.cap_par * (capacity**techs.cap_exp), - prices.timeslice, - QuantityType.EXTENSIVE, - ) - - # Cost related to environmental products - prices_environmental = filter_input( - prices, commodity=environmentals, year=years.values - ).ffill("year") - environmental_costs = (production * prices_environmental * rates).sum( - ("commodity", "year") - ) - - # Fuel/energy costs - prices_fuel = filter_input(prices, commodity=fuels, year=years.values).ffill("year") - fuel = consumption(technologies=techs, production=production, prices=prices) - fuel_costs = (fuel * prices_fuel * rates).sum(("commodity", "year")) - - # Cost related to material other than fuel/energy and environmentals - prices_material = filter_input(prices, commodity=material, year=years.values).ffill( - "year" - ) - material_costs = (production * prices_material * rates).sum(("commodity", "year")) - - # Fixed and Variable costs - fixed_costs = convert_timeslice( - techs.fix_par * (capacity**techs.fix_exp), - prices.timeslice, - QuantityType.EXTENSIVE, - ) - variable_costs = ( - techs.var_par * production.sel(commodity=products) ** techs.var_exp - ).sum("commodity") - fixed_and_variable_costs = ((fixed_costs + variable_costs) * rates).sum("year") - denominator = production.where(production > 0.0, 1e-6) - result = ( - installed_capacity_costs - + fuel_costs - + environmental_costs - + material_costs - + fixed_and_variable_costs - ) / (denominator.sel(commodity=products).sum("commodity") * rates).sum("year") - - return result - - -def annual_levelized_cost_of_energy( - prices: xr.DataArray, - technologies: xr.Dataset, - interpolation: str = "linear", - fill_value: Union[int, str] = "extrapolate", - **filters, -) -> xr.DataArray: - """Undiscounted levelized cost of energy (LCOE) of technologies on each given year. - - It mostly follows the `simplified LCOE`_ given by NREL. In the argument description, - we use the following: - - * [h]: hour - * [y]: year - * [$]: unit of currency - * [E]: unit of energy - * [1]: dimensionless - - Arguments: - prices: [$/(Eh)] the price of all commodities, including consumables and fuels. - This dataarray contains at least timeslice and commodity dimensions. - - technologies: Describe the technologies, with at least the following parameters: - - * cap_par: [$/E] overnight capital cost - * interest_rate: [1] - * fix_par: [$/(Eh)] fixed costs of operation and maintenance costs - * var_par: [$/(Eh)] variable costs of operation and maintenance costs - * fixed_inputs: [1] == [(Eh)/(Eh)] ratio indicating the amount of commodity - consumed per units of energy created. - * fixed_outputs: [1] == [(Eh)/(Eh)] ration indicating the amount of - environmental pollutants produced per units of energy created. - - interpolation: interpolation method. - fill_value: Fill value for values outside the extrapolation range. - **filters: Anything by which prices can be filtered. - - Return: - The lifetime LCOE in [$/(Eh)] for each technology at each timeslice. - - .. _simplified LCOE: https://www.nrel.gov/analysis/tech-lcoe-documentation.html - """ - from muse.commodities import is_pollutant - from muse.timeslices import QuantityType, convert_timeslice - - techs = technologies[ - [ - "technical_life", - "interest_rate", - "cap_par", - "var_par", - "fix_par", - "fixed_inputs", - "flexible_inputs", - "fixed_outputs", - "utilization_factor", - ] - ] - if "year" in techs.dims: - techs = techs.interp( - year=prices.year, method=interpolation, kwargs={"fill_value": fill_value} - ) - if filters is not None: - prices = prices.sel({k: v for k, v in filters.items() if k in prices.dims}) - techs = techs.sel({k: v for k, v in filters.items() if k in techs.dims}) - - assert {"timeslice", "commodity"}.issubset(prices.dims) - - life = techs.technical_life.astype(int) - - rates = techs.interest_rate / (1 - (1 + techs.interest_rate) ** (-life)) - - annualized_capital_costs = ( - convert_timeslice( - techs.cap_par * rates, - prices.timeslice, - QuantityType.EXTENSIVE, - ) - / techs.utilization_factor - ) - - o_and_e_costs = ( - convert_timeslice( - (techs.fix_par + techs.var_par), - prices.timeslice, - QuantityType.EXTENSIVE, - ) - / techs.utilization_factor - ) - - fuel_costs = (techs.fixed_inputs * prices).sum("commodity") - - fuel_costs += (techs.flexible_inputs * prices).sum("commodity") - if "region" in techs.dims: - env_costs = ( - (techs.fixed_outputs * prices) - .sel(region=techs.region) - .sel(commodity=is_pollutant(techs.comm_usage)) - .sum("commodity") - ) - else: - env_costs = ( - (techs.fixed_outputs * prices) - .sel(commodity=is_pollutant(techs.comm_usage)) - .sum("commodity") - ) - return annualized_capital_costs + o_and_e_costs + env_costs + fuel_costs - - def maximum_production(technologies: xr.Dataset, capacity: xr.DataArray, **filters): r"""Production for a given capacity. @@ -757,12 +374,13 @@ def demand_matched_production( **filters: keyword arguments with which to filter the input datasets and data arrays., e.g. region, or year. """ + from muse.costs import annual_levelized_cost_of_energy as ALCOE from muse.demand_matching import demand_matching from muse.timeslices import QuantityType, convert_timeslice from muse.utilities import broadcast_techs technodata = cast(xr.Dataset, broadcast_techs(technologies, capacity)) - cost = annual_levelized_cost_of_energy(prices, technodata, **filters) + cost = ALCOE(prices, technodata, **filters) max_production = maximum_production(technodata, capacity, **filters) assert ("timeslice" in demand.dims) == ("timeslice" in cost.dims) if "timeslice" in demand.dims and "timeslice" not in max_production.dims: @@ -823,34 +441,6 @@ def capacity_in_use( return capa_in_use -def supply_cost( - production: xr.DataArray, lcoe: xr.DataArray, asset_dim: Optional[str] = "asset" -) -> xr.DataArray: - """Supply cost given production and the levelized cost of energy. - - In practice, the supply cost is the weighted average LCOE over assets (`asset_dim`), - where the weights are the production. - - Arguments: - production: Amount of goods produced. In practice, production can be obtained - from the capacity for each asset via the method - `muse.quantities.production`. - lcoe: Levelized cost of energy for each good produced. In practice, it can be - obtained from market prices via - `muse.quantities.annual_levelized_cost_of_energy` or - `muse.quantities.lifetime_levelized_cost_of_energy`. - asset_dim: Name of the dimension(s) holding assets, processes or technologies. - """ - data = xr.Dataset(dict(production=production, prices=production * lcoe)) - if asset_dim is not None: - if "region" not in data.coords or len(data.region.dims) == 0: - data = data.sum(asset_dim) - else: - data = data.groupby("region").sum(asset_dim) - - return data.prices / data.production.where(np.abs(data.production) > 1e-15, np.inf) - - def costed_production( demand: xr.Dataset, costs: xr.DataArray, @@ -949,29 +539,3 @@ def capacity_to_service_demand( / commodity_output.where(commodity_output > 0, 1) ).max(("commodity", "timeslice")) return max_demand / technologies.utilization_factor / max_hours - - -def capital_recovery_factor(technologies: xr.Dataset) -> xr.DataArray: - """Capital recovery factor using interest rate and expected lifetime. - - The `capital recovery factor`_ is computed using the expression given by HOMER - Energy. - - .. _capital recovery factor: - https://www.homerenergy.com/products/pro/docs/3.15/capital_recovery_factor.html - - Arguments: - technologies: All the technologies - - Return: - xr.DataArray with the CRF calculated for the relevant technologies - """ - nyears = technologies.technical_life.astype(int) - return technologies.interest_rate / ( - 1 - (1 / (1 + technologies.interest_rate) ** nyears) - ) - - -def discount_factor(years, interest_rate, mask=1.0): - """Calculate an array with the rate (aka discount factor) values over the years.""" - return mask / (1 + interest_rate) ** years From 1641fc9929d89699b4f948d0aaee516c63564086 Mon Sep 17 00:00:00 2001 From: Tom Bland Date: Thu, 22 Aug 2024 09:20:28 +0100 Subject: [PATCH 07/23] Fix imports, homogenise objectives code --- src/muse/costs.py | 4 +- src/muse/objectives.py | 108 ++++++++++++++++++------------------- src/muse/production.py | 4 +- src/muse/sectors/sector.py | 7 +-- tests/test_quantities.py | 14 ++--- 5 files changed, 67 insertions(+), 70 deletions(-) diff --git a/src/muse/costs.py b/src/muse/costs.py index 986b07f73..2a722ae89 100644 --- a/src/muse/costs.py +++ b/src/muse/costs.py @@ -395,8 +395,8 @@ def supply_cost( `muse.quantities.production`. lcoe: Levelized cost of energy for each good produced. In practice, it can be obtained from market prices via - `muse.quantities.annual_levelized_cost_of_energy` or - `muse.quantities.lifetime_levelized_cost_of_energy`. + `muse.costs.annual_levelized_cost_of_energy` or + `muse.costs.lifetime_levelized_cost_of_energy`. asset_dim: Name of the dimension(s) holding assets, processes or technologies. """ data = xr.Dataset(dict(production=production, prices=production * lcoe)) diff --git a/src/muse/objectives.py b/src/muse/objectives.py index e5ac665cd..dcf40956e 100644 --- a/src/muse/objectives.py +++ b/src/muse/objectives.py @@ -203,12 +203,12 @@ def comfort( **kwargs, ) -> xr.DataArray: """Comfort value provided by technologies.""" - output = agent.filter_input( - technologies.comfort, - year=agent.forecast_year, + techs = agent.filter_input( + technologies, technology=search_space.replacement, + year=agent.forecast_year, ).drop_vars("technology") - return output + return techs.comfort @register_objective @@ -221,13 +221,12 @@ def efficiency( **kwargs, ) -> xr.DataArray: """Efficiency of the technologies.""" - result = agent.filter_input( - technologies.efficiency, - year=agent.forecast_year, + techs = agent.filter_input( + technologies, technology=search_space.replacement, + year=agent.forecast_year, ).drop_vars("technology") - assert isinstance(result, xr.DataArray) - return result + return techs.efficiency def _represent_hours(market: xr.Dataset, search_space: xr.DataArray) -> xr.DataArray: @@ -263,10 +262,9 @@ def capacity_to_service_demand( from muse.quantities import capacity_to_service_demand techs = agent.filter_input( - technologies[["utilization_factor", "fixed_outputs"]], - year=agent.forecast_year, - region=agent.region, + technologies, technology=search_space.replacement, + year=agent.forecast_year, ).drop_vars("technology") hours = _represent_hours(market, search_space) return capacity_to_service_demand(demand=demand, technologies=techs, hours=hours) @@ -286,16 +284,16 @@ def capacity_in_use( hours = _represent_hours(market, search_space) - ufac = agent.filter_input( - technologies.utilization_factor, + techs = agent.filter_input( + technologies, technology=search_space.replacement, year=agent.forecast_year, ).drop_vars("technology") - enduses = is_enduse(technologies.comm_usage.sel(commodity=demand.commodity)) + enduses = is_enduse(techs.comm_usage.sel(commodity=demand.commodity)) return ( (demand.sel(commodity=enduses).sum("commodity") / hours).sum("timeslice") * hours.sum() - / ufac + / techs.utilization_factor ) @@ -315,15 +313,15 @@ def consumption( """ from muse.quantities import consumption - params = agent.filter_input( - technologies[["fixed_inputs", "flexible_inputs"]], + techs = agent.filter_input( + technologies, + technology=search_space.replacement, year=agent.forecast_year, - technology=search_space.replacement.values, - ) + ).drop_vars("technology") prices = agent.filter_input(market.prices, year=agent.forecast_year) - demand = demand.where(search_space, 0).rename(replacement="technology") - result = consumption(technologies=params, prices=prices, production=demand) - return result.sum("commodity").rename(technology="replacement") + demand = demand.where(search_space, 0) + result = consumption(technologies=techs, prices=prices, production=demand) + return result.sum("commodity") @register_objective @@ -351,16 +349,18 @@ def fixed_costs( """ from muse.timeslices import QuantityType, convert_timeslice - cfd = capacity_to_service_demand( - agent, demand, search_space, technologies, market, *args, **kwargs - ) - data = agent.filter_input( - technologies[["fix_par", "fix_exp"]], + techs = agent.filter_input( + technologies, technology=search_space.replacement, year=agent.forecast_year, ).drop_vars("technology") + + capacity = capacity_to_service_demand( + agent, demand, search_space, techs, market, *args, **kwargs + ) + result = convert_timeslice( - data.fix_par * (cfd**data.fix_exp), + techs.fix_par * (capacity**techs.fix_exp), demand.timeslice, QuantityType.EXTENSIVE, ) @@ -385,13 +385,13 @@ def capital_costs( """ from muse.timeslices import QuantityType, convert_timeslice - data = agent.filter_input( - technologies[["cap_par", "scaling_size", "cap_exp"]], + techs = agent.filter_input( + technologies, technology=search_space.replacement, year=agent.forecast_year, ).drop_vars("technology") result = convert_timeslice( - data.cap_par * (data.scaling_size**data.cap_exp), + techs.cap_par * (techs.scaling_size**techs.cap_exp), demand.timeslice, QuantityType.EXTENSIVE, ) @@ -422,17 +422,17 @@ def emission_cost( """ from muse.commodities import is_enduse, is_pollutant - enduses = is_enduse(technologies.comm_usage.sel(commodity=demand.commodity)) - total = demand.sel(commodity=enduses).sum("commodity") - allemissions = agent.filter_input( - technologies.fixed_outputs, - commodity=is_pollutant(technologies.comm_usage), + techs = agent.filter_input( + technologies, technology=search_space.replacement, year=agent.forecast_year, ).drop_vars("technology") + + enduses = is_enduse(technologies.comm_usage.sel(commodity=demand.commodity)) + total = demand.sel(commodity=enduses).sum("commodity") envs = is_pollutant(technologies.comm_usage) prices = agent.filter_input(market.prices, year=agent.forecast_year, commodity=envs) - return total * (allemissions * prices).sum("commodity") + return total * (techs.fixed_outputs * prices).sum("commodity") @register_objective @@ -449,22 +449,18 @@ def fuel_consumption_cost( from muse.commodities import is_fuel from muse.quantities import consumption - commodity = is_fuel(technologies.comm_usage.sel(commodity=market.commodity)) - params = agent.filter_input( - technologies[["fixed_inputs", "flexible_inputs"]], + techs = agent.filter_input( + technologies, + technology=search_space.replacement, year=agent.forecast_year, - technology=search_space.replacement.values, - ) + ).drop_vars("technology") + + commodity = is_fuel(techs.comm_usage.sel(commodity=market.commodity)) prices = agent.filter_input(market.prices, year=agent.forecast_year) - demand = demand.where(search_space, 0).rename(replacement="technology") - fcons = consumption(technologies=params, prices=prices, production=demand) + demand = demand.where(search_space, 0) + fcons = consumption(technologies=techs, prices=prices, production=demand) - return ( - (fcons * prices) - .sel(commodity=commodity) - .sum("commodity") - .rename(technology="replacement") - ) + return (fcons * prices).sel(commodity=commodity).sum("commodity") @register_objective(name=["ALCOE"]) @@ -496,10 +492,14 @@ def annual_levelized_cost_of_energy( """ from muse.costs import annual_levelized_cost_of_energy as aLCOE - techs = agent.filter_input(technologies, technology=search_space.replacement.values) - assert isinstance(techs, xr.Dataset) + techs = agent.filter_input( + technologies, + technology=search_space.replacement, + year=agent.forecast_year, + ).drop_vars("technology") + prices = cast(xr.DataArray, agent.filter_input(market.prices)) - return aLCOE(prices, techs).rename(technology="replacement").max("timeslice") + return aLCOE(prices, techs).max("timeslice") @register_objective(name=["LCOE", "LLCOE"]) diff --git a/src/muse/production.py b/src/muse/production.py index b8155bacc..95124c31c 100644 --- a/src/muse/production.py +++ b/src/muse/production.py @@ -133,7 +133,7 @@ def demand_matched_production( costs: str = "prices", ) -> xr.DataArray: """Production from matching demand via annual lcoe.""" - from muse.quantities import annual_levelized_cost_of_energy as lcoe + from muse.costs import annual_levelized_cost_of_energy as lcoe from muse.quantities import demand_matched_production, gross_margin from muse.utilities import broadcast_techs @@ -173,8 +173,8 @@ def costed_production( minimum service is applied first. """ from muse.commodities import CommodityUsage, check_usage, is_pollutant + from muse.costs import annual_levelized_cost_of_energy from muse.quantities import ( - annual_levelized_cost_of_energy, costed_production, emission, ) diff --git a/src/muse/sectors/sector.py b/src/muse/sectors/sector.py index bd4b68daf..92c39d3a8 100644 --- a/src/muse/sectors/sector.py +++ b/src/muse/sectors/sector.py @@ -298,11 +298,8 @@ def save_outputs(self) -> None: def market_variables(self, market: xr.Dataset, technologies: xr.Dataset) -> Any: """Computes resulting market: production, consumption, and costs.""" from muse.commodities import is_pollutant - from muse.quantities import ( - annual_levelized_cost_of_energy, - consumption, - supply_cost, - ) + from muse.costs import annual_levelized_cost_of_energy, supply_cost + from muse.quantities import consumption from muse.timeslices import QuantityType, convert_timeslice from muse.utilities import broadcast_techs diff --git a/tests/test_quantities.py b/tests/test_quantities.py index 4fc9f9fb8..d6150354d 100644 --- a/tests/test_quantities.py +++ b/tests/test_quantities.py @@ -333,7 +333,7 @@ def test_capacity_in_use(production: xr.DataArray, technologies: xr.Dataset): def test_supply_cost(production: xr.DataArray, timeslice: xr.Dataset): - from muse.quantities import supply_cost + from muse.costs import supply_cost from numpy import average from numpy.random import random @@ -360,7 +360,7 @@ def test_supply_cost(production: xr.DataArray, timeslice: xr.Dataset): def test_supply_cost_zero_prod(production: xr.DataArray, timeslice: xr.Dataset): - from muse.quantities import supply_cost + from muse.costs import supply_cost from numpy.random import randn timeslice = timeslice.timeslice @@ -425,8 +425,8 @@ def test_demand_matched_production( def test_costed_production_exact_match(market, capacity, technologies): + from muse.costs import annual_levelized_cost_of_energy from muse.quantities import ( - annual_levelized_cost_of_energy, costed_production, maximum_production, ) @@ -460,8 +460,8 @@ def test_costed_production_exact_match(market, capacity, technologies): def test_costed_production_single_region(market, capacity, technologies): + from muse.costs import annual_levelized_cost_of_energy from muse.quantities import ( - annual_levelized_cost_of_energy, costed_production, maximum_production, ) @@ -491,8 +491,8 @@ def test_costed_production_single_region(market, capacity, technologies): def test_costed_production_single_year(market, capacity, technologies): + from muse.costs import annual_levelized_cost_of_energy from muse.quantities import ( - annual_levelized_cost_of_energy, costed_production, maximum_production, ) @@ -524,8 +524,8 @@ def test_costed_production_single_year(market, capacity, technologies): def test_costed_production_over_capacity(market, capacity, technologies): + from muse.costs import annual_levelized_cost_of_energy from muse.quantities import ( - annual_levelized_cost_of_energy, costed_production, maximum_production, ) @@ -560,8 +560,8 @@ def test_costed_production_over_capacity(market, capacity, technologies): def test_costed_production_with_minimum_service(market, capacity, technologies, rng): + from muse.costs import annual_levelized_cost_of_energy from muse.quantities import ( - annual_levelized_cost_of_energy, costed_production, maximum_production, ) From a5c80a9ffc65181275d6f3bead23b03c7074a5b0 Mon Sep 17 00:00:00 2001 From: Tom Bland Date: Thu, 22 Aug 2024 10:00:05 +0100 Subject: [PATCH 08/23] Remove search_space from objectives --- src/muse/agents/agent.py | 28 ++++-- src/muse/objectives.py | 182 ++++++++------------------------------- 2 files changed, 57 insertions(+), 153 deletions(-) diff --git a/src/muse/agents/agent.py b/src/muse/agents/agent.py index c1280a75d..63a27c088 100644 --- a/src/muse/agents/agent.py +++ b/src/muse/agents/agent.py @@ -278,7 +278,26 @@ def next( getLogger(__name__).critical("Search space is empty") self.year += time_period return None - decision = self._compute_objective(demand, search_space, technologies, market) + + # Filter technologies according to the search space + techs = self.filter_input( + technologies, + technology=search_space.replacement, + year=self.forecast_year, + ).drop_vars("technology") + + # Filter demand according to the search space + reduced_demand = demand.sel( + { + k: search_space[k] + for k in set(demand.dims).intersection(search_space.dims) + } + ) + + # Compute the objective + decision = self._compute_objective( + demand=reduced_demand, technologies=techs, market=market + ) self.year += time_period return xr.Dataset(dict(search_space=search_space, decision=decision)) @@ -286,15 +305,12 @@ def next( def _compute_objective( self, demand: xr.DataArray, - search_space: xr.DataArray, technologies: xr.Dataset, market: xr.Dataset, ) -> xr.DataArray: - objectives = self.objectives(self, demand, search_space, technologies, market) + objectives = self.objectives(self, demand, technologies, market) decision = self.decision(objectives) - nobroadcast_dims = [d for d in decision.dims if d not in search_space.dims] - decision = xr.broadcast(decision, search_space, exclude=nobroadcast_dims)[0] - return decision.sel({k: search_space[k] for k in search_space.dims}) + return decision def add_investments( self, diff --git a/src/muse/objectives.py b/src/muse/objectives.py index dcf40956e..26622d55e 100644 --- a/src/muse/objectives.py +++ b/src/muse/objectives.py @@ -22,7 +22,6 @@ def comfort( agent: Agent, demand: xr.DataArray, - search_space: xr.DataArray, technologies: xr.Dataset, market: xr.Dataset, **kwargs @@ -34,8 +33,6 @@ def comfort( the agent for parameters, e.g. the current year, the interpolation method, the tolerance, etc. demand: Demand to fulfill. - search_space: A boolean matrix represented as a ``xr.DataArray``, listing - replacement technologies for each asset. technologies: A data set characterising the technologies from which the agent can draw assets. market: Market variables, such as prices or current capacity and retirement @@ -137,11 +134,11 @@ def factory( functions = [(param["name"], objective_factory(param)) for param in params] def objectives( - agent: Agent, demand: xr.DataArray, search_space: xr.DataArray, *args, **kwargs + agent: Agent, demand: xr.DataArray, technologies: xr.Dataset, *args, **kwargs ) -> xr.Dataset: - result = xr.Dataset(coords=search_space.coords) + result = xr.Dataset() for name, objective in functions: - obj = objective(agent, demand, search_space, *args, **kwargs) + obj = objective(agent, demand, technologies, *args, **kwargs) if "timeslice" in obj.dims and "timeslice" in result.dims: obj = drop_timeslice(obj) result[name] = obj @@ -165,17 +162,11 @@ def register_objective(function: OBJECTIVE_SIGNATURE): @wraps(function) def decorated_objective( - agent: Agent, demand: xr.DataArray, search_space: xr.DataArray, *args, **kwargs + agent: Agent, demand: xr.DataArray, technologies: xr.Dataset, *args, **kwargs ) -> xr.DataArray: from logging import getLogger - reduced_demand = demand.sel( - { - k: search_space[k] - for k in set(demand.dims).intersection(search_space.dims) - } - ) - result = function(agent, reduced_demand, search_space, *args, **kwargs) + result = function(agent, demand, technologies, *args, **kwargs) dtype = result.values.dtype if not (np.issubdtype(dtype, np.number) or np.issubdtype(dtype, np.bool_)): @@ -197,44 +188,31 @@ def decorated_objective( def comfort( agent: Agent, demand: xr.DataArray, - search_space: xr.DataArray, technologies: xr.Dataset, *args, **kwargs, ) -> xr.DataArray: """Comfort value provided by technologies.""" - techs = agent.filter_input( - technologies, - technology=search_space.replacement, - year=agent.forecast_year, - ).drop_vars("technology") - return techs.comfort + return technologies.comfort @register_objective def efficiency( agent: Agent, demand: xr.DataArray, - search_space: xr.DataArray, technologies: xr.Dataset, *args, **kwargs, ) -> xr.DataArray: """Efficiency of the technologies.""" - techs = agent.filter_input( - technologies, - technology=search_space.replacement, - year=agent.forecast_year, - ).drop_vars("technology") - return techs.efficiency + return technologies.efficiency -def _represent_hours(market: xr.Dataset, search_space: xr.DataArray) -> xr.DataArray: +def _represent_hours(market: xr.Dataset) -> xr.DataArray: """Retrieves the appropriate value for represent_hours. Args: market: The simulation market. - search_space: The search space for new tehcnologies. Returns: DataArray with the hours of each timeslice. @@ -243,8 +221,6 @@ def _represent_hours(market: xr.Dataset, search_space: xr.DataArray) -> xr.DataA if "represent_hours" in market: return market.represent_hours - if "represent_hours" in search_space.coords: - return search_space.represent_hours return represent_hours(market.timeslice) @@ -252,7 +228,6 @@ def _represent_hours(market: xr.Dataset, search_space: xr.DataArray) -> xr.DataA def capacity_to_service_demand( agent: Agent, demand: xr.DataArray, - search_space: xr.DataArray, technologies: xr.Dataset, market: xr.Dataset, *args, @@ -261,20 +236,16 @@ def capacity_to_service_demand( """Minimum capacity required to fulfill the demand.""" from muse.quantities import capacity_to_service_demand - techs = agent.filter_input( - technologies, - technology=search_space.replacement, - year=agent.forecast_year, - ).drop_vars("technology") - hours = _represent_hours(market, search_space) - return capacity_to_service_demand(demand=demand, technologies=techs, hours=hours) + hours = _represent_hours(market) + return capacity_to_service_demand( + demand=demand, technologies=technologies, hours=hours + ) @register_objective def capacity_in_use( agent: Agent, demand: xr.DataArray, - search_space: xr.DataArray, technologies: xr.Dataset, market: xr.Dataset, *args, @@ -282,18 +253,13 @@ def capacity_in_use( ): from muse.commodities import is_enduse - hours = _represent_hours(market, search_space) + hours = _represent_hours(market) - techs = agent.filter_input( - technologies, - technology=search_space.replacement, - year=agent.forecast_year, - ).drop_vars("technology") - enduses = is_enduse(techs.comm_usage.sel(commodity=demand.commodity)) + enduses = is_enduse(technologies.comm_usage.sel(commodity=demand.commodity)) return ( (demand.sel(commodity=enduses).sum("commodity") / hours).sum("timeslice") * hours.sum() - / techs.utilization_factor + / technologies.utilization_factor ) @@ -301,7 +267,6 @@ def capacity_in_use( def consumption( agent: Agent, demand: xr.DataArray, - search_space: xr.DataArray, technologies: xr.Dataset, market: xr.Dataset, *args, @@ -313,14 +278,8 @@ def consumption( """ from muse.quantities import consumption - techs = agent.filter_input( - technologies, - technology=search_space.replacement, - year=agent.forecast_year, - ).drop_vars("technology") prices = agent.filter_input(market.prices, year=agent.forecast_year) - demand = demand.where(search_space, 0) - result = consumption(technologies=techs, prices=prices, production=demand) + result = consumption(technologies=technologies, prices=prices, production=demand) return result.sum("commodity") @@ -328,7 +287,6 @@ def consumption( def fixed_costs( agent: Agent, demand: xr.DataArray, - search_space: xr.DataArray, technologies: xr.Dataset, market: xr.Dataset, *args, @@ -349,18 +307,12 @@ def fixed_costs( """ from muse.timeslices import QuantityType, convert_timeslice - techs = agent.filter_input( - technologies, - technology=search_space.replacement, - year=agent.forecast_year, - ).drop_vars("technology") - capacity = capacity_to_service_demand( - agent, demand, search_space, techs, market, *args, **kwargs + agent, demand, technologies, market, *args, **kwargs ) result = convert_timeslice( - techs.fix_par * (capacity**techs.fix_exp), + technologies.fix_par * (capacity**technologies.fix_exp), demand.timeslice, QuantityType.EXTENSIVE, ) @@ -371,7 +323,6 @@ def fixed_costs( def capital_costs( agent: Agent, demand: xr.DataArray, - search_space: xr.DataArray, technologies: xr.Dataset, *args, **kwargs, @@ -385,13 +336,8 @@ def capital_costs( """ from muse.timeslices import QuantityType, convert_timeslice - techs = agent.filter_input( - technologies, - technology=search_space.replacement, - year=agent.forecast_year, - ).drop_vars("technology") result = convert_timeslice( - techs.cap_par * (techs.scaling_size**techs.cap_exp), + technologies.cap_par * (technologies.scaling_size**technologies.cap_exp), demand.timeslice, QuantityType.EXTENSIVE, ) @@ -402,7 +348,6 @@ def capital_costs( def emission_cost( agent: Agent, demand: xr.DataArray, - search_space: xr.DataArray, technologies: xr.Dataset, market: xr.Dataset, *args, @@ -422,24 +367,17 @@ def emission_cost( """ from muse.commodities import is_enduse, is_pollutant - techs = agent.filter_input( - technologies, - technology=search_space.replacement, - year=agent.forecast_year, - ).drop_vars("technology") - enduses = is_enduse(technologies.comm_usage.sel(commodity=demand.commodity)) total = demand.sel(commodity=enduses).sum("commodity") envs = is_pollutant(technologies.comm_usage) prices = agent.filter_input(market.prices, year=agent.forecast_year, commodity=envs) - return total * (techs.fixed_outputs * prices).sum("commodity") + return total * (technologies.fixed_outputs * prices).sum("commodity") @register_objective def fuel_consumption_cost( agent: Agent, demand: xr.DataArray, - search_space: xr.DataArray, technologies: xr.Dataset, market: xr.Dataset, *args, @@ -449,16 +387,9 @@ def fuel_consumption_cost( from muse.commodities import is_fuel from muse.quantities import consumption - techs = agent.filter_input( - technologies, - technology=search_space.replacement, - year=agent.forecast_year, - ).drop_vars("technology") - - commodity = is_fuel(techs.comm_usage.sel(commodity=market.commodity)) + commodity = is_fuel(technologies.comm_usage.sel(commodity=market.commodity)) prices = agent.filter_input(market.prices, year=agent.forecast_year) - demand = demand.where(search_space, 0) - fcons = consumption(technologies=techs, prices=prices, production=demand) + fcons = consumption(technologies=technologies, prices=prices, production=demand) return (fcons * prices).sel(commodity=commodity).sum("commodity") @@ -467,7 +398,6 @@ def fuel_consumption_cost( def annual_levelized_cost_of_energy( agent: Agent, demand: xr.DataArray, - search_space: xr.DataArray, technologies: xr.Dataset, market: xr.Dataset, *args, @@ -481,7 +411,6 @@ def annual_levelized_cost_of_energy( Arguments: agent: The agent of interest demand: Demand for commodities - search_space: The search space space for replacement technologies technologies: All the technologies market: The market parameters *args: Extra arguments (unused) @@ -492,21 +421,14 @@ def annual_levelized_cost_of_energy( """ from muse.costs import annual_levelized_cost_of_energy as aLCOE - techs = agent.filter_input( - technologies, - technology=search_space.replacement, - year=agent.forecast_year, - ).drop_vars("technology") - prices = cast(xr.DataArray, agent.filter_input(market.prices)) - return aLCOE(prices, techs).max("timeslice") + return aLCOE(prices, technologies).max("timeslice") @register_objective(name=["LCOE", "LLCOE"]) def lifetime_levelized_cost_of_energy( agent: Agent, demand: xr.DataArray, - search_space: xr.DataArray, technologies: xr.Dataset, market: xr.Dataset, *args, @@ -521,7 +443,6 @@ def lifetime_levelized_cost_of_energy( Arguments: agent: The agent of interest demand: Demand for commodities - search_space: The search space space for replacement technologies technologies: All the technologies market: The market parameters *args: Extra arguments (unused) @@ -533,22 +454,15 @@ def lifetime_levelized_cost_of_energy( from muse.costs import lifetime_levelized_cost_of_energy as LCOE from muse.timeslices import QuantityType, convert_timeslice - techs = agent.filter_input( - technologies, - technology=search_space.replacement, - year=agent.forecast_year, - ).drop_vars("technology") prices = cast(xr.DataArray, agent.filter_input(market.prices)) - capacity = capacity_to_service_demand( - agent, demand, search_space, technologies, market - ) - production = capacity * techs.fixed_outputs * techs.utilization_factor + capacity = capacity_to_service_demand(agent, demand, technologies, market) + production = capacity * technologies.fixed_outputs * technologies.utilization_factor production = convert_timeslice(production, demand.timeslice, QuantityType.EXTENSIVE) results = LCOE( prices=prices, - technologies=techs, + technologies=technologies, capacity=capacity, production=production, year=agent.forecast_year, @@ -561,7 +475,6 @@ def lifetime_levelized_cost_of_energy( def net_present_value( agent: Agent, demand: xr.DataArray, - search_space: xr.DataArray, technologies: xr.Dataset, market: xr.Dataset, *args, @@ -593,7 +506,6 @@ def net_present_value( Arguments: agent: The agent of interest demand: Demand for commodities - search_space: The search space space for replacement technologies technologies: All the technologies market: The market parameters *args: Extra arguments (unused) @@ -605,22 +517,15 @@ def net_present_value( from muse.costs import net_present_value as NPV from muse.timeslices import QuantityType, convert_timeslice - techs = agent.filter_input( - technologies, - technology=search_space.replacement, - year=agent.forecast_year, - ).drop_vars("technology") prices = cast(xr.DataArray, agent.filter_input(market.prices)) - capacity = capacity_to_service_demand( - agent, demand, search_space, technologies, market - ) - production = capacity * techs.fixed_outputs * techs.utilization_factor + capacity = capacity_to_service_demand(agent, demand, technologies, market) + production = capacity * technologies.fixed_outputs * technologies.utilization_factor production = convert_timeslice(production, demand.timeslice, QuantityType.EXTENSIVE) results = NPV( prices=prices, - technologies=techs, + technologies=technologies, capacity=capacity, production=production, year=agent.forecast_year, @@ -632,7 +537,6 @@ def net_present_value( def net_present_cost( agent: Agent, demand: xr.DataArray, - search_space: xr.DataArray, technologies: xr.Dataset, market: xr.Dataset, *args, @@ -650,22 +554,15 @@ def net_present_cost( from muse.costs import net_present_cost as NPC from muse.timeslices import QuantityType, convert_timeslice - techs = agent.filter_input( - technologies, - technology=search_space.replacement, - year=agent.forecast_year, - ).drop_vars("technology") prices = cast(xr.DataArray, agent.filter_input(market.prices)) - capacity = capacity_to_service_demand( - agent, demand, search_space, technologies, market - ) - production = capacity * techs.fixed_outputs * techs.utilization_factor + capacity = capacity_to_service_demand(agent, demand, technologies, market) + production = capacity * technologies.fixed_outputs * technologies.utilization_factor production = convert_timeslice(production, demand.timeslice, QuantityType.EXTENSIVE) results = NPC( prices=prices, - technologies=techs, + technologies=technologies, capacity=capacity, production=production, year=agent.forecast_year, @@ -677,7 +574,6 @@ def net_present_cost( def equivalent_annual_cost( agent: Agent, demand: xr.DataArray, - search_space: xr.DataArray, technologies: xr.Dataset, market: xr.Dataset, *args, @@ -696,7 +592,6 @@ def equivalent_annual_cost( Arguments: agent: The agent of interest demand: Demand for commodities - search_space: The search space space for replacement technologies technologies: All the technologies market: The market parameters *args: Extra arguments (unused) @@ -708,22 +603,15 @@ def equivalent_annual_cost( from muse.costs import equivalent_annual_cost as EAC from muse.timeslices import QuantityType, convert_timeslice - techs = agent.filter_input( - technologies, - technology=search_space.replacement, - year=agent.forecast_year, - ).drop_vars("technology") prices = cast(xr.DataArray, agent.filter_input(market.prices)) - capacity = capacity_to_service_demand( - agent, demand, search_space, technologies, market - ) - production = capacity * techs.fixed_outputs * techs.utilization_factor + capacity = capacity_to_service_demand(agent, demand, technologies, market) + production = capacity * technologies.fixed_outputs * technologies.utilization_factor production = convert_timeslice(production, demand.timeslice, QuantityType.EXTENSIVE) results = EAC( prices=prices, - technologies=techs, + technologies=technologies, capacity=capacity, production=production, year=agent.forecast_year, From 830027252e14bb063b0827b7465367fd5bfb6481 Mon Sep 17 00:00:00 2001 From: Tom Bland Date: Thu, 22 Aug 2024 10:40:24 +0100 Subject: [PATCH 09/23] Pass prices to objectives rather than full market object --- src/muse/agents/agent.py | 6 +-- src/muse/objectives.py | 79 +++++++++++++++------------------------- 2 files changed, 33 insertions(+), 52 deletions(-) diff --git a/src/muse/agents/agent.py b/src/muse/agents/agent.py index 63a27c088..d2c9f49ee 100644 --- a/src/muse/agents/agent.py +++ b/src/muse/agents/agent.py @@ -296,7 +296,7 @@ def next( # Compute the objective decision = self._compute_objective( - demand=reduced_demand, technologies=techs, market=market + demand=reduced_demand, technologies=techs, prices=market.prices ) self.year += time_period @@ -306,9 +306,9 @@ def _compute_objective( self, demand: xr.DataArray, technologies: xr.Dataset, - market: xr.Dataset, + prices: xr.DataArray, ) -> xr.DataArray: - objectives = self.objectives(self, demand, technologies, market) + objectives = self.objectives(self, demand, technologies, prices) decision = self.decision(objectives) return decision diff --git a/src/muse/objectives.py b/src/muse/objectives.py index 26622d55e..a1ca23fa4 100644 --- a/src/muse/objectives.py +++ b/src/muse/objectives.py @@ -208,35 +208,19 @@ def efficiency( return technologies.efficiency -def _represent_hours(market: xr.Dataset) -> xr.DataArray: - """Retrieves the appropriate value for represent_hours. - - Args: - market: The simulation market. - - Returns: - DataArray with the hours of each timeslice. - """ - from muse.timeslices import represent_hours - - if "represent_hours" in market: - return market.represent_hours - return represent_hours(market.timeslice) - - @register_objective(name="capacity") def capacity_to_service_demand( agent: Agent, demand: xr.DataArray, technologies: xr.Dataset, - market: xr.Dataset, *args, **kwargs, ) -> xr.DataArray: """Minimum capacity required to fulfill the demand.""" from muse.quantities import capacity_to_service_demand + from muse.timeslices import represent_hours - hours = _represent_hours(market) + hours = represent_hours(demand.timeslice) return capacity_to_service_demand( demand=demand, technologies=technologies, hours=hours ) @@ -247,13 +231,13 @@ def capacity_in_use( agent: Agent, demand: xr.DataArray, technologies: xr.Dataset, - market: xr.Dataset, *args, **kwargs, ): from muse.commodities import is_enduse + from muse.timeslices import represent_hours - hours = _represent_hours(market) + hours = represent_hours(demand.timeslice) enduses = is_enduse(technologies.comm_usage.sel(commodity=demand.commodity)) return ( @@ -268,7 +252,7 @@ def consumption( agent: Agent, demand: xr.DataArray, technologies: xr.Dataset, - market: xr.Dataset, + prices: xr.DataArray, *args, **kwargs, ) -> xr.DataArray: @@ -278,7 +262,7 @@ def consumption( """ from muse.quantities import consumption - prices = agent.filter_input(market.prices, year=agent.forecast_year) + prices = agent.filter_input(prices, year=agent.forecast_year) result = consumption(technologies=technologies, prices=prices, production=demand) return result.sum("commodity") @@ -288,7 +272,6 @@ def fixed_costs( agent: Agent, demand: xr.DataArray, technologies: xr.Dataset, - market: xr.Dataset, *args, **kwargs, ) -> xr.DataArray: @@ -307,9 +290,7 @@ def fixed_costs( """ from muse.timeslices import QuantityType, convert_timeslice - capacity = capacity_to_service_demand( - agent, demand, technologies, market, *args, **kwargs - ) + capacity = capacity_to_service_demand(agent, demand, technologies, *args, **kwargs) result = convert_timeslice( technologies.fix_par * (capacity**technologies.fix_exp), @@ -349,7 +330,7 @@ def emission_cost( agent: Agent, demand: xr.DataArray, technologies: xr.Dataset, - market: xr.Dataset, + prices: xr.DataArray, *args, **kwargs, ) -> xr.DataArray: @@ -370,7 +351,7 @@ def emission_cost( enduses = is_enduse(technologies.comm_usage.sel(commodity=demand.commodity)) total = demand.sel(commodity=enduses).sum("commodity") envs = is_pollutant(technologies.comm_usage) - prices = agent.filter_input(market.prices, year=agent.forecast_year, commodity=envs) + prices = agent.filter_input(prices, year=agent.forecast_year, commodity=envs) return total * (technologies.fixed_outputs * prices).sum("commodity") @@ -379,7 +360,7 @@ def fuel_consumption_cost( agent: Agent, demand: xr.DataArray, technologies: xr.Dataset, - market: xr.Dataset, + prices: xr.DataArray, *args, **kwargs, ): @@ -387,8 +368,8 @@ def fuel_consumption_cost( from muse.commodities import is_fuel from muse.quantities import consumption - commodity = is_fuel(technologies.comm_usage.sel(commodity=market.commodity)) - prices = agent.filter_input(market.prices, year=agent.forecast_year) + commodity = is_fuel(technologies.comm_usage.sel(commodity=demand.commodity)) + prices = agent.filter_input(prices, year=agent.forecast_year) fcons = consumption(technologies=technologies, prices=prices, production=demand) return (fcons * prices).sel(commodity=commodity).sum("commodity") @@ -399,7 +380,7 @@ def annual_levelized_cost_of_energy( agent: Agent, demand: xr.DataArray, technologies: xr.Dataset, - market: xr.Dataset, + prices: xr.DataArray, *args, **kwargs, ): @@ -412,7 +393,7 @@ def annual_levelized_cost_of_energy( agent: The agent of interest demand: Demand for commodities technologies: All the technologies - market: The market parameters + prices: Commodity prices *args: Extra arguments (unused) **kwargs: Extra keyword arguments (unused) @@ -421,7 +402,7 @@ def annual_levelized_cost_of_energy( """ from muse.costs import annual_levelized_cost_of_energy as aLCOE - prices = cast(xr.DataArray, agent.filter_input(market.prices)) + prices = cast(xr.DataArray, agent.filter_input(prices)) return aLCOE(prices, technologies).max("timeslice") @@ -430,7 +411,7 @@ def lifetime_levelized_cost_of_energy( agent: Agent, demand: xr.DataArray, technologies: xr.Dataset, - market: xr.Dataset, + prices: xr.DataArray, *args, **kwargs, ): @@ -444,7 +425,7 @@ def lifetime_levelized_cost_of_energy( agent: The agent of interest demand: Demand for commodities technologies: All the technologies - market: The market parameters + prices: Commodity prices *args: Extra arguments (unused) **kwargs: Extra keyword arguments (unused) @@ -454,9 +435,9 @@ def lifetime_levelized_cost_of_energy( from muse.costs import lifetime_levelized_cost_of_energy as LCOE from muse.timeslices import QuantityType, convert_timeslice - prices = cast(xr.DataArray, agent.filter_input(market.prices)) + prices = cast(xr.DataArray, agent.filter_input(prices)) - capacity = capacity_to_service_demand(agent, demand, technologies, market) + capacity = capacity_to_service_demand(agent, demand, technologies, prices) production = capacity * technologies.fixed_outputs * technologies.utilization_factor production = convert_timeslice(production, demand.timeslice, QuantityType.EXTENSIVE) @@ -476,7 +457,7 @@ def net_present_value( agent: Agent, demand: xr.DataArray, technologies: xr.Dataset, - market: xr.Dataset, + prices: xr.DataArray, *args, **kwargs, ): @@ -507,7 +488,7 @@ def net_present_value( agent: The agent of interest demand: Demand for commodities technologies: All the technologies - market: The market parameters + prices: Commodity prices *args: Extra arguments (unused) **kwargs: Extra keyword arguments (unused) @@ -517,9 +498,9 @@ def net_present_value( from muse.costs import net_present_value as NPV from muse.timeslices import QuantityType, convert_timeslice - prices = cast(xr.DataArray, agent.filter_input(market.prices)) + prices = cast(xr.DataArray, agent.filter_input(prices)) - capacity = capacity_to_service_demand(agent, demand, technologies, market) + capacity = capacity_to_service_demand(agent, demand, technologies) production = capacity * technologies.fixed_outputs * technologies.utilization_factor production = convert_timeslice(production, demand.timeslice, QuantityType.EXTENSIVE) @@ -538,7 +519,7 @@ def net_present_cost( agent: Agent, demand: xr.DataArray, technologies: xr.Dataset, - market: xr.Dataset, + prices: xr.DataArray, *args, **kwargs, ): @@ -554,9 +535,9 @@ def net_present_cost( from muse.costs import net_present_cost as NPC from muse.timeslices import QuantityType, convert_timeslice - prices = cast(xr.DataArray, agent.filter_input(market.prices)) + prices = cast(xr.DataArray, agent.filter_input(prices)) - capacity = capacity_to_service_demand(agent, demand, technologies, market) + capacity = capacity_to_service_demand(agent, demand, technologies) production = capacity * technologies.fixed_outputs * technologies.utilization_factor production = convert_timeslice(production, demand.timeslice, QuantityType.EXTENSIVE) @@ -575,7 +556,7 @@ def equivalent_annual_cost( agent: Agent, demand: xr.DataArray, technologies: xr.Dataset, - market: xr.Dataset, + prices: xr.DataArray, *args, **kwargs, ): @@ -593,7 +574,7 @@ def equivalent_annual_cost( agent: The agent of interest demand: Demand for commodities technologies: All the technologies - market: The market parameters + prices: Commodity prices *args: Extra arguments (unused) **kwargs: Extra keyword arguments (unused) @@ -603,9 +584,9 @@ def equivalent_annual_cost( from muse.costs import equivalent_annual_cost as EAC from muse.timeslices import QuantityType, convert_timeslice - prices = cast(xr.DataArray, agent.filter_input(market.prices)) + prices = cast(xr.DataArray, agent.filter_input(prices)) - capacity = capacity_to_service_demand(agent, demand, technologies, market) + capacity = capacity_to_service_demand(agent, demand, technologies, prices) production = capacity * technologies.fixed_outputs * technologies.utilization_factor production = convert_timeslice(production, demand.timeslice, QuantityType.EXTENSIVE) From a72163656284b94559a48e0a20541ec1bb47173e Mon Sep 17 00:00:00 2001 From: Tom Bland Date: Thu, 22 Aug 2024 11:21:56 +0100 Subject: [PATCH 10/23] Agent object no longer passed to objectives --- src/muse/agents/agent.py | 7 ++-- src/muse/objectives.py | 70 ++++++++++++---------------------------- 2 files changed, 25 insertions(+), 52 deletions(-) diff --git a/src/muse/agents/agent.py b/src/muse/agents/agent.py index d2c9f49ee..1cc8fcaee 100644 --- a/src/muse/agents/agent.py +++ b/src/muse/agents/agent.py @@ -294,9 +294,12 @@ def next( } ) + # Filter prices + prices = self.filter_input(market.prices) + # Compute the objective decision = self._compute_objective( - demand=reduced_demand, technologies=techs, prices=market.prices + demand=reduced_demand, technologies=techs, prices=prices ) self.year += time_period @@ -308,7 +311,7 @@ def _compute_objective( technologies: xr.Dataset, prices: xr.DataArray, ) -> xr.DataArray: - objectives = self.objectives(self, demand, technologies, prices) + objectives = self.objectives(demand, technologies, prices) decision = self.decision(objectives) return decision diff --git a/src/muse/objectives.py b/src/muse/objectives.py index a1ca23fa4..951250691 100644 --- a/src/muse/objectives.py +++ b/src/muse/objectives.py @@ -20,7 +20,6 @@ @register_objective def comfort( - agent: Agent, demand: xr.DataArray, technologies: xr.Dataset, market: xr.Dataset, @@ -29,9 +28,6 @@ def comfort( pass Arguments: - agent: the agent relevant to the search space. The filters may need to query - the agent for parameters, e.g. the current year, the interpolation - method, the tolerance, etc. demand: Demand to fulfill. technologies: A data set characterising the technologies from which the agent can draw assets. @@ -69,7 +65,7 @@ def comfort( ] from collections.abc import Mapping, MutableMapping, Sequence -from typing import Any, Callable, Union, cast +from typing import Any, Callable, Union import numpy as np import xarray as xr @@ -134,11 +130,11 @@ def factory( functions = [(param["name"], objective_factory(param)) for param in params] def objectives( - agent: Agent, demand: xr.DataArray, technologies: xr.Dataset, *args, **kwargs + demand: xr.DataArray, technologies: xr.Dataset, *args, **kwargs ) -> xr.Dataset: result = xr.Dataset() for name, objective in functions: - obj = objective(agent, demand, technologies, *args, **kwargs) + obj = objective(demand, technologies, *args, **kwargs) if "timeslice" in obj.dims and "timeslice" in result.dims: obj = drop_timeslice(obj) result[name] = obj @@ -162,11 +158,11 @@ def register_objective(function: OBJECTIVE_SIGNATURE): @wraps(function) def decorated_objective( - agent: Agent, demand: xr.DataArray, technologies: xr.Dataset, *args, **kwargs + demand: xr.DataArray, technologies: xr.Dataset, *args, **kwargs ) -> xr.DataArray: from logging import getLogger - result = function(agent, demand, technologies, *args, **kwargs) + result = function(demand, technologies, *args, **kwargs) dtype = result.values.dtype if not (np.issubdtype(dtype, np.number) or np.issubdtype(dtype, np.bool_)): @@ -186,7 +182,6 @@ def decorated_objective( @register_objective def comfort( - agent: Agent, demand: xr.DataArray, technologies: xr.Dataset, *args, @@ -198,7 +193,6 @@ def comfort( @register_objective def efficiency( - agent: Agent, demand: xr.DataArray, technologies: xr.Dataset, *args, @@ -210,7 +204,6 @@ def efficiency( @register_objective(name="capacity") def capacity_to_service_demand( - agent: Agent, demand: xr.DataArray, technologies: xr.Dataset, *args, @@ -228,7 +221,6 @@ def capacity_to_service_demand( @register_objective def capacity_in_use( - agent: Agent, demand: xr.DataArray, technologies: xr.Dataset, *args, @@ -249,7 +241,6 @@ def capacity_in_use( @register_objective def consumption( - agent: Agent, demand: xr.DataArray, technologies: xr.Dataset, prices: xr.DataArray, @@ -261,15 +252,14 @@ def consumption( Currently, the consumption is implemented for commodity_max == +infinity. """ from muse.quantities import consumption + from muse.utilities import filter_input - prices = agent.filter_input(prices, year=agent.forecast_year) result = consumption(technologies=technologies, prices=prices, production=demand) return result.sum("commodity") @register_objective def fixed_costs( - agent: Agent, demand: xr.DataArray, technologies: xr.Dataset, *args, @@ -290,7 +280,7 @@ def fixed_costs( """ from muse.timeslices import QuantityType, convert_timeslice - capacity = capacity_to_service_demand(agent, demand, technologies, *args, **kwargs) + capacity = capacity_to_service_demand(demand, technologies) result = convert_timeslice( technologies.fix_par * (capacity**technologies.fix_exp), @@ -302,7 +292,6 @@ def fixed_costs( @register_objective def capital_costs( - agent: Agent, demand: xr.DataArray, technologies: xr.Dataset, *args, @@ -327,7 +316,6 @@ def capital_costs( @register_objective(name="emissions") def emission_cost( - agent: Agent, demand: xr.DataArray, technologies: xr.Dataset, prices: xr.DataArray, @@ -347,17 +335,17 @@ def emission_cost( with :math:`s` the timeslices and :math:`c` the commodity. """ from muse.commodities import is_enduse, is_pollutant + from muse.utilities import filter_input enduses = is_enduse(technologies.comm_usage.sel(commodity=demand.commodity)) total = demand.sel(commodity=enduses).sum("commodity") envs = is_pollutant(technologies.comm_usage) - prices = agent.filter_input(prices, year=agent.forecast_year, commodity=envs) + prices = filter_input(prices, year=demand.year.item(), commodity=envs) return total * (technologies.fixed_outputs * prices).sum("commodity") @register_objective def fuel_consumption_cost( - agent: Agent, demand: xr.DataArray, technologies: xr.Dataset, prices: xr.DataArray, @@ -367,17 +355,16 @@ def fuel_consumption_cost( """Cost of fuels when fulfilling whole demand.""" from muse.commodities import is_fuel from muse.quantities import consumption + from muse.utilities import filter_input commodity = is_fuel(technologies.comm_usage.sel(commodity=demand.commodity)) - prices = agent.filter_input(prices, year=agent.forecast_year) fcons = consumption(technologies=technologies, prices=prices, production=demand) - - return (fcons * prices).sel(commodity=commodity).sum("commodity") + prices = filter_input(prices, year=demand.year.item(), commodity=commodity) + return (fcons * prices).sum("commodity") @register_objective(name=["ALCOE"]) def annual_levelized_cost_of_energy( - agent: Agent, demand: xr.DataArray, technologies: xr.Dataset, prices: xr.DataArray, @@ -390,7 +377,6 @@ def annual_levelized_cost_of_energy( the `simplified LCOE` given by NREL. Arguments: - agent: The agent of interest demand: Demand for commodities technologies: All the technologies prices: Commodity prices @@ -402,13 +388,11 @@ def annual_levelized_cost_of_energy( """ from muse.costs import annual_levelized_cost_of_energy as aLCOE - prices = cast(xr.DataArray, agent.filter_input(prices)) return aLCOE(prices, technologies).max("timeslice") @register_objective(name=["LCOE", "LLCOE"]) def lifetime_levelized_cost_of_energy( - agent: Agent, demand: xr.DataArray, technologies: xr.Dataset, prices: xr.DataArray, @@ -422,7 +406,6 @@ def lifetime_levelized_cost_of_energy( factor. Arguments: - agent: The agent of interest demand: Demand for commodities technologies: All the technologies prices: Commodity prices @@ -435,9 +418,7 @@ def lifetime_levelized_cost_of_energy( from muse.costs import lifetime_levelized_cost_of_energy as LCOE from muse.timeslices import QuantityType, convert_timeslice - prices = cast(xr.DataArray, agent.filter_input(prices)) - - capacity = capacity_to_service_demand(agent, demand, technologies, prices) + capacity = capacity_to_service_demand(demand, technologies) production = capacity * technologies.fixed_outputs * technologies.utilization_factor production = convert_timeslice(production, demand.timeslice, QuantityType.EXTENSIVE) @@ -446,7 +427,7 @@ def lifetime_levelized_cost_of_energy( technologies=technologies, capacity=capacity, production=production, - year=agent.forecast_year, + year=demand.year.item(), ) return results.where(np.isfinite(results)).fillna(0.0) @@ -454,7 +435,6 @@ def lifetime_levelized_cost_of_energy( @register_objective(name="NPV") def net_present_value( - agent: Agent, demand: xr.DataArray, technologies: xr.Dataset, prices: xr.DataArray, @@ -485,7 +465,6 @@ def net_present_value( installation year of the technology. Arguments: - agent: The agent of interest demand: Demand for commodities technologies: All the technologies prices: Commodity prices @@ -498,9 +477,7 @@ def net_present_value( from muse.costs import net_present_value as NPV from muse.timeslices import QuantityType, convert_timeslice - prices = cast(xr.DataArray, agent.filter_input(prices)) - - capacity = capacity_to_service_demand(agent, demand, technologies) + capacity = capacity_to_service_demand(demand, technologies) production = capacity * technologies.fixed_outputs * technologies.utilization_factor production = convert_timeslice(production, demand.timeslice, QuantityType.EXTENSIVE) @@ -509,14 +486,13 @@ def net_present_value( technologies=technologies, capacity=capacity, production=production, - year=agent.forecast_year, + year=demand.year.item(), ) return results @register_objective(name="NPC") def net_present_cost( - agent: Agent, demand: xr.DataArray, technologies: xr.Dataset, prices: xr.DataArray, @@ -535,9 +511,7 @@ def net_present_cost( from muse.costs import net_present_cost as NPC from muse.timeslices import QuantityType, convert_timeslice - prices = cast(xr.DataArray, agent.filter_input(prices)) - - capacity = capacity_to_service_demand(agent, demand, technologies) + capacity = capacity_to_service_demand(demand, technologies) production = capacity * technologies.fixed_outputs * technologies.utilization_factor production = convert_timeslice(production, demand.timeslice, QuantityType.EXTENSIVE) @@ -546,14 +520,13 @@ def net_present_cost( technologies=technologies, capacity=capacity, production=production, - year=agent.forecast_year, + year=demand.year.item(), ) return results @register_objective(name="EAC") def equivalent_annual_cost( - agent: Agent, demand: xr.DataArray, technologies: xr.Dataset, prices: xr.DataArray, @@ -571,7 +544,6 @@ def equivalent_annual_cost( https://www.homerenergy.com/products/pro/docs/3.15/annualized_cost.html Arguments: - agent: The agent of interest demand: Demand for commodities technologies: All the technologies prices: Commodity prices @@ -584,9 +556,7 @@ def equivalent_annual_cost( from muse.costs import equivalent_annual_cost as EAC from muse.timeslices import QuantityType, convert_timeslice - prices = cast(xr.DataArray, agent.filter_input(prices)) - - capacity = capacity_to_service_demand(agent, demand, technologies, prices) + capacity = capacity_to_service_demand(demand, technologies) production = capacity * technologies.fixed_outputs * technologies.utilization_factor production = convert_timeslice(production, demand.timeslice, QuantityType.EXTENSIVE) @@ -595,6 +565,6 @@ def equivalent_annual_cost( technologies=technologies, capacity=capacity, production=production, - year=agent.forecast_year, + year=demand.year.item(), ) return results From 35dc9427d23c2b99c3b8dd86171292a05c9c92a5 Mon Sep 17 00:00:00 2001 From: Tom Bland Date: Thu, 22 Aug 2024 14:55:36 +0100 Subject: [PATCH 11/23] Rewrite tests, change objective function signature --- src/muse/agents/agent.py | 8 +- src/muse/objectives.py | 99 ++++----- tests/test_objectives.py | 430 ++++++++++----------------------------- 3 files changed, 148 insertions(+), 389 deletions(-) diff --git a/src/muse/agents/agent.py b/src/muse/agents/agent.py index 1cc8fcaee..79e75ba6c 100644 --- a/src/muse/agents/agent.py +++ b/src/muse/agents/agent.py @@ -299,7 +299,7 @@ def next( # Compute the objective decision = self._compute_objective( - demand=reduced_demand, technologies=techs, prices=prices + technologies=techs, demand=reduced_demand, prices=prices ) self.year += time_period @@ -307,11 +307,13 @@ def next( def _compute_objective( self, - demand: xr.DataArray, technologies: xr.Dataset, + demand: xr.DataArray, prices: xr.DataArray, ) -> xr.DataArray: - objectives = self.objectives(demand, technologies, prices) + objectives = self.objectives( + technologies=technologies, demand=demand, prices=prices + ) decision = self.decision(objectives) return decision diff --git a/src/muse/objectives.py b/src/muse/objectives.py index 951250691..0777712fc 100644 --- a/src/muse/objectives.py +++ b/src/muse/objectives.py @@ -20,19 +20,18 @@ @register_objective def comfort( - demand: xr.DataArray, technologies: xr.Dataset, - market: xr.Dataset, + demand: xr.DataArray, + prices: xr.DataArray, **kwargs ) -> xr.DataArray: pass Arguments: - demand: Demand to fulfill. technologies: A data set characterising the technologies from which the agent can draw assets. - market: Market variables, such as prices or current capacity and retirement - profile. + demand: Demand to fulfill. + prices: Commodity prices. kwargs: Extra input parameters. These parameters are expected to be set from the input file. @@ -42,10 +41,8 @@ def comfort( these parameters. Returns: - A dataArray with at least one dimension corresponding to ``replacement``. Only the - technologies in ``search_space.replacement`` should be present. Furthermore, if an - ``asset`` dimension is present, then it should correspond to ``search_space.asset``. - Other dimensions can be present, as long as the subsequent decision function nows + A dataArray with at least one dimension corresponding to ``replacement``. + Other dimensions can be present, as long as the subsequent decision function knows how to reduce them. """ @@ -71,14 +68,12 @@ def comfort( import xarray as xr from mypy_extensions import KwArg -from muse.agents import Agent from muse.outputs.cache import cache_quantity from muse.registration import registrator -from muse.timeslices import drop_timeslice +from muse.utilities import filter_input OBJECTIVE_SIGNATURE = Callable[ - [Agent, xr.DataArray, xr.DataArray, xr.Dataset, xr.Dataset, KwArg(Any)], - xr.DataArray, + [xr.Dataset, xr.DataArray, xr.DataArray, KwArg(Any)], xr.DataArray ] """Objectives signature.""" @@ -130,13 +125,11 @@ def factory( functions = [(param["name"], objective_factory(param)) for param in params] def objectives( - demand: xr.DataArray, technologies: xr.Dataset, *args, **kwargs + technologies: xr.Dataset, demand: xr.DataArray, *args, **kwargs ) -> xr.Dataset: result = xr.Dataset() for name, objective in functions: - obj = objective(demand, technologies, *args, **kwargs) - if "timeslice" in obj.dims and "timeslice" in result.dims: - obj = drop_timeslice(obj) + obj = objective(technologies=technologies, demand=demand, *args, **kwargs) result[name] = obj return result @@ -158,11 +151,11 @@ def register_objective(function: OBJECTIVE_SIGNATURE): @wraps(function) def decorated_objective( - demand: xr.DataArray, technologies: xr.Dataset, *args, **kwargs + technologies: xr.Dataset, demand: xr.DataArray, *args, **kwargs ) -> xr.DataArray: from logging import getLogger - result = function(demand, technologies, *args, **kwargs) + result = function(technologies, demand, *args, **kwargs) dtype = result.values.dtype if not (np.issubdtype(dtype, np.number) or np.issubdtype(dtype, np.bool_)): @@ -173,6 +166,8 @@ def decorated_objective( raise RuntimeError("Objective should not return a dimension 'technology'") if "technology" in result.coords: raise RuntimeError("Objective should not return a coordinate 'technology'") + if "year" in result.dims: + raise RuntimeError("Objective should not return a dimension 'year'") result.name = function.__name__ cache_quantity(**{result.name: result}) return result @@ -182,8 +177,8 @@ def decorated_objective( @register_objective def comfort( - demand: xr.DataArray, technologies: xr.Dataset, + demand: xr.DataArray, *args, **kwargs, ) -> xr.DataArray: @@ -193,8 +188,8 @@ def comfort( @register_objective def efficiency( - demand: xr.DataArray, technologies: xr.Dataset, + demand: xr.DataArray, *args, **kwargs, ) -> xr.DataArray: @@ -204,8 +199,8 @@ def efficiency( @register_objective(name="capacity") def capacity_to_service_demand( - demand: xr.DataArray, technologies: xr.Dataset, + demand: xr.DataArray, *args, **kwargs, ) -> xr.DataArray: @@ -221,8 +216,8 @@ def capacity_to_service_demand( @register_objective def capacity_in_use( - demand: xr.DataArray, technologies: xr.Dataset, + demand: xr.DataArray, *args, **kwargs, ): @@ -241,8 +236,8 @@ def capacity_in_use( @register_objective def consumption( - demand: xr.DataArray, technologies: xr.Dataset, + demand: xr.DataArray, prices: xr.DataArray, *args, **kwargs, @@ -252,7 +247,6 @@ def consumption( Currently, the consumption is implemented for commodity_max == +infinity. """ from muse.quantities import consumption - from muse.utilities import filter_input result = consumption(technologies=technologies, prices=prices, production=demand) return result.sum("commodity") @@ -260,8 +254,8 @@ def consumption( @register_objective def fixed_costs( - demand: xr.DataArray, technologies: xr.Dataset, + demand: xr.DataArray, *args, **kwargs, ) -> xr.DataArray: @@ -278,22 +272,15 @@ def fixed_costs( :math:`\alpha` and :math:`\beta` are "fix_par" and "fix_exp" in :ref:`inputs-technodata`, respectively. """ - from muse.timeslices import QuantityType, convert_timeslice - - capacity = capacity_to_service_demand(demand, technologies) - - result = convert_timeslice( - technologies.fix_par * (capacity**technologies.fix_exp), - demand.timeslice, - QuantityType.EXTENSIVE, - ) - return xr.DataArray(result) + capacity = capacity_to_service_demand(technologies, demand) + result = technologies.fix_par * (capacity**technologies.fix_exp) + return result @register_objective def capital_costs( - demand: xr.DataArray, technologies: xr.Dataset, + demand: xr.DataArray, *args, **kwargs, ) -> xr.DataArray: @@ -304,20 +291,14 @@ def capital_costs( :math:`\alpha` is "cap_exp". In other words, capital costs are constant across the simulation for each technology. """ - from muse.timeslices import QuantityType, convert_timeslice - - result = convert_timeslice( - technologies.cap_par * (technologies.scaling_size**technologies.cap_exp), - demand.timeslice, - QuantityType.EXTENSIVE, - ) - return xr.DataArray(result) + result = technologies.cap_par * (technologies.scaling_size**technologies.cap_exp) + return result @register_objective(name="emissions") def emission_cost( - demand: xr.DataArray, technologies: xr.Dataset, + demand: xr.DataArray, prices: xr.DataArray, *args, **kwargs, @@ -335,7 +316,6 @@ def emission_cost( with :math:`s` the timeslices and :math:`c` the commodity. """ from muse.commodities import is_enduse, is_pollutant - from muse.utilities import filter_input enduses = is_enduse(technologies.comm_usage.sel(commodity=demand.commodity)) total = demand.sel(commodity=enduses).sum("commodity") @@ -346,8 +326,8 @@ def emission_cost( @register_objective def fuel_consumption_cost( - demand: xr.DataArray, technologies: xr.Dataset, + demand: xr.DataArray, prices: xr.DataArray, *args, **kwargs, @@ -355,7 +335,6 @@ def fuel_consumption_cost( """Cost of fuels when fulfilling whole demand.""" from muse.commodities import is_fuel from muse.quantities import consumption - from muse.utilities import filter_input commodity = is_fuel(technologies.comm_usage.sel(commodity=demand.commodity)) fcons = consumption(technologies=technologies, prices=prices, production=demand) @@ -365,8 +344,8 @@ def fuel_consumption_cost( @register_objective(name=["ALCOE"]) def annual_levelized_cost_of_energy( - demand: xr.DataArray, technologies: xr.Dataset, + demand: xr.DataArray, prices: xr.DataArray, *args, **kwargs, @@ -388,13 +367,15 @@ def annual_levelized_cost_of_energy( """ from muse.costs import annual_levelized_cost_of_energy as aLCOE - return aLCOE(prices, technologies).max("timeslice") + return filter_input( + aLCOE(prices, technologies).max("timeslice"), year=demand.year.item() + ) @register_objective(name=["LCOE", "LLCOE"]) def lifetime_levelized_cost_of_energy( - demand: xr.DataArray, technologies: xr.Dataset, + demand: xr.DataArray, prices: xr.DataArray, *args, **kwargs, @@ -418,7 +399,7 @@ def lifetime_levelized_cost_of_energy( from muse.costs import lifetime_levelized_cost_of_energy as LCOE from muse.timeslices import QuantityType, convert_timeslice - capacity = capacity_to_service_demand(demand, technologies) + capacity = capacity_to_service_demand(technologies, demand) production = capacity * technologies.fixed_outputs * technologies.utilization_factor production = convert_timeslice(production, demand.timeslice, QuantityType.EXTENSIVE) @@ -435,8 +416,8 @@ def lifetime_levelized_cost_of_energy( @register_objective(name="NPV") def net_present_value( - demand: xr.DataArray, technologies: xr.Dataset, + demand: xr.DataArray, prices: xr.DataArray, *args, **kwargs, @@ -477,7 +458,7 @@ def net_present_value( from muse.costs import net_present_value as NPV from muse.timeslices import QuantityType, convert_timeslice - capacity = capacity_to_service_demand(demand, technologies) + capacity = capacity_to_service_demand(technologies, demand) production = capacity * technologies.fixed_outputs * technologies.utilization_factor production = convert_timeslice(production, demand.timeslice, QuantityType.EXTENSIVE) @@ -493,8 +474,8 @@ def net_present_value( @register_objective(name="NPC") def net_present_cost( - demand: xr.DataArray, technologies: xr.Dataset, + demand: xr.DataArray, prices: xr.DataArray, *args, **kwargs, @@ -511,7 +492,7 @@ def net_present_cost( from muse.costs import net_present_cost as NPC from muse.timeslices import QuantityType, convert_timeslice - capacity = capacity_to_service_demand(demand, technologies) + capacity = capacity_to_service_demand(technologies, demand) production = capacity * technologies.fixed_outputs * technologies.utilization_factor production = convert_timeslice(production, demand.timeslice, QuantityType.EXTENSIVE) @@ -527,8 +508,8 @@ def net_present_cost( @register_objective(name="EAC") def equivalent_annual_cost( - demand: xr.DataArray, technologies: xr.Dataset, + demand: xr.DataArray, prices: xr.DataArray, *args, **kwargs, @@ -556,7 +537,7 @@ def equivalent_annual_cost( from muse.costs import equivalent_annual_cost as EAC from muse.timeslices import QuantityType, convert_timeslice - capacity = capacity_to_service_demand(demand, technologies) + capacity = capacity_to_service_demand(technologies, demand) production = capacity * technologies.fixed_outputs * technologies.utilization_factor production = convert_timeslice(production, demand.timeslice, QuantityType.EXTENSIVE) diff --git a/tests/test_objectives.py b/tests/test_objectives.py index 5cfa28168..e94b939b8 100644 --- a/tests/test_objectives.py +++ b/tests/test_objectives.py @@ -1,12 +1,32 @@ -from pytest import approx, mark -from xarray import DataArray +from pytest import fixture, mark -def add_var(coordinates, *dims, factor=100.0): - from numpy.random import rand +@fixture +def _demand(demand_share, search_space): + reduced_demand = demand_share.sel( + { + k: search_space[k] + for k in set(demand_share.dims).intersection(search_space.dims) + } + ) + reduced_demand["year"] = 2030 + return reduced_demand - shape = tuple(len(coordinates[u]) for u in dims) - return dims, (rand(*shape) * factor).astype(type(factor)) + +@fixture +def _technologies(technologies, retro_agent, search_space): + techs = retro_agent.filter_input( + technologies, + technology=search_space.replacement, + year=retro_agent.forecast_year, + ).drop_vars("technology") + return techs + + +@fixture +def _prices(retro_agent, agent_market): + prices = retro_agent.filter_input(agent_market.prices) + return prices @mark.usefixtures("save_registries") @@ -21,7 +41,7 @@ def a_objective(*args, **kwargs): assert OBJECTIVES["a_objective"] is a_objective @register_objective(name="something") - def b_objective(search_space: DataArray): + def b_objective(*args, **kwargs): pass assert "something" in OBJECTIVES @@ -29,15 +49,15 @@ def b_objective(search_space: DataArray): @mark.usefixtures("save_registries") -def test_computing_objectives(demand_share, search_space): +def test_computing_objectives(_technologies, _demand, search_space): from muse.objectives import factory, register_objective @register_objective - def first(retro_agent, demand_share, search_space, switch=True, assets=None): - return (1 if switch else 2) * (search_space == search_space) + def first(technologies, demand, switch=True, assets=None): + return 1 if switch else 2 @register_objective - def second(retro_agent, demand_share, search_space, switch=True, assets=None): + def second(technologies, demand, switch=True, assets=None): from numpy import full from xarray import DataArray @@ -46,16 +66,14 @@ def second(retro_agent, demand_share, search_space, switch=True, assets=None): result[{"asset": assets}] = 3 return result - objectives = factory("first")(None, demand_share, search_space) + objectives = factory("first")(_technologies, _demand, True) assert set(objectives.data_vars) == {"first"} assert (objectives.first == 1).all() - objectives = factory("first")(None, demand_share, search_space, False) + objectives = factory("first")(_technologies, _demand, False) assert (objectives.first == 2).all() - objectives = factory(["first", "second"])( - None, demand_share, search_space, False, 0 - ) + objectives = factory(["first", "second"])(_technologies, _demand, False, 0) assert set(objectives.data_vars) == {"first", "second"} assert (objectives.first == 2).all() if len(objectives.asset) > 0: @@ -64,351 +82,109 @@ def second(retro_agent, demand_share, search_space, switch=True, assets=None): assert (objectives.second.isel(asset=1) == 5).all() -def test_comfort(search_space, demand_share, technologies, retro_agent): +def test_comfort(_technologies, _demand): from muse.objectives import comfort - technologies["comfort"] = add_var(technologies, "technology") + _technologies["comfort"] = add_var(_technologies, "replacement") + result = comfort(_technologies, _demand) + assert set(result.dims) == {"replacement"} - expected = technologies.comfort.sel(technology=search_space.replacement) - actual = comfort(retro_agent, demand_share, search_space, technologies) - assert set(actual.dims) == {"replacement"} - assert actual.values == approx(expected.values) +def test_efficiency(_technologies, _demand): + from muse.objectives import efficiency -def test_capital_costs(demand_share, search_space, technologies, retro_agent): - import numpy as np - from muse.objectives import capital_costs + _technologies["efficiency"] = add_var(_technologies, "replacement") + result = efficiency(_technologies, _demand) + assert set(result.dims) == {"replacement"} - technologies["cap_par"] = add_var(technologies, "technology", "region", "year") - technologies["cap_exp"] = add_var(technologies, "technology", "region", "year") - technologies["scaling_size"] = add_var(technologies, "technology", "region", "year") - minyear = technologies.year.values.min() - maxyear = technologies.year.values.max() - years = np.linspace(minyear, maxyear, retro_agent.forecast).astype(int) - technologies = technologies.interp(year=years, method=retro_agent.interpolation) - # exp == 0 - technologies.cap_exp.loc[ - {"region": retro_agent.region, "technology": search_space.replacement} - ] = 0 - actual = capital_costs(retro_agent, demand_share, search_space, technologies) - actual = actual.sum("timeslice") - assert set(actual.dims) == {"replacement"} - expected = technologies.cap_par.sel( - technology=search_space.replacement, - region=retro_agent.region, - year=retro_agent.forecast_year, - ) - assert actual.values == approx(expected.values) - - # exp == 1 - technologies.cap_exp.loc[ - {"region": retro_agent.region, "technology": search_space.replacement} - ] = 1 - actual = capital_costs(retro_agent, demand_share, search_space, technologies) - actual = actual.sum("timeslice") - assert set(actual.dims) == {"replacement"} - expected = technologies.cap_par * technologies.scaling_size - expected = expected.sel( - technology=search_space.replacement, - region=retro_agent.region, - year=retro_agent.forecast_year, - ) +def test_capacity_to_service_demand(_technologies, _demand): + from muse.objectives import capacity_to_service_demand - assert actual.values == approx(expected.values) + result = capacity_to_service_demand(_technologies, _demand) + assert set(result.dims) == {"replacement", "asset"} - # exp == numbers - technologies["scaling_size"] = add_var(technologies, "technology", "region", "year") - expected = technologies.cap_par * technologies.scaling_size**technologies.cap_exp - expected = expected.sel( - technology=search_space.replacement, - region=retro_agent.region, - year=retro_agent.forecast_year, - ) - actual = capital_costs(retro_agent, demand_share, search_space, technologies) - actual = actual.sum("timeslice") - assert set(actual.dims) == {"replacement"} - assert actual.values == approx(expected.values) +def test_capacity_in_use(_technologies, _demand): + from muse.objectives import capacity_in_use + result = capacity_in_use(_technologies, _demand) + assert set(result.dims) == {"replacement", "asset"} -def test_emission_cost( - demand_share, search_space, technologies, retro_agent, agent_market -): - from muse.commodities import is_enduse, is_pollutant - from muse.objectives import emission_cost - from xarray import broadcast - fouts = technologies.fixed_outputs.sel( - commodity=is_pollutant(technologies.comm_usage) - ) - envs = is_pollutant(technologies.comm_usage.sel(commodity=agent_market.commodity)) - prices = ( - agent_market.prices.sel(commodity=envs) - .interp(year=retro_agent.forecast_year, method="linear") - .drop_vars("year") - ) +def test_consumption(_technologies, _demand, _prices): + from muse.objectives import consumption - expected = ( - ( - demand_share.sel( - commodity=is_enduse( - technologies.comm_usage.sel(commodity=demand_share.commodity) - ), - asset=search_space.asset, - ).sum("commodity") - * (prices * fouts).sum("commodity") - ) - .sum("timeslice") - .interp(year=retro_agent.forecast_year) - .sel(region=retro_agent.region, technology=search_space.replacement) - ) + result = consumption(_technologies, _demand, _prices) + assert set(result.dims) == {"replacement", "asset", "timeslice"} - actual = emission_cost( - retro_agent, demand_share, search_space, technologies, agent_market - ).sum("timeslice") - assert {"asset", "replacement"}.issuperset(actual.dims) - actual, expected = broadcast(actual, expected) - assert actual.values == approx(expected.values) +def test_fixed_costs(_technologies, _demand): + from muse.objectives import fixed_costs + result = fixed_costs(_technologies, _demand) + assert set(result.dims) == {"replacement", "asset"} -def test_capacity_fulfilling_demand( - search_space, demand_share, technologies, retro_agent, agent_market -): - import numpy as np - from muse.objectives import capacity_to_service_demand, fixed_costs - minyear = technologies.year.values.min() - maxyear = technologies.year.values.max() - years = np.linspace(minyear, maxyear, retro_agent.forecast).astype(int) - technologies = technologies.interp(year=years, method=retro_agent.interpolation) +def test_capital_costs(_technologies, _demand): + from muse.objectives import capital_costs - # capacity - outs = technologies.fixed_outputs.sel( - region=retro_agent.region, - technology=search_space.replacement, - year=retro_agent.forecast_year, - commodity=demand_share.commodity, - ) + _technologies["scaling_size"] = add_var(_technologies, "replacement") + result = capital_costs(_technologies, _demand) + assert set(result.dims) == {"replacement"} - max_demand = (demand_share.where(outs > 0, 0) / outs.where(outs > 0, 1)).max( - ("commodity", "timeslice") - ) - max_hours = agent_market.represent_hours.max() / agent_market.represent_hours.sum() - ufac = technologies.utilization_factor.sel( - region=retro_agent.region, - technology=search_space.replacement, - year=retro_agent.forecast_year, - ) +def test_emission_cost(_technologies, _demand, _prices): + from muse.objectives import emission_cost - capacity = (max_demand / ufac / max_hours).sel(asset=search_space.asset) - actual = capacity_to_service_demand( - retro_agent, demand_share, search_space, technologies, agent_market - ) + result = emission_cost(_technologies, _demand, _prices) + assert set(result.dims) == {"replacement", "asset", "timeslice"} - assert actual.dims == capacity.dims - assert actual.values == approx(capacity.values) - # fixed costs - # TODO Move to a separate test - technologies["fix_par"] = add_var(technologies, "technology", "region", "year") - technologies["fix_exp"] = add_var(technologies, "technology", "region", "year") - fpar = technologies.fix_par.sel( - region=retro_agent.region, - technology=search_space.replacement, - year=retro_agent.forecast_year, - ) - fexp = technologies.fix_exp.sel( - region=retro_agent.region, - technology=search_space.replacement, - year=retro_agent.forecast_year, - ) - expected = fpar * capacity**fexp +def test_fuel_consumption(_technologies, _demand, _prices): + from muse.objectives import fuel_consumption_cost - actual = fixed_costs( - retro_agent, demand_share, search_space, technologies, agent_market - ).sum("timeslice") - assert actual.values == approx(expected.values) - assert set(actual.dims) == set(expected.dims) + result = fuel_consumption_cost(_technologies, _demand, _prices) + assert set(result.dims) == {"replacement", "asset", "timeslice"} -def test_fuel_consumption( - demand_share, search_space, technologies, retro_agent, agent_market -): - from muse.commodities import is_fuel - from muse.objectives import fuel_consumption_cost - from muse.quantities import consumption - from xarray import broadcast - - actual = fuel_consumption_cost( - retro_agent, demand_share, search_space, technologies, agent_market - ).sum("timeslice") - assert {"asset", "replacement"}.issuperset(actual.dims) - - demand = ( - demand_share.sel(asset=search_space.asset) - .where(search_space, 0) - .rename(replacement="technology") - ) - cons = consumption( - production=demand, - technologies=technologies.sel(region=retro_agent.region).interp( - year=retro_agent.forecast_year, method="linear" - ), - prices=agent_market.prices.sel(region=retro_agent.region).interp( - year=retro_agent.forecast_year, method="linear" - ), - ) - fuels = is_fuel(technologies.comm_usage) - prices = agent_market.prices.sel(commodity=fuels, region=retro_agent.region).interp( - year=retro_agent.forecast_year, method="linear" - ) +def test_annual_levelized_cost_of_energy(_technologies, _demand, _prices): + from muse.objectives import annual_levelized_cost_of_energy - dims = list(set((cons * prices).dims) - {"asset", "technology"}) - expected = (cons * prices).sum(dims).sel(technology=search_space.replacement) - actual, expected = broadcast(actual, expected) - assert actual.values == approx(expected.values) - - -def test_net_present_value( - demand_share, search_space, technologies, retro_agent, agent_market -): - """Test the net present value objective. - - It is essentially the same maths but filtering - the inputs using the "sel" method - rather than the agent.filter_input method, - as well as changing the other in which - things are added together. - . - """ - import xarray - from muse.commodities import is_enduse, is_fuel, is_material, is_pollutant - from muse.objectives import capacity_to_service_demand, net_present_value - from muse.quantities import consumption, discount_factor - - technologies.technical_life.loc[{"region": retro_agent.region}] = 10 - - actual = net_present_value( - retro_agent, - demand_share, - search_space, - technologies, - agent_market, - ).sum("timeslice") - tech = retro_agent.filter_input( - technologies, - technology=search_space.replacement, - region=retro_agent.region, - year=retro_agent.forecast_year, - ) + result = annual_levelized_cost_of_energy(_technologies, _demand, _prices) + assert set(result.dims) == {"replacement"} - nyears = tech.technical_life.astype(int) - years = range( - retro_agent.forecast_year, - max( - retro_agent.forecast_year + nyears.values.max(), - retro_agent.forecast_year + 1, - ), - ) - # mask in discoutn rate could give error if different dimension - # used if all_ears is array - # ifxarray used, dimension differences d not tirgger errors - all_years = xarray.DataArray(years, coords={"year": years}, dims="year") - interest_rate = tech.interest_rate - cap_par = tech.cap_par - cap_exp = tech.cap_exp - var_par = tech.var_par - var_exp = tech.var_exp - fix_par = tech.fix_par - fix_exp = tech.fix_exp - fixed_outputs = tech.fixed_outputs - utilization_factor = tech.utilization_factor - - # All years the simulation is running and the prices - prices = agent_market.prices.interp(year=all_years) - prices = retro_agent.filter_input( - agent_market.prices, year=all_years, region=retro_agent.region - ) - # Evolution of rates with time - rates = discount_factor( - years=all_years - retro_agent.year + 1, - interest_rate=interest_rate, - mask=all_years <= retro_agent.year + nyears, - ) - # The individual prices - prices_environmental = prices.sel( - commodity=is_pollutant(technologies.comm_usage) - ).ffill("year") - prices_material = prices.sel(commodity=is_material(technologies.comm_usage)).ffill( - "year" - ) - prices_non_env = prices.sel(commodity=is_enduse(technologies.comm_usage)).ffill( - "year" - ) - prices_fuel = prices.sel(commodity=is_fuel(technologies.comm_usage)).ffill("year") - # Capacity - capacity = capacity_to_service_demand( - retro_agent, demand_share, search_space, technologies, agent_market - ) +def test_lifetime_levelized_cost_of_energy(_technologies, _demand, _prices): + from muse.objectives import lifetime_levelized_cost_of_energy - # Hours ratio - hours_ratio = agent_market.represent_hours / agent_market.represent_hours.sum() - - # raw revenues --> Make the NPV more positive - # This production is the absolute maximum production, given the capacity - production = hours_ratio * capacity * fixed_outputs * utilization_factor - raw_revenues = (production * prices_non_env * rates).sum(("commodity", "year")) - - # raw costs --> make the NPV more negative - # Cost of installed capacity - installed_capacity_costs = hours_ratio * cap_par * capacity**cap_exp - - # Fuel/energy costs - fuel = consumption( - technologies=tech, - production=production, - prices=prices, - ).sel(commodity=is_fuel(tech.comm_usage)) - fuel_consumption_costs = ( - (fuel * prices_fuel * rates).sum(("commodity", "year")) - ).drop_vars("technology") + result = lifetime_levelized_cost_of_energy(_technologies, _demand, _prices) + assert set(result.dims) == {"replacement", "asset", "timeslice"} - # Cost related to environmental products - environmental_costs = (production * prices_environmental * rates).sum( - ("commodity", "year") - ) - # Cost related to material other than fuel/energy and environmentals - material_costs = (production * prices_material * rates).sum(("commodity", "year")) +def test_net_present_value(_technologies, _demand, _prices): + from muse.objectives import net_present_value - # Fixed and Variable costs - non_env_production = production.sel(commodity=is_enduse(tech.comm_usage)).sum( - "commodity" - ) - fix_costs = (rates * hours_ratio * fix_par * capacity**fix_exp).sum("year") - - variable_costs = (rates * var_par * (non_env_production**var_exp)).sum("year") - fixed_and_variable_costs = fix_costs + variable_costs - - assert set(installed_capacity_costs.dims) == set(fuel_consumption_costs.dims) - assert set(environmental_costs.dims) == set(fuel_consumption_costs.dims) - assert set(material_costs.dims) == set(fuel_consumption_costs.dims) - assert set(fixed_and_variable_costs.dims) == set(fuel_consumption_costs.dims) - - raw_costs = ( - installed_capacity_costs - + fuel_consumption_costs - + environmental_costs - + material_costs - + fixed_and_variable_costs - ) + result = net_present_value(_technologies, _demand, _prices) + assert set(result.dims) == {"replacement", "asset", "timeslice"} + + +def test_net_present_cost(_technologies, _demand, _prices): + from muse.objectives import net_present_cost - assert set(raw_revenues.dims) == set(raw_costs.dims) - expected = (raw_revenues - raw_costs).sum("timeslice") + result = net_present_cost(_technologies, _demand, _prices) + assert set(result.dims) == {"replacement", "asset", "timeslice"} - assert {"replacement", "asset"}.issuperset(actual.dims) - assert actual.values == approx(expected.values, rel=1e1) + +def test_equivalent_annual_cost(_technologies, _demand, _prices): + from muse.objectives import equivalent_annual_cost + + result = equivalent_annual_cost(_technologies, _demand, _prices) + assert set(result.dims) == {"replacement", "asset", "timeslice"} + + +def add_var(coordinates, *dims, factor=100.0): + from numpy.random import rand + + shape = tuple(len(coordinates[u]) for u in dims) + return dims, (rand(*shape) * factor).astype(type(factor)) From 380986e91edc655a19dbf08a32d2041a20902556 Mon Sep 17 00:00:00 2001 From: Tom Bland Date: Thu, 22 Aug 2024 15:02:59 +0100 Subject: [PATCH 12/23] Make demand data optional for objectives --- src/muse/objectives.py | 15 ++++----------- tests/test_objectives.py | 12 ++++++------ 2 files changed, 10 insertions(+), 17 deletions(-) diff --git a/src/muse/objectives.py b/src/muse/objectives.py index 0777712fc..3974825fa 100644 --- a/src/muse/objectives.py +++ b/src/muse/objectives.py @@ -124,12 +124,10 @@ def factory( functions = [(param["name"], objective_factory(param)) for param in params] - def objectives( - technologies: xr.Dataset, demand: xr.DataArray, *args, **kwargs - ) -> xr.Dataset: + def objectives(technologies: xr.Dataset, *args, **kwargs) -> xr.Dataset: result = xr.Dataset() for name, objective in functions: - obj = objective(technologies=technologies, demand=demand, *args, **kwargs) + obj = objective(technologies=technologies, *args, **kwargs) result[name] = obj return result @@ -150,12 +148,10 @@ def register_objective(function: OBJECTIVE_SIGNATURE): from functools import wraps @wraps(function) - def decorated_objective( - technologies: xr.Dataset, demand: xr.DataArray, *args, **kwargs - ) -> xr.DataArray: + def decorated_objective(technologies: xr.Dataset, *args, **kwargs) -> xr.DataArray: from logging import getLogger - result = function(technologies, demand, *args, **kwargs) + result = function(technologies, *args, **kwargs) dtype = result.values.dtype if not (np.issubdtype(dtype, np.number) or np.issubdtype(dtype, np.bool_)): @@ -178,7 +174,6 @@ def decorated_objective( @register_objective def comfort( technologies: xr.Dataset, - demand: xr.DataArray, *args, **kwargs, ) -> xr.DataArray: @@ -189,7 +184,6 @@ def comfort( @register_objective def efficiency( technologies: xr.Dataset, - demand: xr.DataArray, *args, **kwargs, ) -> xr.DataArray: @@ -280,7 +274,6 @@ def fixed_costs( @register_objective def capital_costs( technologies: xr.Dataset, - demand: xr.DataArray, *args, **kwargs, ) -> xr.DataArray: diff --git a/tests/test_objectives.py b/tests/test_objectives.py index e94b939b8..783b0ed4d 100644 --- a/tests/test_objectives.py +++ b/tests/test_objectives.py @@ -82,19 +82,19 @@ def second(technologies, demand, switch=True, assets=None): assert (objectives.second.isel(asset=1) == 5).all() -def test_comfort(_technologies, _demand): +def test_comfort(_technologies): from muse.objectives import comfort _technologies["comfort"] = add_var(_technologies, "replacement") - result = comfort(_technologies, _demand) + result = comfort(_technologies) assert set(result.dims) == {"replacement"} -def test_efficiency(_technologies, _demand): +def test_efficiency(_technologies): from muse.objectives import efficiency _technologies["efficiency"] = add_var(_technologies, "replacement") - result = efficiency(_technologies, _demand) + result = efficiency(_technologies) assert set(result.dims) == {"replacement"} @@ -126,11 +126,11 @@ def test_fixed_costs(_technologies, _demand): assert set(result.dims) == {"replacement", "asset"} -def test_capital_costs(_technologies, _demand): +def test_capital_costs(_technologies): from muse.objectives import capital_costs _technologies["scaling_size"] = add_var(_technologies, "replacement") - result = capital_costs(_technologies, _demand) + result = capital_costs(_technologies) assert set(result.dims) == {"replacement"} From 578e1f8d5d797305ece875f319f6bebe0d5b2282 Mon Sep 17 00:00:00 2001 From: Tom Bland Date: Thu, 22 Aug 2024 15:55:31 +0100 Subject: [PATCH 13/23] Fix remaining test --- src/muse/objectives.py | 2 ++ tests/test_objectives.py | 31 +++++++++++++++++++------------ 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/src/muse/objectives.py b/src/muse/objectives.py index 3974825fa..fce85ae57 100644 --- a/src/muse/objectives.py +++ b/src/muse/objectives.py @@ -158,6 +158,8 @@ def decorated_objective(technologies: xr.Dataset, *args, **kwargs) -> xr.DataArr msg = f"dtype of objective {function.__name__} is not a number ({dtype})" getLogger(function.__module__).warning(msg) + if "replacement" not in result.dims: + raise RuntimeError("Objective should return a dimension 'replacement'") if "technology" in result.dims: raise RuntimeError("Objective should not return a dimension 'technology'") if "technology" in result.coords: diff --git a/tests/test_objectives.py b/tests/test_objectives.py index 783b0ed4d..a14fa1241 100644 --- a/tests/test_objectives.py +++ b/tests/test_objectives.py @@ -49,31 +49,38 @@ def b_objective(*args, **kwargs): @mark.usefixtures("save_registries") -def test_computing_objectives(_technologies, _demand, search_space): +def test_computing_objectives(_technologies, _demand): from muse.objectives import factory, register_objective @register_objective - def first(technologies, demand, switch=True, assets=None): - return 1 if switch else 2 + def first(technologies, switch=True, *args, **kwargs): + from xarray import full_like + + value = 1 if switch else 2 + result = full_like(technologies["replacement"], value, dtype=float) + return result @register_objective - def second(technologies, demand, switch=True, assets=None): - from numpy import full - from xarray import DataArray + def second(technologies, demand, assets=None, *args, **kwargs): + from xarray import broadcast, full_like - shape = len(search_space.asset), len(search_space.replacement) - result = DataArray(full(shape, 5), dims=search_space.coords) + result = full_like( + broadcast(technologies["replacement"], demand["asset"])[0], 5, dtype=float + ) result[{"asset": assets}] = 3 return result - objectives = factory("first")(_technologies, _demand, True) + # Test first objective with/without switch + objectives = factory("first")(technologies=_technologies, switch=True) assert set(objectives.data_vars) == {"first"} assert (objectives.first == 1).all() - - objectives = factory("first")(_technologies, _demand, False) + objectives = factory("first")(technologies=_technologies, switch=False) assert (objectives.first == 2).all() - objectives = factory(["first", "second"])(_technologies, _demand, False, 0) + # Test multiple objectives + objectives = factory(["first", "second"])( + technologies=_technologies, demand=_demand, switch=False, assets=0 + ) assert set(objectives.data_vars) == {"first", "second"} assert (objectives.first == 2).all() if len(objectives.asset) > 0: From 90cbb4fd0aab0bf27e1fa06111550ca379f32402 Mon Sep 17 00:00:00 2001 From: Tom Bland Date: Thu, 22 Aug 2024 18:17:03 +0100 Subject: [PATCH 14/23] Create tests for costs functions --- src/muse/costs.py | 10 ++--- src/muse/objectives.py | 1 - tests/test_costs.py | 96 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 101 insertions(+), 6 deletions(-) create mode 100644 tests/test_costs.py diff --git a/src/muse/costs.py b/src/muse/costs.py index 2a722ae89..c95dd834a 100644 --- a/src/muse/costs.py +++ b/src/muse/costs.py @@ -119,11 +119,11 @@ def net_present_value(prices, technologies: xr.Dataset, capacity, production, ye assert set(fixed_costs.dims) == set(variable_costs.dims) fixed_and_variable_costs = ((fixed_costs + variable_costs) * rates).sum("year") - assert set(raw_revenues.dims) == set(installed_capacity_costs.dims) - assert set(raw_revenues.dims) == set(environmental_costs.dims) - assert set(raw_revenues.dims) == set(fuel_costs.dims) - assert set(raw_revenues.dims) == set(material_costs.dims) - assert set(raw_revenues.dims) == set(fixed_and_variable_costs.dims) + # assert set(raw_revenues.dims) == set(installed_capacity_costs.dims) + # assert set(raw_revenues.dims) == set(environmental_costs.dims) + # assert set(raw_revenues.dims) == set(fuel_costs.dims) + # assert set(raw_revenues.dims) == set(material_costs.dims) + # assert set(raw_revenues.dims) == set(fixed_and_variable_costs.dims) results = raw_revenues - ( installed_capacity_costs diff --git a/src/muse/objectives.py b/src/muse/objectives.py index fce85ae57..c452f6617 100644 --- a/src/muse/objectives.py +++ b/src/muse/objectives.py @@ -221,7 +221,6 @@ def capacity_in_use( from muse.timeslices import represent_hours hours = represent_hours(demand.timeslice) - enduses = is_enduse(technologies.comm_usage.sel(commodity=demand.commodity)) return ( (demand.sel(commodity=enduses).sum("commodity") / hours).sum("timeslice") diff --git a/tests/test_costs.py b/tests/test_costs.py new file mode 100644 index 000000000..a7c1c00dc --- /dev/null +++ b/tests/test_costs.py @@ -0,0 +1,96 @@ +from pytest import fixture + + +@fixture +def _prices(market): + prices = market.prices + assert set(prices.dims) == {"commodity", "region", "year", "timeslice"} + return prices + + +@fixture +def _capacity(technologies, demand_share): + from muse.quantities import capacity_to_service_demand + + assert set(technologies.dims) == {"region", "year", "technology", "commodity"} + capacity = capacity_to_service_demand( + technologies=technologies, demand=demand_share + ) + assert set(capacity.dims) == {"asset", "region", "year", "technology"} + return capacity + + +@fixture +def _production(technologies, _capacity, demand_share): + from muse.timeslices import QuantityType, convert_timeslice + + production = ( + _capacity * technologies.fixed_outputs * technologies.utilization_factor + ) + production = convert_timeslice( + production, demand_share.timeslice, QuantityType.EXTENSIVE + ) + assert set(production.dims) == { + "asset", + "timeslice", + "commodity", + "region", + "year", + "technology", + } + return production + + +def test_net_present_value(_prices, technologies, _capacity, _production, year=2030): + from muse.costs import net_present_value + + result = net_present_value(_prices, technologies, _capacity, _production, year) + assert set(result.dims) == {"asset", "timeslice", "region", "year", "technology"} + + +def test_net_present_cost(_prices, technologies, _capacity, _production, year=2030): + from muse.costs import net_present_cost + + result = net_present_cost(_prices, technologies, _capacity, _production, year) + assert set(result.dims) == {"asset", "timeslice", "region", "year", "technology"} + + +def test_equivalent_annual_cost( + _prices, technologies, _capacity, _production, year=2030 +): + from muse.costs import equivalent_annual_cost + + result = equivalent_annual_cost(_prices, technologies, _capacity, _production, year) + assert set(result.dims) == {"asset", "timeslice", "region", "year", "technology"} + + +def test_lifetime_levelized_cost_of_energy( + _prices, technologies, _capacity, _production, year=2030 +): + from muse.costs import lifetime_levelized_cost_of_energy + + result = lifetime_levelized_cost_of_energy( + _prices, technologies, _capacity, _production, year + ) + assert set(result.dims) == {"asset", "timeslice", "region", "year", "technology"} + + +def test_annual_levelized_cost_of_energy(_prices, technologies): + from muse.costs import annual_levelized_cost_of_energy + + result = annual_levelized_cost_of_energy(_prices, technologies) + assert set(result.dims) == {"timeslice", "region", "year", "technology"} + + +def test_supply_cost(_production, _prices, technologies): + from muse.costs import supply_cost, annual_levelized_cost_of_energy + + lcoe = annual_levelized_cost_of_energy(_prices, technologies) + result = supply_cost(_production, lcoe) + assert set(result.dims) == { + "timeslice", + "region", + "year", + "technology", + "commodity", + } From 6d25c3762594f7484d33e677877734cbeb312ea9 Mon Sep 17 00:00:00 2001 From: Tom Bland Date: Thu, 22 Aug 2024 18:45:25 +0100 Subject: [PATCH 15/23] Address ruff complaints --- src/muse/costs.py | 65 +++++++++++++++++++++++++++++++++++---------- tests/test_costs.py | 2 +- 2 files changed, 52 insertions(+), 15 deletions(-) diff --git a/src/muse/costs.py b/src/muse/costs.py index c95dd834a..2f27ee374 100644 --- a/src/muse/costs.py +++ b/src/muse/costs.py @@ -9,13 +9,18 @@ from muse.utilities import filter_input -def net_present_value(prices, technologies: xr.Dataset, capacity, production, year): +def net_present_value( + prices: xr.DataArray, + technologies: xr.Dataset, + capacity: xr.DataArray, + production: xr.DataArray, + year: int, +) -> xr.DataArray: """Net present value (NPV) of the relevant technologies. - The net present value of a Component is the present value of all the revenues that - a Component earns over its lifetime minus all the costs of installing and operating + The net present value of a technology is the present value of all the revenues that + a technology earns over its lifetime minus all the costs of installing and operating it. Follows the definition of the `net present cost`_ given by HOMER Energy. - Metrics are calculated .. _net present cost: .. https://www.homerenergy.com/products/pro/docs/3.15/net_present_cost.html @@ -34,7 +39,11 @@ def net_present_value(prices, technologies: xr.Dataset, capacity, production, ye installation year of the technology. Arguments: - technologies: All the technologies + prices: xr.DataArray with commodity prices + technologies: xr.Dataset of technology parameters + capacity: xr.DataArray with the capacity of the relevant technologies + production: xr.DataArray with the production of the relevant technologies + year: int, the year of the forecast Return: xr.DataArray with the NPV calculated for the relevant technologies @@ -136,7 +145,13 @@ def net_present_value(prices, technologies: xr.Dataset, capacity, production, ye return results -def net_present_cost(prices, technologies: xr.Dataset, capacity, production, year): +def net_present_cost( + prices: xr.DataArray, + technologies: xr.Dataset, + capacity: xr.DataArray, + production: xr.DataArray, + year: int, +) -> xr.DataArray: """Net present cost (NPC) of the relevant technologies. The net present cost of a Component is the present value of all the costs of @@ -145,13 +160,27 @@ def net_present_cost(prices, technologies: xr.Dataset, capacity, production, yea .. seealso:: :py:func:`net_present_value`. + + Arguments: + prices: xr.DataArray with commodity prices + technologies: xr.Dataset of technology parameters + capacity: xr.DataArray with the capacity of the relevant technologies + production: xr.DataArray with the production of the relevant technologies + year: int, the year of the forecast + + Return: + xr.DataArray with the NPC calculated for the relevant technologies """ return -net_present_value(prices, technologies, capacity, production, year) def equivalent_annual_cost( - prices, technologies: xr.Dataset, capacity, production, year -): + prices: xr.DataArray, + technologies: xr.Dataset, + capacity: xr.DataArray, + production: xr.DataArray, + year: int, +) -> xr.DataArray: """Equivalent annual costs (or annualized cost) of a technology. This is the cost that, if it were to occur equally in every year of the @@ -163,7 +192,11 @@ def equivalent_annual_cost( https://www.homerenergy.com/products/pro/docs/3.15/annualized_cost.html Arguments: - technologies: All the technologies + prices: xr.DataArray with commodity prices + technologies: xr.Dataset of technology parameters + capacity: xr.DataArray with the capacity of the relevant technologies + production: xr.DataArray with the production of the relevant technologies + year: int, the year of the forecast Return: xr.DataArray with the EAC calculated for the relevant technologies @@ -176,10 +209,10 @@ def equivalent_annual_cost( def lifetime_levelized_cost_of_energy( prices: xr.DataArray, technologies: xr.Dataset, - capacity, - production, - year, -): + capacity: xr.DataArray, + production: xr.DataArray, + year: int, +) -> xr.DataArray: """Levelized cost of energy (LCOE) of technologies over their lifetime. It follows the `simplified LCOE` given by NREL. The LCOE is set to zero for those @@ -187,7 +220,11 @@ def lifetime_levelized_cost_of_energy( factor. Arguments: - technologies: All the technologies + prices: xr.DataArray with commodity prices + technologies: xr.Dataset of technology parameters + capacity: xr.DataArray with the capacity of the relevant technologies + production: xr.DataArray with the production of the relevant technologies + year: int, the year of the forecast Return: xr.DataArray with the LCOE calculated for the relevant technologies diff --git a/tests/test_costs.py b/tests/test_costs.py index a7c1c00dc..1abd9cf8c 100644 --- a/tests/test_costs.py +++ b/tests/test_costs.py @@ -83,7 +83,7 @@ def test_annual_levelized_cost_of_energy(_prices, technologies): def test_supply_cost(_production, _prices, technologies): - from muse.costs import supply_cost, annual_levelized_cost_of_energy + from muse.costs import annual_levelized_cost_of_energy, supply_cost lcoe = annual_levelized_cost_of_energy(_prices, technologies) result = supply_cost(_production, lcoe) From 2a316d5a90aa5ec0bf5434befbf6cf5dcea8a361 Mon Sep 17 00:00:00 2001 From: Tom Bland Date: Thu, 22 Aug 2024 19:11:34 +0100 Subject: [PATCH 16/23] Hopefully fix failing test --- src/muse/objectives.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/muse/objectives.py b/src/muse/objectives.py index c452f6617..7ebc1530d 100644 --- a/src/muse/objectives.py +++ b/src/muse/objectives.py @@ -70,6 +70,7 @@ def comfort( from muse.outputs.cache import cache_quantity from muse.registration import registrator +from muse.timeslices import drop_timeslice from muse.utilities import filter_input OBJECTIVE_SIGNATURE = Callable[ @@ -124,10 +125,20 @@ def factory( functions = [(param["name"], objective_factory(param)) for param in params] - def objectives(technologies: xr.Dataset, *args, **kwargs) -> xr.Dataset: + def objectives( + technologies: xr.Dataset, + demand: xr.DataArray, + prices: xr.DataArray, + *args, + **kwargs, + ) -> xr.Dataset: result = xr.Dataset() for name, objective in functions: - obj = objective(technologies=technologies, *args, **kwargs) + obj = objective( + technologies=technologies, demand=demand, prices=prices, *args, **kwargs + ) + if "timeslice" in obj.dims and "timeslice" in result.dims: + obj = drop_timeslice(obj) result[name] = obj return result From 4c7aea0be0aab13e45d13bd33ac6780747ebe0dc Mon Sep 17 00:00:00 2001 From: Tom Bland Date: Thu, 22 Aug 2024 19:22:47 +0100 Subject: [PATCH 17/23] Update another test to reflect previous change --- tests/test_objectives.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/tests/test_objectives.py b/tests/test_objectives.py index a14fa1241..7d15cb1ad 100644 --- a/tests/test_objectives.py +++ b/tests/test_objectives.py @@ -49,7 +49,7 @@ def b_objective(*args, **kwargs): @mark.usefixtures("save_registries") -def test_computing_objectives(_technologies, _demand): +def test_computing_objectives(_technologies, _demand, _prices): from muse.objectives import factory, register_objective @register_objective @@ -71,15 +71,23 @@ def second(technologies, demand, assets=None, *args, **kwargs): return result # Test first objective with/without switch - objectives = factory("first")(technologies=_technologies, switch=True) + objectives = factory("first")( + technologies=_technologies, demand=_demand, prices=_prices, switch=True + ) assert set(objectives.data_vars) == {"first"} assert (objectives.first == 1).all() - objectives = factory("first")(technologies=_technologies, switch=False) + objectives = factory("first")( + technologies=_technologies, demand=_demand, prices=_prices, switch=False + ) assert (objectives.first == 2).all() # Test multiple objectives objectives = factory(["first", "second"])( - technologies=_technologies, demand=_demand, switch=False, assets=0 + technologies=_technologies, + demand=_demand, + prices=_prices, + switch=False, + assets=0, ) assert set(objectives.data_vars) == {"first", "second"} assert (objectives.first == 2).all() From 8012550221a97dac340acfc4acbd226f52fe2e95 Mon Sep 17 00:00:00 2001 From: Tom Bland Date: Thu, 22 Aug 2024 19:39:53 +0100 Subject: [PATCH 18/23] Re-order inputs for costs functions --- src/muse/costs.py | 34 ++++++++++++++-------------------- src/muse/objectives.py | 11 ++++++----- tests/test_costs.py | 22 +++++++++++----------- 3 files changed, 31 insertions(+), 36 deletions(-) diff --git a/src/muse/costs.py b/src/muse/costs.py index 2f27ee374..844115801 100644 --- a/src/muse/costs.py +++ b/src/muse/costs.py @@ -10,8 +10,8 @@ def net_present_value( - prices: xr.DataArray, technologies: xr.Dataset, + prices: xr.DataArray, capacity: xr.DataArray, production: xr.DataArray, year: int, @@ -85,9 +85,7 @@ def net_present_value( fuels = is_fuel(technologies.comm_usage) # Revenue - prices_non_env = filter_input(prices, commodity=products, year=years.values).ffill( - "year" - ) + prices_non_env = filter_input(prices, commodity=products, year=years.values) raw_revenues = (production * prices_non_env * rates).sum(("commodity", "year")) # Cost of installed capacity @@ -100,20 +98,18 @@ def net_present_value( # Cost related to environmental products prices_environmental = filter_input( prices, commodity=environmentals, year=years.values - ).ffill("year") + ) environmental_costs = (production * prices_environmental * rates).sum( ("commodity", "year") ) # Fuel/energy costs - prices_fuel = filter_input(prices, commodity=fuels, year=years.values).ffill("year") + prices_fuel = filter_input(prices, commodity=fuels, year=years.values) fuel = consumption(technologies=techs, production=production, prices=prices) fuel_costs = (fuel * prices_fuel * rates).sum(("commodity", "year")) # Cost related to material other than fuel/energy and environmentals - prices_material = filter_input(prices, commodity=material, year=years.values).ffill( - "year" - ) + prices_material = filter_input(prices, commodity=material, year=years.values) material_costs = (production * prices_material * rates).sum(("commodity", "year")) # Fixed and Variable costs @@ -146,8 +142,8 @@ def net_present_value( def net_present_cost( - prices: xr.DataArray, technologies: xr.Dataset, + prices: xr.DataArray, capacity: xr.DataArray, production: xr.DataArray, year: int, @@ -171,12 +167,12 @@ def net_present_cost( Return: xr.DataArray with the NPC calculated for the relevant technologies """ - return -net_present_value(prices, technologies, capacity, production, year) + return -net_present_value(technologies, prices, capacity, production, year) def equivalent_annual_cost( - prices: xr.DataArray, technologies: xr.Dataset, + prices: xr.DataArray, capacity: xr.DataArray, production: xr.DataArray, year: int, @@ -201,14 +197,14 @@ def equivalent_annual_cost( Return: xr.DataArray with the EAC calculated for the relevant technologies """ - npc = net_present_cost(prices, technologies, capacity, production, year) + npc = net_present_cost(technologies, prices, capacity, production, year) crf = capital_recovery_factor(technologies) return npc * crf def lifetime_levelized_cost_of_energy( - prices: xr.DataArray, technologies: xr.Dataset, + prices: xr.DataArray, capacity: xr.DataArray, production: xr.DataArray, year: int, @@ -274,20 +270,18 @@ def lifetime_levelized_cost_of_energy( # Cost related to environmental products prices_environmental = filter_input( prices, commodity=environmentals, year=years.values - ).ffill("year") + ) environmental_costs = (production * prices_environmental * rates).sum( ("commodity", "year") ) # Fuel/energy costs - prices_fuel = filter_input(prices, commodity=fuels, year=years.values).ffill("year") + prices_fuel = filter_input(prices, commodity=fuels, year=years.values) fuel = consumption(technologies=techs, production=production, prices=prices) fuel_costs = (fuel * prices_fuel * rates).sum(("commodity", "year")) # Cost related to material other than fuel/energy and environmentals - prices_material = filter_input(prices, commodity=material, year=years.values).ffill( - "year" - ) + prices_material = filter_input(prices, commodity=material, year=years.values) material_costs = (production * prices_material * rates).sum(("commodity", "year")) # Fixed and Variable costs @@ -313,8 +307,8 @@ def lifetime_levelized_cost_of_energy( def annual_levelized_cost_of_energy( - prices: xr.DataArray, technologies: xr.Dataset, + prices: xr.DataArray, interpolation: str = "linear", fill_value: Union[int, str] = "extrapolate", **filters, diff --git a/src/muse/objectives.py b/src/muse/objectives.py index 7ebc1530d..028b6b05e 100644 --- a/src/muse/objectives.py +++ b/src/muse/objectives.py @@ -373,7 +373,8 @@ def annual_levelized_cost_of_energy( from muse.costs import annual_levelized_cost_of_energy as aLCOE return filter_input( - aLCOE(prices, technologies).max("timeslice"), year=demand.year.item() + aLCOE(technologies=technologies, prices=prices).max("timeslice"), + year=demand.year.item(), ) @@ -409,8 +410,8 @@ def lifetime_levelized_cost_of_energy( production = convert_timeslice(production, demand.timeslice, QuantityType.EXTENSIVE) results = LCOE( - prices=prices, technologies=technologies, + prices=prices, capacity=capacity, production=production, year=demand.year.item(), @@ -468,8 +469,8 @@ def net_present_value( production = convert_timeslice(production, demand.timeslice, QuantityType.EXTENSIVE) results = NPV( - prices=prices, technologies=technologies, + prices=prices, capacity=capacity, production=production, year=demand.year.item(), @@ -502,8 +503,8 @@ def net_present_cost( production = convert_timeslice(production, demand.timeslice, QuantityType.EXTENSIVE) results = NPC( - prices=prices, technologies=technologies, + prices=prices, capacity=capacity, production=production, year=demand.year.item(), @@ -547,8 +548,8 @@ def equivalent_annual_cost( production = convert_timeslice(production, demand.timeslice, QuantityType.EXTENSIVE) results = EAC( - prices=prices, technologies=technologies, + prices=prices, capacity=capacity, production=production, year=demand.year.item(), diff --git a/tests/test_costs.py b/tests/test_costs.py index 1abd9cf8c..bcd42c6c5 100644 --- a/tests/test_costs.py +++ b/tests/test_costs.py @@ -41,51 +41,51 @@ def _production(technologies, _capacity, demand_share): return production -def test_net_present_value(_prices, technologies, _capacity, _production, year=2030): +def test_net_present_value(technologies, _prices, _capacity, _production, year=2030): from muse.costs import net_present_value - result = net_present_value(_prices, technologies, _capacity, _production, year) + result = net_present_value(technologies, _prices, _capacity, _production, year) assert set(result.dims) == {"asset", "timeslice", "region", "year", "technology"} -def test_net_present_cost(_prices, technologies, _capacity, _production, year=2030): +def test_net_present_cost(technologies, _prices, _capacity, _production, year=2030): from muse.costs import net_present_cost - result = net_present_cost(_prices, technologies, _capacity, _production, year) + result = net_present_cost(technologies, _prices, _capacity, _production, year) assert set(result.dims) == {"asset", "timeslice", "region", "year", "technology"} def test_equivalent_annual_cost( - _prices, technologies, _capacity, _production, year=2030 + technologies, _prices, _capacity, _production, year=2030 ): from muse.costs import equivalent_annual_cost - result = equivalent_annual_cost(_prices, technologies, _capacity, _production, year) + result = equivalent_annual_cost(technologies, _prices, _capacity, _production, year) assert set(result.dims) == {"asset", "timeslice", "region", "year", "technology"} def test_lifetime_levelized_cost_of_energy( - _prices, technologies, _capacity, _production, year=2030 + technologies, _prices, _capacity, _production, year=2030 ): from muse.costs import lifetime_levelized_cost_of_energy result = lifetime_levelized_cost_of_energy( - _prices, technologies, _capacity, _production, year + technologies, _prices, _capacity, _production, year ) assert set(result.dims) == {"asset", "timeslice", "region", "year", "technology"} -def test_annual_levelized_cost_of_energy(_prices, technologies): +def test_annual_levelized_cost_of_energy(technologies, _prices): from muse.costs import annual_levelized_cost_of_energy - result = annual_levelized_cost_of_energy(_prices, technologies) + result = annual_levelized_cost_of_energy(technologies, _prices) assert set(result.dims) == {"timeslice", "region", "year", "technology"} def test_supply_cost(_production, _prices, technologies): from muse.costs import annual_levelized_cost_of_energy, supply_cost - lcoe = annual_levelized_cost_of_energy(_prices, technologies) + lcoe = annual_levelized_cost_of_energy(technologies, _prices) result = supply_cost(_production, lcoe) assert set(result.dims) == { "timeslice", From 263c09a983f4f7c2341141945783cfede8a627fd Mon Sep 17 00:00:00 2001 From: Tom Bland Date: Thu, 22 Aug 2024 21:05:07 +0100 Subject: [PATCH 19/23] Improve tests and docstrings --- src/muse/costs.py | 9 +---- src/muse/objectives.py | 85 ++-------------------------------------- tests/test_costs.py | 41 ++++++++++++------- tests/test_objectives.py | 27 ++++++++----- 4 files changed, 48 insertions(+), 114 deletions(-) diff --git a/src/muse/costs.py b/src/muse/costs.py index 844115801..cc8dfa8d9 100644 --- a/src/muse/costs.py +++ b/src/muse/costs.py @@ -124,12 +124,6 @@ def net_present_value( assert set(fixed_costs.dims) == set(variable_costs.dims) fixed_and_variable_costs = ((fixed_costs + variable_costs) * rates).sum("year") - # assert set(raw_revenues.dims) == set(installed_capacity_costs.dims) - # assert set(raw_revenues.dims) == set(environmental_costs.dims) - # assert set(raw_revenues.dims) == set(fuel_costs.dims) - # assert set(raw_revenues.dims) == set(material_costs.dims) - # assert set(raw_revenues.dims) == set(fixed_and_variable_costs.dims) - results = raw_revenues - ( installed_capacity_costs + fuel_costs @@ -456,9 +450,10 @@ def capital_recovery_factor(technologies: xr.Dataset) -> xr.DataArray: xr.DataArray with the CRF calculated for the relevant technologies """ nyears = technologies.technical_life.astype(int) - return technologies.interest_rate / ( + crf = technologies.interest_rate / ( 1 - (1 / (1 + technologies.interest_rate) ** nyears) ) + return crf def discount_factor(years, interest_rate, mask=1.0): diff --git a/src/muse/objectives.py b/src/muse/objectives.py index 028b6b05e..75b8b484b 100644 --- a/src/muse/objectives.py +++ b/src/muse/objectives.py @@ -359,16 +359,6 @@ def annual_levelized_cost_of_energy( It needs to be used for trade agents where the actual service is unknown. It follows the `simplified LCOE` given by NREL. - - Arguments: - demand: Demand for commodities - technologies: All the technologies - prices: Commodity prices - *args: Extra arguments (unused) - **kwargs: Extra keyword arguments (unused) - - Return: - xr.DataArray with the LCOE calculated for the relevant technologies """ from muse.costs import annual_levelized_cost_of_energy as aLCOE @@ -391,16 +381,6 @@ def lifetime_levelized_cost_of_energy( It follows the `simplified LCOE` given by NREL. The LCOE is set to zero for those timeslices where the production is zero, normally due to a zero utilisation factor. - - Arguments: - demand: Demand for commodities - technologies: All the technologies - prices: Commodity prices - *args: Extra arguments (unused) - **kwargs: Extra keyword arguments (unused) - - Return: - xr.DataArray with the LCOE calculated for the relevant technologies """ from muse.costs import lifetime_levelized_cost_of_energy as LCOE from muse.timeslices import QuantityType, convert_timeslice @@ -428,39 +408,7 @@ def net_present_value( *args, **kwargs, ): - """Net present value (NPV) of the relevant technologies. - - The net present value of a Component is the present value of all the revenues that - a Component earns over its lifetime minus all the costs of installing and operating - it. Follows the definition of the `net present cost`_ given by HOMER Energy. - Metrics are calculated - .. _net present cost: - .. https://www.homerenergy.com/products/pro/docs/3.15/net_present_cost.html - - - energy commodities INPUTS are related to fuel costs - - environmental commodities OUTPUTS are related to environmental costs - - material and service commodities INPUTS are related to consumable costs - - fixed and variable costs are given as technodata inputs and depend on the - installed capacity and production (non-environmental), respectively - - capacity costs are given as technodata inputs and depend on the installed capacity - - Note: - Here, the installation year is always agent.forecast_year, - since objectives compute the - NPV for technologies to be installed in the current year. A more general NPV - computation (which would then live in quantities.py) would have to refer to - installation year of the technology. - - Arguments: - demand: Demand for commodities - technologies: All the technologies - prices: Commodity prices - *args: Extra arguments (unused) - **kwargs: Extra keyword arguments (unused) - - Return: - xr.DataArray with the NPV calculated for the relevant technologies - """ + """Net present value (NPV) of the relevant technologies.""" from muse.costs import net_present_value as NPV from muse.timeslices import QuantityType, convert_timeslice @@ -486,15 +434,7 @@ def net_present_cost( *args, **kwargs, ): - """Net present cost (NPC) of the relevant technologies. - - The net present cost of a Component is the present value of all the costs of - installing and operating the Component over the project lifetime, minus the present - value of all the revenues that it earns over the project lifetime. - - .. seealso:: - :py:func:`net_present_value`. - """ + """Net present cost (NPC) of the relevant technologies.""" from muse.costs import net_present_cost as NPC from muse.timeslices import QuantityType, convert_timeslice @@ -520,26 +460,7 @@ def equivalent_annual_cost( *args, **kwargs, ): - """Equivalent annual costs (or annualized cost) of a technology. - - This is the cost that, if it were to occur equally in every year of the - project lifetime, would give the same net present cost as the actual cash - flow sequence associated with that component. The cost is computed using the - `annualized cost`_ expression given by HOMER Energy. - - .. _annualized cost: - https://www.homerenergy.com/products/pro/docs/3.15/annualized_cost.html - - Arguments: - demand: Demand for commodities - technologies: All the technologies - prices: Commodity prices - *args: Extra arguments (unused) - **kwargs: Extra keyword arguments (unused) - - Return: - xr.DataArray with the EAC calculated for the relevant technologies - """ + """Equivalent annual costs (or annualized cost) of a technology.""" from muse.costs import equivalent_annual_cost as EAC from muse.timeslices import QuantityType, convert_timeslice diff --git a/tests/test_costs.py b/tests/test_costs.py index bcd42c6c5..4270d5b2a 100644 --- a/tests/test_costs.py +++ b/tests/test_costs.py @@ -4,7 +4,6 @@ @fixture def _prices(market): prices = market.prices - assert set(prices.dims) == {"commodity", "region", "year", "timeslice"} return prices @@ -12,11 +11,9 @@ def _prices(market): def _capacity(technologies, demand_share): from muse.quantities import capacity_to_service_demand - assert set(technologies.dims) == {"region", "year", "technology", "commodity"} capacity = capacity_to_service_demand( technologies=technologies, demand=demand_share ) - assert set(capacity.dims) == {"asset", "region", "year", "technology"} return capacity @@ -30,29 +27,36 @@ def _production(technologies, _capacity, demand_share): production = convert_timeslice( production, demand_share.timeslice, QuantityType.EXTENSIVE ) - assert set(production.dims) == { + return production + + +def test_fixtures(technologies, _prices, _capacity, _production): + """Validating that the fixtures have appropriate dimensions.""" + assert set(technologies.dims) == {"commodity", "region", "technology", "year"} + assert set(_prices.dims) == {"commodity", "region", "timeslice", "year"} + assert set(_capacity.dims) == {"asset", "region", "technology", "year"} + assert set(_production.dims) == { "asset", - "timeslice", "commodity", "region", - "year", "technology", + "timeslice", + "year", } - return production def test_net_present_value(technologies, _prices, _capacity, _production, year=2030): from muse.costs import net_present_value result = net_present_value(technologies, _prices, _capacity, _production, year) - assert set(result.dims) == {"asset", "timeslice", "region", "year", "technology"} + assert set(result.dims) == {"asset", "region", "technology", "timeslice", "year"} def test_net_present_cost(technologies, _prices, _capacity, _production, year=2030): from muse.costs import net_present_cost result = net_present_cost(technologies, _prices, _capacity, _production, year) - assert set(result.dims) == {"asset", "timeslice", "region", "year", "technology"} + assert set(result.dims) == {"asset", "region", "technology", "timeslice", "year"} def test_equivalent_annual_cost( @@ -61,7 +65,7 @@ def test_equivalent_annual_cost( from muse.costs import equivalent_annual_cost result = equivalent_annual_cost(technologies, _prices, _capacity, _production, year) - assert set(result.dims) == {"asset", "timeslice", "region", "year", "technology"} + assert set(result.dims) == {"asset", "region", "technology", "timeslice", "year"} def test_lifetime_levelized_cost_of_energy( @@ -72,14 +76,14 @@ def test_lifetime_levelized_cost_of_energy( result = lifetime_levelized_cost_of_energy( technologies, _prices, _capacity, _production, year ) - assert set(result.dims) == {"asset", "timeslice", "region", "year", "technology"} + assert set(result.dims) == {"asset", "region", "technology", "timeslice", "year"} def test_annual_levelized_cost_of_energy(technologies, _prices): from muse.costs import annual_levelized_cost_of_energy result = annual_levelized_cost_of_energy(technologies, _prices) - assert set(result.dims) == {"timeslice", "region", "year", "technology"} + assert set(result.dims) == {"region", "technology", "timeslice", "year"} def test_supply_cost(_production, _prices, technologies): @@ -88,9 +92,16 @@ def test_supply_cost(_production, _prices, technologies): lcoe = annual_levelized_cost_of_energy(technologies, _prices) result = supply_cost(_production, lcoe) assert set(result.dims) == { - "timeslice", + "commodity", "region", - "year", "technology", - "commodity", + "timeslice", + "year", } + + +def test_capital_recovery_factor(technologies): + from muse.costs import capital_recovery_factor + + result = capital_recovery_factor(technologies) + assert set(result.dims) == {"region", "technology", "year"} diff --git a/tests/test_objectives.py b/tests/test_objectives.py index 7d15cb1ad..a8030634d 100644 --- a/tests/test_objectives.py +++ b/tests/test_objectives.py @@ -1,6 +1,16 @@ from pytest import fixture, mark +@fixture +def _technologies(technologies, retro_agent, search_space): + techs = retro_agent.filter_input( + technologies, + technology=search_space.replacement, + year=retro_agent.forecast_year, + ).drop_vars("technology") + return techs + + @fixture def _demand(demand_share, search_space): reduced_demand = demand_share.sel( @@ -13,22 +23,19 @@ def _demand(demand_share, search_space): return reduced_demand -@fixture -def _technologies(technologies, retro_agent, search_space): - techs = retro_agent.filter_input( - technologies, - technology=search_space.replacement, - year=retro_agent.forecast_year, - ).drop_vars("technology") - return techs - - @fixture def _prices(retro_agent, agent_market): prices = retro_agent.filter_input(agent_market.prices) return prices +def test_fixtures(_technologies, _demand, _prices): + """Validating that the fixtures have appropriate dimensions.""" + assert set(_technologies.dims) == {"commodity", "replacement"} + assert set(_demand.dims) == {"asset", "commodity", "timeslice"} + assert set(_prices.dims) == {"commodity", "timeslice", "year"} + + @mark.usefixtures("save_registries") def test_objective_registration(): from muse.objectives import OBJECTIVES, register_objective From 72ff48167ac30346c93af69c57cbde572692acdd Mon Sep 17 00:00:00 2001 From: Tom Bland Date: Thu, 22 Aug 2024 21:22:09 +0100 Subject: [PATCH 20/23] Fix issue with ALCOE argument order --- src/muse/production.py | 4 +++- src/muse/quantities.py | 2 +- src/muse/sectors/sector.py | 2 +- tests/test_quantities.py | 10 +++++----- 4 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/muse/production.py b/src/muse/production.py index 95124c31c..b23d4666d 100644 --- a/src/muse/production.py +++ b/src/muse/production.py @@ -186,7 +186,9 @@ def costed_production( raise ValueError(f"Unknown cost {costs}") if callable(costs): technodata = cast(xr.Dataset, broadcast_techs(technologies, capacity)) - costs = costs(market.prices.sel(region=technodata.region), technodata) + costs = costs( + prices=market.prices.sel(region=technodata.region), technologies=technodata + ) else: costs = costs assert isinstance(costs, xr.DataArray) diff --git a/src/muse/quantities.py b/src/muse/quantities.py index 6d232d571..7bd7e1769 100644 --- a/src/muse/quantities.py +++ b/src/muse/quantities.py @@ -380,7 +380,7 @@ def demand_matched_production( from muse.utilities import broadcast_techs technodata = cast(xr.Dataset, broadcast_techs(technologies, capacity)) - cost = ALCOE(prices, technodata, **filters) + cost = ALCOE(prices=prices, technologies=technodata, **filters) max_production = maximum_production(technodata, capacity, **filters) assert ("timeslice" in demand.dims) == ("timeslice" in cost.dims) if "timeslice" in demand.dims and "timeslice" not in max_production.dims: diff --git a/src/muse/sectors/sector.py b/src/muse/sectors/sector.py index 92c39d3a8..b25a7455e 100644 --- a/src/muse/sectors/sector.py +++ b/src/muse/sectors/sector.py @@ -319,7 +319,7 @@ def market_variables(self, market: xr.Dataset, technologies: xr.Dataset) -> Any: costs = supply_cost( supply.where(~is_pollutant(supply.comm_usage), 0), annual_levelized_cost_of_energy( - market.prices.sel(region=supply.region), technodata + prices=market.prices.sel(region=supply.region), technologies=technodata ), asset_dim="asset", ) diff --git a/tests/test_quantities.py b/tests/test_quantities.py index d6150354d..314afee15 100644 --- a/tests/test_quantities.py +++ b/tests/test_quantities.py @@ -439,7 +439,7 @@ def test_costed_production_exact_match(market, capacity, technologies): ) technodata = broadcast_techs(technologies, capacity) costs = annual_levelized_cost_of_energy( - market.prices.sel(region=technodata.region), technodata + prices=market.prices.sel(region=technodata.region), technologies=technodata ) maxdemand = convert_timeslice( xr.Dataset(dict(mp=maximum_production(technologies, capacity))) @@ -479,7 +479,7 @@ def test_costed_production_single_region(market, capacity, technologies): market["consumption"] = drop_timeslice(0.9 * maxdemand) technodata = broadcast_techs(technologies, capacity) costs = annual_levelized_cost_of_energy( - market.prices.sel(region=technodata.region), technodata + prices=market.prices.sel(region=technodata.region), technologies=technodata ) result = costed_production(market.consumption, costs, capacity, technologies) assert isinstance(result, xr.DataArray) @@ -512,7 +512,7 @@ def test_costed_production_single_year(market, capacity, technologies): market["consumption"] = drop_timeslice(0.9 * maxdemand) technodata = broadcast_techs(technologies, capacity) costs = annual_levelized_cost_of_energy( - market.prices.sel(region=technodata.region), technodata + prices=market.prices.sel(region=technodata.region), technologies=technodata ) result = costed_production(market.consumption, costs, capacity, technologies) assert isinstance(result, xr.DataArray) @@ -548,7 +548,7 @@ def test_costed_production_over_capacity(market, capacity, technologies): market["consumption"] = drop_timeslice(maxdemand * 0.9) technodata = broadcast_techs(technologies, capacity) costs = annual_levelized_cost_of_energy( - market.prices.sel(region=technodata.region), technodata + prices=market.prices.sel(region=technodata.region), technologies=technodata ) result = costed_production(market.consumption, costs, capacity, technologies) assert isinstance(result, xr.DataArray) @@ -584,7 +584,7 @@ def test_costed_production_with_minimum_service(market, capacity, technologies, market["consumption"] = drop_timeslice(maxdemand * 0.9) technodata = broadcast_techs(technologies, capacity) costs = annual_levelized_cost_of_energy( - market.prices.sel(region=technodata.region), technodata + prices=market.prices.sel(region=technodata.region), technologies=technodata ) result = costed_production(market.consumption, costs, capacity, technologies) assert isinstance(result, xr.DataArray) From 9eb9b7e26ace35ab7078c8ac2186ac425fde8c39 Mon Sep 17 00:00:00 2001 From: Tom Bland Date: Fri, 23 Aug 2024 10:07:33 +0100 Subject: [PATCH 21/23] Docstrings --- src/muse/agents/agent.py | 9 +++++---- src/muse/costs.py | 26 +++++++++++++++----------- src/muse/objectives.py | 30 ++++++++++++++++++++++-------- src/muse/quantities.py | 4 +++- 4 files changed, 45 insertions(+), 24 deletions(-) diff --git a/src/muse/agents/agent.py b/src/muse/agents/agent.py index 79e75ba6c..82ff0a1e8 100644 --- a/src/muse/agents/agent.py +++ b/src/muse/agents/agent.py @@ -279,14 +279,15 @@ def next( self.year += time_period return None - # Filter technologies according to the search space + # Filter technologies according to the search space, forecast year and region techs = self.filter_input( technologies, technology=search_space.replacement, year=self.forecast_year, + region=self.region, ).drop_vars("technology") - # Filter demand according to the search space + # Reduce dimensions of the demand array reduced_demand = demand.sel( { k: search_space[k] @@ -294,8 +295,8 @@ def next( } ) - # Filter prices - prices = self.filter_input(market.prices) + # Filter prices according to the region + prices = self.filter_input(market.prices, region=self.region) # Compute the objective decision = self._compute_objective( diff --git a/src/muse/costs.py b/src/muse/costs.py index cc8dfa8d9..16d10d1a4 100644 --- a/src/muse/costs.py +++ b/src/muse/costs.py @@ -1,3 +1,11 @@ +"""Collection of functions for calculating cost metrics (e.g. LCOE, EAC). + +In general, these functions take a Dataset of technology parameters, and return a +DataArray of the calculated cost for each technology. Functions may also take additional +data such as commodity prices, capacity of the technologies, and commodity-production +data for the technologies, where appropriate. +""" + from typing import Optional, Union import numpy as np @@ -35,12 +43,11 @@ def net_present_value( Here, the installation year is always agent.forecast_year, since objectives compute the NPV for technologies to be installed in the current year. A more general NPV - computation (which would then live in quantities.py) would have to refer to - installation year of the technology. + computation would have to refer to installation year of the technology. Arguments: - prices: xr.DataArray with commodity prices technologies: xr.Dataset of technology parameters + prices: xr.DataArray with commodity prices capacity: xr.DataArray with the capacity of the relevant technologies production: xr.DataArray with the production of the relevant technologies year: int, the year of the forecast @@ -152,8 +159,8 @@ def net_present_cost( :py:func:`net_present_value`. Arguments: - prices: xr.DataArray with commodity prices technologies: xr.Dataset of technology parameters + prices: xr.DataArray with commodity prices capacity: xr.DataArray with the capacity of the relevant technologies production: xr.DataArray with the production of the relevant technologies year: int, the year of the forecast @@ -182,8 +189,8 @@ def equivalent_annual_cost( https://www.homerenergy.com/products/pro/docs/3.15/annualized_cost.html Arguments: - prices: xr.DataArray with commodity prices technologies: xr.Dataset of technology parameters + prices: xr.DataArray with commodity prices capacity: xr.DataArray with the capacity of the relevant technologies production: xr.DataArray with the production of the relevant technologies year: int, the year of the forecast @@ -210,8 +217,8 @@ def lifetime_levelized_cost_of_energy( factor. Arguments: - prices: xr.DataArray with commodity prices technologies: xr.Dataset of technology parameters + prices: xr.DataArray with commodity prices capacity: xr.DataArray with the capacity of the relevant technologies production: xr.DataArray with the production of the relevant technologies year: int, the year of the forecast @@ -319,11 +326,7 @@ def annual_levelized_cost_of_energy( * [1]: dimensionless Arguments: - prices: [$/(Eh)] the price of all commodities, including consumables and fuels. - This dataarray contains at least timeslice and commodity dimensions. - technologies: Describe the technologies, with at least the following parameters: - * cap_par: [$/E] overnight capital cost * interest_rate: [1] * fix_par: [$/(Eh)] fixed costs of operation and maintenance costs @@ -332,7 +335,8 @@ def annual_levelized_cost_of_energy( consumed per units of energy created. * fixed_outputs: [1] == [(Eh)/(Eh)] ration indicating the amount of environmental pollutants produced per units of energy created. - + prices: [$/(Eh)] the price of all commodities, including consumables and fuels. + This dataarray contains at least timeslice and commodity dimensions. interpolation: interpolation method. fill_value: Fill value for values outside the extrapolation range. **filters: Anything by which prices can be filtered. diff --git a/src/muse/objectives.py b/src/muse/objectives.py index 75b8b484b..13639bca9 100644 --- a/src/muse/objectives.py +++ b/src/muse/objectives.py @@ -29,7 +29,8 @@ def comfort( Arguments: technologies: A data set characterising the technologies from which the - agent can draw assets. + agent can draw assets. This has been pre-filtered according to the agent's + search space. demand: Demand to fulfill. prices: Commodity prices. kwargs: Extra input parameters. These parameters are expected to be set from the @@ -41,7 +42,7 @@ def comfort( these parameters. Returns: - A dataArray with at least one dimension corresponding to ``replacement``. + A DataArray with at least one dimension corresponding to ``replacement``. Other dimensions can be present, as long as the subsequent decision function knows how to reduce them. """ @@ -359,6 +360,9 @@ def annual_levelized_cost_of_energy( It needs to be used for trade agents where the actual service is unknown. It follows the `simplified LCOE` given by NREL. + + See :py:func:`muse.costs.annual_levelized_cost_of_energy` for more details. + """ from muse.costs import annual_levelized_cost_of_energy as aLCOE @@ -378,9 +382,10 @@ def lifetime_levelized_cost_of_energy( ): """Levelized cost of energy (LCOE) of technologies over their lifetime. - It follows the `simplified LCOE` given by NREL. The LCOE is set to zero for those - timeslices where the production is zero, normally due to a zero utilisation - factor. + See :py:func:`muse.costs.lifetime_levelized_cost_of_energy` for more details. + + The LCOE is set to zero for those timeslices where the production is zero, normally + due to a zero utilisation factor. """ from muse.costs import lifetime_levelized_cost_of_energy as LCOE from muse.timeslices import QuantityType, convert_timeslice @@ -408,7 +413,10 @@ def net_present_value( *args, **kwargs, ): - """Net present value (NPV) of the relevant technologies.""" + """Net present value (NPV) of the relevant technologies. + + See :py:func:`muse.costs.net_present_value` for more details. + """ from muse.costs import net_present_value as NPV from muse.timeslices import QuantityType, convert_timeslice @@ -434,7 +442,10 @@ def net_present_cost( *args, **kwargs, ): - """Net present cost (NPC) of the relevant technologies.""" + """Net present cost (NPC) of the relevant technologies. + + See :py:func:`muse.costs.net_present_cost` for more details. + """ from muse.costs import net_present_cost as NPC from muse.timeslices import QuantityType, convert_timeslice @@ -460,7 +471,10 @@ def equivalent_annual_cost( *args, **kwargs, ): - """Equivalent annual costs (or annualized cost) of a technology.""" + """Equivalent annual costs (or annualized cost) of a technology. + + See :py:func:`muse.costs.equivalent_annual_cost` for more details. + """ from muse.costs import equivalent_annual_cost as EAC from muse.timeslices import QuantityType, convert_timeslice diff --git a/src/muse/quantities.py b/src/muse/quantities.py index 7bd7e1769..7daf87b15 100644 --- a/src/muse/quantities.py +++ b/src/muse/quantities.py @@ -1,8 +1,10 @@ """Collection of functions to compute model quantities. This module is meant to collect functions computing quantities of interest to the model, -e.g. lcoe, maximum production for a given capacity, etc, especially where these +e.g. maximum production for a given capacity, etc, especially where these functions are used in different areas of the model. + +Functions for calculating costs (e.g. LCOE, EAC) are in the `costs` module. """ from collections.abc import Sequence From 77f4a170120a5f4c47a3786a239639a2d37115e7 Mon Sep 17 00:00:00 2001 From: Tom Bland Date: Fri, 23 Aug 2024 10:09:38 +0100 Subject: [PATCH 22/23] Revert a previous change --- src/muse/agents/agent.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/muse/agents/agent.py b/src/muse/agents/agent.py index 82ff0a1e8..fb34dffb4 100644 --- a/src/muse/agents/agent.py +++ b/src/muse/agents/agent.py @@ -284,7 +284,6 @@ def next( technologies, technology=search_space.replacement, year=self.forecast_year, - region=self.region, ).drop_vars("technology") # Reduce dimensions of the demand array @@ -296,7 +295,7 @@ def next( ) # Filter prices according to the region - prices = self.filter_input(market.prices, region=self.region) + prices = self.filter_input(market.prices) # Compute the objective decision = self._compute_objective( From 1065b0a0bfbec11ad757f297386c6403dbc4e198 Mon Sep 17 00:00:00 2001 From: Tom Bland Date: Fri, 23 Aug 2024 16:59:26 +0100 Subject: [PATCH 23/23] Fix mistake in docstring --- src/muse/costs.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/muse/costs.py b/src/muse/costs.py index 16d10d1a4..10ef893d4 100644 --- a/src/muse/costs.py +++ b/src/muse/costs.py @@ -212,9 +212,7 @@ def lifetime_levelized_cost_of_energy( ) -> xr.DataArray: """Levelized cost of energy (LCOE) of technologies over their lifetime. - It follows the `simplified LCOE` given by NREL. The LCOE is set to zero for those - timeslices where the production is zero, normally due to a zero utilisation - factor. + It follows the `simplified LCOE` given by NREL. Arguments: technologies: xr.Dataset of technology parameters