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

org.powertac.common.TariffSubscription Maven / Gradle / Ivy

The newest version!
/*
 * Copyright (c) 2011-2018 by 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.common;

//import org.codehaus.groovy.grails.commons.ApplicationHolder
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.joda.time.Instant;
import org.powertac.common.interfaces.Accounting;
import org.powertac.common.interfaces.TariffMarket;
import org.powertac.common.spring.SpringApplicationContext;
import org.powertac.common.state.Domain;
import org.powertac.common.state.StateChange;

/**
 * A TariffSubscription is an entity representing an association between a Customer
 * and a Tariff. Instances of this class are not intended to be serialized.
 * You get one by calling the subscribe() method on Tariff. If there is no
 * current subscription for that Customer (which in most cases is actually
 * a population model), then a new TariffSubscription is created and
 * returned from the Tariff.  
 * @author John Collins, Carsten Block
 */
@Domain
public class TariffSubscription 
{
  static private Logger log = LogManager.getLogger(TariffSubscription.class.getName());

  long id = IdGenerator.createId();

  private TimeService timeService;

  private Accounting accountingService;

  private TariffMarket tariffMarketService;

  /** The customer who has this Subscription */
  private CustomerInfo customer;

  /** The tariff for which this subscription applies */
  private Tariff tariff;

  // id of tariff to allow construction by log analyzer
  private long tariffId;

  /** Total number of customers within a customer model that are committed 
   * to this tariff subscription. This needs to be a count, otherwise tiered 
   * rates cannot be applied properly. */
  private int customersCommitted = 0 ;

  /** Arbitrary data needed by population customers who may be divided among multiple
   * subscriptions and need to keep data on the join. */
  private Map customerDecorators;

  /** List of expiration dates. This is used only if the Tariff has a minDuration,
   *  before which a subscribed Customer cannot back out without a penalty. Each
   *  entry in this list is a pair [expiration-date, customer-count]. New entries
   *  are added chronologically at the end of the list, so the front of the list
   *  holds the oldest subscriptions - the ones that can be unsubscribed soonest
   *  without penalty. */
  private List expirations;

  /** Total usage so far in the current day, needed to compute charges for
   *  tiered rates. */
  private double totalUsage = 0.0;
  
  /** Count of customers who will not be subscribers in the next timeslot */
  private int pendingUnsubscribeCount = 0; 

  // ------------- Regulation capacity ----------------
  /** Pending economic regulation (from phase 1) */
  private double pendingRegulationRatio = 0.0;

  /** Available regulation capacity for the current timeslot. */
  RegulationAccumulator regulationAccumulator;

  /** Actual up-regulation (positive) or down-regulation (negative)
   * from previous timeslot.
   * Should always be zero after the customer model has run. */
  private double regulation = 0.0;

  /**
   * You need a CustomerInfo and a Tariff to create one of these.
   */
  public TariffSubscription (CustomerInfo customer, Tariff tariff)
  {
    super();
    this.customer = customer;
    this.tariff = tariff;
    this.tariffId = tariff.getId();
    expirations = new ArrayList();
    setRegulationCap(new RegulationAccumulator(0.0, 0.0));
  }

  /**
   * Alternate constructor for logtool analyzers in which Tariffs cannot
   * be reconstructed. Many features won't work if the Tariff does not exist.
   */
  public TariffSubscription (CustomerInfo customer, long tariffId)
  {
    super();
    this.customer = customer;
    this.tariffId = tariffId;
    expirations = new ArrayList();
    setRegulationCap(new RegulationAccumulator(0.0, 0.0));
  }

  public long getId ()
  {
    return id;
  }

  public CustomerInfo getCustomer ()
  {
    return customer;
  }

  public Tariff getTariff ()
  {
    return tariff;
  }

  public long getTariffId ()
  {
    return tariffId;
  }

  public int getCustomersCommitted ()
  {
    return customersCommitted;
  }

  @StateChange
  public void setCustomersCommitted (int value)
  {
    customersCommitted = value;
  }

  public double getTotalUsage ()
  {
    return totalUsage;
  }

  // ============================ Customer API ===============================

