Building a customer model - Pyosch/powertac-server GitHub Wiki

up

A customer model for the Power TAC simulator must provide several capabilities:

  • Initialize, with properties set by a configuration file.
    • create and configure tariff evaluator
  • Advertise capabilities in the CustomerInfo.
  • Simulate the model elements in each timestep.
    • use energy
    • calculate and communicate regulation capacity for current timeslot
    • deal with regulation actions in earlier timeslots
  • Record state transitions in the state log file.
  • Record significant events in the trace log file.
  • Evaluate and subscribe to tariffs.
  • Save model state to the bootstrap record, and reload state at the start of a sim session.
  • Pass unit tests to ensure that all the major capabilities are functioning correctly.

Examples

There are two example customer models that use the structure described here.

One is the cold-storage warehouse model in the customer-models module. It models a small collection of large cold-storage facilities, treating them as consumers with configurable amounts of thermal storage. Stock is moved in and out of them, so outgoing stock reduces thermal mass, and incoming stock has to be cooled down. Weather affects thermal leakage, and therefore energy consumption. There is a residual power draw for lighting and other uses that is not affected by temperature.

The other is the electric-vehicle model in the ev-customer module. It represents a collection of individual electric vehicles of various types, owned by a population across a spectrum of social and economic circumstances, with a variety of activities that involve driving. They support vehicle-to-grid behavior, in which energy stored in the vehicle's battery can be delivered back into the grid. This model is more complex than the cold-storage model because many details of population sizes and vehicle types are randomly selected at initialization, and those choices as well as the states of individual vehicles must be propagated from the end of a boot session to the start of a sim session through the bootstrap record.

Architectural context

The Power TAC simulation server is a [Spring application](Server structure), composed of a set of Spring "services" that are activated by various events, such as the arrival of messages or the passage of time. Power TAC is a discrete-time simulation, in which each "tick" represents one hour in the simulated world. Each tick is processed in a series of "phases" to ensure consistency. The wholesale market runs in phase 1, customer models run in phase 2, the balancing market runs in phase 3, the tariff market runs in phase 4, etc. The actual assignment of services to phases is controlled by the Spring configuration file in server-main/src/main/resources/powertac.xml.

There are a number of services that customers generally need to access, including time services, repositories of tariffs and subscriptions, the configuration service, weather data, and random-number generators. Because customer instances are generally not "services" in the Spring sense, they lack the ability to access other services through the Spring "autowire" process. Some of the older customer models access services through the Spring runtime context, but this complicates coding and testing, and is prone to obscure errors. Therefore, customer models are provided with a reference to a Spring service that implements the CustomerServiceAccessor interface, accessible through the service attribute provided by AbstractCustomer.

Many of the features needed to construct customer models are accessed through Java annotations. For example, if a class is labeled as @Domain, its constructor will generate a record in the state log. If a field or setter method is labeled as @ConfigurableValue, it can be initialized from a configuration file with little or no code other than the annotation.

Initialization

Model initialization involves constructing and configuring the various elements of the model, and setting the initial state of the model. For simple models like the cold-storage warehouse model, construction and configuration are automatic, given a properly-formed configuration file such as the one in customer-models/src/main/resources/config/properties.xml This file must be either a Java properties file, or an xml file that follows the format for a commons-configuration XMLConfiguration file. Note that in the absence of an XML Schema, this format is more restrictive than the format used by XStream for the bootstrap record.

For the curious, construction and configuration are handled by the code in server-interface/../customer/CustomerModelService.initialize(). The configuration must supply a list of instance names, and configurable settings for each instance. The target class must be a subclass of customer.AbstractCustomer, must be annotated with @ConfigurableInstance, and its configurable values must be annotated with @ConfigurableValue on either the fields or the corresponding setter methods. If a setter method is used, there is no requirement for a field with the same name.

Once an instance is created and configured, the initialization process will invoke the initialize() method in each instance. This method must create and save (by calling the inherited addCustomerinfo() method) one or more CustomerInfo instances to represent the entities within the model that can independently subscribe to tariffs. This data structure is communicated to brokers, and must specify the PowerType, controllable capacity (maximum curtailable usage within a single timeslot), storage capacity, and regulation capacity (maximum energy that could be sourced or sunk in a single timeslot). Model initialization must also set the initial state of the model, and must set up its TariffEvaluator. Once model initialization is complete, the CustomerModelService will subscribe each CustomerInfo instance to the appropriate default tariff.

Initialization commonly requires some amount of randomization to set initial state, and the TariffEvaluator needs access to a sequence of random values. Customer models are forbidden to use the raw Random type from Java, because of the requirement to be able to re-run simulation scenarios with repeatable random sequences. Therefore, all random values must be generated through named RandomSeed instances, which are recorded in the simulation state log as they are constructed. New RandomSeed instances are acquired through the RandomSeedRepo service as

  service.getRandomSeedRepo().getRandomSeed()

