edu.utexas.cs.tactex.servercustomers.factoredcustomer.DefaultCapacityOriginator.java Source code

Java tutorial

Introduction

Here is the source code for edu.utexas.cs.tactex.servercustomers.factoredcustomer.DefaultCapacityOriginator.java

Source

/*
 * TacTex - a power trading agent that competed in the Power Trading Agent Competition (Power TAC) www.powertac.org
 * Copyright (c) 2013-2016 Daniel Urieli and Peter Stone {urieli,pstone}@cs.utexas.edu               
 *
 *
 * This file is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This file is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 * 
 * This file incorporates work covered by the following copyright and  
 * permission notice:  
 *
*     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 edu.utexas.cs.tactex.servercustomers.factoredcustomer;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.HashMap;

import org.apache.commons.math3.linear.ArrayRealVector;
import org.apache.commons.math3.linear.RealVector;
import org.apache.log4j.Logger;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
//import org.powertac.common.ConfigServerBroker;
import org.powertac.common.Tariff;
import org.powertac.common.TariffSpecification;
import org.powertac.common.WeatherForecast;
import org.powertac.common.WeatherForecastPrediction;
import org.powertac.common.WeatherReport;
import org.powertac.common.enumerations.PowerType;
import org.powertac.common.state.Domain;

import edu.utexas.cs.tactex.servercustomers.common.TariffSubscription;
import edu.utexas.cs.tactex.servercustomers.factoredcustomer.CapacityStructure.BaseCapacityType;
import edu.utexas.cs.tactex.servercustomers.factoredcustomer.CapacityStructure.InfluenceKind;
import edu.utexas.cs.tactex.servercustomers.factoredcustomer.interfaces.*;

/**
 * Key class responsible for drawing from a base capacity and ajusting that
 * capacity in response to various static and dynamic factors for each timeslot.
 * 
 * @author Prashant Reddy
 */
@Domain
class DefaultCapacityOriginator implements CapacityOriginator {
    protected Logger log = Logger.getLogger(DefaultCapacityOriginator.class.getName());

    protected final FactoredCustomerService service;
    // protected final TimeService timeService;
    // protected final TimeslotRepo timeslotRepo;
    // protected final WeatherReportRepo weatherReportRepo;
    // protected final WeatherForecastRepo weatherForecastRepo;

    private final double SMOOTHING_WEIGHT = 0.4; // 0.0 => ignore previous value

    private final TimeseriesGenerator tsGenerator;

    private final CapacityStructure capacityStructure;
    private final CapacityBundle parentBundle;

    protected final String logIdentifier;

    private final Map<Integer, Double> baseCapacities = new HashMap<Integer, Double>();
    protected final Map<Integer, Double> forecastCapacities = new HashMap<Integer, Double>();
    protected final Map<Integer, Double> actualCapacities = new HashMap<Integer, Double>();
    protected final Map<Integer, Double> curtailedCapacities = new HashMap<Integer, Double>();
    protected final Map<Integer, Double> shiftedCurtailments = new HashMap<Integer, Double>();

    // cache
    private WeatherReport currentWeatherReport;
    private WeatherForecast currentWeatherForecast;

    DefaultCapacityOriginator(FactoredCustomerService service, CapacityStructure structure, CapacityBundle bundle) {
        this.service = service;
        capacityStructure = structure;
        parentBundle = bundle;

        logIdentifier = capacityStructure.capacityName.isEmpty() ? bundle.getName()
                : bundle.getName() + "#" + capacityStructure.capacityName;

        // timeService = (TimeService)
        // SpringApplicationContext.getBean("timeService");
        // timeslotRepo = (TimeslotRepo)
        // SpringApplicationContext.getBean("timeslotRepo");
        // weatherReportRepo = (WeatherReportRepo)
        // SpringApplicationContext.getBean("weatherReportRepo");
        // weatherForecastRepo = (WeatherForecastRepo)
        // SpringApplicationContext.getBean("weatherForecastRepo");

        if (capacityStructure.baseCapacityType == BaseCapacityType.TIMESERIES) {
            tsGenerator = new TimeseriesGenerator(service, capacityStructure.baseTimeseriesStructure);
        } else
            tsGenerator = null;
    }

    // changed to propagate currentTimeslot and avoid sync issues
    @Override
    public CapacityProfile getCurrentForecast(int currentTimeslot) {
        //int timeslot = service.getTimeslotRepo().currentSerialNumber();
        return getForecastStartingAt(currentTimeslot, currentTimeslot);
    }

    @Override
    public CapacityProfile getForecastStartingAt(int currentTimeslot, int startingTimeslot) {
        int timeslot = startingTimeslot;
        List<Double> values = new ArrayList<Double>();
        for (int i = 0; i < CapacityProfile.NUM_TIMESLOTS; ++i) {
            Double forecastCapacity = forecastCapacities.get(timeslot);
            if (forecastCapacity != null) {
                values.add(forecastCapacity);
            } else {
                values.add(getForecastCapacity(currentTimeslot, timeslot));
            }
            timeslot += 1;
        }
        return new CapacityProfile(values);
    }

    @Override
    public CapacityProfile getCurrentForecastPerSub(int currentTimeslot, TariffSubscription sub) {
        // DefaultCapacityOriginator doesn't track subscriptions, so:
        return getCurrentForecast(currentTimeslot);
    }

    protected double getForecastCapacity(int currentTimeslot, int timeslot) {
        Double ret = forecastCapacities.get(timeslot);
        if (ret == null)
            ret = computeForecastCapacity(currentTimeslot, timeslot);
        return ret;
    }

    private double computeForecastCapacity(int currentTimeslot, int future) {
        //log.info("computeForecastCapacity()");
        //int now = service.getTimeslotRepo().currentSerialNumber();
        int now = currentTimeslot;
        int timeToFuture = future - now;
        Weather weather = null;
        if (timeToFuture == 0) {
            weather = new Weather(getCurrentWeatherReport(now));
        } else {
            WeatherForecast forecast = getCurrentWeatherForecast(now);
            List<WeatherForecastPrediction> predictions = forecast.getPredictions();
            for (WeatherForecastPrediction prediction : predictions) {
                if (prediction.getForecastTime() == timeToFuture) {
                    weather = new Weather(prediction);
                }
            }
        }
        if (weather == null)
            throw new Error("Could not find weather forecast for timeslot " + future);

        //log.info("wind speed =" + weather.getWindSpeed());

        double baseCapacity = getBaseCapacity(future);
        //log.info("baseCapacity" + baseCapacity);
        if (Double.isNaN(baseCapacity))
            throw new Error("Base capacity is NaN!");

        // Compute for full population ignoring current tariff rates
        double forecastCapacity = baseCapacity;
        forecastCapacity = adjustCapacityForPeriodicSkew(forecastCapacity,
                service.getTimeslotRepo().getTimeForIndex(future).toDateTime(DateTimeZone.UTC), false);
        //log.info("forecastCapacity after periodic skew: " + forecastCapacity);
        forecastCapacity = adjustCapacityForWeather(forecastCapacity, weather, false);
        //log.info("forecastCapacity after weather skew: " + forecastCapacity);
        if (Double.isNaN(forecastCapacity))
            throw new Error("Adjusted capacity is NaN for base capacity = " + baseCapacity);

        forecastCapacity = truncateTo2Decimals(forecastCapacity);
        //log.info("forecastCapacity after truncation do decimals: " + forecastCapacity);
        forecastCapacities.put(future, forecastCapacity);
        log.debug(logIdentifier + ": Forecast capacity for timeslot " + future + " = " + forecastCapacity);
        return forecastCapacity;
    }

    private double getBaseCapacity(int future) {
        Double ret = baseCapacities.get(future);
        if (ret == null)
            ret = drawBaseCapacitySample(future);
        return ret;
    }

