Java tutorial
/* * Copyright 2011 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 org.powertac.du; import static org.powertac.util.MessageDispatcher.dispatch; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import org.apache.log4j.Logger; import org.joda.time.Instant; import org.powertac.common.Broker; import org.powertac.common.CashPosition; import org.powertac.common.Competition; import org.powertac.common.CustomerInfo; import org.powertac.common.MarketPosition; import org.powertac.common.MarketTransaction; import org.powertac.common.RandomSeed; import org.powertac.common.Rate; import org.powertac.common.Order; import org.powertac.common.TariffSpecification; import org.powertac.common.TariffTransaction; import org.powertac.common.Timeslot; import org.powertac.common.WeatherReport; import org.powertac.common.config.ConfigurableValue; import org.powertac.common.enumerations.PowerType; import org.powertac.common.interfaces.BootstrapDataCollector; import org.powertac.common.interfaces.BrokerProxy; import org.powertac.common.interfaces.CompetitionControl; import org.powertac.common.interfaces.InitializationService; import org.powertac.common.interfaces.ServerConfiguration; import org.powertac.common.interfaces.TariffMarket; import org.powertac.common.msg.CustomerBootstrapData; import org.powertac.common.msg.MarketBootstrapData; import org.powertac.common.msg.TimeslotComplete; import org.powertac.common.repo.BrokerRepo; import org.powertac.common.repo.CustomerRepo; import org.powertac.common.repo.RandomSeedRepo; import org.powertac.common.repo.TimeslotRepo; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; /** * Default broker implementation. We do the implementation in a service, because * the default broker is a singleton and it's convenient. The actual Broker * instance is implemented in an inner class. Note that this is not a type of * TimeslotPhaseProcessor. It's a broker, and so it runs after the last message * of the timeslot goes out, the TimeslotComplete message. As implemented, it runs * in the message-sending thread. If this turns out to cause problems with real * brokers, it could run in its own thread. * @author John Collins */ @Service public class DefaultBrokerService implements BootstrapDataCollector, InitializationService { static private Logger log = Logger.getLogger(DefaultBrokerService.class.getName()); @Autowired // routing of outgoing messages private BrokerProxy brokerProxyService; @Autowired // needed to discover sim mode private CompetitionControl competitionControlService; @Autowired // tariff publication private TariffMarket tariffMarketService; @Autowired private TimeslotRepo timeslotRepo; @Autowired private CustomerRepo customerRepo; @Autowired private BrokerRepo brokerRepo; @Autowired private RandomSeedRepo randomSeedRepo; @Autowired private ServerConfiguration serverPropertiesService; private LocalBroker face; /** parameters */ // keep in mind that brokers need to deal with two viewpoints. Tariffs // types take the viewpoint of the customer, while market-related types // take the viewpoint of the broker. private double defaultConsumptionRate = -1.0; // customer pays private double defaultProductionRate = 0.01; // broker pays private double initialBidKWh = 500.0; // max and min offer prices. Max means "sure to trade" private double buyLimitPriceMax = -1.0; // broker pays private double buyLimitPriceMin = -100.0; // broker pays private double sellLimitPriceMax = 100.0; // other broker pays private double sellLimitPriceMin = 0.2; // other broker pays private int usageRecordLength = 7 * 24; // one week // bootstrap-mode data - uninitialized for normal sim mode private boolean bootstrapMode = false; //private HashMap<CustomerInfo, ArrayList<Double>> netUsageMap; private HashMap<Timeslot, ArrayList<MarketTransaction>> marketTxMap; private ArrayList<Double> marketMWh; private ArrayList<Double> marketPrice; private ArrayList<WeatherReport> weather; // local state private TariffSpecification defaultConsumption; private TariffSpecification defaultInterruptibleConsumption; private TariffSpecification defaultProduction; private HashMap<TariffSpecification, HashMap<CustomerInfo, CustomerRecord>> customerSubscriptions; private RandomSeed randomSeed; private HashMap<Timeslot, Order> lastOrder; /** * Default constructor, called once when the server starts, before * any application-specific initialization has been done. */ public DefaultBrokerService() { super(); } @Override public void setDefaults() { // create the default broker instance, register it with the repo brokerRepo.add(createBroker("default broker")); } /** * Called by initialization service once at the beginning of each game. * Configures parameters, sets up and publishes default tariffs. */ @Override public String initialize(Competition competition, List<String> completedInits) { // defer for TariffMarket initialization int index = completedInits.indexOf("TariffMarket"); if (index == -1) { return null; } // log in to ccs competitionControlService.loginBroker(face.getUsername()); // set up local state bootstrapMode = competitionControlService.isBootstrapMode(); log.info("init, bootstrapMode=" + bootstrapMode); customerSubscriptions = new HashMap<TariffSpecification, HashMap<CustomerInfo, CustomerRecord>>(); lastOrder = new HashMap<Timeslot, Order>(); randomSeed = randomSeedRepo.getRandomSeed(this.getClass().getName(), 0, "pricing"); // if we are in bootstrap mode, we need to set up the dataset if (bootstrapMode) { //netUsageMap = new HashMap<CustomerInfo, ArrayList<Double>>(); marketTxMap = new HashMap<Timeslot, ArrayList<MarketTransaction>>(); marketMWh = new ArrayList<Double>(); marketPrice = new ArrayList<Double>(); weather = new ArrayList<WeatherReport>(); } // pull down configuration serverPropertiesService.configureMe(this); // create and publish default tariffs defaultConsumption = new TariffSpecification(face, PowerType.CONSUMPTION) .addRate(new Rate().withValue(defaultConsumptionRate)); tariffMarketService.setDefaultTariff(defaultConsumption); customerSubscriptions.put(defaultConsumption, new HashMap<CustomerInfo, CustomerRecord>()); defaultInterruptibleConsumption = new TariffSpecification(face, PowerType.INTERRUPTIBLE_CONSUMPTION) .addRate(new Rate().withValue(defaultConsumptionRate)); tariffMarketService.setDefaultTariff(defaultInterruptibleConsumption); customerSubscriptions.put(defaultInterruptibleConsumption, new HashMap<CustomerInfo, CustomerRecord>()); defaultProduction = new TariffSpecification(face, PowerType.PRODUCTION) .addRate(new Rate().withValue(defaultProductionRate)); tariffMarketService.setDefaultTariff(defaultProduction); customerSubscriptions.put(defaultProduction, new HashMap<CustomerInfo, CustomerRecord>()); return "DefaultBroker"; } /** * Creates the internal Broker instance that can receive messages intended * for local Brokers. It would be a Really Bad Idea to call this at any time * other than during the pre-game phase of a competition, because this method * does not register the broker in the BrokerRepo, which is a requirement to * see the messages. */ public Broker createBroker(String username) { face = new LocalBroker(username); //face.setEnabled(true); // log in -- do not set this directly return face; } public LocalBroker getFace() { return face; } // ----------- per-timeslot activation ------------- /** * In each timeslot, we must trade in the wholesale market to satisfy the * predicted load of our current customer base. */ public void activate() { Timeslot current = timeslotRepo.currentTimeslot(); log.info("activate: timeslot " + current.getSerialNumber()); // In the first through 23rd timeslot, we buy enough to meet what was // used in the previous timeslot. Note that this is called after the // customer model has run in the current timeslot, for a market clearing // at the beginning of the following timeslot. if (current.getSerialNumber() < 24) { // we already have usage data for the current timeslot. double currentKWh = collectUsage(current.getSerialNumber()); double neededKWh = 0.0; for (Timeslot timeslot : timeslotRepo.enabledTimeslots()) { // use data already collected if we have it, otherwise use data from // the current timeslot. int index = (timeslot.getSerialNumber()) % 24; double historicalKWh = collectUsage(index); if (historicalKWh != 0.0) neededKWh = historicalKWh; else neededKWh = currentKWh; // subtract out the current market position, and we know what to // buy or sell submitOrder(neededKWh, timeslot); } } // Once we have 24 hours of records, assume we need enough to meet // the mean of what we have used at this time of day so far else if (current.getSerialNumber() <= usageRecordLength) { for (Timeslot timeslot : timeslotRepo.enabledTimeslots()) { double neededKWh = 0.0; int index = (timeslot.getSerialNumber()) % 24; int count = 0; while (index <= current.getSerialNumber()) { neededKWh += collectUsage(index); index += 24; count += 1; } submitOrder((neededKWh / count), timeslot); } } // Finally, once we have a full week of records, we use the data for // the hour and day-of-week. else { double neededKWh = 0.0; for (Timeslot timeslot : timeslotRepo.enabledTimeslots()) { int index = (timeslot.getSerialNumber()) % usageRecordLength; neededKWh = collectUsage(index); submitOrder(neededKWh, timeslot); } } } // default visibility for testing double collectUsage(int index) { double result = 0.0; for (HashMap<CustomerInfo, CustomerRecord> customerMap : customerSubscriptions.values()) { for (CustomerRecord record : customerMap.values()) { result += record.getUsage(index); } } log.debug("Usage(" + index + ")=" + result); return -result; // convert to needed energy account balance } private void submitOrder(double neededKWh, Timeslot timeslot) { double neededMWh = neededKWh / 1000.0; Double limitPrice; MarketPosition posn = face.findMarketPositionByTimeslot(timeslot); if (posn != null) neededMWh -= posn.getOverallBalance(); log.debug("needed mWh=" + neededMWh); if (neededMWh == 0.0) { log.info("no power required in timeslot " + timeslot.getSerialNumber()); return; } else { limitPrice = computeLimitPrice(timeslot, neededMWh); } log.info("new order for " + neededMWh + " at " + limitPrice + " in timeslot " + timeslot.getSerialNumber()); Order result = new Order(face, timeslot, neededMWh, limitPrice); lastOrder.put(timeslot, result); brokerProxyService.routeMessage(result); } /** * Computes a limit price with a random element. */ private Double computeLimitPrice(Timeslot timeslot, double amountNeeded) { // start with default limits Double oldLimitPrice; double minPrice; if (amountNeeded > 0.0) { // buying oldLimitPrice = buyLimitPriceMax; minPrice = buyLimitPriceMin; } else { // selling oldLimitPrice = sellLimitPriceMax; minPrice = sellLimitPriceMin; } // check for escalation Order lastTry = lastOrder.get(timeslot); if (lastTry != null && Math.signum(amountNeeded) == Math.signum(lastTry.getMWh())) oldLimitPrice = lastTry.getLimitPrice(); // set price between oldLimitPrice and maxPrice, according to number of // remaining chances we have to get what we need. double newLimitPrice = minPrice; // default value Timeslot current = timeslotRepo.currentTimeslot(); int remainingTries = (timeslot.getSerialNumber() - current.getSerialNumber() - Competition.currentCompetition().getDeactivateTimeslotsAhead()); if (remainingTries > 0) { double range = (minPrice - oldLimitPrice) * 2.0 / (double) remainingTries; log.debug("oldLimitPrice=" + oldLimitPrice + ", range=" + range); double computedPrice = oldLimitPrice + randomSeed.nextDouble() * range; return Math.max(newLimitPrice, computedPrice); } else return null; // market order } // ------------ process incoming messages ------------- /** * Incoming messages for brokers include: * <ul> * <li>TariffTransaction tells us about customer subscription * activity and power usage,</li> * <li>MarketPosition tells us how much power we have bought * or sold in a given timeslot,</li> * <li>TimeslotComplete that tell us it's time to send in our bids/asks</li> * </ul> * along with a number of other message types that we can safely ignore. */ public void receiveBrokerMessage(Object msg) { if (msg != null) { dispatch(this, "handleMessage", msg); } } /** * Handles a TariffTransaction. We only care about certain types: PRODUCE, * CONSUME, SIGNUP, and WITHDRAW. */ public void handleMessage(TariffTransaction ttx) { TariffTransaction.Type txType = ttx.getTxType(); CustomerInfo customer = ttx.getCustomerInfo(); HashMap<CustomerInfo, CustomerRecord> customerMap = customerSubscriptions.get(ttx.getTariffSpec()); CustomerRecord record = customerMap.get(customer); if (TariffTransaction.Type.SIGNUP == txType) { // keep track of customer counts if (record == null) { record = new CustomerRecord(customer, ttx.getCustomerCount()); customerMap.put(customer, record); } else { record.signup(ttx.getCustomerCount()); } } else if (TariffTransaction.Type.WITHDRAW == txType) { // customers presumably found a better deal if (customerMap.get(customer) == null) { // should not happen log.warn("unknown customer withdraws subscription"); } else { record.withdraw(ttx.getCustomerCount()); } } else if (TariffTransaction.Type.PRODUCE == txType) { // if ttx count and subscribe population don't match, it will be hard // to estimate per-individual production if (ttx.getCustomerCount() != record.subscribedPopulation) { log.warn("production by subset " + ttx.getCustomerCount() + " of subscribed population " + record.subscribedPopulation); } record.produceConsume(ttx.getKWh(), ttx.getPostedTime()); } else if (TariffTransaction.Type.CONSUME == txType) { if (ttx.getCustomerCount() != record.subscribedPopulation) { log.warn("consumption by subset " + ttx.getCustomerCount() + " of subscribed population " + record.subscribedPopulation); } record.produceConsume(ttx.getKWh(), ttx.getPostedTime()); } } /** * Receives a new WeatherReport. We only care about this if in bootstrap * mode, in which case we simply store it in the bootstrap dataset. */ public void handleMessage(WeatherReport report) { // only in bootstrap mode if (bootstrapMode) { weather.add(report); } } /** * Receives a new MarketTransaction. In bootstrapMode, we need to record * these as they arrive in order to be able to compute delivered price of * power purchased in the wholesale market. Note that this computation will * ignore balancing cost. This is intentional. */ public void handleMessage(MarketTransaction tx) { // Save all transactions in bootstrapMode if (bootstrapMode) { ArrayList<MarketTransaction> txs = marketTxMap.get(tx.getTimeslot()); if (txs == null) { txs = new ArrayList<MarketTransaction>(); marketTxMap.put(tx.getTimeslot(), txs); } txs.add(tx); } // reset price escalation when a trade fully clears. Order lastTry = lastOrder.get(tx.getTimeslot()); if (lastTry == null) // should not happen log.error("order corresponding to market tx " + tx + " is null"); else if (tx.getMWh() == lastTry.getMWh()) // fully cleared lastOrder.put(tx.getTimeslot(), null); } /** * Handles CustomerBootstrapData by populating the customer model * corresponding to the given customer and power type. This gives the * broker a running start. */ public void handleMessage(CustomerBootstrapData cbd) { CustomerInfo customer = customerRepo.findByNameAndPowerType(cbd.getCustomerName(), cbd.getPowerType()); TariffSpecification tariff = null; for (TariffSpecification spec : customerSubscriptions.keySet()) { if (cbd.getPowerType().canUse(spec.getPowerType())) { tariff = spec; break; } } if (tariff == null) { log.error("Failed to find tariff for powerType " + cbd.getPowerType()); } HashMap<CustomerInfo, CustomerRecord> customerMap = customerSubscriptions.get(tariff); CustomerRecord record = customerMap.get(customer); // subscription exists if (record == null) { record = new CustomerRecord(customer, customer.getPopulation()); customerMap.put(customer, record); } int offset = (timeslotRepo.currentTimeslot().getSerialNumber() - cbd.getNetUsage().length); for (int i = 0; i < cbd.getNetUsage().length; i++) { record.produceConsume(cbd.getNetUsage()[i], i + offset); } } /** * CashPosition is the last message sent by Accounting. * In bootstrapMode, this is when we collect customer * usage data. */ public void handleMessage(CashPosition cp) { // collect usage and price data if (bootstrapMode) { // the wholesale market transactions can be mined for the net cost of // purchased power in the current timeslot. recordDeliveredPrice(); } } /** * TimeslotComplete is the last message sent in each timeslot. * This is normally when any broker would submit its bids, so that's when * the DefaultBroker will do it. Any earlier, and we will find ourselves * unable to trade in the furthest slot, because it will not yet have * been enabled. */ public void handleMessage(TimeslotComplete tc) { this.activate(); } /** * Records the delivered price of purchased power in the current timeslot. * If the broker has purchased more than it has sold, this will be a negative * number. */ private void recordDeliveredPrice() { Timeslot current = timeslotRepo.currentTimeslot(); ArrayList<MarketTransaction> txList = marketTxMap.get(current); if (txList == null) { txList = new ArrayList<MarketTransaction>(); marketTxMap.put(current, txList); } double totalMWh = 0.0; double totalCost = 0.0; for (MarketTransaction tx : txList) { // only include buy orders if (tx.getMWh() > 0.0) { log.info("record price: mwh=" + tx.getMWh() + ", price=" + tx.getPrice()); totalMWh += tx.getMWh(); totalCost += tx.getPrice() * tx.getMWh(); } } log.info("market totals: mwh=" + totalMWh + ", price=" + totalCost / totalMWh); marketMWh.add(totalMWh); if (totalMWh == 0.0) { marketPrice.add(0.0); } else { marketPrice.add(totalCost / totalMWh); } } // -------------------- Bootstrap data queries -------------------------- /** * Collects and returns a list of messages representing collected customer * demand, market price, and weather records for the bootstrap period. Note * that the customer and weather info is flattened. */ @Override public List<Object> collectBootstrapData(int maxTimeslots) { ArrayList<Object> result = new ArrayList<Object>(); for (Object item : getCustomerBootstrapData(maxTimeslots)) { result.add(item); } result.add(getMarketBootstrapData(maxTimeslots)); for (Object item : getWeatherReports(maxTimeslots)) { result.add(item); } return result; } /** * Returns a list of CustomerBootstrapData instances, one for each * (tariff, customer) pair. Note that this only * makes sense at the end of a bootstrap sim run. */ List<CustomerBootstrapData> getCustomerBootstrapData(int maxTimeslots) { ArrayList<CustomerBootstrapData> result = new ArrayList<CustomerBootstrapData>(); // iterate through the tariffs for (TariffSpecification spec : customerSubscriptions.keySet()) { HashMap<CustomerInfo, CustomerRecord> customerMap = customerSubscriptions.get(spec); // then iterate through the customers for (CustomerInfo customer : customerMap.keySet()) { CustomerRecord record = customerMap.get(customer); ArrayList<Double> usageList = record.bootstrapUsage; int startIndex = Math.max(0, usageList.size() - maxTimeslots); double[] usage = new double[usageList.size() - startIndex]; for (int i = 0; i < usage.length; i++) usage[i] = usageList.get(i + startIndex); result.add(new CustomerBootstrapData(customer, customer.getPowerType(), usage)); } } return result; } /** * Returns a single MarketBootstrapData instances representing the quantities * and prices paid by the default broker for the power it purchased over * the bootstrap period. */ MarketBootstrapData getMarketBootstrapData(int maxTimeslots) { if (marketMWh.size() != marketPrice.size()) { // should not happen log.error("marketMWh.size()=" + marketMWh.size() + " != " + "marketPrice.size()=" + marketPrice.size()); } int startOffset = Math.max(0, marketMWh.size() - maxTimeslots); int size = marketMWh.size() - startOffset; // ARRRGH - autoboxing does not work for arrays... double[] mwh = new double[size]; double[] price = new double[size]; for (int i = 0; i < size; i++) { mwh[i] = marketMWh.get(i + startOffset); price[i] = marketPrice.get(i + startOffset); } return new MarketBootstrapData(mwh, price); } /** * Returns the accumulated list of WeatherReport instances */ List<WeatherReport> getWeatherReports(int maxTimeslotCount) { int discardCount = weather.size() - maxTimeslotCount; if (discardCount > 0) { for (int i = 0; i < discardCount; i++) { weather.remove(0); } } return weather; } // ------------------ Property access for configuration ------------------ public double getConsumptionRate() { return defaultConsumptionRate; } @ConfigurableValue(valueType = "Double", description = "Fixed price/kwh for default consumption tariff") public void setConsumptionRate(double defaultConsumptionRate) { this.defaultConsumptionRate = defaultConsumptionRate; } public double getProductionRate() { return defaultProductionRate; } @ConfigurableValue(valueType = "Double", description = "Fixed price/kwh for default production tariff") public void setProductionRate(double defaultProductionRate) { this.defaultProductionRate = defaultProductionRate; } public double getInitialBidKWh() { return initialBidKWh; } @ConfigurableValue(valueType = "Double", description = "Quantity to buy in day-ahead market before seeing actual customer data") public void setInitialBidKWh(double initialBidKWh) { this.initialBidKWh = initialBidKWh; } public double getBuyLimitPriceMax() { return buyLimitPriceMax; } @ConfigurableValue(valueType = "Double", description = "Initial limit price/mwh for bids in day-ahead market") public void setBuyLimitPriceMax(double buyLimitPriceMax) { this.buyLimitPriceMax = buyLimitPriceMax; } public double getBuyLimitPriceMin() { return buyLimitPriceMin; } @ConfigurableValue(valueType = "Double", description = "Final limit price/mwh for bids in day-ahead market") public void setBuyLimitPriceMin(double buyLimitPriceMin) { this.buyLimitPriceMin = buyLimitPriceMin; } public double getSellLimitPriceMax() { return sellLimitPriceMax; } @ConfigurableValue(valueType = "Double", description = "Initial limit price/mwh for asks in day-ahead market") public void setSellLimitPriceMax(double sellLimitPriceMax) { this.sellLimitPriceMax = sellLimitPriceMax; } public double getSellLimitPriceMin() { return sellLimitPriceMin; } @ConfigurableValue(valueType = "Double", description = "Final limit price/mwh for asks in day-ahead market") public void setSellLimitPriceMin(double sellLimitPriceMin) { this.sellLimitPriceMin = sellLimitPriceMin; } // ------------------- LocalBroker implementation ----------------------- /** * Here's the actual "default broker". This is needed to intercept messages * sent to the broker. */ class LocalBroker extends Broker { public LocalBroker(String username) { super(username); setLocal(true); } /** * Receives a message intended for the broker, forwards it to the * message handler in the enclosing service. */ @Override public void receiveMessage(Object object) { receiveBrokerMessage(object); } } //-------------------- Customer-model recording --------------------- /** * Keeps track of customer status and usage. Usage is stored * per-customer-unit, but reported as the product of the per-customer * quantity and the subscribed population. This allows the broker to use * historical usage data as the subscribed population shifts. */ class CustomerRecord { CustomerInfo customer; int subscribedPopulation = 0; double[] usage = new double[usageRecordLength]; ArrayList<Double> bootstrapUsage = new ArrayList<Double>(); Instant base = null; double alpha = 0.3; CustomerRecord(CustomerInfo customer, int population) { super(); this.customer = customer; this.subscribedPopulation = population; base = timeslotRepo.findBySerialNumber(0).getStartInstant(); } // Returns the CustomerInfo for this record CustomerInfo getCustomerInfo() { return customer; } // Adds new individuals to the count void signup(int population) { subscribedPopulation = Math.min(customer.getPopulation(), subscribedPopulation + population); } // Removes individuals from the count void withdraw(int population) { subscribedPopulation -= population; } // Customer produces or consumes power. We assume the kwh value is negative // for production, positive for consumption void produceConsume(double kwh, Instant when) { int index = getIndex(when); produceConsume(kwh, index); } // store profile data at the given index void produceConsume(double kwh, int rawIndex) { // in bootstrap mode, we also record everything raw if (bootstrapMode) { if (bootstrapUsage.size() <= rawIndex) { while (bootstrapUsage.size() < rawIndex) bootstrapUsage.add(0.0); bootstrapUsage.add(kwh); } else { bootstrapUsage.set(rawIndex, bootstrapUsage.get(rawIndex) + kwh); } //log.info("bootstrapUsage customer " + customer.getName() // + "[" + rawIndex + "]=" + bootstrapUsage.get(rawIndex)); } int index = getIndex(rawIndex); double kwhPerCustomer = kwh / (double) subscribedPopulation; double oldUsage = usage[index]; if (oldUsage == 0.0) { // assume this is the first time usage[index] = kwhPerCustomer; } else { // exponential smoothing usage[index] = alpha * kwhPerCustomer + (1.0 - alpha) * oldUsage; } log.debug("consume " + kwh + " at " + index + ", customer " + customer.getName()); } double getUsage(int index) { if (index < 0) { log.warn("usage requested for negative index " + index); index = 0; } return (usage[getIndex(index)] * (double) subscribedPopulation); } // we assume here that timeslot index always matches the number of // timeslots that have passed since the beginning of the simulation. int getIndex(Instant when) { int result = (int) ((when.getMillis() - base.getMillis()) / (Competition.currentCompetition().getTimeslotDuration())); return result; } private int getIndex(int rawIndex) { return rawIndex % usage.length; } } // test-support method HashMap<String, Integer> getCustomerCounts() { HashMap<String, Integer> result = new HashMap<String, Integer>(); for (TariffSpecification spec : customerSubscriptions.keySet()) { HashMap<CustomerInfo, CustomerRecord> customerMap = customerSubscriptions.get(spec); for (CustomerRecord record : customerMap.values()) { result.put(record.customer.getName() + spec.getPowerType(), record.subscribedPopulation); } } return result; } // test-support methods double getUsageForCustomer(CustomerInfo customer, TariffSpecification tariffSpec, int index) { CustomerRecord record = customerSubscriptions.get(tariffSpec).get(customer); return record.getUsage(index); } boolean isBootstrapMode() { return bootstrapMode; } }