It is important to use separate RandomSeed instances for processes that may be affected by different external processes. For example, the tariff-evaluation process uses random values to implement stochastic behaviors, and the number of draws in an evaluation cycle may vary depending on how many tariffs are issued by brokers.

The instances created by CustomerModelService must be subclasses of AbstractCustomer, but they need not be the types that implement the behaviors of individual customers in the model. For example, in ev-customer, they are "customer groups" which create the actual customer instances through a further configuration process. The populations and detailed composition of these groups is randomized. The activity simulation, energy use, and tariff evaluation processes are delegated to the individual customer instances.

Simulation

Once all the elements of the simulation are initialized, the clock starts ticking. At each tick, CustomerModelService calls the step() method on each AbstractCustomer instance. Most customer models must implement three behaviors at every step of the simulation. Many of these actions involve interactions with the customer's current TariffSubscription.

  1. Find out what regulation actions were performed during the last timeslot (see the game specification Section 4.2 for details), and update the model state to reflect the results of these actions. Regulation actions are allowed only for customers whose PowerType.isInterruptible() is true, and only if the currently subscribed tariff includes a RegulationRate or an ordinary Rate for which maxCurtailment != 0.0. Regulation actions effectively change the amount of energy actually consumed or produced during the previous timeslot, and therefore will change the amount of energy stored, or the temperature, or some other attribute of the customer model.
  2. Run the customer simulation forward by one timestep, update the customer state accordingly, and record the resulting energy consumption or production by calling tariffSubscription.usePower(). If the model depends on current or predicted weather conditions, this data can be retrieved from the WeatherReportRepo and the WeatherForecastRepo. If the customer might subscribe to a variable-rate or time-of-use tariff, then it might want to access Tariff.getUsageCharge() to price out a near-term energy usage profile.
  3. For storage devices, compute the available up-regulation and down-regulation capacity, and communicate the result to the system using tariffSubscription.setRegulationCapacity().

Tariff selection

Brokers periodically publish new tariffs, and customers need to decide whether to subscribe to them. The evaluation process is described in Section 4.1 of the game specification. Most of the details are factored out of individual customer models in to TariffEvaluator and TariffEvaluationHelper, but customer models must set up these structures with appropriate parameters prior to using them. During a simulation session, customers are notified of new tariff offerings by the CustomerModelService through calls to evaluateTariffs() on each instance of AbstractCustomer. In most cases, all that is required is that the customer in turn calls TariffEvaluator.evaluateTariffs(). This must be done separately for each model element that can independently subscribe to tariffs. For models that represent populations rather than individual customers, it is possible to allow portions of the population to subscribe to different tariffs by setting the multiContracting flag in the CustomerInfo, and by appropriately setting the chunkSize in the TariffEvaluator to determine the minimum number of individuals in the population than can be grouped together for tariff evaluation purposes.

Tariff selection requires determination of the expected cost of the various tariffs on offer. Computation of expected cost depends on an energy-use profile retrieved through the method CustomerModelAccessor.getCapacityProfile(tariff). For flat-rate tariffs this is easily done by providing a usage profile over some representative time period under the default tariff. For time-of-use tariffs, however, the customer may be willing to change its usage patterns to take advantage of lower night or weekend rates, and the profile returned should represent the expected customer response to such a tariff. This can be done using a linear program of the form

min_t c x s.t. a x = b, lb <= x <= ub

where

  • x is the energy usage profile over time,
  • c is the cost/kWh of energy in each timeslot t,
  • lb is the lower bound on x (usually {0, 0, ...}),
  • ub is the upper bound in each timeslot, and may depend on various factors in the model such as whether people are at home, vehicles are plugged in, etc.,
  • a is a two-dimensional array of -1/0/1 values and b is a vector of cumulative energy requirements indexed by time in some way.

This style of lp formulation includes the decision variables x as well as slack variables that capture the "slack" in the result for each row of the a array. Therefore, the "time" dimension of the a and c arrays include the x values as well as the slack variables. The number of rows in a may be the number of timeslots, or a smaller number in cases where time can be "blocked" in some way. For example, if an EV is always unplugged from 7:00 until 18:00 and plugged in between 18:00 and 7:00, then it is sufficient to simply distinguish between those two states, and a 1-week profile would require 14 rows. Each row includes an entry in the b vector representing the minimum cumulative energy requirement from the start of the profile through the end of the time block (negated), and the corresponding row in the a array will contain -1 values for each timeslot in this interval, 0 values for each timeslot in the remainder of the profile, and a single 1 value to represent the nth slack variable for the nth block. The c vector contains the (positive) cost/kWh for each timeslot under the given tariff (from tariff.getUsageCharge()), and a 0 entry for each slack variable.

Handling the bootstrap-sim boundary

When the Power TAC simulation starts up, it can be in one of two modes: bootstrap mode or sim mode as described in the game specification Section 7.1. A bootstrap session runs for typically 15 days of simulated time, recording energy usage, wholesale prices, weather data, and other data, with only the "default" broker in the tariff market. At the end of the boot session, this data is collected into a "bootstrap record" and used to initialize a sim session. Most of the bootstrap data is also communicated to brokers to allow them to compose their initial tariff offerings and their market trading strategies. The requirement is that the sim session starts exactly where the bootstrap session left off; for a customer model, this means that the composition and state of the model must be saved at the end of a bootstrap session and restored at the start of a sim session.