  /**
   * Subscribes some number of discrete customers. This is typically some portion of the population in a
   * population model. We assume this is called from Tariff, as a result of calling tariff.subscribe().
   * Also, we record the expiration date of the tariff contract, just in case the tariff has a
   * minDuration. For the purpose of computing expiration, all contracts are assumed to begin at
   * 00:00 on the day of the subscription.
   */
  @StateChange
  public void subscribe (int customerCount)
  {
    // first, update the customer count
    setCustomersCommitted(getCustomersCommitted() + customerCount);
    
    // if the Tariff has a minDuration, then we have to record the expiration date.
    // we do this by adding an entry to end of list, or updating the entry at the end.
    // An entry is a pair [Instant, count]
    long minDuration = tariff.getMinDuration();
    //if (minDuration > 0) {
    // Compute the 00:00 Instant for the current time
    Instant start = getTimeService().getCurrentTime();
    if (expirations.size() > 0 &&
        expirations.get(expirations.size() - 1).getHorizon() == start.getMillis() + minDuration) {
      // update existing entry
      expirations.get(expirations.size() - 1).updateCount(customerCount);
    }
    else {
      // need a new entry
      expirations.add(new ExpirationRecord(start.getMillis() + minDuration,
                                           customerCount));
    }
    //}
    // post the signup bonus
    if (tariff.getSignupPayment() != 0.0) {
      log.debug("signup bonus: " + customerCount + 
                " customers, total = " + customerCount * tariff.getSignupPayment());
    }
    // signup payment is positive for a bonus, so it's a debit for the broker.
    getAccounting().addTariffTransaction(TariffTransaction.Type.SIGNUP,
                                         tariff, customer, 
                                         customerCount, 0.0,
                                         customerCount * -tariff.getSignupPayment());
  }

  /**
   * Removes customerCount customers (at most) from this subscription,
   * posts early-withdrawal fees if appropriate. 
   */
  public void unsubscribe (int customerCount)
  {
    getTariffMarket().subscribeToTariff(getTariff(),
                                          getCustomer(),
                                          -customerCount);
    pendingUnsubscribeCount += customerCount;
  }

  /**
   * Handles the actual unsubscribe operation. Intended to be called by
   * the TariffMarket (phase 4) to avoid subscription changes between customer
   * consumption/production and balancing.
   */
  @StateChange
  public void deferredUnsubscribe (int customerCount)
  {
    pendingUnsubscribeCount = 0;
    if (customerCount == customersCommitted) {
      // common case
      setRegulationCap(new RegulationAccumulator(0.0, 0.0));
      setRegulation(0.0);
    }

    // first, make customerCount no larger than the subscription count
    if (customerCount > customersCommitted) {
      log.error("tariff " + tariff.getId() +
                " customer " + customer.getName() +
                ": attempt to unsubscribe " + customerCount +
                " from subscription of " + customersCommitted);
      customerCount = customersCommitted;
    }
//    customerCount = Math.min(customerCount, customersCommitted);
//    adjustRegulationCapacity((double)(customersCommitted - customerCount)
//                             / customersCommitted);
    // find the number of customers who can withdraw without penalty
    int freeAgentCount = getExpiredCustomerCount();
    int penaltyCount = Math.max (customerCount - freeAgentCount, 0);
    // update the expirations list
    int expCount = customerCount;
    while (expCount > 0 && expirations.get(0) != null) {
      int cec = expirations.get(0).getCount();
      if (cec <= expCount) {
        expCount -= cec;
        expirations.remove(0);
      }
      else {
        expirations.get(0).updateCount(-expCount);
        expCount = 0;
      }
    }
    setCustomersCommitted(getCustomersCommitted() - customerCount);
    // if count is now zero, set regulation capacity to zero
    if (0 == getCustomersCommitted()) {
      regulationAccumulator.setDownRegulationCapacity(0.0);
      regulationAccumulator.setUpRegulationCapacity(0.0);
    }
    // Post withdrawal and possible penalties
    double withdrawPayment = -tariff.getEarlyWithdrawPayment();
    if (tariff.isRevoked()) {
      withdrawPayment = 0.0;
    }
    getAccounting().addTariffTransaction(TariffTransaction.Type.WITHDRAW,
                                         tariff, customer, customerCount, 0.0,
                                         penaltyCount * withdrawPayment);
    if (tariff.getSignupPayment() < 0.0) {
      // Refund signup payment
      getAccounting().addTariffTransaction(TariffTransaction.Type.REFUND,
                                           tariff, customer,
                                           customerCount, 0.0,
                                           customerCount * tariff.getSignupPayment());
    }
  }

//  private void adjustRegulationCapacity (double ratio)
//  {
//    regulationAccumulator.setUpRegulationCapacity(regulationAccumulator
//        .getUpRegulationCapacity() * ratio);
//    regulationAccumulator.setDownRegulationCapacity(regulationAccumulator
//        .getDownRegulationCapacity() * ratio);
//  }

