/******************************************************************************* * 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.investment; import java.util.HashMap; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.TreeMap; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.transaction.annotation.Transactional; import agentspring.role.Role; import agentspring.role.RoleComponent; import emlab.domain.agent.BigBank; import emlab.domain.agent.EnergyProducer; import emlab.domain.agent.PowerPlantManufacturer; import emlab.domain.contract.CashFlow; import emlab.domain.contract.Loan; import emlab.domain.gis.Zone; import emlab.domain.market.electricity.ElectricitySpotMarket; import emlab.domain.market.electricity.Segment; import emlab.domain.market.electricity.SegmentLoad; import emlab.domain.technology.PowerGeneratingTechnology; import emlab.domain.technology.PowerGridNode; import emlab.domain.technology.PowerPlant; import emlab.domain.technology.Substance; import emlab.domain.technology.SubstanceShareInFuelMix; import emlab.repository.Reps; import emlab.role.AbstractEnergyProducerRole; import emlab.util.MapValueComparator; /** * {@link EnergyProducer}s decide to invest in new {@link PowerPlant} * * @author <a href="mailto:E.J.L.Chappin@tudelft.nl">Emile Chappin</a> @author <a href="mailto:A.Chmieliauskas@tudelft.nl">Alfredas Chmieliauskas</a> * @author JCRichstein */ @RoleComponent public class InvestInPowerGenerationTechnologiesRole extends AbstractEnergyProducerRole implements Role<EnergyProducer> { @Autowired Reps reps; public Reps getReps() { return reps; } // market expectations Map<ElectricitySpotMarket, MarketInformation> marketInfoMap = new HashMap<ElectricitySpotMarket, MarketInformation>(); public void act(EnergyProducer agent) { long futureTimePoint = getCurrentTick() + agent.getInvestmentFutureTimeHorizon(); // logger.warn(agent + " is looking at timepoint " + futureTimePoint); // ==== Expectations === // Fuel Prices Map<Substance, Double> expectedFuelPrices = new HashMap<Substance, Double>(); for (Substance substance : reps.genericRepository.findAll(Substance.class)) { // use last price expectedFuelPrices.put(substance, findLastKnownPriceForSubstance(substance));// TODO // use // expected // fuel // price } // CO2 Map<ElectricitySpotMarket, Double> expectedCO2Price = determineExpectedCO2PriceInclTax(futureTimePoint, 3);// TODO // use // expected // co2 // price // Investment decision for (ElectricitySpotMarket market : reps.genericRepository.findAllAtRandom(ElectricitySpotMarket.class)) { MarketInformation marketInformation = new MarketInformation(market, expectedFuelPrices, expectedCO2Price.get(market) .doubleValue(), futureTimePoint); /* * if (marketInfoMap.containsKey(market) && marketInfoMap.get(market).time == futureTimePoint) { marketInformation = marketInfoMap.get(market); } else { marketInformation = new * MarketInformation(market, expectedFuelPrices, expectedCO2Price, futureTimePoint); marketInfoMap.put(market, marketInformation); } */ // logger.warn(agent + " is expecting a CO2 price of " + // expectedCO2Price.get(market) + " Euro/MWh at timepoint " // + futureTimePoint + " in Market " + market); double highestValue = Double.MIN_VALUE; PowerGeneratingTechnology bestTechnology = null; for (PowerGeneratingTechnology technology : reps.genericRepository.findAll(PowerGeneratingTechnology.class)) { PowerPlant plant = new PowerPlant(); plant.specifyNotPersist(getCurrentTick(), agent, getNodeForZone(market.getZone()), technology); // if too much capacity of this technology in the pipeline (not // limited to the 5 years) double expectedInstalledCapacityOfTechnology = reps.powerPlantRepository .calculateCapacityOfExpectedOperationalPowerPlantsInMarketAndTechnology(market, technology, futureTimePoint); double expectedOwnedTotalCapacityInMarket = reps.powerPlantRepository .calculateCapacityOfExpectedOperationalPowerPlantsInMarketByOwner(market, futureTimePoint, agent); double expectedOwnedCapacityInMarketOfThisTechnology = reps.powerPlantRepository .calculateCapacityOfExpectedOperationalPowerPlantsInMarketByOwnerAndTechnology(market, technology, futureTimePoint, agent); double capacityOfTechnologyInPipeline = reps.powerPlantRepository.calculateCapacityOfPowerPlantsByTechnologyInPipeline( technology, getCurrentTick()); double operationalCapacityOfTechnology = reps.powerPlantRepository.calculateCapacityOfOperationalPowerPlantsByTechnology( technology, getCurrentTick()); if ((expectedInstalledCapacityOfTechnology + technology.getCapacity()) / (marketInformation.maxExpectedLoad + technology.getCapacity()) > technology .getMaximumInstalledCapacityFractionInCountry()) { // logger.warn(agent + // " will not invest in {} technology because there's too much of this type in the market", // technology); } else if (expectedOwnedCapacityInMarketOfThisTechnology > expectedOwnedTotalCapacityInMarket * technology.getMaximumInstalledCapacityFractionPerAgent()) { // logger.warn(agent + // " will not invest in {} technology because there's too much capacity planned by him", // technology); } else if ((capacityOfTechnologyInPipeline > operationalCapacityOfTechnology) && capacityOfTechnologyInPipeline > 3000) { // TODO: // Dirty // hack, // but // reflects // that // you // cannot // expand // a // technology // out // of // zero. // logger.warn(agent + // " will not invest in {} technology because there's too much capacity in the pipeline", // technology); } else if (plant.getActualInvestedCapital() * (1 - agent.getDebtRatioOfInvestments()) > agent .getDownpaymentFractionOfCash() * agent.getCash()) { // logger.warn(agent + // " will not invest in {} technology as he does not have enough money for downpayment", // technology); // TODO: // Modifier // for // investment // costs // is // missing // here } else { Map<Substance, Double> myFuelPrices = new HashMap<Substance, Double>(); for (Substance fuel : technology.getFuels()) { myFuelPrices.put(fuel, expectedFuelPrices.get(fuel)); } Set<SubstanceShareInFuelMix> fuelMix = calculateFuelMix(plant, myFuelPrices, expectedCO2Price.get(market)); plant.setFuelMix(fuelMix); double expectedMarginalCost = determineExpectedMarginalCost(plant, expectedFuelPrices, expectedCO2Price.get(market)); double runningHours = 0d; double expectedGrossProfit = 0d; // logger.warn("Agent {} found that the installed capacity in the market {} in future to be " // + marketInformation.capacitySum + // "and expectde maximum demand to be " + // marketInformation.maxExpectedLoad, // agent, market); long numberOfSegments = reps.segmentRepository.count(); // TODO somehow the prices of long-term contracts could also // be used here to determine the expected profit. Maybe not // though... for (SegmentLoad segmentLoad : market.getLoadDurationCurve()) { double expectedElectricityPrice = marketInformation.expectedElectricityPricesPerSegment.get(segmentLoad .getSegment()); double hours = segmentLoad.getSegment().getLengthInHours(); if (expectedMarginalCost <= expectedElectricityPrice) { runningHours += hours; expectedGrossProfit += (expectedElectricityPrice - expectedMarginalCost) * hours * plant.getAvailableCapacity(futureTimePoint, segmentLoad.getSegment(), numberOfSegments); } } // logger.warn(agent + // "expects technology {} to have {} running", technology, // runningHours); // expect to meet minimum running hours? if (runningHours < plant.getTechnology().getMinimumRunningHours()) { // logger.warn(agent // + " will not invest in {} technology as he expect to have {} running, which is lower then required", // technology, runningHours); } else { double fixedOMCost = calculateFixedOperatingCost(plant);// / // plant.getTechnology().getCapacity(); double operatingProfit = expectedGrossProfit - fixedOMCost; // TODO // should // we // not // exclude // fixed // cost, // or // name // that // NET // profit? // TODO Alter discount rate on the basis of the amount // in long-term contracts? // TODO Alter discount rate on the basis of other stuff, // such as amount of money, market share, portfolio // size. // Calculation of weighted average cost of capital, // based on the companies debt-ratio double wacc = (1 - agent.getDebtRatioOfInvestments()) * agent.getEquityInterestRate() + agent.getDebtRatioOfInvestments() * agent.getLoanInterestRate(); // Creation of out cash-flow during power plant building // phase (note that the cash-flow is negative!) TreeMap<Integer, Double> discountedProjectCapitalOutflow = calculateSimplePowerPlantInvestmentCashFlow( technology.getDepreciationTime(), technology.getExpectedLeadtime(), plant.getActualInvestedCapital(), 0); // Creation of in cashflow during operation TreeMap<Integer, Double> discountedProjectCashInflow = calculateSimplePowerPlantInvestmentCashFlow( technology.getDepreciationTime(), technology.getExpectedLeadtime(), 0, operatingProfit); double discountedCapitalCosts = npv(discountedProjectCapitalOutflow, wacc);// are // defined // negative!! // technology.getCapacity(); // logger.warn("Agent {} found that the discounted capital for technology {} to be " // + discountedCapitalCosts, agent, // technology); double discountedOpProfit = npv(discountedProjectCashInflow, wacc); // logger.warn("Agent {} found the expected prices to be {}", // agent, // marketInformation.expectedElectricityPricesPerSegment); // logger.warn("Agent {} found that the projected discounted inflows for technology {} to be " // + discountedOpProfit, // agent, technology); double projectValue = discountedOpProfit + discountedCapitalCosts; // logger.warn( // "Agent {} found the project value for technology {} to be " // + Math.round(projectValue / // plant.getTechnology().getCapacity()) + // " EUR/kW (running hours: " // + runningHours + "", agent, technology); // double projectTotalValue = projectValuePerMW * // plant.getTechnology().getCapacity(); // double projectReturnOnInvestment = discountedOpProfit // / (-discountedCapitalCosts); /* * Divide by capacity, in order not to favour large power plants (which have the single largest NPV */ if (projectValue > 0 && projectValue / plant.getTechnology().getCapacity() > highestValue) { highestValue = projectValue / plant.getTechnology().getCapacity(); bestTechnology = plant.getTechnology(); } } } } if (bestTechnology != null) { // logger.warn("Agent {} invested in technology {} at tick " + getCurrentTick(), agent, bestTechnology); PowerPlant plant = new PowerPlant(); plant.specifyAndPersist(getCurrentTick(), agent, getNodeForZone(market.getZone()), bestTechnology); PowerPlantManufacturer manufacturer = reps.genericRepository.findFirst(PowerPlantManufacturer.class); BigBank bigbank = reps.genericRepository.findFirst(BigBank.class); double investmentCostPayedByEquity = plant.getActualInvestedCapital() * (1 - agent.getDebtRatioOfInvestments()); double investmentCostPayedByDebt = plant.getActualInvestedCapital() * agent.getDebtRatioOfInvestments(); double downPayment = investmentCostPayedByEquity; createSpreadOutDownPayments(agent, manufacturer, downPayment, plant); double amount = determineLoanAnnuities(investmentCostPayedByDebt, plant.getTechnology().getDepreciationTime(), agent.getLoanInterestRate()); // logger.warn("Loan amount is: " + amount); Loan loan = reps.loanRepository.createLoan(agent, bigbank, amount, plant.getTechnology().getDepreciationTime(), getCurrentTick(), plant); // Create the loan plant.createOrUpdateLoan(loan); } else { // logger.warn("{} found no suitable technology anymore to invest in at tick " // + getCurrentTick(), agent); // agent will not participate in the next round of investment if // he does not invest now setNotWillingToInvest(agent); } } } // Creates n downpayments of equal size in each of the n building years of a // power plant @Transactional private void createSpreadOutDownPayments(EnergyProducer agent, PowerPlantManufacturer manufacturer, double totalDownPayment, PowerPlant plant) { int buildingTime = plant.getTechnology().getExpectedLeadtime(); for (int i = 0; i < buildingTime; i++) { reps.nonTransactionalCreateRepository.createCashFlow(agent, manufacturer, totalDownPayment / buildingTime, CashFlow.DOWNPAYMENT, getCurrentTick() + i, plant); } } @Transactional private void setNotWillingToInvest(EnergyProducer agent) { agent.setWillingToInvest(false); } // Create a powerplant investment and operation cash-flow in the form of a // map. If only investment, or operation costs should be considered set // totalInvestment or operatingProfit to 0 private TreeMap<Integer, Double> calculateSimplePowerPlantInvestmentCashFlow(int depriacationTime, int buildingTime, double totalInvestment, double operatingProfit) { TreeMap<Integer, Double> investmentCashFlow = new TreeMap<Integer, Double>(); double equalTotalDownPaymentInstallement = totalInvestment / buildingTime; for (int i = 0; i < buildingTime; i++) { investmentCashFlow.put(new Integer(i), -equalTotalDownPaymentInstallement); } for (int i = buildingTime; i < depriacationTime + buildingTime; i++) { investmentCashFlow.put(new Integer(i), operatingProfit); } return investmentCashFlow; } private double npv(TreeMap<Integer, Double> netCashFlow, double wacc) { double npv = 0; for (Integer iterator : netCashFlow.keySet()) { npv += netCashFlow.get(iterator).doubleValue() / Math.pow(1 + wacc, iterator.intValue()); } return npv; } public double determineExpectedMarginalCost(PowerPlant plant, Map<Substance, Double> expectedFuelPrices, double expectedCO2Price) { double mc = determineExpectedMarginalFuelCost(plant, expectedFuelPrices); double co2Intensity = plant.calculateEmissionIntensity(); mc += co2Intensity * expectedCO2Price; return mc; } public double determineExpectedMarginalFuelCost(PowerPlant powerPlant, Map<Substance, Double> expectedFuelPrices) { double fc = 0d; for (SubstanceShareInFuelMix mix : powerPlant.getFuelMix()) { double amount = mix.getShare(); double fuelPrice = expectedFuelPrices.get(mix.getSubstance()); fc += amount * fuelPrice; } return fc; } private PowerGridNode getNodeForZone(Zone zone) { for (PowerGridNode node : reps.genericRepository.findAll(PowerGridNode.class)) { if (node.getZone().equals(zone)) { return node; } } return null; } private class MarketInformation { Map<Segment, Double> expectedElectricityPricesPerSegment; double maxExpectedLoad = 0d; Map<PowerPlant, Double> meritOrder; double capacitySum; MarketInformation(ElectricitySpotMarket market, Map<Substance, Double> fuelPrices, double co2price, long time) { // determine expected power prices expectedElectricityPricesPerSegment = new HashMap<Segment, Double>(); Map<PowerPlant, Double> marginalCostMap = new HashMap<PowerPlant, Double>(); capacitySum = 0d; // get merit order for this market for (PowerPlant plant : reps.powerPlantRepository.findExpectedOperationalPowerPlantsInMarket(market, time)) { double plantMarginalCost = determineExpectedMarginalCost(plant, fuelPrices, co2price); marginalCostMap.put(plant, plantMarginalCost); capacitySum += plant.getTechnology().getCapacity(); } MapValueComparator comp = new MapValueComparator(marginalCostMap); meritOrder = new TreeMap<PowerPlant, Double>(comp); meritOrder.putAll(marginalCostMap); long numberOfSegments = reps.segmentRepository.count(); double demandFactor = market.getDemandGrowthTrend().getValue(time); // find expected prices per segment given merit order for (SegmentLoad segmentLoad : market.getLoadDurationCurve()) { double expectedSegmentLoad = segmentLoad.getBaseLoad() * demandFactor; if (expectedSegmentLoad > maxExpectedLoad) { maxExpectedLoad = expectedSegmentLoad; } double segmentSupply = 0d; double segmentPrice = 0d; for (Entry<PowerPlant, Double> plantCost : meritOrder.entrySet()) { PowerPlant plant = plantCost.getKey(); double plantCapacity = 0d; // Determine available capacity in the future in this // segment plantCapacity = plant.getExpectedAvailableCapacity(time, segmentLoad.getSegment(), numberOfSegments); // logger.warn("Capacity of plant " + plant.toString() + // " is " + // plantCapacity/plant.getTechnology().getCapacity()); if (segmentSupply < expectedSegmentLoad) { segmentSupply += plantCapacity; segmentPrice = plantCost.getValue(); } } // logger.warn("Segment " + // segmentLoad.getSegment().getSegmentID() + " supply equals " + // segmentSupply + " and segment demand equals " + // expectedSegmentLoad); if (segmentSupply >= expectedSegmentLoad) { expectedElectricityPricesPerSegment.put(segmentLoad.getSegment(), segmentPrice); } else { expectedElectricityPricesPerSegment.put(segmentLoad.getSegment(), market.getValueOfLostLoad()); } } } } }