Package: oes
Optimal Energy Storage (oes)Multiple solutions for optimal energy storage control and scheduling.Table of ContentsInstallUsageBattery ModelScenarioUnitsControllersSchedulersTestingInstallTo install this package, simply run:pip install oesIf you want todevelopthis package, please install this locally by running:makelocal-installUsageSome simple examples of how this library can be used are included in the jupyter notebookexample_usage.ipynb.A basic implementation requires aBatteryModel, which specifies thebattery parameters, a subclass
ofAbstractBattery, which represents the battery instance and has access to its state-of-charge, ascenario, which is provided as PandasDataFrameand anoptimisation controller. All these
are explained in more detail below.Battery ModelA simple battery model is provided that keeps track of battery-specific parameters.Here are some example parameters if you callget_default_battery_params():{'capacity':13500,# battery capacity, in Wh'max_charge_rate':7000,# peak charge rate, in W'max_discharge_rate':7000,# peak discharge rate, in W'max_soc':94.0,# max soc we can charge to, in %'min_soc':20.0,# current soc, in %'degradation_cost_per_kWh_charge':0.0,# degradation cost per kWh of charge, in $'degradation_cost_per_kWh_discharge':0.0,# degradation cost per kWh of discharge, in $'efficiency_charging':100.0,# efficiency of charging, in %'efficiency_discharging':100.0,# efficiency of discharging, in %}A battery model only maintains parameters. To get an instance of an actual battery (which keeps track of state of
charge, and any other changes in state), we need to pass the model to an object that is a sub-class ofAbstractBatterysuch asSimulatedBatteryor your own implementation.fromoesimportBatteryModel,get_default_battery_params,SimulatedBatterybattery_model=BatteryModel(get_default_battery_params())battery=SimulatedBattery(battery_model,initial_soc=50.0)If you want to control your own battery, you have to provide an implementation ofAbstractBatterythat provides aget_current_soc()method. This method implementation (that you have to write) can access the actual hardware and
retrieve the current state-of-charge (provided as number between 0 and 100) and will be called by the optimisation
controller.ScenarioA scenario is a set of values for some given horizon (for example the next 24 hours), and includes the following:demand (W)generation (W)import_tariff ($/kWh)export_tariff  ($/kWh)The scenario must be provided as a pandasDataFramehaving an index of timestamps.Any handling of time varying import and export tariffs, or forecasts of generation and demand,
must be done outside of this package.  Likewise, this package assumes regularly spaced intervals,
and assumes that any gaps or interpolation are handled outside of this package.Here is some example data (provided with this package) showing how a "scenario" should look:importpicklescenario=pickle.load(open('oes/data/example_data.pickle','rb'))scenario.head()generationdemandtariff_importtariff_exporttimestamp2017-11-2900:00:00013700.20.082017-11-2900:01:00013700.20.082017-11-2900:02:00013600.20.082017-11-2900:03:00014200.20.082017-11-2900:04:00013800.20.08UnitsThe following conventions regarding units are used throughout this package (chosen to match common industry usage):All time series data related to energy demand and generation: WAll import and export tariffs: $/kWhAll charge / discharge decisions:  WBattery degradation values:  $/kWhAny time series data representing energy over the course of an interval (Wh) needs to be converted to power (W)
before being used within this package.ControllersA number of different "controllers" are provided.  Each controller is instantiated with
its relevant parameters, and can then be used to "solve" a provided scenario.Controllers use the same temporal resolution as the provided scenario -- in other words,
if half-hourly data is provided, the controller will provide half-hourly charge/discharge values.The "solution" that a controller provides is a pandas DataFrame having the following structure:index oftimestamps(same as the timestamps in the provided scenario)columncharge_rate: battery charge (positive) or discharge (negative) value at each interval (in W)columnsoc: battery state of charge (in percentage) as a result of charge / discharge decisionsBy default, a controller will keep track of battery SOC when generating a solution, and will
not return charging values that lead to battery exceeding max/min SOC.  This can be
overridden by passing parameter'constrain_charge_rate': False.  This can be useful, for example,
when calculating a schedule (see below).Here is an example of how to create a very simple controller that only charges at a static rate:fromoesimportChargeControllercharge_controller=ChargeController(battery=battery)# see battery definition aboveIf we want to set a specific charge rate (7000W), and avoid constraining it by battery max/min soc, we can
instead instantiate it like this:fromoesimportChargeControllerparams={'charge_rate':7000,'constrain_charge_rate':False}charge_controller=ChargeController(params,battery=battery)# see battery definition aboveThe following controllers have been implemented:Basic controllersThese are very simple controllers that can be used as baselines or to build more complex controllers or schedules.ControllerDescriptionDoNothingControllerDo nothing (no charge or discharge).  This controller can be helpful as a baseline, e.g. to determine cost when battery is not used at all.ChargeControllerCharge at a static rateDischargeControllerDischarge at a static rateRule-based controllersRule-based controllers make a decision in each interval based on information available in that interval.  In other words, they do not conduct any kind of optimisation over a longer horizon.All rule-based controllers (and all basic controllers) must implement a function,solve_one_interval(self, scenario_interval: pd.DataFrame) -> floatto ensure they can be used to build schedules of controllers later.ControllerDescriptionSolarSelfConsumptionControllerCharge rate is generation minus demand. In other words, when there is more generation than demand, charge with the excess generation; when there is more demand, discharge to meet this.ImportTariffOptimisationControllerDischarge battery to meet demand when the import tariff is higher than average; charge battery at maximum possible rate when the import tariff is lower than averageSpotPriceArbitrageNaiveControllerAssumes that both import and export tariff represent whole sale market price (plus maybe a network charge). Takes the average of max export tariff and min import tariff, and discharges when the current price is below this value, and charges when the current price is above this value. It ignores demand and generation.Optimisation-based controllersThese controllers determine the best possible set of charge and discharge values to minimise cost across the full scenario.Optimisation-based controllersdo notneed to implement thesolve_one_intervalfunction that rule-based controllers do.ControllerDescriptionDynamicProgramControllerFull optimisation using dynamic programmingSpotPriceArbitrageOptimalControllerOptimisation taking only import and export tariffs into account.  No consideration of demand and generationSchedulersOptimal controllers will find the best possible set of charge / discharge rates in discrete intervals for the full scenario.
However, in reality circumstances can change in seconds (loads being switched on and off, clouds passing over solar panels, etc.).
Sometimes a discrete solution is not good enough, and what is really needed is real-time fast response using a basic controller.That's where a scheduler comes in.  The point of a scheduler is to take a number of very basic, simple controllers
(that can respond to changes instantly), and to find a schedule that specifies which controller should be used when.
The goal is to ultimately emulate an optimal solution, without needing to choose specific charge rates for every
interval.For now, just a single approach to scheduling has been provided as part of this package, which uses the output
of the optimal dynamic program controller as a way to choose a schedule of simpler controllers.
This can be instantiated simply:fromoesimportDPSchedulerscheduler=DPScheduler()The scheduler then needs to be passed the scenario and battery, but also a list of basic controllers that should
be considered when determining the schedule.  Finally, it also needs the outputs of the (previously calculated)
optimal solution:# Generate list of controllers to use when generating schedulefromoesimportDoNothingController,ChargeController,DischargeController,\SolarSelfConsumption,ImportTariffOptimisation,SpotPriceArbitrageNaivecontrollers=[('DN',DoNothingController),('C',ChargeController),('D',DischargeController),('SSC',SolarSelfConsumption),('TO',ImportTariffOptimisation),('SPA',SpotPriceArbitrageNaive)]scheduler.solve(scenario,battery,controllers,solution_dp)The scheduler subsequently:Determines the charge rates at every interval for all controllersDetermines which controllers match optimal (DP) most closely in each intervalGenerates a full schedule (one specific controller for every interval)Conducts some clean up (handles intervals where no near-optimal controller was found)Converts to a short scheduleThis schedule typically performs as well as an optimal solution -- or even sometimes better
(since it can handle changes over very short intervals better).For some examples, see the provided jupyter notebookexample_usage.ipynb.TestingThe following command will run the test suite (tests still to be written):python -m pytest -s tests