org.powertac.customer.coldstorage.ColdStorage.java Source code

Java tutorial

Introduction

Here is the source code for org.powertac.customer.coldstorage.ColdStorage.java

Source

/*
 * Copyright (c) 2014 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.customer.coldstorage;

import java.util.List;

import org.apache.commons.math3.distribution.NormalDistribution;
import org.apache.commons.math3.random.JDKRandomGenerator;
import org.apache.commons.math3.random.RandomGenerator;
import org.apache.log4j.Logger;
import org.powertac.common.CustomerInfo;
import org.powertac.common.RandomSeed;
import org.powertac.common.RegulationCapacity;
import org.powertac.common.Tariff;
import org.powertac.common.TariffEvaluator;
import org.powertac.common.TariffSubscription;
import org.powertac.common.WeatherReport;
import org.powertac.common.config.ConfigurableInstance;
import org.powertac.common.config.ConfigurableValue;
import org.powertac.common.enumerations.PowerType;
import org.powertac.common.interfaces.CustomerModelAccessor;
import org.powertac.common.state.Domain;
import org.powertac.common.state.StateChange;
import org.powertac.customer.AbstractCustomer;

/**
 * Model of a cold-storage warehouse with multiple refrigeration units.
 * The size of the refrigeration units is specified as stockCapacity. The
 * number is indeterminate - as many as needed will be used, depending on
 * heat loss and current internal temperature. If currentTemp < nominalTemp and
 * falling or steady, then a unit will be de-energized. If currentTemp >=
 * nominalTemp and rising or steady, then another unit will be activated. 
 * 
 * @author John Collins
 */
@Domain
@ConfigurableInstance
public class ColdStorage extends AbstractCustomer implements CustomerModelAccessor {
    static private Logger log = Logger.getLogger(ColdStorage.class.getName());

    // handy contstants
    static final double R_CONVERSION = 3.1545 / 1000.0; // kW/m^2-K
    static final double TON_CONVERSION = 3.504; // kW heat
    static final double CP_ICE = 0.564; // kWh/tonne-K
    static final double GROUND_TEMP = 3.0; // don't freeze the ground

    // model parameters
    private double minTemp = -35.0; // deg C
    private double maxTemp = -10.0;
    private double nominalTemp = -20.0;

    private double roofArea = 900.0; //m^2
    private double roofRValue = 40.0;
    private double wallArea = 1440.0; //m^2
    private double wallRValue = 22.0;
    private double floorRValue = 15.0; // area same as roof
    private double infiltrationRatio = 0.5; // added to (wall + roof) loss

    private double cop = 1.5; // coefficient of performance
    private double stockCapacity = 500.0; // tonnes of water ice
    private double turnoverRatio = 0.1; // new stock/day
    private double turnoverSd = 0.015; // sd of turnover
    private double newStockTemp = -5.0; // temperature of incoming stock
    private double nonCoolingUsage = 15.0; // kW nominal
    private double ncUsageVariability = 0.2; // for m-r random walk
    private double ncMeanReversion = 0.06;
    private double unitSize = 40.0; // tons
    private double hysteresis = 0.04; // control range

    // model state
    private PowerType powerType;
    private RandomSeed opSeed;
    private NormalDistribution normal01;
    private RandomSeed evalSeed;

    private double totalEnergyUsed = 0.0;
    private double currentNcUsage;
    private double coolingLossPerK = 0.0; // kWh/K -- lazy computation

    // bootstrap state elements
    @ConfigurableValue(valueType = "Double", bootstrapState = true, description = "current temperature")
    private Double currentTemp = null;

    @ConfigurableValue(valueType = "Double", bootstrapState = true, description = "current thermal mass")
    private double currentStock = 0.0;

    private TariffEvaluator tariffEvaluator;
    private int profileSize = 14; // two weeks of weather data

    /**
     * Default constructor, requires manual setting of name
     */
    public ColdStorage() {
        super();
    }

    /**
     * Constructor with name
     */
    public ColdStorage(String name) {
        super(name);
    }

    @Override
    public void initialize() {
        log.info("Initialize " + name);
        // fill out CustomerInfo
        powerType = PowerType.THERMAL_STORAGE_CONSUMPTION;
        CustomerInfo info = new CustomerInfo(name, 1);
        info.withPowerType(powerType).withControllableKW(-unitSize / cop)
                .withStorageCapacity(stockCapacity * CP_ICE * (maxTemp - minTemp))
                .withUpRegulationKW(-unitSize / cop).withDownRegulationKW(unitSize / cop); // optimistic, perhaps
        addCustomerInfo(info);
        ensureSeeds();
        // randomize current temp only if state not set
        if (null == currentTemp) {
            setCurrentTemp(minTemp + (maxTemp - minTemp) * opSeed.nextDouble());
            currentStock = stockCapacity;
        }
        currentNcUsage = nonCoolingUsage;
        // set up the tariff evaluator. We are wide-open to variable pricing.
        tariffEvaluator = new TariffEvaluator(this);
        tariffEvaluator.withInertia(0.7).withPreferredContractDuration(14);
        tariffEvaluator.initializeInconvenienceFactors(0.0, 0.01, 0.0, 0.0);
        tariffEvaluator.initializeRegulationFactors(-unitSize * TON_CONVERSION * 0.05, 0.0,
                unitSize * TON_CONVERSION * 0.04);
    }

    // Gets a new random-number opSeed just in case we don't already have one.
    // Useful for mock-based testing.
    private void ensureSeeds() {
        if (null == opSeed) {
            opSeed = randomSeedRepo.getRandomSeed(ColdStorage.class.getName() + "-" + name, 0, "model");
            evalSeed = randomSeedRepo.getRandomSeed(ColdStorage.class.getName() + "-" + name, 0, "eval");
            normal01 = new NormalDistribution(0.0, 1.0);
            normal01.reseedRandomGenerator(opSeed.nextLong());
        }
    }

    public CustomerInfo getCustomerInfo() {
        return getCustomerInfo(powerType);
    }

    // ----------------------- Run the model ------------------------
    @Override
    public void step() {
        totalEnergyUsed = 0.0;

        // First, we have to account for controls exercised in the last timeslot.
        // If there was non-zero regulation, we have to adjust the temperature.
        double regulation = getSubscription().getRegulation();
        if (regulation != 0.0) {
            // positive value is up-regulation, which means we lost that much
            double tempChange = regulation * cop / currentStock / CP_ICE;
            log.info(getName() + ": regulation = " + regulation + ", tempChange = " + tempChange);
            currentTemp += tempChange;
        }

        // add in temp change due to stock turnover
        currentTemp += turnoverRise();

        // start with the non-cooling load - this part is not subject to regulation
        updateNcUsage();
        useEnergy(currentNcUsage);

        // use cooling energy to maintain and adjust current temp
        WeatherReport weather = weatherReportRepo.currentWeatherReport();
        double outsideTemp = weather.getTemperature();
        double energyNeeded = computeCoolingEnergy(outsideTemp, unitSize * TON_CONVERSION);

        // Now we need to record available regulation capacity. Note that only
        // the cooling portion is available for regulation.
        // Note also that we have to stay within the min-max temp range
        double availableUp = energyNeeded / cop;
        if (currentTemp >= maxTemp)
            // can't regulate up above max temp
            availableUp = 0.0;
        double availableDown = -(unitSize * TON_CONVERSION - energyNeeded) / cop;
        if (currentTemp <= minTemp)
            // and can't regulate down below min
            availableDown = 0.0;
        RegulationCapacity capacity = new RegulationCapacity(availableUp, availableDown);
        getSubscription().setRegulationCapacity(capacity);
        log.info(getName() + ": regulation capacity (" + capacity.getUpRegulationCapacity() + ", "
                + capacity.getDownRegulationCapacity() + ")");

        useEnergy(energyNeeded / cop);

        log.debug("total energy = " + totalEnergyUsed);
        getSubscription().usePower(totalEnergyUsed);
    }

    // digs out the current subscription for this thing. Since the population is
    // always one, there should only ever be one of them
    private TariffSubscription getSubscription() {
        List<TariffSubscription> subs = getCurrentSubscriptions(powerType);
        if (subs.size() > 1) {
            log.warn("Multiple subscriptions " + subs.size() + " for " + getName());
        }
        return subs.get(0);
    }

    // separated out to help create profiles
    double computeCoolingEnergy(double outsideTemp, double maxAvail) {
        double coolingLoss = computeCoolingLoss(outsideTemp);
        // at this point, coolingLoss is the energy needed to maintain current temp
        double adjustmentCooling = 0.0;
        if (getCurrentTemp() < (getNominalTemp() - hysteresis / 2.0)) {
            // go to nominal as quickly as possible
            double maxWarming = coolingLoss;
            double neededWarming = currentStock * CP_ICE * (getNominalTemp() - getCurrentTemp());
            adjustmentCooling = -Math.min(maxWarming, neededWarming);
        } else if (getCurrentTemp() > (getNominalTemp() + hysteresis / 2.0)) {
            double maxCooling = maxAvail - coolingLoss;
            double neededCooling = currentStock * CP_ICE * (getCurrentTemp() - getNominalTemp());
            adjustmentCooling = Math.min(neededCooling, maxCooling);
        }
        currentTemp -= adjustmentCooling / (currentStock * CP_ICE);
        double energyNeeded = coolingLoss + adjustmentCooling;
        log.info(getName() + ": temp = " + currentTemp + ", adjustmentCooling = " + adjustmentCooling
                + ", total cooling energy = " + energyNeeded + ", temp change = "
                + (-adjustmentCooling / (currentStock * CP_ICE)));
        return energyNeeded;
    }