  /**
   * Adds a (name, value) pair to the CustomerDecorators map
   */
  public void addCustomerDecorator (String name, Object value)
  {
    if (null == customerDecorators) {
      customerDecorators = new HashMap<>();
    }
    customerDecorators.put(name, value);
  }

  /**
   * Returns the named customerDecorator, if any
   */
  public Object getCustomerDecorator (String name)
  {
    if (null == customerDecorators) {
      return null;
    }
    else {
      return customerDecorators.get(name);
    }
  }

  /**
   * Handles the subscription switch in case the underlying Tariff has been
   * revoked. The actual processing of tariff revocations, including switching
   * subscriptions to superseding tariffs, is deferred to be handled by the
   * tariff market.
   */
  public Tariff handleRevokedTariff ()
  {
    // if the tariff is not revoked, then just return this subscription
    if (!tariff.isRevoked()) {
      log.warn("Tariff " + tariff.getId() + " is not revoked.");
      return tariff;
    }
    // if no subscribers, we can ignore this
    if (0 == customersCommitted) {
      return null;
    }
    // if the tariff has already been superseded, then switch subscription to
    // that new tariff
    Tariff newTariff = null;
    //Tariff newTariff = tariff.getIsSupersededBy();
    if (newTariff == null) {
      // there is no superseding tariff, so we have to revert to the default tariff.
      newTariff =
        getTariffMarket().getDefaultTariff(tariff.getTariffSpec()
                .getPowerType());
    }
    if (newTariff == null) {
      // there is no exact match for original power type - choose generic
      newTariff =
        getTariffMarket().getDefaultTariff(tariff.getTariffSpec()
                .getPowerType().getGenericType());
    }

    getTariffMarket().subscribeToTariff(tariff, customer,
                                          -customersCommitted);
    getTariffMarket().subscribeToTariff(newTariff, customer,
                                          customersCommitted);
    log.info("Tariff " + tariff.getId() + " superseded by " + newTariff.getId()
             + " for " + customersCommitted + " customers");
    // customersCommitted = 0;
    return newTariff;
  }

  /**
   * Generates and posts a TariffTransaction instance for the current timeslot that
   * represents the amount of production (negative amount) or consumption
   * (positive amount), along with the credit/debit that results. Also generates
   * a separate TariffTransaction for the fixed periodic payment if it's non-zero.
   * Note that the power usage value and the numbers in the
   * TariffTransaction are aggregated across the subscribed population,
   * not per-member values. This is where the signs on energy and cost are inverted
   * to convert from the customer-centric view in the tariff to the broker-centric
   * view in the transactions.
   */
  public void usePower (double kwh)
  {
    // deal with no-regulation customers
    ensureRegulationCapacity();
    // do economic control first
    double kWhPerMember = kwh / customersCommitted;
    double actualKwh =
      (kWhPerMember - getEconomicRegulation(kWhPerMember, totalUsage))
          * customersCommitted;
    log.info("usePower " + kwh + ", actual " + actualKwh + 
             ", customer=" + customer.getName());
    // generate the usage transaction
    TariffTransaction.Type txType =
        actualKwh < 0 ? TariffTransaction.Type.PRODUCE: TariffTransaction.Type.CONSUME;
    getAccounting().addTariffTransaction(txType, tariff,
        customer, customersCommitted, -actualKwh,
        customersCommitted * -tariff.getUsageCharge(actualKwh / customersCommitted, totalUsage, true));
    if (getTimeService().getHourOfDay() == 0) {
      //reset the daily usage counter
      totalUsage = 0.0;
    }
    totalUsage += actualKwh / customersCommitted;
    // generate the periodic payment if necessary
    if (tariff.getPeriodicPayment() != 0.0) {
      getAccounting().addTariffTransaction(TariffTransaction.Type.PERIODIC,
          tariff, customer, customersCommitted, 0.0,
          customersCommitted * -tariff.getPeriodicPayment() / 24.0);
    }
  }

  /**
   * Returns the regulation in kwh, aggregated across the subscribed population,
   * for the previous timeslot. 
   * Intended to be called by Customer models only. Value is non-negative for
   * consumption power types, non-positive for production types. For storage
   * types it may be positive or negative.
   * 
   * NOTE: this method is not idempotent; if you call it twice
   * in the same timeslot, the second time returns zero.
   */
  public synchronized double getCurtailment ()
  {
    double sgn = 1.0;
    if (tariff.getPowerType().isProduction())
      sgn = -1.0;
    double result = sgn * Math.max(sgn * regulation, 0.0) * customersCommitted;
    setRegulation(0.0);
    return result;
  }

