All Downloads are FREE. Search and download functionalities are using the official Maven repository.

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