    // computes rise in temperature due to stock turnover
    double turnoverRise() {
        double turnoverMean = turnoverRatio * stockCapacity / 24.0;
        double sd = turnoverSd * stockCapacity / 24.0;
        // draw turnover quantity this hour from normal distribution
        double outgoing = Math.max(0.0, (normal01.sample() * sd + turnoverMean));
        double incoming = Math.max(0.0, (normal01.sample() * sd + turnoverMean));
        currentStock -= outgoing;
        double newStock = incoming; // daily-hourly
        double newTemp = ((currentStock * currentTemp + newStock * newStockTemp) / (currentStock + newStock));
        log.info(getName() + ": remove " + outgoing + "T, add " + incoming + "T raises temp "
                + (newTemp - currentTemp) + "K");
        currentStock += incoming;
        return (newTemp - currentTemp);
    }

    void updateNcUsage() // pkg visibility for testing
    {
        if (ncUsageVariability == 0)
            return;
        currentNcUsage = currentNcUsage
                + (nonCoolingUsage * (ncUsageVariability * (opSeed.nextDouble() * 2.0 - 1.0)))
                + ncMeanReversion * (nonCoolingUsage - currentNcUsage);
        currentNcUsage = Math.max(0.0, currentNcUsage);
        log.info(getName() + ": Non-cooling usage = " + currentNcUsage);
    }

    // computes kWh cooling energy to maintain current inside temp
    double computeCoolingLoss(double outsideTemp) {
        double upperLoss = getCoolingLossPerK() * (outsideTemp - currentTemp);
        double floorLoss = (R_CONVERSION / getFloorRValue() * getRoofArea()) * (GROUND_TEMP - currentTemp);
        log.info(getName() + ": heat loss walls & roof: " + upperLoss + ", floor: " + floorLoss + ", heat load: "
                + currentNcUsage);
        return upperLoss + floorLoss + currentNcUsage;
    }

    // Lazy evaluation for walls + roof + infiltration loss rate kW per K
    double getCoolingLossPerK() {
        if (0.0 == coolingLossPerK) {
            double roofLoss = R_CONVERSION / getRoofRValue() * getRoofArea();
            double wallLoss = R_CONVERSION / getWallRValue() * getWallArea();
            double infiltrationLoss = getInfiltrationRatio() * (roofLoss + wallLoss);
            log.debug(": Heat loss per K -- roof: " + roofLoss + ", walls: " + wallLoss + ", infiltration: "
                    + infiltrationLoss);
            coolingLossPerK = roofLoss + wallLoss + infiltrationLoss;
        }
        return coolingLossPerK;
    }

    // -------------------------- Evaluate tariffs ------------------------
    @Override
    public void evaluateTariffs(List<Tariff> tariffs) {
        log.info(getName() + ": evaluate tariffs");
        tariffEvaluator.evaluateTariffs();
    }

    // ------------- CustomerModelAccessor methods -----------------
    @Override
    public double[] getCapacityProfile(Tariff tariff) {
        List<WeatherReport> weather = weatherReportRepo.allWeatherReports();
        int offset = (weather.size() >= profileSize) ? (weather.size() - profileSize) : 0;
        double[] result = new double[profileSize];
        for (int i = 0; i < profileSize; i++) {
            int wi = i + offset;
            double temperature;
            if (weather.size() > wi)
                temperature = weather.get(wi).getTemperature();
            else
                temperature = 18.0; // default temp
            double cooling = computeCoolingEnergy(temperature, unitSize * TON_CONVERSION);
            result[i] = cooling / cop + nonCoolingUsage;
        }
        return result;
    }

    @Override
    public double getBrokerSwitchFactor(boolean isSuperseding) {
        if (isSuperseding)
            return 0;
        else
            return 0.02;
    }

    @Override
    public double getTariffChoiceSample() {
        return evalSeed.nextDouble();
    }

    @Override
    public double getInertiaSample() {
        return evalSeed.nextDouble();
    }

    // --------------- State and state change -------------
    public double getCurrentTemp() {
        return currentTemp;
    }

    void setCurrentTemp(double temp) {
        currentTemp = temp;
    }

    void useEnergy(double kWh) {
        totalEnergyUsed += kWh;
    }

    double getCurrentNcUsage() {
        return currentNcUsage;
    }

    // ----------------- Parameter access -----------------

    public double getMinTemp() {
        return minTemp;
    }

    @ConfigurableValue(valueType = "Double", description = "minimum allowable temperature")
    @StateChange
    public ColdStorage withMinTemp(double temp) {
        minTemp = temp;
        return this;
    }

    public double getMaxTemp() {
        return maxTemp;
    }

