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