    private double drawBaseCapacitySample(int timeslot) {
        double baseCapacity = 0.0;
        switch (capacityStructure.baseCapacityType) {
        case POPULATION:
            baseCapacity = capacityStructure.basePopulationCapacity.drawSample();
            break;
        case INDIVIDUAL:
            for (int i = 0; i < parentBundle.getPopulation(); ++i) {
                double draw = capacityStructure.baseIndividualCapacity.drawSample();
                baseCapacity += draw;
            }
            break;
        case TIMESERIES:
            baseCapacity = getBaseCapacityFromTimeseries(timeslot);
            break;
        default:
            throw new Error(
                    logIdentifier + ": Unexpected base capacity type: " + capacityStructure.baseCapacityType);
        }
        Double prevCapacity = baseCapacities.get(timeslot - 1);
        if (prevCapacity != null) {
            baseCapacity = SMOOTHING_WEIGHT * prevCapacity + (1 - SMOOTHING_WEIGHT) * baseCapacity;
        }
        baseCapacity = truncateTo2Decimals(baseCapacity);
        baseCapacities.put(timeslot, baseCapacity);
        return baseCapacity;
    }

    private double getBaseCapacityFromTimeseries(int timeslot) {
        try {
            return tsGenerator.generateNext(timeslot);
        } catch (ArrayIndexOutOfBoundsException e) {
            log.error(logIdentifier + ": Tried to get base capacity from time series at index beyond maximum!");
            throw e;
        }
    }

    @Override
    public double getShiftingInconvenienceFactor(Tariff tariff, int recordLength) {
        return 0; // not shifting should take place in DefaultCapacityOriginator
    }

    @Override
    public double useCapacity(TariffSubscription subscription) {
        //log.info("useCapacity()");
        int timeslot = service.getTimeslotRepo().currentSerialNumber();

        double baseCapacity = getBaseCapacity(timeslot);
        if (Double.isNaN(baseCapacity))
            throw new Error("Base capacity is NaN!");
        logCapacityDetails(logIdentifier + ": Base capacity for timeslot " + timeslot + " = " + baseCapacity);

        // total adjusted capacity
        double adjustedCapacity = baseCapacity;
        if (parentBundle.getPowerType().isInterruptible()) {
            adjustedCapacity = adjustCapacityForCurtailments(timeslot, adjustedCapacity, subscription);
        }
        adjustedCapacity = adjustCapacityForPeriodicSkew(adjustedCapacity,
                service.getTimeService().getCurrentDateTime(), true);
        adjustedCapacity = adjustCapacityForCurrentWeather(adjustedCapacity, true);

        // capacity for this subscription
        adjustedCapacity = adjustCapacityForSubscription(timeslot, adjustedCapacity, subscription);
        if (Double.isNaN(adjustedCapacity)) {
            throw new Error("Adjusted capacity is NaN for base capacity = " + baseCapacity);
        }

        adjustedCapacity = truncateTo2Decimals(adjustedCapacity);
        actualCapacities.put(timeslot, adjustedCapacity);
        log.info(logIdentifier + ": Adjusted capacity for tariff " + subscription.getTariff().getId() + " = "
                + adjustedCapacity);
        return adjustedCapacity;
    }

    private double adjustCapacityForCurtailments(int timeslot, double capacity, TariffSubscription subscription) {
        double lastCurtailment = subscription.getCurtailment();
        if (Math.abs(lastCurtailment) > 0.01) { // != 0
            curtailedCapacities.put(timeslot - 1, lastCurtailment);
            if (capacityStructure.curtailmentShifts != null) {
                for (int i = 0; i < capacityStructure.curtailmentShifts.length; ++i) {
                    double shiftingFactor = capacityStructure.curtailmentShifts[i];
                    double shiftedCapacity = lastCurtailment * shiftingFactor;
                    Double previousShifts = shiftedCurtailments.get(timeslot + i);
                    if (previousShifts == null) {
                        shiftedCurtailments.put(timeslot + i, shiftedCapacity);
                    } else {
                        shiftedCurtailments.put(timeslot + i, previousShifts + shiftedCapacity);
                    }
                }
            }
        }
        Double currentShift = shiftedCurtailments.get(timeslot);
        return (currentShift == null) ? capacity : capacity + currentShift;
    }

    private double adjustCapacityForPeriodicSkew(double capacity, DateTime when, boolean verbose) {
        int day = when.getDayOfWeek(); // 1=Monday, 7=Sunday
        int hour = when.getHourOfDay(); // 0-23

        double periodicSkew = capacityStructure.dailySkew[day - 1] * capacityStructure.hourlySkew[hour];
        if (verbose)
            logCapacityDetails(logIdentifier + ": periodic skew = " + periodicSkew);
        return capacity * periodicSkew;
    }

    private double adjustCapacityForCurrentWeather(double capacity, boolean verbose) {
        // unused code, just fixed it to compile 
        WeatherReport weatherReport =
                //service.getWeatherReportRepo().currentWeatherReport();
                currentWeatherReport;
        return adjustCapacityForWeather(capacity, new Weather(weatherReport), verbose);
    }

    private double adjustCapacityForWeather(double capacity, Weather weather, boolean verbose) {
        if (verbose)
            logCapacityDetails(
                    logIdentifier + ": weather = (" + weather.getTemperature() + ", " + weather.getWindSpeed()
                            + ", " + weather.getWindDirection() + ", " + weather.getCloudCover() + ")");

        double weatherFactor = 1.0;
        if (capacityStructure.temperatureInfluence == InfluenceKind.DIRECT) {
            int temperature = (int) Math.round(weather.getTemperature());
            weatherFactor = weatherFactor * capacityStructure.temperatureMap.get(temperature);
            //log.info("weatherFactor after temperatureInfluence.DIRECT: " + weatherFactor);
        } else if (capacityStructure.temperatureInfluence == InfluenceKind.DEVIATION) {
            int curr = (int) Math.round(weather.getTemperature());
            int ref = (int) Math.round(capacityStructure.temperatureReference);
            double deviationFactor = 1.0;
            if (curr > ref) {
                for (int t = ref + 1; t <= curr; ++t) {
                    deviationFactor += capacityStructure.temperatureMap.get(t);
                }
            } else if (curr < ref) {
                for (int t = curr; t < ref; ++t) {
                    deviationFactor += capacityStructure.temperatureMap.get(t);
                }
            }
            weatherFactor = weatherFactor * deviationFactor;
            //log.info("weatherFactor after temperatureInfluence.DEVIATION: " + weatherFactor);
        }
        if (capacityStructure.windSpeedInfluence == InfluenceKind.DIRECT) {
            int windSpeed = (int) Math.round(weather.getWindSpeed());
            weatherFactor = weatherFactor * capacityStructure.windSpeedMap.get(windSpeed);
            //log.info("weatherFactor after windSpeedInfluence.DIRECT: " + weatherFactor);
            if (windSpeed > 0.0 && capacityStructure.windDirectionInfluence == InfluenceKind.DIRECT) {
                int windDirection = (int) Math.round(weather.getWindDirection());
                weatherFactor = weatherFactor * capacityStructure.windDirectionMap.get(windDirection);
                //log.info("weatherFactor after windDirectionInfluence.DIRECT: " + weatherFactor);
            }
        }
        if (capacityStructure.cloudCoverInfluence == InfluenceKind.DIRECT) {
            int cloudCover = (int) Math.round(100 * weather.getCloudCover()); // [0,1]
                                                                              // to
                                                                              // ##%
            weatherFactor = weatherFactor * capacityStructure.cloudCoverMap.get(cloudCover);
            //log.info("weatherFactor after cloudCoverInfluence.DIRECT: " + weatherFactor);
        }
        if (verbose)
            logCapacityDetails(logIdentifier + ": weather factor = " + weatherFactor);
        //log.info("adjusted capacity " + capacity * weatherFactor);
        return capacity * weatherFactor;
    }

    @Override
    public double adjustCapacityForSubscription(int timeslot, double totalCapacity,
            TariffSubscription subscription) {
        //log.info("adjustCapacityForSubscription()");
        //log.info("totalCapacity=" + totalCapacity);
        double subCapacity = adjustCapacityForPopulationRatio(totalCapacity, subscription);
        //log.info("subCapacity=" + subCapacity);
        //log.info("adjustCapacityForTariffRates()=" + adjustCapacityForTariffRates(timeslot, subCapacity, subscription));
        return adjustCapacityForTariffRates(timeslot, subCapacity, subscription);
    }

    private double adjustCapacityForPopulationRatio(double capacity, TariffSubscription subscription) {
        //log.info("adjustCapacityForPopulationRatio()");
        //log.info("subscription.getCustomersCommitted()=" + subscription.getCustomersCommitted());
        //log.info("parentBundle.getPopulation()=" + parentBundle.getPopulation());
        double popRatio = getPopulationRatio(subscription.getCustomersCommitted(), parentBundle.getPopulation());
        logCapacityDetails(logIdentifier + ": population ratio = " + popRatio);
        return capacity * popRatio;
    }

    private double getPopulationRatio(int customerCount, int population) {
        return ((double) customerCount) / ((double) population);
    }

    protected double adjustCapacityForTariffRates(int timeslot, double baseCapacity,
            TariffSubscription subscription) {
        if ((baseCapacity - 0.0) < 0.01)
            return baseCapacity;

        double chargeForBase = subscription.getTariff().getUsageCharge(
                service.getTimeslotRepo().getTimeForIndex(timeslot), baseCapacity, subscription.getTotalUsage());
        double rateForBase = chargeForBase / baseCapacity;

        double benchmarkRate =
                // changed, to use propagated 'timeslot' instead of current time,
                // in cases we are late 
                //
                // 
                //capacityStructure.benchmarkRates.get(service.getTimeService().getHourOfDay());
                capacityStructure.benchmarkRates.get(service.getTimeslotRepo().getTimeForIndex(timeslot)
                        .toDateTime(DateTimeZone.UTC).getHourOfDay());
        double rateRatio = rateForBase / benchmarkRate;

        double tariffRatesFactor = determineTariffRatesFactor(rateRatio);
        logCapacityDetails(logIdentifier + ": tariff rates factor = " + tariffRatesFactor);
        return baseCapacity * tariffRatesFactor;
    }

    private double determineTariffRatesFactor(double rateRatio) {
        switch (capacityStructure.elasticityModelType) {
        case CONTINUOUS:
            return determineContinuousElasticityFactor(rateRatio);
        case STEPWISE:
            return determineStepwiseElasticityFactor(rateRatio);
        default:
            throw new Error("Unexpected elasticity model type: " + capacityStructure.elasticityModelType);
        }
    }

    private double determineContinuousElasticityFactor(double rateRatio) {
        double percentChange = (rateRatio - 1.0) / 0.01;
        double elasticityRatio = Double.parseDouble(capacityStructure.elasticityModelXml.getAttribute("ratio"));

        String range = capacityStructure.elasticityModelXml.getAttribute("range");
        String[] minmax = range.split("~");
        double low = Double.parseDouble(minmax[0]);
        double high = Double.parseDouble(minmax[1]);

        return Math.max(low, Math.min(high, 1.0 + (percentChange * elasticityRatio)));
    }

    private double determineStepwiseElasticityFactor(double rateRatio) {
        double[][] elasticity = null;
        if (elasticity == null) {
            elasticity = ParserFunctions
                    .parseMapToDoubleArray(capacityStructure.elasticityModelXml.getAttribute("map"));
        }
        if (Math.abs(rateRatio - 1) < 0.01 || elasticity.length == 0)
            return 1.0;
        PowerType powerType = parentBundle.getPowerType();
        if (powerType.isConsumption() && rateRatio < 1.0)
            return 1.0;
        if (powerType.isProduction() && rateRatio > 1.0)
            return 1.0;

        final int RATE_RATIO_INDEX = 0;
        final int CAPACITY_FACTOR_INDEX = 1;
        double rateLowerBound = Double.NEGATIVE_INFINITY;
        double rateUpperBound = Double.POSITIVE_INFINITY;
        double lowerBoundCapacityFactor = 1.0;
        double upperBoundCapacityFactor = 1.0;
        for (int i = 0; i < elasticity.length; ++i) {
            double r = elasticity[i][RATE_RATIO_INDEX];
            if (r <= rateRatio && r > rateLowerBound) {
                rateLowerBound = r;
                lowerBoundCapacityFactor = elasticity[i][CAPACITY_FACTOR_INDEX];
            }
            if (r >= rateRatio && r < rateUpperBound) {
                rateUpperBound = r;
                upperBoundCapacityFactor = elasticity[i][CAPACITY_FACTOR_INDEX];
            }
        }
        return (rateRatio < 1) ? upperBoundCapacityFactor : lowerBoundCapacityFactor;
    }

    @Override
    public String getCapacityName() {
        return capacityStructure.capacityName;
    }

    @Override
    public CapacityBundle getParentBundle() {
        return parentBundle;
    }

    protected double truncateTo2Decimals(double x) {
        double fract, whole;
        if (x > 0) {
            whole = Math.floor(x);
            fract = Math.floor((x - whole) * 100) / 100;
        } else {
            whole = Math.ceil(x);
            fract = Math.ceil((x - whole) * 100) / 100;
        }
        return whole + fract;
    }

    protected void logCapacityDetails(String msg) {
        if (service.getCapacityDetailsLogging() == true) {
            log.info(msg);
        }
    }

    @Override
    public String toString() {
        return this.getClass().getCanonicalName() + ":" + logIdentifier;
    }

    // Convenience class to unify the interface to
    // WeatherReport and WeatherForecastPrediction.
    private class Weather {
        final double temperature;
        final double windSpeed;
        final double windDirection;
        final double cloudCover;

        Weather(WeatherReport report) {
            temperature = report.getTemperature();
            windSpeed = report.getWindSpeed();
            windDirection = report.getWindDirection();
            cloudCover = report.getCloudCover();
        }

        Weather(WeatherForecastPrediction prediction) {
            temperature = prediction.getTemperature();
            windSpeed = prediction.getWindSpeed();
            windDirection = prediction.getWindDirection();
            cloudCover = prediction.getCloudCover();
        }

        double getTemperature() {
            return temperature;
        }

        double getWindSpeed() {
            return windSpeed;
        }

        double getWindDirection() {
            return windDirection;
        }

        double getCloudCover() {
            return cloudCover;
        }

    }

    // adding data access methods

    @Override
    public ArrayRealVector getPredictedEnergy(TariffSubscription subscription, int recordLength,
            int currentTimeslot) throws Exception {
        CapacityProfile predictedEnergyProfile = getCurrentForecast(currentTimeslot);
        // elasticity
        //if (ConfigServerBroker.useElasticity()) {
        predictedEnergyProfile = adjustCapacityProfileForTariffRates(predictedEnergyProfile, currentTimeslot,
                subscription);
        //}
        //log.info("defaultcaporig " + Arrays.toString(predictedEnergyProfile.values.toArray()));
        return convertEnergyProfileFromServerToBroker(predictedEnergyProfile, recordLength);
    }

    /**
     * Implementing elasticity adjustment
     * 
     * Note: TODO This is an other place where we assume that 
     * server's capacity profile starts from current timeslot
     *  
     * @param predictedEnergyProfile
     * @param currentTimeslot
     * @param subscription
     * @return
     */
    protected CapacityProfile adjustCapacityProfileForTariffRates(CapacityProfile predictedEnergyProfile,
            int currentTimeslot, TariffSubscription subscription) {

        List<Double> adjustedProfile = new ArrayList<Double>();
        for (int i = 0; i < predictedEnergyProfile.NUM_TIMESLOTS; ++i) {
            double c = predictedEnergyProfile.getCapacity(i);
            double adjusted = adjustCapacityForTariffRates(currentTimeslot + i, c, subscription);
            adjustedProfile.add(adjusted);
        }

        return new CapacityProfile(adjustedProfile);
    }