    @ConfigurableValue(valueType = "Double", description = "maximum allowable temperature")
    @StateChange
    public ColdStorage withMaxTemp(double temp) {
        maxTemp = temp;
        return this;
    }

    public double getNominalTemp() {
        return nominalTemp;
    }

    @ConfigurableValue(valueType = "Double", description = "nominal internal temperature")
    @StateChange
    public ColdStorage withNominalTemp(double temp) {
        maxTemp = temp;
        return this;
    }

    public double getNewStockTemp() {
        return newStockTemp;
    }

    @ConfigurableValue(valueType = "Double", description = "Temperature of incoming stock")
    @StateChange
    public ColdStorage withNewStockTemp(double temp) {
        newStockTemp = temp;
        return this;
    }

    public double getStockCapacity() {
        return stockCapacity;
    }

    @ConfigurableValue(valueType = "Double", description = "Typical inventory in tonnes of H2O")
    @StateChange
    public ColdStorage withStockCapacity(double value) {
        if (value < 0.0)
            log.error(getName() + ": Negative stock capacity " + value + " not allowed");
        else
            stockCapacity = value;
        return this;
    }

    public double getTurnoverRatio() {
        return turnoverRatio;
    }

    @ConfigurableValue(valueType = "Double", description = "Ratio of stock that gets replaced daily")
    @StateChange
    public ColdStorage withTurnoverRatio(double ratio) {
        if (ratio < 0.0 || ratio > 1.0)
            log.error(getName() + ": turnover ratio " + ratio + " out of range");
        else
            turnoverRatio = ratio;
        return this;
    }

    public double getRoofArea() {
        return roofArea;
    }

    @ConfigurableValue(valueType = "Double", description = "Area of roof")
    @StateChange
    public ColdStorage withRoofArea(double area) {
        roofArea = area;
        return this;
    }

    public double getRoofRValue() {
        return roofRValue;
    }

    @ConfigurableValue(valueType = "Double", description = "R-value of roof insulation")
    @StateChange
    public ColdStorage withRoofRValue(double value) {
        roofRValue = value;
        return this;
    }

    public double getWallArea() {
        return wallArea;
    }

    @ConfigurableValue(valueType = "Double", description = "Total area of outside walls")
    @StateChange
    public ColdStorage withWallArea(double area) {
        wallArea = area;
        return this;
    }

    public double getWallRValue() {
        return wallRValue;
    }

    @ConfigurableValue(valueType = "Double", description = "R-value of wall insulation")
    @StateChange
    public ColdStorage withWallRValue(double value) {
        wallRValue = value;
        return this;
    }

    public double getFloorRValue() {
        return floorRValue;
    }

    @ConfigurableValue(valueType = "Double", description = "R-value of floor insulation")
    @StateChange
    public ColdStorage withFloorRValue(double value) {
        floorRValue = value;
        return this;
    }

    public double getInfiltrationRatio() {
        return infiltrationRatio;
    }

    @ConfigurableValue(valueType = "Double", description = "Infiltration loss as proportion of wall + roof loss")
    @StateChange
    public ColdStorage withInfiltrationRatio(double value) {
        if (value < 0.0)
            log.error(getName() + ": Infiltration ratio " + value + " cannot be negative");
        else
            infiltrationRatio = value;
        return this;
    }

    public double getUnitSize() {
        return unitSize;
    }

    @ConfigurableValue(valueType = "Double", description = "Thermal capacity in tons of cooling plant")
    @StateChange
    public ColdStorage withUnitSize(double cap) {
        if (cap < 0.0)
            log.error(getName() + ": Cooling capacity " + cap + " cannot be negative");
        else
            unitSize = cap;
        return this;
    }

    public double getCop() {
        return cop;
    }

    @ConfigurableValue(valueType = "Double", description = "Coefficient of Performance of refrigeration unit")
    @StateChange
    public ColdStorage withCop(double value) {
        if (value < 0.0)
            log.error(getName() + ": Coefficient of performance " + value + " cannot be negative");
        else
            cop = value;
        return this;
    }

    public double getHysteresis() {
        return hysteresis;
    }

    @ConfigurableValue(valueType = "Double", description = "Control range for refrigeration unit")
    @StateChange
    public ColdStorage withHysteresis(double value) {
        if (value < 0.0)
            log.error(getName() + ": Hysteresis " + value + " cannot be negative");
        else
            hysteresis = value;
        return this;
    }

    public double getNonCoolingUsage() {
        return nonCoolingUsage;
    }

    @ConfigurableValue(valueType = "Double", description = "Mean hourly energy usage for non-cooling purposes")
    @StateChange
    public ColdStorage withNonCoolingUsage(double value) {
        if (value < 0.0)
            log.error(getName() + ": Non-cooling usage " + value + " cannot be negative");
        else
            nonCoolingUsage = value;
        return this;
    }
}