  /**
   * Returns the regulation quantity exercised per member
   * in the previous timeslot. For non-storage devices,
   * only up-regulation through curtailment is supported, 
   * and the result will be a non-negative value.
   * For storage devices, it may be positive (up-regulation) or negative
   * (down-regulation). 
   * Intended to be called by customer models.
   * 
   * NOTE: This method is not idempotent,
   * because the regulation quantity is reset to zero after it's accessed.
   */
  public synchronized double getRegulation ()
  {
    double result = regulation;
    setRegulation(0.0);
    return result;
  }

  //@StateChange
  public void setRegulation (double newValue)
  {
    regulation = newValue;
  }

  /**
   * Communicates the ability of the customer model to handle regulation
   * requests. Quantities are per-member.
   * 
   * NOTE: This method must be called once/timeslot for any customer that
   * offers regulation capacity, because otherwise the capacity will be
   * carried over from the previous timeslot.
   */
  //@StateChange
  public void setRegulationCapacity (RegulationCapacity capacity)
  {
    double upRegulation = capacity.getUpRegulationCapacity();
    double downRegulation = capacity.getDownRegulationCapacity();
    regulationAccumulator =
            new RegulationAccumulator(upRegulation, downRegulation);
  }

  // Local Regulation management
  void setRegulationCap (RegulationAccumulator capacity)
  {
    regulationAccumulator = capacity;
  }

  /**
   * Ensures that regulationAccumulator is non-null -
   * needed for non-regulatable customer models
   */
  public void ensureRegulationCapacity ()
  {
    if (null == regulationAccumulator) {
      setRegulationCap(new RegulationAccumulator(0.0, 0.0));
    }
  }

  /**
   * True just in case this subscription allows regulation.
   */
  public boolean hasRegulationRate ()
  {
    return this.tariff.hasRegulationRate();
  }

