/*******************************************************************************
* 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());
}
}
}
}
}