    /**
     * NOTE: subtle conversion that caused a bug - we use predictions
     * starting next time-step, but forecastCapacities are assumed
     * to be from current - so to return data to broker we need to
     * offset the record by 1
     */
    protected ArrayRealVector convertEnergyProfileFromServerToBroker(CapacityProfile predictedEnergyProfile,
            int recordLength) throws Exception {
        //log.info("scaleEnergyProfile()");
        //log.info("predictedEnergyProfile" + Arrays.toString(predictedEnergyProfile.values.toArray()));
        int profileLength = predictedEnergyProfile.NUM_TIMESLOTS;
        // verify divides
        boolean divides = (recordLength / profileLength * profileLength) == recordLength;
        if (!divides) {
            throw new Exception("How come profileLength doesn't divide recordLength");
        }
        //log.info("recordLength=" + recordLength);
        ArrayRealVector result = new ArrayRealVector(recordLength);

        for (int i = 0; i < recordLength; ++i) {
            result.setEntry(i, predictedEnergyProfile.getCapacity((i + 1) % profileLength));
            //log.info("appending " + predictedEnergyProfile.getCapacity( i % profileLength ) + " at " + i);
        }

        //log.info("result" + Arrays.toString(result.toArray()));
        return result;
    }

    /**
     * NOTE: subtle conversion that caused a bug - we use predictions
     * starting next time-step, but forecastCapacities are assumed
     * to be from current - so to return data to broker we need to
     * offset the record by 1
     */
    @Override
    public void convertEnergyProfileFromBrokerToServer(RealVector originatorEnergy, int currentTimeslot) {

        final int brokerEnergyRecordLength = originatorEnergy.getDimension();
        for (int i = 0; i < CapacityProfile.NUM_TIMESLOTS; ++i) {
            //log.info("updateForecastCapacities(" + (currentTimeslot + i) + ")=" +  originatorEnergy.getEntry(i));
            int i_minus_1_that_will_work_for_modulo = CapacityProfile.NUM_TIMESLOTS + i - 1;
            forecastCapacities.put(currentTimeslot + i,
                    originatorEnergy.getEntry(i_minus_1_that_will_work_for_modulo % CapacityProfile.NUM_TIMESLOTS));
        }
    }

    @Override
    public void clearSubscriptionRelatedData() {
        // no such data here currently
    }

    private WeatherReport getCurrentWeatherReport(int currentTimeslot) {
        // we don't want to run into sync issues
        // currentWeatherReport = service.getWeatherReportRepo().currentWeatherReport();

        // if missing/outdated - update cache
        if (null == currentWeatherReport || currentWeatherReport.getTimeslotIndex() != currentTimeslot) {
            List<WeatherReport> allWeatherReports = service.getWeatherReportRepo()
                    .allWeatherReports(currentTimeslot);
            for (WeatherReport report : allWeatherReports) {
                if (report.getTimeslotIndex() == currentTimeslot) {
                    currentWeatherReport = report;
                    break;
                }
            }
        }

        // if still missing/outdated - meaning we haven't found current
        if (null == currentWeatherReport || currentWeatherReport.getTimeslotIndex() != currentTimeslot) {
            log.error("WeatherReport missing for timeslot " + currentTimeslot);
        }

        return currentWeatherReport;
    }

    private WeatherForecast getCurrentWeatherForecast(int currentTimeslot) {
        // we don't want to run into sync issues
        // currentWeatherForecast = service.getWeatherReportRepo().currentWeatherForecast();

        // if missing/outdated - update cache
        if (null == currentWeatherForecast || currentWeatherForecast.getTimeslotIndex() != currentTimeslot) {
            List<WeatherForecast> allWeatherForecasts = service.getWeatherForecastRepo()
                    .allWeatherForecasts(currentTimeslot);
            for (WeatherForecast forecast : allWeatherForecasts) {
                if (forecast.getTimeslotIndex() == currentTimeslot) {
                    currentWeatherForecast = forecast;
                    break;
                }
            }
        }

        // if still missing/outdated - meaning we haven't found current
        if (null == currentWeatherForecast || currentWeatherForecast.getTimeslotIndex() != currentTimeslot) {
            log.error("WeatherForecast missing for timeslot " + currentTimeslot);
        }

        return currentWeatherForecast;
    }

} // end class