  /**
   * Returns the result of economic control in kwh for the current timeslot.
   * Value is the minimum of what's requested and what's allowed by the
   * Rates in effect given the time and cumulative usage. Intended to be
   * called within usePower() to implement economic control.
   * The parameters and return value are per-member values, not aggregated
   * across the subscription.
   * 
   * Depending on the value of pendingRegulationRatio, there are three possible
   * outcomes:
   * 
    *
  • (0.0 <= pendingRegulationRatio <= 1.0) represents simple curtailment. * The returned value will be the minimum of the proposedUsage. * This is the only possible result for a tariff without * RegulationRates.
  • *
  • (-1.0 <= pendingRegulationRatio < 0.0) represents down-regulation, * dumping energy into a thermal or electrical storage device. Amount is * limited by the available regulation capacity. This case is only * supported under a RegulationRate.
  • *
  • (1.0 < pendingRegulationRatio <= 2.0) represents discharge of an * electrical storage device. Amount is * limited by the available regulation capacity. This case is only * supported under a RegulationRate.
  • *
* * Note that this method is not idempotent -- it should be called at most * once in each timeslot; this scheme makes one call every time the customer * uses power. */ double getEconomicRegulation (double proposedUsage, double cumulativeUsage) { // reset the regulation qty here setRegulation(0.0); double result = 0.0; if (getTariff().hasRegulationRate()) { if (pendingRegulationRatio < 0.0) { // down-regulation - negative result result = (-pendingRegulationRatio) * regulationAccumulator.getDownRegulationCapacity(); regulationAccumulator.setDownRegulationCapacity(regulationAccumulator .getDownRegulationCapacity() - result); } else if (pendingRegulationRatio > 1.0) { // discharge: between proposed usage and up-regulation capacity if (regulationAccumulator.getUpRegulationCapacity() > proposedUsage) { double excess = regulationAccumulator.getUpRegulationCapacity() - proposedUsage; result = proposedUsage + (pendingRegulationRatio - 1.0) * excess; regulationAccumulator.setUpRegulationCapacity(regulationAccumulator .getUpRegulationCapacity() - result); } } else { // curtailment based on regulation capacity result = pendingRegulationRatio * regulationAccumulator.getUpRegulationCapacity(); regulationAccumulator.setUpRegulationCapacity(regulationAccumulator .getUpRegulationCapacity() - result); } } else { // find the minimum of what's asked for and what's allowed. double proposedUpRegulation = proposedUsage * pendingRegulationRatio; double mur = tariff.getMaxUpRegulation(proposedUsage, cumulativeUsage); result = Math.min(proposedUpRegulation, mur); log.debug("proposedUpRegulation=" + proposedUpRegulation + ", maxUpRegulation=" + mur); regulationAccumulator.setUpRegulationCapacity(mur - result); } if (0.0 != result) log.info("Economic control of {} by {}", customer.getName(), result); addRegulation(result); // saved until next timeslot pendingRegulationRatio = 0.0; return result; } // ===================== Demand Response / Balancing API ==================== /** * Posts the ratio for an EconomicControlEvent to the subscription for the * current timeslot. */ @StateChange public synchronized void postRatioControl (double ratio) { pendingRegulationRatio = ratio; } /** * Posts a BalancingControlEvent to the subscription and generates the correct * TariffTransaction. This updates * the regulation for the current timeslot by the amount of the control. * A positive value for kwh represents up-regulation, or an * increase in production - in other words, a net gain for the broker's * energy account balance. The kwh value is a population value, not a * per-member value. */ @StateChange public synchronized void postBalancingControl (double kwh) { // issue compensating tariff transaction getAccounting().addRegulationTransaction(tariff, customer, customersCommitted, kwh, customersCommitted * -tariff.getRegulationCharge(-kwh / customersCommitted, totalUsage, true)); double kWhPerMember = kwh / customersCommitted; addRegulation(kWhPerMember); if (kWhPerMember >= 0.0) { // up-regulation regulationAccumulator.setUpRegulationCapacity(regulationAccumulator .getUpRegulationCapacity() - kWhPerMember); } else { regulationAccumulator.setDownRegulationCapacity(regulationAccumulator .getDownRegulationCapacity() - kWhPerMember); } totalUsage -= kWhPerMember; } /** * Returns the maximum aggregate up-regulation possible after the * customer model has run and possibly applied economic controls. * Since this is potentially * accessed through the balancing market after customers have updated their * subscriptions, it's possible * that the value will have to be changed due to a change in customer count. * TODO: may need to be modified -- see issue #733. */ public RegulationAccumulator getRemainingRegulationCapacity () { if (0 == customersCommitted) { // nothing to do here... return new RegulationAccumulator(0.0, 0.0); } // generate aggregate value here double up = regulationAccumulator.getUpRegulationCapacity() * customersCommitted; double down = regulationAccumulator.getDownRegulationCapacity() * customersCommitted; if (0 == pendingUnsubscribeCount) { log.info("regulation capacity for " + getCustomer().getName() + ":" + this.getTariff().getId() + " (" + up + ", " + down + ")"); return new RegulationAccumulator(up, down); } else { // we have some unsubscribes - need to adjust double ratio = (double)(customersCommitted - pendingUnsubscribeCount) / customersCommitted; log.info("remaining regulation capacity for " + getCustomer().getName() + ":" + this.getTariff().getId() + " reduced by " + ratio + " to (" + up * ratio + ", " + down * ratio + ")"); return new RegulationAccumulator(up * ratio, down * ratio); } } /** * Adds kwh to the regulation exercised in the current timeslot. * Intended to be called during exercise of economic or balancing controls. * The kwh argument is a per-member value; positive for up-regulation, * negative for down-regulation. */ void addRegulation (double kwh) { setRegulation(regulation + kwh); } // ================= access to Spring components ======================= private TimeService getTimeService () { if (null == timeService) timeService = (TimeService)SpringApplicationContext.getBean("timeService"); return timeService; } private Accounting getAccounting () { if (null == accountingService) accountingService = (Accounting)SpringApplicationContext.getBean("accountingService"); return accountingService; } private TariffMarket getTariffMarket () { if (null == tariffMarketService) tariffMarketService = (TariffMarket)SpringApplicationContext.getBean("tariffMarketService"); return tariffMarketService; } // -------------------- Expiration data ------------------- /** * Returns the number of individual customers who may withdraw from this * subscription without penalty. Should return the total customer count * for a non-expiring tariff. */ public int getExpiredCustomerCount () { int cc = 0; Instant now = getTimeService().getCurrentTime(); for (ExpirationRecord exp : expirations) { if (exp.getHorizon() <= now.getMillis()) { cc += exp.getCount(); } } return cc; } private class ExpirationRecord { private long horizon; private int count; ExpirationRecord (long horizon, int count) { super(); this.horizon = horizon; this.count = count; } long getHorizon () { return horizon; } int getCount () { return count; } int updateCount (int increment) { count += increment; return count; } } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy