Java tutorial
/* * Copyright (c) 2015 by the original author * * 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 org.powertac.customer.model; import java.util.ArrayList; import java.util.Arrays; import java.util.Date; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import org.apache.commons.math3.distribution.NormalDistribution; import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.LogManager; import org.joda.time.DateTimeFieldType; import org.joda.time.Instant; import org.powertac.common.CapacityProfile; import org.powertac.common.CustomerInfo; import org.powertac.common.CustomerInfo.CustomerClass; import org.powertac.common.RandomSeed; import org.powertac.common.RegulationCapacity; import org.powertac.common.Tariff; import org.powertac.common.TariffEvaluator; import org.powertac.common.TariffSubscription; import org.powertac.common.TimeService; import org.powertac.common.config.ConfigurableInstance; import org.powertac.common.config.ConfigurableValue; import org.powertac.common.enumerations.PowerType; import org.powertac.common.interfaces.CustomerModelAccessor; import org.powertac.common.repo.RandomSeedRepo; import org.powertac.common.state.Domain; import org.powertac.common.state.StateChange; import org.powertac.customer.AbstractCustomer; import com.joptimizer.optimizers.LPOptimizationRequest; import com.joptimizer.optimizers.LPPrimalDualMethod; import com.joptimizer.optimizers.OptimizationResponse; /** * Models the complement of lift trucks in a warehouse. There may be * multiple trucks, some number of battery packs, and a daily/weekly work * schedule. Since the lead-acid batteries in most lift trucks have * a limited number of charge/discharge cycles, we do not assume the * ability to discharge batteries into the grid to provide balancing * capacity. However, the charging rate is variable, and balancing capacity * can be provided by adjusting the rate. Batteries and battery chargers * are not modeled directly; * instead, we simply keep track of the overall battery capacity and how * it changes when shifts start and end, and when chargers run. * * Instances are created using the configureInstances() method. In addition * to simple parameters, configuration can specify a shift schedule and * the number and initial state-of-charge of battery packs. * * The work schedule is specified with a list of strings called * weeklySchedule that lays out * blocks of shifts. Each block is of the form<br> * "block", d1, d2, ..., "shift" start, duration, ntrucks, "shift", ...<br> * where d1, d2, etc. are integers giving the days of the week covered by * the block, with Sunday=0; start is an integer hour, duration is an * integer number of hours, and ntrucks is the number of trucks that will * be active during the shift. * * @author John Collins */ @Domain @ConfigurableInstance public class LiftTruck extends AbstractCustomer implements CustomerModelAccessor { static private Logger log = LogManager.getLogger(LiftTruck.class.getName()); // ==== static constants ==== static final int HOURS_DAY = 24; static final int DAYS_WEEK = 7; //static final long HOUR = 3600*1000; // need a name so we can configure it (in case it's not an AbstractCustomer) //private String name; private double truckKW = 4.0; private double truckStd = 0.8; private double batteryCapacity = 50.0; private int nBatteries = 15; private int nChargers = 8; private double maxChargeKW = 6.0; private double chargeEfficiency = 0.9; private int planningHorizon = 60; private int minPlanningHorizon = 24; // ==== Shift data ==== // These List values are configured through their setter methods. // The default value is set in the constructor to serialize the // construction-configuration process private List<String> shiftData; private List<String> defaultShiftData = Arrays.asList("block", "1", "2", "3", "4", "5", "shift", "8", "8", "8", "shift", "16", "8", "6", "shift", "0", "8", "3"); private Shift[] shiftSchedule = new Shift[DAYS_WEEK * HOURS_DAY]; private Shift currentShift = null; // ==== Current state ==== // private double currentChargeRate = 1.0; // private double stateOfCharge = 0.7; @ConfigurableValue(valueType = "Double", dump = false, bootstrapState = true, description = "Battery capacity currently being used in trucks") private double capacityInUse = 0.0; // capacity in use during shift @ConfigurableValue(valueType = "Double", dump = false, bootstrapState = true, description = "Offline battery energy, currently in the trucks") private double energyInUse = 0.0; // energy content of in-use batteries @ConfigurableValue(valueType = "Double", dump = false, bootstrapState = true, description = "Online battery energy, currently being charged") private double energyCharging = 0.0; // energy content of charging batteries // constraint on current charging energy usage private PowerType powerType; //private ShiftEnergy[] futureEnergyNeeds = null; private CapacityPlan plan; // random seeds private RandomSeed opSeed = null; private RandomSeed evalSeed = null; private NormalDistribution normal; // context references private TariffEvaluator tariffEvaluator; /** * Default constructor, requires manual setting of name */ public LiftTruck() { super(); } /** * Standard constructor for named configurable type */ public LiftTruck(String name) { super(name); } /** * Initialization must provide accessor to Customer instance and time. * We assume configuration has already happened. We also start with no * active trucks, and the weakest batteries on availableChargers. Trucks will not * be active (in bootstrap mode) until the first shift change. */ @Override public void initialize() { super.initialize(); log.info("Initialize " + name); // fill out CustomerInfo. We label this model as thermal storage // because we don't allow battery discharge powerType = PowerType.THERMAL_STORAGE_CONSUMPTION; CustomerInfo info = new CustomerInfo(name, 1); // conservative interruptible capacity double interruptible = Math.min(nChargers * maxChargeKW, nBatteries * maxChargeKW / 3.0); info.withPowerType(powerType).withCustomerClass(CustomerClass.LARGE).withControllableKW(-interruptible) .withStorageCapacity(nBatteries * maxChargeKW / 3.0).withUpRegulationKW(-nChargers * maxChargeKW) .withDownRegulationKW(nChargers * maxChargeKW); // optimistic, perhaps addCustomerInfo(info); ensureSeeds(); // use default values when not configured ensureShifts(); // make sure we have enough batteries and availableChargers validateBatteries(); validateChargers(); // all batteries are charging //energyCharging = getStateOfCharge() * getBatteryCapacity(); //capacityInUse = 0.0; //energyInUse = 0.0; // set up the tariff evaluator. We are wide-open to variable pricing. tariffEvaluator = new TariffEvaluator(this); tariffEvaluator.withInertia(0.7).withPreferredContractDuration(14); tariffEvaluator.initializeInconvenienceFactors(0.0, 0.01, 0.0, 0.0); tariffEvaluator.initializeRegulationFactors(-nChargers * maxChargeKW * 0.05, 0.0, nChargers * maxChargeKW * 0.04); } // Gets a new random-number opSeed just in case we don't already have one. // Useful for mock-based testing. private void ensureSeeds() { if (null == opSeed) { RandomSeedRepo repo = service.getRandomSeedRepo(); opSeed = repo.getRandomSeed(LiftTruck.class.getName() + "-" + name, 0, "model"); evalSeed = repo.getRandomSeed(LiftTruck.class.getName() + "-" + name, 0, "eval"); normal = new NormalDistribution(0.0, 1.0); normal.reseedRandomGenerator(opSeed.nextLong()); } } // use default data if unconfigured void ensureShifts() { for (Shift s : shiftSchedule) { if (!(null == s)) return; // there's at least one non-empty hour with data } // we get here only if the schedule is empty setShiftData(defaultShiftData); } // We have to ensure that there are enough batteries to // support the shift schedule. There must be at least enough batteries to // supply the two largest adjacent shifts, and there must also be enough // to power the two largest adjacent shifts, in case a single battery // cannot power an entire shift. void validateBatteries() { int minBatteries = 0; Shift s1 = null; Shift s2 = null; for (int i = 0; i < shiftSchedule.length; i++) { Shift s = shiftSchedule[i]; if (null == s) { s1 = s2; } else if (s2 != s) { s1 = s2; s2 = s; if (null != s1) { int n1 = s1.getTrucks(); int d1 = s1.getDuration(); int n2 = s2.getTrucks(); int d2 = s2.getDuration(); double neededBatteries = (n1 * d1 + n2 * d2) * truckKW / getBatteryCapacity(); minBatteries = (int) Math.max(minBatteries, (n1 + n2)); minBatteries = (int) Math.max(minBatteries, Math.ceil(neededBatteries)); } } } int neededBatteries = minBatteries - nBatteries; if (neededBatteries > 0) { log.error("Not enough batteries (" + nBatteries + ") for " + getName()); // Add discharged batteries to fill out battery complement log.warn("Adding " + neededBatteries + " batteries for " + getName()); setNBatteries(getNBatteries() + neededBatteries); } } // make sure we have enough charging capacity to support // the shift schedule void validateChargers() { // ToDo -- A single charging should be able to charge a truck worth // of batteries in a single shift // The total output of the availableChargers should be at least enough // to power the trucks over a 24-hour period. Note that the shift schedule // starts at midnight, which may not be the start of the current shift. double maxNeeded = 0.0; int offset = 0; while (null == shiftSchedule[offset]) { offset += 1; } Shift currentShift = shiftSchedule[offset]; int remainingDuration = 0; int hoursInShift = (HOURS_DAY - currentShift.getStart()) % HOURS_DAY; remainingDuration = currentShift.getDuration() - hoursInShift; for (int i = offset; i < (shiftSchedule.length - HOURS_DAY); i++) { double totalEnergy = 0.0; Shift thisShift = shiftSchedule[i]; if (thisShift != currentShift) { currentShift = thisShift; if (null != currentShift) { // first block of energy in 24h window starting at i remainingDuration = currentShift.getDuration(); } } if (null != currentShift) { totalEnergy += currentShift.getTrucks() * remainingDuration * truckKW; } // now run fwd 24h and add energy from future shifts Shift current = currentShift; //int shiftStart = i; for (int j = i + 1; j < (i + HOURS_DAY); j++) { Shift newShift = shiftSchedule[j]; if (null != newShift && current != newShift) { int durationInWindow = (int) Math.min((i + HOURS_DAY - j), newShift.getDuration()); totalEnergy += newShift.getTrucks() * durationInWindow * truckKW; current = newShift; } } maxNeeded = Math.max(maxNeeded, totalEnergy); remainingDuration -= 1; } double chargeEnergy = nChargers * maxChargeKW * HOURS_DAY; if (maxNeeded > chargeEnergy) { double need = (maxNeeded - chargeEnergy) / (maxChargeKW * HOURS_DAY); int add = (int) Math.ceil(need); log.error("Insufficient charging capacity for " + getName() + ": have " + chargeEnergy + ", need " + maxNeeded + ". Adding " + add + " availableChargers."); setNChargers(getNChargers() + add); } } @Override public CustomerInfo getCustomerInfo() { return getCustomerInfo(powerType); } // ======== per-timeslot activities ======== @Override public void step() { // check for end-of-shift Shift newShift = shiftSchedule[indexOfShift(getNowInstant())]; if (newShift != currentShift) { log.info(getName() + " start of shift"); // Take all batteries out of service double totalEnergy = getEnergyCharging() + getEnergyInUse(); setEnergyCharging(getEnergyCharging() + getEnergyInUse()); setCapacityInUse(0.0); setEnergyInUse(0.0); // Put the strongest batteries in trucks for the next shift if (null != newShift) { setCapacityInUse(newShift.getTrucks() * batteryCapacity); setEnergyInUse(Math.min(getCapacityInUse(), totalEnergy)); setEnergyCharging(totalEnergy - getEnergyInUse()); } log.info(getName() + ": new shift cInUse " + capacityInUse + ", eInUse " + energyInUse + ", eCharging " + energyCharging); currentShift = newShift; } // discharge batteries on active trucks if (null != currentShift) { double usage = Math.max(0.0, normal.sample() * truckStd + truckKW * currentShift.getTrucks()); double deficit = usage - getEnergyInUse(); log.debug(getName() + ": trucks use " + usage + " kWh"); if (deficit > 0.0) { log.warn(getName() + ": trucks use more energy than available by " + deficit + " kWh"); addEnergyInUse(deficit); addEnergyCharging(-deficit); } addEnergyInUse(-usage); } // use energy on chargers, accounting for regulation double regulation = getSubscription().getRegulation(); log.info(getName() + ": regulation " + regulation); double energyUsed = useEnergy(regulation); // Record energy used getSubscription().usePower(energyUsed); log.info(getName() + " cInUse " + capacityInUse + ", eInUse " + energyInUse + ", eCharging " + energyCharging); } // Computes energy use by chargers in the current timeslot. // Remember that the plan has computed usage in terms of AC power, // while the energy going into the batteries is lower by // the chargeEfficiency value. double useEnergy(double regulation) { TariffSubscription subscription = getSubscription(); Tariff tariff = subscription.getTariff(); ensureCapacityPlan(tariff); // positive regulation means we lost energy in the last timeslot // and should make it up in the remainder of the shift addEnergyCharging(-regulation * chargeEfficiency); ShiftEnergy need = plan.getCurrentNeed(getNowInstant()); if (need.getDuration() <= 0) { log.error(getName() + " negative need duration " + need.getDuration()); } need.addEnergy(-regulation); // Compute the max and min we could possibly use in this timeslot // -- start with max and avail for remainder of shift double max = nChargers * maxChargeKW * need.getDuration(); // shift double avail = // for remainder of shift nBatteries * batteryCapacity - getCapacityInUse() - getEnergyCharging(); double maxUsable = Math.min(max, avail) / chargeEfficiency; double needed = need.getEnergyNeeded(); double used = 0; RegulationCapacity regCapacity = null; if (needed >= maxUsable) { // we just use the max, and allow no regulation capacity log.info(getName() + ": no slack - need " + needed + ", max " + max + ", avail " + avail + ", dur " + need.getDuration()); used = Math.min(maxUsable, (needed / need.getDuration())); regCapacity = new RegulationCapacity(subscription, 0.0, 0.0); } else if (tariff.isTimeOfUse() || tariff.isVariableRate()) { // if the current tariff is not a flat rate, we will just use the // planned amout, without offering regulation capacity // TODO - figure out how to combine variable prices with regulation used = need.getRecommendedUsage()[need.getUsageIndex()]; regCapacity = new RegulationCapacity(subscription, 0.0, 0.0); } else { // otherwise use energy to maximize regulation capacity double slack = (maxUsable - needed) / need.getDuration() / 2.0; log.info(getName() + " needed " + needed + ", maxUsable " + maxUsable + ", duration " + need.getDuration()); used = needed / need.getDuration() + slack; regCapacity = new RegulationCapacity(subscription, slack, -slack); } // use it addEnergyCharging(used * chargeEfficiency); getSubscription().setRegulationCapacity(regCapacity); log.info(getName() + " uses " + used + "kWh, reg cap (" + regCapacity.getUpRegulationCapacity() + ", " + regCapacity.getDownRegulationCapacity() + ")"); need.tick(); need.addEnergy(used); return used; } // Ensures that there is a valid capacity plan in place void ensureCapacityPlan(Tariff tariff) { if (null == plan || !plan.isValid(getNowInstant(), tariff)) { plan = getCapacityPlan(tariff, getNowInstant(), getPlanningHorizon()); plan.createPlan(getEnergyCharging()); } } // Computes constraints on future energy needs // Amounts are energy needed to run the chargers. Energy input to trucks // will be smaller due to charge efficiency. ShiftEnergy[] getFutureEnergyNeeds(Instant start, int horizon, double initialCharging) { Instant seStart = start; int index = indexOfShift(start); // current time is likely to be partway into first shift Shift currentShift = shiftSchedule[index]; // might be null int duration = 0; while (shiftSchedule[index] == currentShift) { duration += 1; index = nextShiftIndex(index); } Shift nextShift = shiftSchedule[index]; // this gives us the info we need to start the sequence ArrayList<ShiftEnergy> data = new ArrayList<ShiftEnergy>(); data.add(new ShiftEnergy(seStart, index, duration)); seStart = seStart.plus(duration * TimeService.HOUR); int elapsed = duration; // add shifts until we run off the end of the horizon // keep in mind that a shift can be null while (elapsed < horizon) { duration = 0; while (nextShift == shiftSchedule[index]) { index = nextShiftIndex(index); duration += 1; } nextShift = shiftSchedule[index]; data.add(new ShiftEnergy(seStart, index, duration)); elapsed += duration; seStart = seStart.plus(duration * TimeService.HOUR); } // now we convert to array, then walk backward and fill in energy needs ShiftEnergy[] result = data.toArray(new ShiftEnergy[data.size()]); double shortage = 0.0; for (int i = result.length - 1; i >= 0; i--) { int endx = result[i].endIndex; int prev = previousShiftIndex(endx); currentShift = shiftSchedule[prev]; Shift end = shiftSchedule[endx]; double needed = 0.0; if (null != end) { // Assume we need, at the end of each shift, enough energy to // run the next shift needed = (end.getTrucks() * end.getDuration() * getTruckKW()) / getChargeEfficiency(); } // chargers is min of charger capacity and battery availability int chargers = getNChargers(); int availableBatteries = nBatteries; if (null != currentShift) { availableBatteries -= currentShift.getTrucks(); } chargers = (int) Math.min(chargers, availableBatteries); double available = getMaxChargeKW() * result[i].getDuration() * chargers / getChargeEfficiency(); double surplus = available - needed - shortage; shortage = Math.max(0.0, -(available - needed - shortage)); result[i].setEnergyNeeded(needed); result[i].setMaxSurplus(surplus); } // finally, we need to update the first element with // the current battery charge. double finalSurplus = result[0].getMaxSurplus(); if (finalSurplus > 0.0) { result[0].setMaxSurplus(finalSurplus + initialCharging); } else if (shortage > 0.0) { result[0].setMaxSurplus(initialCharging - shortage); } return result; } CapacityPlan getCapacityPlan(Tariff tariff, Instant start, int size) { CapacityPlan result = new CapacityPlan(tariff, start, size); return result; } // Returns the index into the shift array corresponding to the given time. int indexOfShift(Instant time) { int hour = time.get(DateTimeFieldType.hourOfDay()); int day = time.get(DateTimeFieldType.dayOfWeek()); return hour + (day - 1) * HOURS_DAY; } // Returns the next index in the shift schedule int nextShiftIndex(int index) { return (index + 1) % shiftSchedule.length; } // Returns the previous index in the shift schedule int previousShiftIndex(int index) { if (0 == index) return shiftSchedule.length - 1; return (index - 1) % shiftSchedule.length; } // Returns the next date/time when the given shift index will occur Instant indexToInstant(int index) { Instant now = getNowInstant(); int probe = index; // get the probe within the shift schedule while (probe < 0) { probe += shiftSchedule.length; } while (probe > shiftSchedule.length) { probe -= shiftSchedule.length; } int nowIndex = indexOfShift(now); if (nowIndex <= index) { return (now.plus(TimeService.HOUR * (index - nowIndex))); } return (now.plus(TimeService.HOUR * (shiftSchedule.length + index - nowIndex))); } private Instant getNowInstant() { return service.getTimeslotRepo().currentTimeslot().getStartInstant(); } // Get a beginning-of-week time for consistent tariff evaluation private Instant getNextSunday() { Instant result = getNowInstant(); int hour = result.get(DateTimeFieldType.hourOfDay()); if (hour > 0) result = result.plus((24 - hour) * TimeService.HOUR); int day = result.get(DateTimeFieldType.dayOfWeek()); result = result.plus((7 - day) * TimeService.DAY); return result; } // ================ getters and setters ===================== // Note that list values must arrive and depart as List<String>, // while internally many of them are lists or arrays of numeric values. // Therefore we provide @ConfigurableValue setters that do the translation. @Override public String getName() { return name; } @Override public void setName(String name) { this.name = name; } @ConfigurableValue(valueType = "Double", dump = false, description = "mean power usage when truck is in use") @StateChange public void setTruckKW(double value) { truckKW = value; } public double getTruckKW() { return truckKW; } @ConfigurableValue(valueType = "Double", dump = false, description = "Std dev of truck power usage") @StateChange public void setTruckStd(double stdDev) { truckStd = stdDev; } public double getTruckStd() { return truckStd; } /** * Converts a list of Strings to a sorted list of Shifts. Entries in the * list represent pairs of (start, duration) values. */ @ConfigurableValue(valueType = "List", dump = false, description = "shift spec [block, shift, ..., block, shift, ...]") public void setShiftData(List<String> data) { int blk = 0; int shf = 1; int state = shf; LinkedList<String> tokens = new LinkedList<String>(data); ArrayList<Integer> blockData = new ArrayList<Integer>(); ArrayList<Integer> shiftData = new ArrayList<Integer>(); while (!(tokens.isEmpty())) { String token = tokens.remove(); if (token.equals("block")) { // finish shift, switch to block if (!shiftData.isEmpty()) { finishShift(blockData, shiftData); shiftData.clear(); } blockData.clear(); state = blk; } else if (token.equals("shift")) { // finish block or previous shift, switch to shift if (!shiftData.isEmpty()) { finishShift(blockData, shiftData); shiftData.clear(); } state = shf; } else { // collect numbers into correct list try { if (state == shf) shiftData.add(Integer.parseInt(token)); else if (state == blk) blockData.add(Integer.parseInt(token)); } catch (NumberFormatException nfe) { log.error("Config error for " + getName() + ": bad numeric token " + token); } } } // finish up last shift if (!shiftData.isEmpty()) { finishShift(blockData, shiftData); } } private void finishShift(ArrayList<Integer> blockData, ArrayList<Integer> shiftData) { if (blockData.isEmpty()) { log.error("Config error for " + getName() + ": empty block for shift " + shiftData.toString()); } else { addShift(shiftData, blockData); } } // Validates and creates a shift instance and populates the schedule // with references to the new shift void addShift(List<Integer> shiftData, List<Integer> blockData) { if (shiftData.size() < 3) { // nothing to do here log.error("Bad shift spec for " + getName() + ": " + shiftData.toString()); return; } if (!validBlock(blockData)) { log.error("Bad block data for " + getName() + ": " + blockData.toString()); return; } int start = shiftData.get(0); if (start < 0 || start > 23) { log.error("Bad shift start time " + start + " for " + getName()); return; } int duration = shiftData.get(1); if (duration < 1 || duration > 24) { log.error("Bad shift duration " + duration + " for " + getName()); return; } int trucks = shiftData.get(2); if (trucks < 0) { log.error("Negative shift truck count " + trucks + " for " + getName()); return; } Shift shift = new Shift(start, duration, trucks); // populate the schedule, ignoring overlaps. Later shifts may overlap // earlier ones. TODO; warn about overlaps for (int day : blockData) { for (int hour = shift.getStart(); hour < shift.getStart() + shift.getDuration(); hour++) { // Remember that Sunday is 1, not 0 int index = (hour + (day - 1) * HOURS_DAY) % shiftSchedule.length; shiftSchedule[index] = shift; } } } // a valid block has integers in the range [1..7] boolean validBlock(List<Integer> data) { if (data.isEmpty()) { return false; } for (Integer datum : data) { if (datum < 1 || datum > 7) { return false; } } return true; } public List<String> getShiftData() { return shiftData; } Shift[] getShiftSchedule() { return shiftSchedule; } @ConfigurableValue(valueType = "Double", dump = false, description = "size of battery pack in kWh") @StateChange public void setBatteryCapacity(double value) { batteryCapacity = value; } public double getBatteryCapacity() { return batteryCapacity; } @ConfigurableValue(valueType = "Integer", dump = false, description = "total number of battery packs") @StateChange public void setNBatteries(int value) { nBatteries = value; } public int getNBatteries() { return nBatteries; } @ConfigurableValue(valueType = "Integer", dump = false, description = "number of battery chargers") @StateChange public void setNChargers(int value) { nChargers = value; } public int getNChargers() { return nChargers; } @ConfigurableValue(valueType = "Double", dump = false, description = "maximum charge rate of one truck's battery pack") @StateChange public void setMaxChargeKW(double value) { maxChargeKW = value; } public double getMaxChargeKW() { return maxChargeKW; } @ConfigurableValue(valueType = "Double", dump = false, description = "ratio of charge energy to battery energy") @StateChange public void setChargeEfficiency(double value) { chargeEfficiency = value; } public double getChargeEfficiency() { return chargeEfficiency; } @ConfigurableValue(valueType = "Integer", dump = false, description = "planning horizon in timeslots - should be at least 48") @StateChange public void setPlanningHorizon(int horizon) { planningHorizon = horizon; } public int getPlanningHorizon() { return planningHorizon; } @ConfigurableValue(valueType = "Integer", dump = false, description = "minimum useful horizon of existing plan") @StateChange public void setMinPlanningHorizon(int horizon) { minPlanningHorizon = horizon; } public int getMinPlanningHorizon() { return minPlanningHorizon; } /** * Updates the energy content of offline batteries */ @StateChange public void setEnergyCharging(double kwh) { energyCharging = kwh; } public double getEnergyCharging() { return energyCharging; } public void addEnergyCharging(double kwh) { setEnergyCharging(getEnergyCharging() + kwh); } /** * Updates the energy content of in-use batteries */ @StateChange public void setEnergyInUse(double kwh) { energyInUse = kwh; } public double getEnergyInUse() { return energyInUse; } public void addEnergyInUse(double kwh) { setEnergyInUse(getEnergyInUse() + kwh); } /** * Updates the total capacity of in-use batteries */ @StateChange public void setCapacityInUse(double kwh) { capacityInUse = kwh; } public double getCapacityInUse() { return capacityInUse; } // ======== CustomerModelAccessor API =========== // digs out the current subscription for this thing. Since the population is // always one, there should only ever be one of them private TariffSubscription getSubscription() { List<TariffSubscription> subs = getCurrentSubscriptions(powerType); if (subs.size() > 1) { log.warn("Multiple subscriptions " + subs.size() + " for " + getName()); } return subs.get(0); } private Map<Tariff, CapacityPlan> profiles = null; @Override public CapacityProfile getCapacityProfile(Tariff tariff) { if (null == profiles) { profiles = new HashMap<Tariff, CapacityPlan>(); } CapacityPlan plan = profiles.get(tariff); if (null != plan) { return plan.getCapacityProfile(); } plan = getCapacityPlan(tariff, getNextSunday(), getPlanningHorizon()); profiles.put(tariff, plan); plan.createPlan(tariff, 0.0); return plan.getCapacityProfile(); } @Override public double getBrokerSwitchFactor(boolean isSuperseding) { if (isSuperseding) return 0; else return 0.02; } @Override public double getTariffChoiceSample() { return evalSeed.nextDouble(); } @Override public double getInertiaSample() { return evalSeed.nextDouble(); } @Override public void evaluateTariffs(List<Tariff> tariffs) { log.info(getName() + ": evaluate tariffs"); tariffEvaluator.evaluateTariffs(); } // ======== start, duration of a shift ======== class Shift implements Comparable<Shift> { private int start; private int duration; private int trucks = 0; Shift(int start, int duration, int trucks) { super(); this.start = start; this.duration = duration; this.trucks = trucks; } int getStart() { return start; } int getDuration() { return duration; } int getTrucks() { return trucks; } void setTrucks(int count) { this.trucks = count; } @Override public int compareTo(Shift s) { return start - s.start; } @Override public String toString() { return ("Shift(" + start + "," + duration + "," + trucks + ")"); } } // ======== Constant-price block within a shift ====== // Each ShiftBlock is one column in the LP problem class ShiftBlock { ShiftEnergy shiftEnergy; int startOffset; int duration = 0; double cost = 0; // per-hour cost ShiftBlock(ShiftEnergy shiftEnergy, int startOffset) { super(); this.shiftEnergy = shiftEnergy; this.startOffset = startOffset; } int getDuration() { return duration; } void incrementDuration() { this.duration += 1; } double getCost() { return cost; } void setCost(double cost) { this.cost = cost; } ShiftEnergy getShiftEnergy() { return shiftEnergy; } int getStartOffset() { return startOffset; } } // ======== Energy needed, available for a Shift ====== class ShiftEnergy { private Instant start; private int endIndex; // shift index at start of next Shift or idle period private int duration; // in hours private double energyNeeded = 0.0; // in kWh at end of duration private double maxSurplus = 0.0; // possible kWh beyond needed - can be negative // Plan recommendations private double[] recommendedUsage; private double slack; private int usageIndex = 0; ShiftEnergy(Instant start, int end, int duration) { super(); this.start = start; this.endIndex = end; Shift next = shiftSchedule[end]; if (null != next) { energyNeeded = next.getTrucks() * next.getDuration() * getTruckKW(); } this.duration = duration; } Instant getStart() { return start; } int getStartIndex() { return previousShiftIndex(endIndex); } Shift getThisShift() { return shiftSchedule[getStartIndex()]; } int getEndIndex() { return endIndex; } Shift getNextShift() { if (endIndex >= shiftSchedule.length) return null; return shiftSchedule[endIndex]; } int getDuration() { return duration; } // reduce duration by one on each tick of the clock void tick() { duration -= 1; if (duration < 0) { log.error(getName() + "SE start at " + start.toString() + " ticked past duration " + duration); } usageIndex += 1; } double getEnergyNeeded() { return energyNeeded; } void setEnergyNeeded(double energyNeeded) { this.energyNeeded = energyNeeded; } // Reduces energyNeeded by the given amount void addEnergy(double energy) { energyNeeded = Math.max(0.0, (energyNeeded - energy)); } double getMaxSurplus() { return maxSurplus; } void setMaxSurplus(double maxSurplus) { this.maxSurplus = maxSurplus; } // Increases the surplus by the given amount. void addSurplus(double surplus) { maxSurplus += surplus; } // ---- access to plan data ---- // Returns the recommendedUsage array. This will be null unless this // instance has been decorated CapacityPlan.getNeeds(). double[] getRecommendedUsage() { return recommendedUsage; } // Returns the recommended usage for the current timeslot, assuming // tick() has been called at the conclusion of each timeslot. double getCurrentRecommendedUsage() { return recommendedUsage[usageIndex]; } // Returns the current index into the recommendedUsage array. This // allows the caller to modify (smash) the array to reflect // the effects of regulation and curtailment events int getUsageIndex() { return usageIndex; } // Returns amount by which usage can vary before plan is violated double getSlack() { return slack; } // ---- decoration by plan ---- void setRecommendedUsage(double[] usagePlan) { if (usagePlan.length != duration) { // These should be the same log.error("usagePlan length " + usagePlan.length + " > duration " + duration); } this.recommendedUsage = usagePlan; } void setSlack(double slack) { this.slack = slack; } } // ======== Consumption plan ======== // Used for tariff evaluation, and for guiding consumption. class CapacityPlan { private double[] usage; private double[] slack; private ShiftEnergy[] needs; private Instant start; private int size; private Tariff tariff; // Creates a plan for the standard planning horizon CapacityPlan(Tariff tariff, Instant start) { this(tariff, start, getPlanningHorizon()); } // Creates a plan for a custom size CapacityPlan(Tariff tariff, Instant start, int size) { super(); this.tariff = tariff; this.start = start; this.size = size; } // A plan is valid if it's for the given tariff and if there is still // sufficient time left on it public boolean isValid(Instant now, Tariff tariff) { if (tariff != this.tariff) return false; int remaining = (int) (size - (now.getMillis() - start.getMillis()) / TimeService.HOUR); if (remaining < getMinPlanningHorizon()) return false; return true; } CapacityProfile getCapacityProfile() { return new CapacityProfile(usage, start); } // Returns the ShiftEnergy instance for the current time // Note that for this to work, the ShiftEnergy.tick() method // must be called once/timeslot. ShiftEnergy getCurrentNeed(Instant when) { if (null == needs) return null; for (int i = 0; i < needs.length; i++) { // if it's the last one, return it. if ((i == needs.length - 1) || (needs[i + 1].getStart().isAfter(when))) { return needs[i]; } } // should never get here log.error(getName() + " at " + when.toString() + " ran off end of plan length " + size + " starting " + start.toString()); return null; } // creates a plan using the default tariff and initial conditions void createPlan(double initialCharging) { createPlan(this.tariff, initialCharging); } // creates a plan, with specified start time and state-of-charge void createPlan(Tariff tariff, double initialCharging) { needs = getFutureEnergyNeeds(start, size, initialCharging); // update size to use all of last ShiftEnergy instance int newSize = 0; for (ShiftEnergy need : needs) newSize += need.getDuration(); size = newSize; LpPlan plan = new LpPlan(tariff, needs, size); usage = plan.getSolution(); slack = plan.getSlack(); updateNeeds(); } // returns the ShiftEnergy array used to create the plan, // decorated with the most recent solution ShiftEnergy[] updateNeeds() { if (null == needs) return null; int usageIndex = 0; for (int i = 0; i < needs.length; i++) { ShiftEnergy se = needs[i]; se.setRecommendedUsage(Arrays.copyOfRange(usage, usageIndex, usageIndex + se.getDuration())); usageIndex += se.getDuration(); se.setSlack(slack[i]); } return needs; } } // Creates a plan using the JOptimizer LP solver, gives access to // solution and slack values class LpPlan { double[] solution; double[] slack; boolean solved = false; Tariff tariff; ShiftEnergy[] needs; int size; // number of hours in plan int blockCount = 0; // number of multi-hour blocks in solution LpPlan(Tariff tariff, ShiftEnergy[] needs, int size) { super(); this.tariff = tariff; this.needs = needs; this.size = size; } // formulate and generate the solution, if necessary private void solve() { if (solved) return; // min obj.x s.t. a.x=b, lb <= x <= ub // x is energy use per block for size hours, b is slack var per block. // Block is a shift, or portion of shift with constant price. // For multi-hour blocks, energy use is evenly distributed across hours // after solution. Date start = new Date(); int shifts = needs.length; // Create blocks that break on both shift boundaries and tariff price // boundaries. ShiftBlock[] blocks = makeBlocks(shifts); int columns = blocks.length; int blockIndex = -1; double[] obj = new double[columns + shifts]; double[][] a = new double[shifts][columns + shifts]; double[] b = new double[shifts]; double[] lb = new double[columns + shifts]; double[] ub = new double[columns + shifts]; int column = 0; double cumulativeMin = 0.0; // this is the primary constraint // construct the problem for (int i = 0; i < shifts; i++) { // one iteration per shift //double kwh = // needs[i].getEnergyNeeded() + needs[i].getMaxSurplus(); //for (int j = 0; j < needs[i].getDuration(); j++) { while ((blockIndex < blocks.length - 1) && (blocks[blockIndex + 1].getShiftEnergy() == needs[i])) { blockIndex += 1; // one iteration per block within a shift // fill in objective function obj[column] = blocks[blockIndex].getCost(); lb[column] = 0.0; ub[column] = (needs[i].getEnergyNeeded() + needs[i].getMaxSurplus()) * (double) blocks[blockIndex].getDuration() / needs[i].getDuration(); column += 1; // construct cumulative usage constraints //a[i][column] = -1.0; //time = time.plus(TimeService.HOUR); } // fill a row up to column for (int j = 0; j < column; j++) { a[i][j] = -1.0; } // b vector - one entry per constraint double need = needs[i].getEnergyNeeded(); if (needs[i].getMaxSurplus() < 0.0) need += needs[i].getMaxSurplus(); cumulativeMin += need; b[i] = -cumulativeMin; // fill in slack values, one per constraint obj[columns + i] = 0.0; a[i][columns + i] = 1.0; lb[columns + i] = 0.0; // upper bound is max possible energy for shift ub[columns + i] = (needs[i].getEnergyNeeded() + needs[i].getMaxSurplus()); } // run the optimization LPOptimizationRequest or = new LPOptimizationRequest(); log.debug("Obj: " + Arrays.toString(obj)); or.setC(obj); log.debug("a:"); for (int i = 0; i < a.length; i++) log.debug(Arrays.toString(a[i])); or.setA(a); log.debug("b: " + Arrays.toString(b)); or.setB(b); or.setLb(lb); log.debug("ub: " + Arrays.toString(ub)); or.setUb(ub); or.setTolerance(1.0e-2); LPPrimalDualMethod opt = new LPPrimalDualMethod(); opt.setLPOptimizationRequest(or); try { int returnCode = opt.optimize(); if (returnCode != OptimizationResponse.SUCCESS) { log.error(getName() + "bad optimization return code " + returnCode); } double[] sol = opt.getOptimizationResponse().getSolution(); Date end = new Date(); log.info("Solution time: " + (end.getTime() - start.getTime())); log.debug("Solution = " + Arrays.toString(sol)); recordSolution(sol, blocks); } catch (Exception e) { log.error(e.toString()); } // we call it solved whether or not the solution was successful solved = true; } ShiftBlock[] makeBlocks(int shifts) { ArrayList<ShiftBlock> blocks = new ArrayList<ShiftBlock>(); double epsilon = 1e-3; // min price difference to ignore Instant time = indexToInstant(needs[0].getStartIndex()); for (int i = 0; i < shifts; i++) { // one iteration per shift double kwh = needs[i].getEnergyNeeded() + needs[i].getMaxSurplus(); ShiftBlock currentBlock = null; double blockCost = 0.0; // per-kWh cost of current block //int blockLength = 0; // length of current block for (int j = 0; j < needs[i].getDuration(); j++) { // one iteration per timeslot within a shift // fill in objective function // cost/kWh based on assumption that shift need is evenly distributed double kwhPerTs = kwh / needs[i].getDuration(); double cost = tariff.getUsageCharge(time, kwhPerTs, kwh) / kwhPerTs; if (null == currentBlock) { blockCost = cost; currentBlock = new ShiftBlock(needs[i], j); currentBlock.setCost(blockCost); blocks.add(currentBlock); } else if (Math.abs(cost - blockCost) > epsilon) { // start of new block -- // finish off last block currentBlock = new ShiftBlock(needs[i], j); currentBlock.setCost(cost); blocks.add(currentBlock); } currentBlock.incrementDuration(); } } ShiftBlock[] result = new ShiftBlock[blocks.size()]; return blocks.toArray(result); } // pulls apart the usage and slack data void recordSolution(double[] lpResult, ShiftBlock[] blocks) { double[] blockSolution = Arrays.copyOfRange(lpResult, 0, blocks.length); log.debug("Block soln: " + Arrays.toString(blockSolution)); solution = new double[size]; int solutionIndex = 0; int lpIndex = 0; for (ShiftBlock block : blocks) { double blockValue = blockSolution[lpIndex++] / block.getDuration(); for (int i = 0; i < block.getDuration(); i++) { solution[solutionIndex++] = blockValue; } } log.debug("Usage: " + Arrays.toString(solution)); slack = Arrays.copyOfRange(lpResult, blocks.length, lpResult.length); log.debug("Slack: " + Arrays.toString(slack)); } double[] getSolution() { solve(); return solution; } double[] getSlack() { solve(); return slack; } } @Override public double getShiftingInconvenienceFactor(Tariff tariff) { return 0; } }