Most of this restoration can be performed by the configuration process, if variable fields are annotated with @ConfigurableValue with bootstrapState = true. In the ColdStorage model, this is pretty much the whole story; if the state values are null at initialization, then it's a bootstrap session and they must be set to randomized values; otherwise they will have been set by the configuration process before initialize() is invoked. Initialization in EvSocialClass is only slightly more complicated. At the start of a boot session, it records the attributes of the EvCustomer instances it creates in a state variable. In a sim initialization, this list is non-null to begin with, so it just has to re-create them from the stored attributes and run the config process over them to restore their individual states.

Logging

Logging in the Power TAC simulator serves two purposes. When a session starts, two log files are opened: a state log and a trace log. Both use the log4j package to simplify the generation of logs and make it consistent across the various modules.

State log

The state log captures events and state changes that might be of use for post-game and cross-game analysis. It should record the creation of important domain types, as well as significant state changes. Output to the state log is not generated directly; rather, annotations are used to signify that the call to a constructor or method should be logged. If a class is annotated with @Domain, then its creation is recorded in the state log. If a method is annotated with @StateChange, then calls to that method will be recorded. For this to be effective, three rules must be followed:

  1. Domain objects (any types that use the @Domain or @StateChange annotations) must use a numerical id, accessible through a getId() method. The value must be generated by IdGenerator.createId(). This call may either be in the field declaration, or in the constructor.
  2. State variables must be updated only through a method annotated by @StateChange, and that method must be the only way a state variable is updated. In other words, if some other method changes the value of a state variable that is supposed to be tracked in the state log, the change will not be recorded.
  3. These annotations are used by AspectJ to weave in code that does the logging. Therefore, the module's pom.xml file must include the necessary mojo to do the AspecJ compilation. For examples, see the bottom of the pom.xml in either customer-models or ev-customer.

Sometimes it is useful to provide multiple constructors for a class, to allow it to be created with default values, or uninterpreted data. For such schemes to work with a Domain type, it is necessary to decide which constructor has the arguments that should be recorded in the state log, and have all other alternate constructors to call that one. These alternate constructors must then be annotated with @ChainedConstructor, which will prevent them from being logged. A number of classes in the common package use this feature.

Note that the customer models are NOT responsible for logging their energy usage, regulation events, or tariff-subscription actions to the state log. These events are already logged through the transactions they generate. As a result, it is not necessary for customer models to do any state logging if all the desired information is contained in transactions. Look through an existing state log to see how this works.

In the powertac-tools project, the logtool module provides the ability to extract data of interest from state logs through fairly simple event-driven programming. Several examples of this are provided in the logtool-examples module.

Trace log

Trace logging generates programmer-readable output that can be used to analyze and verify proper functioning of the elements of the Power TAC simulation server. There are few if any circumstances in the Power TAC simulation server when it is acceptable to directly print to System.out, because in many cases the server is run on a remote machine by the tournament scheduler, and console output is not readily accessible. Each class that needs to generate trace log output must create its own logger so that its output can be traced back to the class. For the ColdStorage class, this is done as

  static private Logger log = Logger.getLogger(ColdStorage.class.getName());

Within the code, output to the log is generated as something like

  log.info(getName() + ": regulation = " + regulation
           + ", tempChange = " + tempChange);

Output to the trace log is generated by calls to a log4j logger at one of four "severity" levels:

  1. ERROR -- when something is definitely wrong, and the operation of a module is compromised in some way, the general rule is to not throw an exception, but rather to generate an error message in the trace log, then do whatever cleanup is possible to avoid unnecessary disturbance to other parts of the simulation.
  2. WARN -- to signal a situation that might reflect a malfunction in some other part of the system, such as finding multiple subscriptions for a customer type that represents a single entity.
  3. INFO -- to record significant activities and results, such as the data used to determine the energy usage during a timeslot. It can be quite useful to log the creation of domain instances that are also logged in the state log, because each entry in both logs is timestamped, and it can be very helpful when tracking down a problem to be able to align the two logs to see what events are generating a particular state change.
  4. DEBUG -- used to generate debug output. The simulator generally does not turn on debug logging because the amount of data produced would be extremely unwieldy, but debug logging is turned on during unit testing. Therefore, a good way to debug is often to write a unit test that exercises the code of interest, and insert debug logging to tell you what is happening.

Testing

Ideally, unit tests should cover all interesting aspects and boundary conditions of the model simulation behavior, especially its responses to curtailment and regulation events, as well as initialization, configuration (in both boot and sim settings), tariff evaluation, and state logging. Test writers are strongly encouraged to use appropriate mocks rather than trying to set up a Spring context for the tests. A number of examples are provided in the ev-customer and customer-model modules.