/******************************************************************************* * Copyright 2012 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. ******************************************************************************/ package emlab.role; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import org.apache.commons.math.optimization.GoalType; import org.apache.commons.math.optimization.OptimizationException; import org.apache.commons.math.optimization.RealPointValuePair; import org.apache.commons.math.optimization.linear.LinearConstraint; import org.apache.commons.math.optimization.linear.LinearObjectiveFunction; import org.apache.commons.math.optimization.linear.Relationship; import org.apache.commons.math.optimization.linear.SimplexSolver; import org.springframework.beans.factory.annotation.Autowired; import agentspring.role.AbstractRole; import agentspring.trend.GeometricTrend; import emlab.domain.agent.CommoditySupplier; import emlab.domain.agent.EnergyProducer; import emlab.domain.agent.Government; import emlab.domain.market.CO2Auction; import emlab.domain.market.ClearingPoint; import emlab.domain.market.DecarbonizationMarket; import emlab.domain.market.electricity.ElectricitySpotMarket; import emlab.domain.market.electricity.PowerPlantDispatchPlan; import emlab.domain.technology.PowerGeneratingTechnology; import emlab.domain.technology.PowerPlant; import emlab.domain.technology.Substance; import emlab.domain.technology.SubstanceShareInFuelMix; import emlab.repository.Reps; public abstract class AbstractEnergyProducerRole extends AbstractRole<EnergyProducer> { @Autowired Reps reps; public double calculateMarketCapacity(PowerGeneratingTechnology technology, long time) { double capacity = 0d; for (PowerPlant plant : getReps().powerPlantRepository.findOperationalPowerPlantsByTechnology(technology, time)) { capacity += plant.getAvailableCapacity(getCurrentTick()); } logger.info("Capacity for technology {} is {}", technology.getName(), capacity); return capacity; } public double calculateMarketCapacity(ElectricitySpotMarket market, PowerGeneratingTechnology technology, long time) { double capacity = 0d; for (PowerPlant plant : getReps().powerPlantRepository.findOperationalPowerPlantsByTechnology(technology, time)) { if (plant.getLocation().getZone().equals(market.getZone())) { capacity += plant.getAvailableCapacity(time); } } logger.info("Capacity for technology {} is {}", technology.getName(), capacity); return capacity; } public double calculateOwnerCapacityOfType(ElectricitySpotMarket market, PowerGeneratingTechnology technology, long time, EnergyProducer owner) { double capacity = 0d; for (PowerPlant plant : getReps().powerPlantRepository.findOperationalPowerPlantsByTechnology(technology, time)) { if (plant.getLocation().getZone().equals(market.getZone()) && plant.getOwner().equals(owner)) { capacity += plant.getAvailableCapacity(time); } } logger.info("Capacity for technology {} is {}", technology.getName(), capacity); return capacity; } public double calculateTotalOwnerCapacity(ElectricitySpotMarket market, long time, EnergyProducer owner) { double capacity = 0d; getReps().powerPlantRepository.findOperationalPowerPlantsByOwnerAndMarket((emlab.domain.agent.EnergyProducer) owner, market, time); for (PowerPlant plant : getReps().powerPlantRepository.findOperationalPowerPlantsByOwnerAndMarket(owner, market, time)) { capacity += plant.getAvailableCapacity(time); } logger.info("Capacity for owner {} is {}", owner, capacity); return capacity; } public double calculateTotalOwnerCapacityInPipeline(ElectricitySpotMarket market, long time, EnergyProducer owner) { double capacity = 0d; for (PowerPlant plant : getReps().powerPlantRepository.findPowerPlantsByOwnerAndMarketInPipeline(owner, market, getCurrentTick())) { capacity += plant.getAvailableCapacity(time); } logger.info("Capacity in pipeline for owner {} is {}", owner, capacity); return capacity; } public double calculateMarketCapacityEverInstalledUpToGivenTime(PowerGeneratingTechnology technology, long time) { double capacity = 0d; for (PowerPlant plant : getReps().powerPlantRepository.findPowerPlantsByTechnology(technology)) { if (plant.getConstructionStartTime() <= time) { capacity += plant.getAvailableCapacity(getCurrentTick()); } } logger.info("Capacity for technology {} is {}", technology.getName(), capacity); return capacity; } public double calculateMarginalCost(PowerPlant powerPlant) { double mc = 0d; // fuel cost mc += calculateMarginalCostExclCO2MarketCost(powerPlant); mc += calculateCO2MarketMarginalCost(powerPlant); logger.info("Margincal cost for plant {} is {}", powerPlant.getName(), mc); return mc; } public double calculateMarginalCO2Cost(PowerPlant powerPlant) { double mc = 0d; // fuel cost mc += calculateCO2TaxMarginalCost(powerPlant); mc += calculateCO2MarketMarginalCost(powerPlant); logger.info("Margincal cost for plant {} is {}", powerPlant.getName(), mc); return mc; } public double calculateMarginalCostExclCO2MarketCost(PowerPlant powerPlant) { double mc = 0d; // fuel cost mc += calculateMarginalFuelCost(powerPlant); mc += calculateCO2TaxMarginalCost(powerPlant); logger.info("Margincal cost excluding CO2 auction/market cost for plant {} is {}", powerPlant.getName(), mc); return mc; } public double calculateMarginalFuelCost(PowerPlant powerPlant) { double fc = 0d; // fuel cost for each fuel for (SubstanceShareInFuelMix mix : powerPlant.getFuelMix()) { double amount = mix.getShare(); logger.info("Calculating need for fuel: {} units of {}", mix.getShare(), mix.getSubstance().getName()); double fuelPrice = findLastKnownPriceForSubstance(mix.getSubstance()); fc += amount * fuelPrice; logger.info("Calculating marginal cost and found a fuel price which is {} per unit of fuel", fuelPrice); } return fc; } /** * Finds the last known price on a specific market. We try to get it for this tick, previous tick, or from a possible supplier directly. If multiple prices are found, the average is returned. This * is the case for electricity spot markets, as they may have segments. * * @param substance * the price we want for * @return the (average) price found */ public double findLastKnownPriceOnMarket(DecarbonizationMarket market) { Double average = calculateAverageMarketPriceBasedOnClearingPoints(getReps().clearingPointRepositoryOld .findClearingPointsForMarketAndTime(market, getCurrentTick())); Substance substance = market.getSubstance(); if (average != null) { logger.info("Average price found on market for this tick for {}", substance.getName()); return average; } average = calculateAverageMarketPriceBasedOnClearingPoints(getReps().clearingPointRepositoryOld.findClearingPointsForMarketAndTime( market, getCurrentTick() - 1)); if (average != null) { logger.info("Average price found on market for previous tick for {}", substance.getName()); return average; } if (market.getReferencePrice() > 0) { logger.info("Found a reference price found for market for {}", substance.getName()); return market.getReferencePrice(); } for (CommoditySupplier supplier : getReps().genericRepository.findAll(CommoditySupplier.class)) { if (supplier.getSubstance().equals(substance)) { return supplier.getPriceOfCommodity().getValue(getCurrentTick()); } } logger.info("No price has been found for {}", substance.getName()); return 0d; } /** * Finds the last known price for a substance. We try to find the market for it and get it get the price on that market for this tick, previous tick, or from a possible supplier directly. If * multiple prices are found, the average is returned. This is the case for electricity spot markets, as they may have segments. * * @param substance * the price we want for * @return the (average) price found */ public double findLastKnownPriceForSubstance(Substance substance) { DecarbonizationMarket market = getReps().marketRepository.findFirstMarketBySubstance(substance); if (market == null) { logger.warn("No market found for {} so no price can be found", substance.getName()); return 0d; } else { return findLastKnownPriceOnMarket(market); } } /** * Calculates the volume-weighted average price on a market based on a set of clearingPoints. * * @param clearingPoints * the clearingPoints with the volumes and prices * @return the weighted average */ private Double calculateAverageMarketPriceBasedOnClearingPoints(Iterable<ClearingPoint> clearingPoints) { double priceTimesVolume = 0d; double volume = 0d; for (ClearingPoint point : clearingPoints) { priceTimesVolume += point.getPrice() * point.getVolume(); volume += point.getVolume(); } if (volume > 0) { return priceTimesVolume / volume; } return null; } public double calculateCO2MarketMarginalCost(PowerPlant powerPlant) { double co2Intensity = powerPlant.calculateEmissionIntensity(); CO2Auction auction = getReps().genericRepository.findFirst(CO2Auction.class); double co2Price = findLastKnownPriceOnMarket(auction); return co2Intensity * co2Price; } public double calculateCO2MarketCost(PowerPlant powerPlant) { double co2Intensity = powerPlant.calculateEmissionIntensity(); CO2Auction auction = getReps().genericRepository.findFirst(CO2Auction.class); double co2Price = findLastKnownPriceOnMarket(auction); double electricityOutput = powerPlant.calculateElectricityOutputAtTime(getCurrentTick()); return co2Intensity * co2Price * electricityOutput; } /** * Calculates the payment effective part of the national CO2 price. In this case you only pay the excess over the EU carbon market price to your own government. * * @param powerPlant * @return */ public double calculatePaymentEffictiveCO2NationalMinimumPriceCost(PowerPlant powerPlant) { double co2Intensity = powerPlant.calculateEmissionIntensity(); CO2Auction auction = getReps().genericRepository.findFirst(CO2Auction.class); double co2Price = findLastKnownPriceOnMarket(auction); double electricityOutput = powerPlant.calculateElectricityOutputAtTime(getCurrentTick()); double nationalMinCo2price = getReps().nationalGovernmentRepository.findNationalGovernmentByPowerPlant(powerPlant) .getMinNationalCo2PriceTrend().getValue(getCurrentTick()); double paymentEffectivePartOfNationalCO2; if (nationalMinCo2price > co2Price) paymentEffectivePartOfNationalCO2 = nationalMinCo2price - co2Price; else paymentEffectivePartOfNationalCO2 = 0; return co2Intensity * paymentEffectivePartOfNationalCO2 * electricityOutput; } public double calculateCO2TaxMarginalCost(PowerPlant powerPlant) { double co2Intensity = powerPlant.calculateEmissionIntensity(); Government government = getReps().genericRepository.findFirst(Government.class); double co2Tax = government.getCO2Tax(getCurrentTick()); return co2Intensity * co2Tax; } public double findLastKnownCO2Price() { Government government = getReps().genericRepository.findFirst(Government.class); CO2Auction auction = getReps().genericRepository.findFirst(CO2Auction.class); double co2Price = findLastKnownPriceOnMarket(auction); double co2Tax = government.getCO2Tax(getCurrentTick()); return co2Price + co2Tax; } public double calculateCO2Tax(PowerPlant powerPlant) { double co2Intensity = powerPlant.calculateEmissionIntensity(); double electricityOutput = powerPlant.calculateElectricityOutputAtTime(getCurrentTick()); Government government = getReps().genericRepository.findFirst(Government.class); double co2Tax = government.getCO2Tax(getCurrentTick()); double taxToPay = (co2Intensity * electricityOutput) * co2Tax; return taxToPay; } public double calculateFixedOperatingCost(PowerPlant powerPlant) { double norm = powerPlant.getTechnology().getFixedOperatingCost(); long timeConstructed = powerPlant.getConstructionStartTime() + powerPlant.calculateActualLeadtime(); double mod = powerPlant.getTechnology().getFixedOperatingCostModifierAfterLifetime(); long lifetime = powerPlant.calculateActualLifetime(); GeometricTrend trend = new GeometricTrend(); trend.setGrowthRate(mod); trend.setStart(norm); double currentCost = trend.getValue(getCurrentTick() - (timeConstructed + lifetime)); return currentCost; } public double calculateAverageEnergyDensityInOperation(PowerPlant powerPlant) { double energyDensity = 0d; for (SubstanceShareInFuelMix share : powerPlant.getFuelMix()) { energyDensity += share.getSubstance().getEnergyDensity() * share.getShare(); } return energyDensity; } public double calculateAveragePastOperatingProfit(PowerPlant pp, long horizon) { double averageFractionInMerit = 0d; for (long i = -horizon; i <= 0; i++) { averageFractionInMerit += calculatePastOperatingProfitInclFixedOMCost(pp, getCurrentTick() + i) / i; } return averageFractionInMerit; } public double calculatePastOperatingProfitInclFixedOMCost(PowerPlant plant, long time) { double pastOP = 0d; // TODO get all accepted supply bids and calculate income // TODO get all accepted demand bids and calculate costs // TODO get the CO2 cost // TODO get the fixed cost pastOP += calculateFixedOperatingCost(plant); return pastOP; } /** * The fuel mix is calculated with a linear optimization model of the possible fuels and the requirements. * * @param substancePriceMap * contains the possible fuels and their market prices * @param minimumFuelMixQuality * is the minimum fuel quality needed for the power plant to work * @param efficiency * of the plant determines the need for fuel per MWhe * @param co2TaxLevel * is part of the cost for CO2 * @param co2AuctionPrice * is part of the cost for CO2 * @return the fuel mix */ public Set<SubstanceShareInFuelMix> calculateFuelMix(PowerPlant plant, Map<Substance, Double> substancePriceMap, double co2Price) { double efficiency = plant.getActualEfficiency(); Set<SubstanceShareInFuelMix> fuelMix = (plant.getFuelMix() == null) ? new HashSet<SubstanceShareInFuelMix>() : plant.getFuelMix(); int numberOfFuels = substancePriceMap.size(); if (numberOfFuels == 0) { logger.info("No fuels, so no operation mode is set. Empty fuel mix is returned"); return new HashSet<SubstanceShareInFuelMix>(); } else if (numberOfFuels == 1) { SubstanceShareInFuelMix ssifm = null; if (!fuelMix.isEmpty()) { ssifm = fuelMix.iterator().next(); } else { ssifm = new SubstanceShareInFuelMix().persist(); fuelMix.add(ssifm); } Substance substance = substancePriceMap.keySet().iterator().next(); ssifm.setShare(calculateFuelConsumptionWhenOnlyOneFuelIsUsed(substance, efficiency)); ssifm.setSubstance(substance); logger.info("Setting fuel consumption for {} to {}", ssifm.getSubstance().getName(), ssifm.getShare()); return fuelMix; } else { double minimumFuelMixQuality = plant.getTechnology().getMinimumFuelQuality(); double[] fuelAndCO2Costs = new double[numberOfFuels]; double[] fuelDensities = new double[numberOfFuels]; double[] fuelQuality = new double[numberOfFuels]; int i = 0; for (Substance substance : substancePriceMap.keySet()) { fuelAndCO2Costs[i] = substancePriceMap.get(substance) + substance.getCo2Density() * (co2Price); fuelDensities[i] = substance.getEnergyDensity(); fuelQuality[i] = substance.getQuality() - minimumFuelMixQuality; i++; } logger.info("Fuel prices: {}", fuelAndCO2Costs); logger.info("Fuel densities: {}", fuelDensities); logger.info("Fuel purities: {}", fuelQuality); // Objective function = minimize fuel cost (fuel // consumption*fuelprices // + CO2 intensity*co2 price/tax) LinearObjectiveFunction function = new LinearObjectiveFunction(fuelAndCO2Costs, 0d); List<LinearConstraint> constraints = new ArrayList<LinearConstraint>(); // Constraint 1: total fuel density * fuel consumption should match // required energy input constraints.add(new LinearConstraint(fuelDensities, Relationship.EQ, (1 / efficiency))); // Constraint 2&3: minimum fuel quality (times fuel consumption) // required // The equation is derived from (example for 2 fuels): q1 * x1 / (x1+x2) + q2 * x2 / (x1+x2) >= qmin // so that the fuelquality weighted by the mass percentages is greater than the minimum fuel quality. constraints.add(new LinearConstraint(fuelQuality, Relationship.GEQ, 0)); try { SimplexSolver solver = new SimplexSolver(); RealPointValuePair solution = solver.optimize(function, constraints, GoalType.MINIMIZE, true); logger.info("Succesfully solved a linear optimization for fuel mix"); int f = 0; Iterator<SubstanceShareInFuelMix> iterator = plant.getFuelMix().iterator(); for (Substance substance : substancePriceMap.keySet()) { double share = solution.getPoint()[f]; SubstanceShareInFuelMix ssifm; if (iterator.hasNext()) { ssifm = iterator.next(); } else { ssifm = new SubstanceShareInFuelMix().persist(); fuelMix.add(ssifm); } double fuelConsumptionPerMWhElectricityProduced = convertFuelShareToMassVolume(share); logger.info("Setting fuel consumption for {} to {}", substance.getName(), fuelConsumptionPerMWhElectricityProduced); ssifm.setShare(fuelConsumptionPerMWhElectricityProduced); ssifm.setSubstance(substance); f++; } logger.info("If single fired, it would have been: {}", calculateFuelConsumptionWhenOnlyOneFuelIsUsed(substancePriceMap.keySet().iterator().next(), efficiency)); return fuelMix; } catch (OptimizationException e) { logger.warn( "Failed to determine the correct fuel mix. Adding only fuel number 1 in fuel mix out of {} substances and minimum quality of {}", substancePriceMap.size(), minimumFuelMixQuality); logger.info("The fuel added is: {}", substancePriceMap.keySet().iterator().next().getName()); // Override the old one fuelMix = new HashSet<SubstanceShareInFuelMix>(); SubstanceShareInFuelMix ssifm = new SubstanceShareInFuelMix().persist(); Substance substance = substancePriceMap.keySet().iterator().next(); ssifm.setShare(calculateFuelConsumptionWhenOnlyOneFuelIsUsed(substance, efficiency)); ssifm.setSubstance(substance); logger.info("Setting fuel consumption for {} to {}", ssifm.getSubstance().getName(), ssifm.getShare()); fuelMix.add(ssifm); return fuelMix; } } } public double convertFuelShareToMassVolume(double share) { return share * 3600; } public double calculateFuelConsumptionWhenOnlyOneFuelIsUsed(Substance substance, double efficiency) { double fuelConsumptionPerMWhElectricityProduced = convertFuelShareToMassVolume(1 / (efficiency * substance.getEnergyDensity())); return fuelConsumptionPerMWhElectricityProduced; } /** * Calculates the actual investment cost of a power plant per year, by using the exogenous modifier. * * @param powerPlant * @return the actual efficiency */ /* * public double determineAnnuitizedInvestmentCost(PowerPlant powerPlant, long time) { * * double invNorm = powerPlant.getTechnology().getAnnuitizedInvestmentCost(); double modifierExo = calculateExogenousModifier(powerPlant.getTechnology(). getInvestmentCostModifierExogenous(), * time); * * double annuitizedInvestmentCost = invNorm * modifierExo; logger.info("Investment cost of plant{} is {}", powerPlant, annuitizedInvestmentCost); return annuitizedInvestmentCost; } */ public double determineLoanAnnuities(double totalLoan, double payBackTime, double interestRate) { double q = 1 + interestRate; double annuity = totalLoan * (Math.pow(q, payBackTime) * (q - 1)) / (Math.pow(q, payBackTime) - 1); return annuity; } /** * Calculates expected * * @param futureTimePoint * @return */ protected HashMap<ElectricitySpotMarket, Double> determineExpectedCO2PriceInclTax(long futureTimePoint, int yearsAveragingOver) { HashMap<ElectricitySpotMarket, Double> co2Prices = new HashMap<ElectricitySpotMarket, Double>(); CO2Auction co2Auction = reps.marketRepository.findCO2Auction(); for (ElectricitySpotMarket esm : reps.marketRepository.findAllElectricitySpotMarkets()) { double averageOfPastPrices = reps.clearingPointRepository.calculateAverageClearingPriceForMarketAndTimeRange(co2Auction, getCurrentTick() - yearsAveragingOver, (long) (getCurrentTick() - 1)); double nationalCo2MinPriceinFutureTick = reps.nationalGovernmentRepository.findNationalGovernmentByElectricitySpotMarket(esm) .getMinNationalCo2PriceTrend().getValue(futureTimePoint); double co2PriceInCountry = 0d; if (averageOfPastPrices > nationalCo2MinPriceinFutureTick) { co2PriceInCountry = averageOfPastPrices; } else { co2PriceInCountry = nationalCo2MinPriceinFutureTick; } co2PriceInCountry += reps.genericRepository.findFirst(Government.class).getCO2Tax(futureTimePoint); co2Prices.put(esm, Double.valueOf(co2PriceInCountry)); } return co2Prices; } public abstract Reps getReps(); }