
org.powertac.genco.MisoBuyer Maven / Gradle / Ivy
/*
* Copyright 2017 by John Collins.
*
* 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.genco;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.LogManager;
import org.joda.time.DateTime;
import org.joda.time.Instant;
import org.powertac.common.Broker;
import org.powertac.common.Competition;
import org.powertac.common.MarketPosition;
import org.powertac.common.Order;
import org.powertac.common.RandomSeed;
import org.powertac.common.Timeslot;
import org.powertac.common.WeatherForecast;
import org.powertac.common.WeatherForecastPrediction;
import org.powertac.common.WeatherReport;
import org.powertac.common.config.ConfigurableInstance;
import org.powertac.common.config.ConfigurableValue;
import org.powertac.common.interfaces.BrokerProxy;
import org.powertac.common.interfaces.ContextService;
import org.powertac.common.repo.RandomSeedRepo;
import org.powertac.common.repo.TimeslotRepo;
import org.powertac.common.repo.WeatherForecastRepo;
import org.powertac.common.repo.WeatherReportRepo;
import org.powertac.common.state.Domain;
import java.util.Arrays;
import java.util.List;
/**
* Buys energy to meet demand in a large wholesale market. Demand is
* determined by running a model composed of a mean value, daily and weekly
* seasonal components, accompanying white noise, and
* a residual trend modeled by a smoothed zero-reverting random walk,
* originally trained on two years of MISO north-central region actual demand.
* The result is further adjusted using matching temperature data to reflect
* heating/cooling load.
*
* @author John Collins
*/
@Domain
@ConfigurableInstance
public class MisoBuyer extends Broker
{
static private Logger log = LogManager.getLogger(MisoBuyer.class.getName());
/** daily seasonal pattern, starting at midnight */
private double[] daily =
{-2393.83341, -2688.62378, -2792.78342, -2660.37941,
-2150.46655, -1174.19920, -51.62303, 718.08210,
1134.23055, 1413.31114, 1562.54868, 1577.97041,
1542.29658, 1432.91036, 1278.64975, 1163.69339,
1156.57610, 1231.43676, 1181.07369, 1043.90000,
678.13380, -181.69625, -1136.53189, -1884.67637};
private int dailyOffset = 0;
@ConfigurableValue(valueType = "Double", dump=false,
description = "Std Deviation of random component for daily decomposition")
private double dailySd = 962.1;
/** weekly seasonal pattern, starting Monday midnight */
private double[] weekly =
// Monday
{-391.956124, -290.683849, -190.908896, -95.941919,
-9.247313, 67.748080, 136.276149, 199.551517,
259.200890, 313.829945, 362.222979, 404.930426,
442.639849, 475.720723, 504.416722, 529.129761,
550.764179, 570.508517, 588.361846, 602.249309,
610.772267, 614.360477, 614.586834, 613.460046,
// Tuesday
612.133501, 611.296938, 611.532207, 612.732043,
613.768341, 614.117323, 614.812678, 616.355549,
618.885444, 621.676821, 624.064983, 625.993192,
627.009478, 627.471051, 628.021006, 628.753266,
629.507393, 630.115352, 629.997488, 629.098931,
628.161883, 627.483189, 626.728283, 625.954901,
// Wednesday
625.331741, 624.923167, 624.703018, 624.606700,
624.968025, 625.652540, 625.862297, 625.737377,
626.178309, 627.357172, 628.824034, 630.389750,
632.076111, 633.756634, 635.328074, 636.819004,
638.049602, 638.373596, 637.695212, 636.723976,
636.427187, 636.970439, 637.781538, 638.210263,
// Thursday
637.533390, 635.553704, 632.108640, 626.988126,
620.496738, 613.695340, 607.934378, 603.624349,
600.281932, 598.144188, 597.303552, 596.420628,
594.815396, 592.637864, 589.973517, 586.716927,
582.221489, 575.388487, 566.432799, 557.630960,
550.424576, 543.957684, 536.750036, 528.266843,
// Friday
518.038246, 505.243999, 489.644433, 471.064326,
448.697725, 420.854854, 386.380165, 346.484864,
305.453093, 268.834939, 238.531056, 211.783197,
185.122345, 157.207042, 127.763091, 94.898191,
51.980893, -12.858454, -103.177756, -203.860646,
-297.669207, -381.460148, -459.586583, -535.636295,
// Saturday
-612.571639, -691.071811, -767.985936, -839.357742,
-902.197286, -954.919676, -998.738977, -1036.815195,
-1071.487920, -1102.300876, -1127.929623, -1148.535939,
-1164.805963, -1177.982452, -1189.640789, -1201.242393,
-1215.083427, -1233.606003, -1257.531599, -1285.099968,
-1313.843867, -1341.722391, -1366.294963, -1384.891082,
// Sunday
-1382.742458, -1390.097279, -1394.546495, -1396.139033,
-1392.962053, -1383.777293, -1369.444464, -1352.444565,
-1336.640762, -1326.272731, -1322.323138, -1320.852110,
-1317.297154, -1309.794006, -1297.312685, -1276.913022,
-1239.673187, -1170.634150, -1064.955721, -940.217736,
-817.917112, -703.353560, -593.813764, -493.662345};
private int weeklyOffset = 0;
@ConfigurableValue(valueType = "Double", dump=false,
description = "Std deviation of random component for weekly decomposition")
private double weeklySd = 586.1;
@ConfigurableValue(valueType = "Double", dump=false,
description = "Mean value of demand timeseries")
private double mean = 13660.0;
@ConfigurableValue(valueType = "Double", dump=false,
description = "Std deviation of residual random walk")
private double walkSd = 60;
@ConfigurableValue(valueType = "Double", dump=false,
description = "mean-reversion parameter for residual random walk")
private double walkz = 0.007;
@ConfigurableValue(valueType = "Double", dump=false,
description = "exponential smoothing parameter for residual random walk")
private double walkAlpha = 0.02;
// Ratio of Power TAC market size to MISO market size
private double scaleFactor = 670.0 / 13660.0;
// Heating and cooling degree-hours are smoothed sequences, which means
// the value in the previous timeslot is used to compute the value for
// the current timeslot. In each timeslot we get 24 forecasts, each of which
// must be used to re-compute the smoothed values for those timeslots.
// Each of these parameters is configurable through a fluent setter.
private double coolThreshold = 20.0;
private double coolCoef = 1200.0;
private double heatThreshold = 17.0;
private double heatCoef = -170.0;
private double tempAlpha = 0.1;
private int timeslotOffset = 0;
private int timeslotsOpen = 0;
//private ContextService service;
private BrokerProxy brokerProxyService;
private WeatherReportRepo weatherReportRepo;
private WeatherForecastRepo weatherForecastRepo;
private RandomSeed tsSeed;
// needed for saving bootstrap state
private TimeslotRepo timeslotRepo;
private ComposedTS timeseries;
public MisoBuyer (String username)
{
super(username, true, true);
}
public void init (BrokerProxy proxy, int seedId, ContextService service)
{
log.info("init(" + seedId + ") " + getUsername());
timeslotsOpen = Competition.currentCompetition().getTimeslotsOpen();
this.brokerProxyService = proxy;
//this.service = service;
this.timeslotRepo = (TimeslotRepo)service.getBean("timeslotRepo");
this.weatherReportRepo =
(WeatherReportRepo)service.getBean("weatherReportRepo");
this.weatherForecastRepo =
(WeatherForecastRepo)service.getBean("weatherForecastRepo");
RandomSeedRepo randomSeedRepo =
(RandomSeedRepo)service.getBean("randomSeedRepo");
// set up the random generator
this.tsSeed =
randomSeedRepo.getRandomSeed(MisoBuyer.class.getName(), seedId, "ts");
// compute offsets for daily and weekly seasonal data
int ts = timeslotRepo.currentSerialNumber();
timeslotOffset = ts;
DateTime start = timeslotRepo.getDateTimeForIndex(ts);
dailyOffset = start.getHourOfDay();
weeklyOffset = (start.getDayOfWeek() - 1) * 24 + dailyOffset;
timeseries = new ComposedTS();
timeseries.initialize(ts);
}
/**
* Generates Orders in the market to sell remaining available capacity.
*/
public void generateOrders (Instant now, List openSlots)
{
log.info("Generate orders for " + getUsername());
double[] tempCorrections =
computeWeatherCorrections();
int i = 0;
for (Timeslot slot: openSlots) {
int index = slot.getSerialNumber();
MarketPosition posn =
findMarketPositionByTimeslot(index);
double start = 0.0;
double demand = computeScaledValue(index, tempCorrections[i++]);
if (posn != null) {
// posn.overallBalance is negative if we have sold power in this slot
start = posn.getOverallBalance();
}
double needed = demand - start;
Order offer = new Order(this, index, needed, null);
log.info(getUsername() + " orders " + needed +
" ts " + index);
brokerProxyService.routeMessage(offer);
}
}
// Computes weather-based demand corrections for each forecast.
// Note that this code produces demand corrections for each forecast
// prediction, not necessarily for each open timeslot.
private double lastHeat = 0.0;
private double lastCool = 0.0;
double[] computeWeatherCorrections ()
{
WeatherReport weather = weatherReportRepo.currentWeatherReport();
WeatherForecastPrediction[] forecasts = getForecastArray();
// smooth the current heat and cool sequences
double thisHeat =
Math.min(0.0, (weather.getTemperature() - heatThreshold));
lastHeat = tempAlpha * thisHeat + (1.0 - tempAlpha) * lastHeat;
double[] smoothedHeat =
smoothForecasts(lastHeat, heatThreshold, -1.0, forecasts);
double thisCool =
Math.max(0.0,(weather.getTemperature() - coolThreshold));
lastCool = tempAlpha * thisCool + (1.0 - tempAlpha) * lastCool;
double[] smoothedCool =
smoothForecasts(lastCool, coolThreshold, 1.0, forecasts);
double[] result = new double[forecasts.length];
Arrays.fill(result, 0.0);
for (int i = 0; i < forecasts.length; i += 1) {
result[i] += smoothedHeat[i] * heatCoef;
result[i] += smoothedCool[i] * coolCoef;
}
return result;
}
// Smooths a forecast sequence. Heat and cool sequences are smoothed using
// the same alpha value. The sign parameter is used to filter relevant
// differences -- positive for cooling, negative for heating.
double[] smoothForecasts (double start, double threshold, double sign,
WeatherForecastPrediction[] forecasts)
{
double[] result = new double[forecasts.length];
double last = start;
for (int i = 0; i < forecasts.length; i += 1) {
double next = forecasts[i].getTemperature() - threshold;
if (Math.signum(next) != Math.signum(sign))
next = 0.0;
last = tempAlpha * next + (1.0 - tempAlpha) * last;
result[i] = last;
}
return result;
}
// Converts prediction list to array, indexed by time offset
WeatherForecastPrediction[] getForecastArray ()
{
WeatherForecast forecast = weatherForecastRepo.currentWeatherForecast();
List fcsts = forecast.getPredictions();
WeatherForecastPrediction[] result =
new WeatherForecastPrediction[fcsts.size()];
fcsts.forEach(fcst -> result[fcst.getForecastTime() - 1] = fcst);
return result;
}
// timeseries parameter access
double getDailyValue (int ts)
{
int index =
Math.floorMod((ts - getTimeslotOffset() + getDailyOffset()),
daily.length);
return daily[index];
}
double getWeeklyValue (int ts)
{
int index =
Math.floorMod((ts - getTimeslotOffset() + getWeeklyOffset()),
weekly.length);
return weekly[index];
}
// Returns the scaled timeseries value for timeslot ts, adjusted for
// weather
double computeScaledValue (int ts, double weatherCorrection)
{
double result = timeseries.getValue(ts);
result += weatherCorrection;
return result * scaleFactor;
}
// parameter and data access
double getMean ()
{
return mean;
}
int getTimeslotOffset ()
{
return timeslotOffset;
}
int getDailyOffset ()
{
return dailyOffset;
}
int getWeeklyOffset ()
{
return weeklyOffset;
}
// configurable parameters, fluent setters
public double getCoolThreshold ()
{
return coolThreshold;
}
@ConfigurableValue(valueType = "Double",
description = "temperature threshold for cooling")
public MisoBuyer withCoolThreshold (double value)
{
coolThreshold = value;
return this;
}
public double getCoolCoef ()
{
return coolCoef;
}
@ConfigurableValue(valueType = "Double",
description = "Multiplier: cooling MWh / degree-hour")
public MisoBuyer withCoolCoef (double value)
{
coolCoef = value;
return this;
}
public double getHeatThreshold ()
{
return heatThreshold;
}
@ConfigurableValue(valueType = "Double",
description = "temperature threshold for heating")
public MisoBuyer withHeatThreshold (double value)
{
heatThreshold = value;
return this;
}
public double getHeatCoef ()
{
return heatCoef;
}
@ConfigurableValue(valueType = "Double",
description = "multiplier: heating MWh / degree-hour (negative for heating)")
public MisoBuyer withHeatCoef (double value)
{
heatCoef = value;
return this;
}
public double getTempAlpha ()
{
return tempAlpha;
}
@ConfigurableValue(valueType = "Double",
description = "exponential smoothing parameter for temperature")
public MisoBuyer withTempAlpha (double value)
{
tempAlpha = value;
return this;
}
public double getScaleFactor ()
{
return scaleFactor;
}
@ConfigurableValue(valueType = "Double",
description = "overall scale factor for demand profile")
public MisoBuyer withScaleFactor (double value)
{
scaleFactor = value;
return this;
}
ComposedTS getTimeseries ()
{
return timeseries;
}
// timeseries implementation
class ComposedTS
{
private double lastWalk = 0.0;
private double lastSmooth = 0.0;
private double ring[] = null;
private int lastTsGenerated = -1;
ComposedTS ()
{
super();
}
// must be called with the first timeslot index to see the timeseries
void initialize (int ts)
{
// set up the ring buffer
ring = new double[timeslotsOpen];
// set up the white noise generator
lastWalk = tsSeed.nextGaussian() * walkSd;
lastSmooth = lastWalk;
}
double generateValue (int ts)
{
// retrieve daily value, add daily noise
double dv = getDailyValue(ts) + tsSeed.nextGaussian() * dailySd;
// retrieve weekly value, add weekly noise
double wv = getWeeklyValue(ts) + tsSeed.nextGaussian() * weeklySd;
// run a step of the random walk, return sum
lastWalk = (1.0 - walkz) * lastWalk + tsSeed.nextGaussian() * walkSd;
lastSmooth = walkAlpha * lastWalk + (1.0 - walkAlpha) * lastSmooth;
double result = mean + dv + wv + lastSmooth;
log.debug("Demand ts {}: {}", ts, result);
return result;
}
// returns the demand value for timeslot, which must be adjusted to
// be zero at the start of the series.
double getValue (int timeslot)
{
if (null == ring) // uninitialized
log.error("Uninitialized");
while (timeslot > lastTsGenerated) {
lastTsGenerated += 1;
ring[lastTsGenerated % ring.length] = generateValue(lastTsGenerated);
}
return (ring[timeslot % ring.length]);
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy