diff --git a/README.md b/README.md
index 31bae236..0357dcfd 100644
--- a/README.md
+++ b/README.md
@@ -145,263 +145,6 @@ wt.region_from_loc(
wt.get_maps_json('co2_moer')
```
-# Using the Optimizer Class
-`WattTime.Optimizer` produces a power consumption schedule that minimizes carbon emissions subject to user and device constraints.
+# Optimizer Package
-The `WattTime.Optimizer` class requires 4 things:
-
-- Watttime’s forecast of marginal emissions
-- device capacity and energy needs
-- region
-- window start
-- window end
-
-
-| optimization\_method | ASAP | Charging curve | Time constraint | Contiguous |
-| :---- | :---- | :---- | :---- | :---- |
-| baseline | Yes | Constant | No | No |
-| simple | No | Constant | No | No |
-| sophisticated | No | Variable | Yes | No |
-| contiguous | No | Variable | Yes | Intervals at fixed lengths |
-| Variable contiguous | No | Variable | Yes | Intervals at variable lengths |
-| auto | No | Chooses the fastest algorithm that can still process all inputs | | |
-
-Click any of the thumbnails below to see the notebook that generated it.
-
-1. Naive Smart device charging: needs 30 minutes to reach full charge, expected plug out time within the next 4 hours. Simple use case.
-2. Requery: update usage plan every 20 minutes using new forecast for the next 4 hours. Simple use case with recalculation
-3. Partial charging guarantee: charge 75% by 8am. User constraint
-4. Data center workloads: estimated runtime is 2 hours and it needs to complete by 12pm Contiguous (single period, fixed length)
-5. Dishwasher: needs to run over two usage intervals of lengths 80 min and 40 min. They must complete in that order. Contiguous (multiple periods, fixed length)
-6. Compressor: needs to run 120 minutes over the next 12 hours; each cycle needs to be at least 20 minutes long, and any number of contiguous intervals (from one to six) is okay. Contiguous (multiple periods, variable length)
-
-**Naive Smart Device Charging**
- * Simple - uses the MOER forecast at window start to find the set of intervals that minimize emissions, and outputs a charge schedule based on that.
-
-```py
-from datetime import datetime, timedelta
-import pandas as pd
-from pytz import UTC
-from watttime import WattTimeOptimizer
-import os
-
-username = os.getenv("WATTTIME_USER")
-password = os.getenv("WATTTIME_PASSWORD")
-wt_opt = WattTimeOptimizer(username, password)
-
-# 12 hour charge window (720/60 = 12)
-now = datetime.now(UTC)
-window_start = now
-window_end = now + timedelta(minutes=720)
-
-usage_plan = wt_opt.get_optimal_usage_plan(
- region="CAISO_NORTH",
- usage_window_start=window_start,
- usage_window_end=window_end,
- usage_time_required_minutes=240,
- usage_power_kw=12,
- optimization_method="auto",
-)
-
-print(usage_plan.head())
-print(usage_plan["usage"].tolist())
-print(usage_plan.sum())
-```
-
-**Partial Charging Guarantee - Introducing Constraints**
- * Sophisticated - total charge window 12 hours long, 75% charged by hour 8.
-
-```py
-from datetime import datetime, timedelta
-import pandas as pd
-from pytz import UTC
-from watttime import WattTimeOptimizer
-import os
-
-username = os.getenv("WATTTIME_USER")
-password = os.getenv("WATTTIME_PASSWORD")
-wt_opt = WattTimeOptimizer(username, password)
-
-# 12 hour charge window (720/60 = 12)
-# Minute 480 is time context when the constraint, i.e. 75% charge, must be satisfied
-# 75% of 240 (required charge expressed in minutes) is 180
-
-now = datetime.now(UTC)
-window_start = now
-window_end = now + timedelta(minutes=720)
-usage_time_required_minutes = 240
-constraint_time = now + timedelta(minutes=480)
-constraint_usage_time_required_minutes = 180
-usage_power_kw = 12.0
-
-# map the constraint to the time context
-constraints = {constraint_time:constraint_usage_time_required_minutes}
-
-usage_plan = wt_opt.get_optimal_usage_plan(
- region="CAISO_NORTH",
- usage_window_start=window_start,
- usage_window_end=window_end,
- usage_time_required_minutes=240,
- usage_power_kw=12,
- constraints=constraints,
- optimization_method="auto",
-)
-
-print(usage_plan.head())
-print(usage_plan["usage"].tolist())
-print(usage_plan.sum())
-```
-
-**Variable Charging Curve - EV**
- * Sophisticated - total charge window 12 hours long, 75% charged by hour 8.
-
-```py
-from datetime import datetime, timedelta
-import pandas as pd
-from pytz import UTC
-from watttime import WattTimeOptimizer
-import os
-
-username = os.getenv("WATTTIME_USER")
-password = os.getenv("WATTTIME_PASSWORD")
-wt_opt = WattTimeOptimizer(username, password)
-
-# 12 hour charge window (720/60 = 12)
-# Minute 480 is time context when the constraint, i.e. 75% charge, must be satisfied
-# 75% of 240 (required charge expressed in minutes) is 180
-
-now = datetime.now(UTC)
-window_start = now
-window_end = now + timedelta(minutes=720)
-variable_usage_power = ''
-
-usage_plan = wt_opt.get_optimal_usage_plan(
- region="CAISO_NORTH",
- usage_window_start=window_start,
- usage_window_end=window_end,
- usage_time_required_minutes=240,
- usage_power_kw=variable_usage_power,
- constraints=constraints,
- optimization_method="auto",
-)
-
-print(usage_plan.head())
-print(usage_plan["usage"].tolist())
-print(usage_plan.sum())
-```
-
-
-* **Data Center Workload**:
- * Fixed Contiguous (single period, fixed length) - charging schedule to be composed of contiguous interval(s) of fixed length
-
-```py
-## AI model training - estimated runtime is 2 hours and it needs to complete within 12 hours
-
-from datetime import datetime, timedelta
-import pandas as pd
-from pytz import UTC
-from watttime.api import WattTimeOptimizer
-import os
-
-username = os.getenv("WATTTIME_USER")
-password = os.getenv("WATTTIME_PASSWORD")
-wt_opt = WattTimeOptimizer(username, password)
-
-now = datetime.now(UTC)
-window_start = now
-window_end = now + timedelta(minutes=720)
-
-usage_power_kw = 12.0
-region = "CAISO_NORTH"
-
-# by passing a single interval of 120 minutes to charge_per_interval, the Optimizer will know to fit call the fixed contigous modeling function.
-usage_plan = wt_opt.get_optimal_usage_plan(
- region=region,
- usage_window_start=window_start,
- usage_window_end=window_end,
- usage_time_required_minutes=120,
- usage_power_kw=12,
- charge_per_interval=[120],
- optimization_method="auto",
- verbose = False
-)
-
-print(usage_plan.head())
-print(usage_plan["usage"].tolist())
-print(usage_plan.sum())
-```
-
-**Dishwasher**:
- * Fixed Contiguous (multiple periods, fixed length) - runs over two usage intervals of lengths 80 min and 40 min. The order of the intervals is immutable.
-
-```py
-## Dishwasher - there are two cycles of length 80 min and 40 min each, and they must be completed in that order.
-
-from datetime import datetime, timedelta
-import pandas as pd
-from pytz import UTC
-from watttime import WattTimeOptimizer
-import os
-
-username = os.getenv("WATTTIME_USER")
-password = os.getenv("WATTTIME_PASSWORD")
-wt_opt = WattTimeOptimizer(username, password)
-
-# Suppose that the time now is 12 midnight
-now = datetime.now(UTC)
-window_start = now
-window_end = now + timedelta(minutes=720)
-
-# Pass two values to charge_per_interval instead of one.
-usage_plan = wt_opt.get_optimal_usage_plan(
- region="CAISO_NORTH",
- usage_window_start=window_start,
- usage_window_end=window_end,
- usage_time_required_minutes=120, # 80 + 40
- usage_power_kw=12,
- charge_per_interval=[80,40],
- optimization_method="auto",
-)
-
-print(usage_plan.head())
-print(usage_plan["usage"].tolist())
-print(usage_plan.sum())
-```
-
-**Compressor**:
- * Contiguous (multiple periods, variable length) - runs 120 minutes over the next 12 hours; each cycle needs to be at least 20 minutes long, and any number of intervals (from one to six) is okay.
-
-```py
-## Compressor - needs to run 120 minutes over the next 12 hours; each cycle needs to be at least 20 minutes long, and any number of contiguous intervals (from one to six) is okay.
-
-from datetime import datetime, timedelta
-import pandas as pd
-from pytz import UTC
-from watttime import WattTimeOptimizer
-import os
-
-username = os.getenv("WATTTIME_USER")
-password = os.getenv("WATTTIME_PASSWORD")
-wt_opt = WattTimeOptimizer(username, password)
-
-# Suppose that the time now is 12 midnight
-now = datetime.now(UTC)
-window_start = now
-window_end = now + timedelta(minutes=720)
-
-usage_plan = wt_opt.get_optimal_usage_plan(
- region="CAISO_NORTH",
- usage_window_start=window_start,
- usage_window_end=window_end,
- usage_time_required_minutes=120,
- usage_power_kw=12,
- # Here _None_ implies that there is no upper bound, and replacing None by 120 would have the exact same effect.
- charge_per_interval=[(20,None),(20,None),(20,None),(20,None),(20,None),(20,None)],
- optimization_method="auto",
- use_all_intervals=False
-)
-
-print(usage_plan.head())
-print(usage_plan["usage"].tolist())
-print(usage_plan.sum())
-```
\ No newline at end of file
+Insert link to Optimizer README.md and add a brief description of functionality
\ No newline at end of file
diff --git a/_example_synthetic_data.ipynb b/_example_synthetic_data.ipynb
deleted file mode 100644
index 8d36e686..00000000
--- a/_example_synthetic_data.ipynb
+++ /dev/null
@@ -1,832 +0,0 @@
-{
- "cells": [
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "# Synthetic Data for Testing"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 3,
- "metadata": {},
- "outputs": [],
- "source": [
- "from watttime.evaluator.sessions import SessionsGenerator"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "## Example: At home EV charging\n",
- "\n",
- "- Covers a 5.5 - 8.5 hour variable length window\n",
- "- The vehicle has a BMW and has an average power draw of 42.5\n",
- "- Battery is usually typically 50% charged at plug in time.\n",
- "- Charging occurs during the workday"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 4,
- "metadata": {},
- "outputs": [],
- "source": [
- "ev_kwargs = {\n",
- " \"max_power_output_rates\": [42.5],\n",
- " \"max_percent_capacity\": 0.95, # highest level of charge achieved by battery\n",
- " \"power_output_efficiency\": 0.75, # power loss. 1 = no power loss.\n",
- " \"minimum_battery_starting_capacity\": 0.2, # minimum starting percent charged\n",
- " \"minimum_usage_window_start_time\": \"08:00:00\", # session can start as early as 8am\n",
- " \"maximum_usage_window_start_time\": \"22:00:00\", # session can start as late as 9pm\n",
- "}"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 5,
- "metadata": {},
- "outputs": [],
- "source": [
- "s_ev = SessionsGenerator(**ev_kwargs)"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "Now we can generate synthetic data for users and devices with the attributes set above. The synthetic data creates one example per day."
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 6,
- "metadata": {},
- "outputs": [],
- "source": [
- "# the class has a helper function to generate a random list of unique dates\n",
- "distinct_date_list = s_ev.assign_random_dates(years=[2025])"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "You can generate data from a single user."
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 7,
- "metadata": {},
- "outputs": [
- {
- "data": {
- "text/html": [
- "
\n",
- "\n",
- "
\n",
- " \n",
- " \n",
- " \n",
- " 0 \n",
- " \n",
- " \n",
- " \n",
- " \n",
- " distinct_dates \n",
- " 2025-01-06 \n",
- " \n",
- " \n",
- " user_type \n",
- " r27.7525_tc55_avglc25812_sdlc7930 \n",
- " \n",
- " \n",
- " usage_window_start \n",
- " 2025-01-06 19:15:00 \n",
- " \n",
- " \n",
- " usage_window_end \n",
- " 2025-01-07 03:30:00 \n",
- " \n",
- " \n",
- " initial_charge \n",
- " 0.607095 \n",
- " \n",
- " \n",
- " usage_time_required_in_minutes \n",
- " 40.774277 \n",
- " \n",
- " \n",
- " expected_baseline_charge_complete_timestamp \n",
- " 2025-01-06 19:55:46.456627530 \n",
- " \n",
- " \n",
- " window_length_in_minutes \n",
- " 495.0 \n",
- " \n",
- " \n",
- " final_charge_time \n",
- " 2025-01-06 19:55:46.456627530 \n",
- " \n",
- " \n",
- " total_capacity \n",
- " 55 \n",
- " \n",
- " \n",
- " usage_power_kw \n",
- " 27.7525 \n",
- " \n",
- " \n",
- " total_intervals_plugged_in \n",
- " 99.0 \n",
- " \n",
- " \n",
- " MWh_fraction \n",
- " 0.002313 \n",
- " \n",
- " \n",
- " early_session_stop \n",
- " False \n",
- " \n",
- " \n",
- "
\n",
- "
"
- ],
- "text/plain": [
- " 0\n",
- "distinct_dates 2025-01-06\n",
- "user_type r27.7525_tc55_avglc25812_sdlc7930\n",
- "usage_window_start 2025-01-06 19:15:00\n",
- "usage_window_end 2025-01-07 03:30:00\n",
- "initial_charge 0.607095\n",
- "usage_time_required_in_minutes 40.774277\n",
- "expected_baseline_charge_complete_timestamp 2025-01-06 19:55:46.456627530\n",
- "window_length_in_minutes 495.0\n",
- "final_charge_time 2025-01-06 19:55:46.456627530\n",
- "total_capacity 55\n",
- "usage_power_kw 27.7525\n",
- "total_intervals_plugged_in 99.0\n",
- "MWh_fraction 0.002313\n",
- "early_session_stop False"
- ]
- },
- "execution_count": 7,
- "metadata": {},
- "output_type": "execute_result"
- }
- ],
- "source": [
- "s_ev.synthetic_user_data(distinct_date_list=[distinct_date_list[0]]).T"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "Or for multiple users."
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 8,
- "metadata": {},
- "outputs": [
- {
- "name": "stderr",
- "output_type": "stream",
- "text": [
- "100%|██████████| 10/10 [00:00<00:00, 344.44it/s]\n"
- ]
- },
- {
- "data": {
- "text/html": [
- "\n",
- "\n",
- "
\n",
- " \n",
- " \n",
- " \n",
- " index \n",
- " distinct_dates \n",
- " user_type \n",
- " usage_window_start \n",
- " usage_window_end \n",
- " initial_charge \n",
- " usage_time_required_in_minutes \n",
- " expected_baseline_charge_complete_timestamp \n",
- " window_length_in_minutes \n",
- " final_charge_time \n",
- " total_capacity \n",
- " usage_power_kw \n",
- " total_intervals_plugged_in \n",
- " MWh_fraction \n",
- " early_session_stop \n",
- " \n",
- " \n",
- " \n",
- " \n",
- " 0 \n",
- " 0 \n",
- " 2025-01-06 \n",
- " r28.05_tc74_avglc25436_sdlc7172 \n",
- " 2025-01-06 09:35:00 \n",
- " 2025-01-06 17:30:00 \n",
- " 0.592768 \n",
- " 56.545822 \n",
- " 2025-01-06 10:31:32.749326972 \n",
- " 475.0 \n",
- " 2025-01-06 10:31:32.749326972 \n",
- " 74 \n",
- " 28.0500 \n",
- " 95.0 \n",
- " 0.002337 \n",
- " False \n",
- " \n",
- " \n",
- " 1 \n",
- " 0 \n",
- " 2025-01-06 \n",
- " r31.7475_tc22_avglc29817_sdlc7098 \n",
- " 2025-01-06 14:35:00 \n",
- " 2025-01-07 00:55:00 \n",
- " 0.603506 \n",
- " 14.406544 \n",
- " 2025-01-06 14:49:24.392666208 \n",
- " 620.0 \n",
- " 2025-01-06 14:49:24.392666208 \n",
- " 22 \n",
- " 31.7475 \n",
- " 124.0 \n",
- " 0.002646 \n",
- " False \n",
- " \n",
- " \n",
- " 2 \n",
- " 0 \n",
- " 2025-01-06 \n",
- " r32.81_tc36_avglc27775_sdlc7141 \n",
- " 2025-01-06 18:45:00 \n",
- " 2025-01-07 03:35:00 \n",
- " 0.482781 \n",
- " 30.758670 \n",
- " 2025-01-06 19:15:45.520208802 \n",
- " 530.0 \n",
- " 2025-01-06 19:15:45.520208802 \n",
- " 36 \n",
- " 32.8100 \n",
- " 106.0 \n",
- " 0.002734 \n",
- " False \n",
- " \n",
- " \n",
- " 3 \n",
- " 0 \n",
- " 2025-01-06 \n",
- " r27.37_tc32_avglc24666_sdlc7235 \n",
- " 2025-01-06 14:00:00 \n",
- " 2025-01-06 23:35:00 \n",
- " 0.470005 \n",
- " 33.671528 \n",
- " 2025-01-06 14:33:40.291679670 \n",
- " 575.0 \n",
- " 2025-01-06 14:33:40.291679670 \n",
- " 32 \n",
- " 27.3700 \n",
- " 115.0 \n",
- " 0.002281 \n",
- " False \n",
- " \n",
- " \n",
- " 4 \n",
- " 0 \n",
- " 2025-01-06 \n",
- " r29.919999999999998_tc88_avglc29832_sdlc7032 \n",
- " 2025-01-06 21:25:00 \n",
- " 2025-01-07 09:55:00 \n",
- " 0.320038 \n",
- " 111.169706 \n",
- " 2025-01-06 23:16:10.182363414 \n",
- " 750.0 \n",
- " 2025-01-06 23:16:10.182363414 \n",
- " 88 \n",
- " 29.9200 \n",
- " 150.0 \n",
- " 0.002493 \n",
- " False \n",
- " \n",
- " \n",
- " 5 \n",
- " 0 \n",
- " 2025-01-06 \n",
- " r25.5425_tc36_avglc28647_sdlc6820 \n",
- " 2025-01-06 21:15:00 \n",
- " 2025-01-07 07:15:00 \n",
- " 0.554208 \n",
- " 33.470132 \n",
- " 2025-01-06 21:48:28.207902324 \n",
- " 600.0 \n",
- " 2025-01-06 21:48:28.207902324 \n",
- " 36 \n",
- " 25.5425 \n",
- " 120.0 \n",
- " 0.002129 \n",
- " False \n",
- " \n",
- " \n",
- " 6 \n",
- " 0 \n",
- " 2025-01-06 \n",
- " r30.09_tc77_avglc22791_sdlc7917 \n",
- " 2025-01-06 19:30:00 \n",
- " 2025-01-07 01:15:00 \n",
- " 0.718115 \n",
- " 35.603536 \n",
- " 2025-01-06 20:05:36.212165958 \n",
- " 345.0 \n",
- " 2025-01-06 20:05:36.212165958 \n",
- " 77 \n",
- " 30.0900 \n",
- " 69.0 \n",
- " 0.002507 \n",
- " False \n",
- " \n",
- " \n",
- " 7 \n",
- " 0 \n",
- " 2025-01-06 \n",
- " r31.365_tc113_avglc26690_sdlc7426 \n",
- " 2025-01-06 20:55:00 \n",
- " 2025-01-07 02:35:00 \n",
- " 0.462890 \n",
- " 105.295837 \n",
- " 2025-01-06 22:40:17.750211624 \n",
- " 340.0 \n",
- " 2025-01-06 22:40:17.750211624 \n",
- " 113 \n",
- " 31.3650 \n",
- " 68.0 \n",
- " 0.002614 \n",
- " False \n",
- " \n",
- " \n",
- " 8 \n",
- " 0 \n",
- " 2025-01-06 \n",
- " r31.28_tc116_avglc24040_sdlc7283 \n",
- " 2025-01-06 19:40:00 \n",
- " 2025-01-07 04:40:00 \n",
- " 0.336079 \n",
- " 136.601402 \n",
- " 2025-01-06 21:56:36.084147294 \n",
- " 540.0 \n",
- " 2025-01-06 21:56:36.084147294 \n",
- " 116 \n",
- " 31.2800 \n",
- " 108.0 \n",
- " 0.002607 \n",
- " False \n",
- " \n",
- " \n",
- " 9 \n",
- " 0 \n",
- " 2025-01-06 \n",
- " r36.975_tc32_avglc23053_sdlc7599 \n",
- " 2025-01-06 21:05:00 \n",
- " 2025-01-07 05:30:00 \n",
- " 0.672352 \n",
- " 14.417431 \n",
- " 2025-01-06 21:19:25.045830924 \n",
- " 505.0 \n",
- " 2025-01-06 21:19:25.045830924 \n",
- " 32 \n",
- " 36.9750 \n",
- " 101.0 \n",
- " 0.003081 \n",
- " False \n",
- " \n",
- " \n",
- "
\n",
- "
"
- ],
- "text/plain": [
- " index distinct_dates user_type \\\n",
- "0 0 2025-01-06 r28.05_tc74_avglc25436_sdlc7172 \n",
- "1 0 2025-01-06 r31.7475_tc22_avglc29817_sdlc7098 \n",
- "2 0 2025-01-06 r32.81_tc36_avglc27775_sdlc7141 \n",
- "3 0 2025-01-06 r27.37_tc32_avglc24666_sdlc7235 \n",
- "4 0 2025-01-06 r29.919999999999998_tc88_avglc29832_sdlc7032 \n",
- "5 0 2025-01-06 r25.5425_tc36_avglc28647_sdlc6820 \n",
- "6 0 2025-01-06 r30.09_tc77_avglc22791_sdlc7917 \n",
- "7 0 2025-01-06 r31.365_tc113_avglc26690_sdlc7426 \n",
- "8 0 2025-01-06 r31.28_tc116_avglc24040_sdlc7283 \n",
- "9 0 2025-01-06 r36.975_tc32_avglc23053_sdlc7599 \n",
- "\n",
- " usage_window_start usage_window_end initial_charge \\\n",
- "0 2025-01-06 09:35:00 2025-01-06 17:30:00 0.592768 \n",
- "1 2025-01-06 14:35:00 2025-01-07 00:55:00 0.603506 \n",
- "2 2025-01-06 18:45:00 2025-01-07 03:35:00 0.482781 \n",
- "3 2025-01-06 14:00:00 2025-01-06 23:35:00 0.470005 \n",
- "4 2025-01-06 21:25:00 2025-01-07 09:55:00 0.320038 \n",
- "5 2025-01-06 21:15:00 2025-01-07 07:15:00 0.554208 \n",
- "6 2025-01-06 19:30:00 2025-01-07 01:15:00 0.718115 \n",
- "7 2025-01-06 20:55:00 2025-01-07 02:35:00 0.462890 \n",
- "8 2025-01-06 19:40:00 2025-01-07 04:40:00 0.336079 \n",
- "9 2025-01-06 21:05:00 2025-01-07 05:30:00 0.672352 \n",
- "\n",
- " usage_time_required_in_minutes expected_baseline_charge_complete_timestamp \\\n",
- "0 56.545822 2025-01-06 10:31:32.749326972 \n",
- "1 14.406544 2025-01-06 14:49:24.392666208 \n",
- "2 30.758670 2025-01-06 19:15:45.520208802 \n",
- "3 33.671528 2025-01-06 14:33:40.291679670 \n",
- "4 111.169706 2025-01-06 23:16:10.182363414 \n",
- "5 33.470132 2025-01-06 21:48:28.207902324 \n",
- "6 35.603536 2025-01-06 20:05:36.212165958 \n",
- "7 105.295837 2025-01-06 22:40:17.750211624 \n",
- "8 136.601402 2025-01-06 21:56:36.084147294 \n",
- "9 14.417431 2025-01-06 21:19:25.045830924 \n",
- "\n",
- " window_length_in_minutes final_charge_time total_capacity \\\n",
- "0 475.0 2025-01-06 10:31:32.749326972 74 \n",
- "1 620.0 2025-01-06 14:49:24.392666208 22 \n",
- "2 530.0 2025-01-06 19:15:45.520208802 36 \n",
- "3 575.0 2025-01-06 14:33:40.291679670 32 \n",
- "4 750.0 2025-01-06 23:16:10.182363414 88 \n",
- "5 600.0 2025-01-06 21:48:28.207902324 36 \n",
- "6 345.0 2025-01-06 20:05:36.212165958 77 \n",
- "7 340.0 2025-01-06 22:40:17.750211624 113 \n",
- "8 540.0 2025-01-06 21:56:36.084147294 116 \n",
- "9 505.0 2025-01-06 21:19:25.045830924 32 \n",
- "\n",
- " usage_power_kw total_intervals_plugged_in MWh_fraction \\\n",
- "0 28.0500 95.0 0.002337 \n",
- "1 31.7475 124.0 0.002646 \n",
- "2 32.8100 106.0 0.002734 \n",
- "3 27.3700 115.0 0.002281 \n",
- "4 29.9200 150.0 0.002493 \n",
- "5 25.5425 120.0 0.002129 \n",
- "6 30.0900 69.0 0.002507 \n",
- "7 31.3650 68.0 0.002614 \n",
- "8 31.2800 108.0 0.002607 \n",
- "9 36.9750 101.0 0.003081 \n",
- "\n",
- " early_session_stop \n",
- "0 False \n",
- "1 False \n",
- "2 False \n",
- "3 False \n",
- "4 False \n",
- "5 False \n",
- "6 False \n",
- "7 False \n",
- "8 False \n",
- "9 False "
- ]
- },
- "execution_count": 8,
- "metadata": {},
- "output_type": "execute_result"
- }
- ],
- "source": [
- "s_ev.generate_synthetic_dataset(distinct_date_list=[distinct_date_list[0]], number_of_users=10)"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "## Example: AI Model Training\n",
- "- Model training can occur at any time of day\n",
- "- There are 3 server models that consume 240, 310, and 640 watt-hour on average\n",
- "- Early stopping is not an option"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 9,
- "metadata": {},
- "outputs": [],
- "source": [
- "ai_kwargs = {\n",
- " \"max_percent_capacity\":1.0, # job must run to completion\n",
- " \"max_power_output_rates\": [24,31,64], # assuming a bare metal usecase, k8s or vm rescale to vCPU\n",
- " \"minimum_usage_window_start_time\": \"00:00:00\", # earliest session can start\n",
- " \"maximum_usage_window_start_time\": \"23:59:00\", # latest session can start\n",
- "}"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 10,
- "metadata": {},
- "outputs": [],
- "source": [
- "s_ai = SessionsGenerator(**ai_kwargs)"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 11,
- "metadata": {},
- "outputs": [
- {
- "name": "stderr",
- "output_type": "stream",
- "text": [
- "100%|██████████| 10/10 [00:00<00:00, 272.29it/s]\n"
- ]
- }
- ],
- "source": [
- "df_ai = s_ai.generate_synthetic_dataset(distinct_date_list=distinct_date_list, number_of_users=10)"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "# Optimization"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 12,
- "metadata": {},
- "outputs": [],
- "source": [
- "import os\n",
- "import numpy as np\n",
- "from watttime.evaluator.evaluator import OptChargeEvaluator\n",
- "from watttime.evaluator.evaluator import ImpactEvaluator\n",
- "from watttime.evaluator.evaluator import RecalculationOptChargeEvaluator"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "username = os.getenv(\"WATTTIME_USER\")\n",
- "password = os.getenv(\"WATTTIME_PASSWORD\")\n",
- "region = \"CAISO_NORTH\"\n",
- "oce = OptChargeEvaluator(username=username,password=password)\n",
- "roce = RecalculationOptChargeEvaluator(username,password)\n",
- "\n",
- "# single instance\n",
- "df_ev_sample = s_ev.synthetic_user_data(distinct_date_list=[distinct_date_list[0]])\n",
- "df_ev_sample = df_ev_sample.rename({\"usage_time_required_in_minutes\":\"time_needed\"},axis=1)"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 15,
- "metadata": {},
- "outputs": [],
- "source": [
- "%%capture\n",
- "input_dict = df_ev_sample[['usage_window_start',\n",
- " 'usage_window_end',\n",
- " 'time_needed',\n",
- " 'usage_power_kw'\n",
- " ]].T.to_dict()\n",
- "\n",
- "value = input_dict[0]\n",
- "value.update({'region':region,'tz_convert':True, \"verbose\":False})"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 16,
- "metadata": {},
- "outputs": [
- {
- "data": {
- "text/plain": [
- "{'usage_window_start': Timestamp('2025-01-06 19:15:00'),\n",
- " 'usage_window_end': Timestamp('2025-01-07 02:55:00'),\n",
- " 'time_needed': 51.67684008874637,\n",
- " 'usage_power_kw': 37.4425,\n",
- " 'region': 'CAISO_NORTH',\n",
- " 'tz_convert': True,\n",
- " 'verbose': False}"
- ]
- },
- "execution_count": 16,
- "metadata": {},
- "output_type": "execute_result"
- }
- ],
- "source": [
- "value"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 17,
- "metadata": {},
- "outputs": [],
- "source": [
- "df = oce.get_schedule_and_cost_api(**value)\n",
- "r = ImpactEvaluator(username,password,df).get_all_emissions_values(region=region)"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 18,
- "metadata": {},
- "outputs": [
- {
- "data": {
- "text/plain": [
- "{'baseline': 31.979059944544282,\n",
- " 'forecast': 29.951449198552897,\n",
- " 'actual': 30.192892487355493}"
- ]
- },
- "execution_count": 18,
- "metadata": {},
- "output_type": "execute_result"
- }
- ],
- "source": [
- "r"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "# Requery"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 19,
- "metadata": {},
- "outputs": [],
- "source": [
- "value.update({\"optimization_method\": \"simple\", \"interval\":15, \"charge_per_interval\":None})"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 20,
- "metadata": {},
- "outputs": [
- {
- "data": {
- "text/plain": [
- "{'usage_window_start': Timestamp('2025-01-06 19:15:00'),\n",
- " 'usage_window_end': Timestamp('2025-01-07 02:55:00'),\n",
- " 'time_needed': 51.67684008874637,\n",
- " 'usage_power_kw': 37.4425,\n",
- " 'region': 'CAISO_NORTH',\n",
- " 'tz_convert': True,\n",
- " 'verbose': False,\n",
- " 'optimization_method': 'simple',\n",
- " 'interval': 15,\n",
- " 'charge_per_interval': None}"
- ]
- },
- "execution_count": 20,
- "metadata": {},
- "output_type": "execute_result"
- }
- ],
- "source": [
- "value"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "%%timeit\n",
- "df_requery = roce.fit_recalculator(**value)\n",
- "r_requery = ImpactEvaluator(username,password,df_requery).get_all_emissions_values(region=region)"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "# Helpful Functions"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "# 4 seconds per row, mostly API call\n",
- "def analysis_loop(region, input_dict):\n",
- " results = {}\n",
- " for key,value in input_dict.items():\n",
- " value.update({'region':region,'tz_convert':True, \"verbose\":False})\n",
- " df = oce.get_schedule_and_cost_api(**value)\n",
- " m, b = np.polyfit(np.arange(len(df.pred_moer.values)),df.pred_moer.values, 1)\n",
- " stddev = df.pred_moer.std()\n",
- " r = ImpactEvaluator(username,password,df).get_all_emissions_values(region=region)\n",
- " r.update({'m':m,'b':b,'stddev':stddev})\n",
- " results.update({key:r})\n",
- " return results\n",
- "\n",
- "# 4 seconds per row, mostly API call\n",
- "def analysis_loop_requery(region, input_dict, interval):\n",
- " results = {}\n",
- " for key,value in tqdm.tqdm(input_dict.items()):\n",
- " value.update(\n",
- " {'region':region,\n",
- " 'tz_convert':True, \n",
- " \"optimization_method\": \"simple\", \n",
- " \"verbose\":False,\n",
- " \"interval\":interval,\n",
- " \"charge_per_interval\":None}\n",
- " )\n",
- " df = roce.fit_recalculator(**value)\n",
- " m, b = np.polyfit(np.arange(len(df.pred_moer.values)),df.pred_moer.values, 1)\n",
- " stddev = df.pred_moer.std()\n",
- " r = ImpactEvaluator(username,password,df).get_all_emissions_values(region=region)\n",
- " r.update({'m':m,'b':b,'stddev':stddev})\n",
- " results.update({key:r})\n",
- " return results"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "df_many_sessions = s_ev.generate_synthetic_dataset(distinct_date_list=[distinct_date_list[0]], number_of_users=10)\n",
- "\n",
- "input_dict = df_many_sessions[['usage_window_start',\n",
- " 'usage_window_end',\n",
- " 'time_needed',\n",
- " 'usage_power_kw'\n",
- " ]].T.to_dict()\n",
- "\n",
- "# no requery\n",
- "df_many_sessions_optimized = analysis_loop(region, input_dict)\n",
- "\n",
- "# requery\n",
- "df_many_sessions_optimized_requery = analysis_loop_requery(region, input_dict, 45)\n",
- "\n",
- "# pd.concat([df_many_sessions, df_many_sessions_optimized]) or pd.concat([df_many_sessions, df_many_sessions_optimized_requery])"
- ]
- }
- ],
- "metadata": {
- "kernelspec": {
- "display_name": "watttime",
- "language": "python",
- "name": "python3"
- },
- "language_info": {
- "codemirror_mode": {
- "name": "ipython",
- "version": 3
- },
- "file_extension": ".py",
- "mimetype": "text/x-python",
- "name": "python",
- "nbconvert_exporter": "python",
- "pygments_lexer": "ipython3",
- "version": "3.9.21"
- }
- },
- "nbformat": 4,
- "nbformat_minor": 2
-}
diff --git a/setup.py b/setup.py
index 86ba5e47..d5823dac 100644
--- a/setup.py
+++ b/setup.py
@@ -6,7 +6,7 @@
long_description=open('README.md').read(),
long_description_content_type="text/markdown",
version="v1.2.1",
- packages=["watttime"],
+ packages=["watttime","watttime_optimizer"],
python_requires=">=3.8",
- install_requires=["requests", "pandas>1.0.0", "python-dateutil"],
+ install_requires=["requests", "pandas>1.0.0", "python-dateutil","tqdm"],
)
diff --git a/tests/test_battery.py b/tests/test_battery.py
new file mode 100644
index 00000000..505a0453
--- /dev/null
+++ b/tests/test_battery.py
@@ -0,0 +1,16 @@
+from watttime_optimizer.battery import Battery, CARS
+import pandas as pd
+
+tesla_charging_curve = pd.DataFrame(
+ columns=["SoC", "kW"],
+ data = CARS['tesla']
+ )
+
+capacity_kWh = 70
+initial_soc = .50
+
+batt = Battery(tesla_charging_curve)
+
+df = batt.get_usage_power_kw_df(capacity_kWh=capacity_kWh, initial_soc=initial_soc)
+
+print(df.head())
\ No newline at end of file
diff --git a/tests/test_optimizer.py b/tests/test_optimizer.py
index 92927895..597363c1 100644
--- a/tests/test_optimizer.py
+++ b/tests/test_optimizer.py
@@ -3,7 +3,7 @@
import unittest
import pandas as pd
from pytz import UTC
-from watttime.api import WattTimeOptimizer
+from watttime_optimizer import WattTimeOptimizer
REGION = "CAISO_NORTH"
@@ -151,7 +151,7 @@ def test_dp_fixed_power_rate_with_uncertainty(self):
optimization_method="sophisticated",
)
print("Using DP Plan w/ fixed power rate and charging uncertainty")
- print(usage_plan["emissions_co2e_lb"].sum())
+ print(usage_plan["emissions_co2_lb"].sum())
# Check time required
self.assertAlmostEqual(usage_plan["usage"].sum(), 240)
@@ -179,7 +179,7 @@ def test_dp_variable_power_rate(self):
optimization_method="auto",
)
print("Using DP Plan w/ variable power rate")
- print(usage_plan["emissions_co2e_lb"].sum())
+ print(usage_plan["emissions_co2_lb"].sum())
# Check time required
self.assertAlmostEqual(usage_plan["usage"].sum(), 320)
@@ -263,19 +263,19 @@ def test_dp_input_constant_power_energy(self):
usage_plan["energy_usage_mwh"].sum() * 1000, 180 * 5 / 60
)
- def test_dp_two_intervals_unbounded(self):
- """Test auto mode with two intervals."""
+ def test_dp_two_segments_unbounded(self):
+ """Test auto mode with two segments."""
usage_plan = self.wt_opt.get_optimal_usage_plan(
region=self.region,
usage_window_start=self.window_start_test,
usage_window_end=self.window_end_test,
usage_time_required_minutes=160,
usage_power_kw=self.usage_power_kw,
- charge_per_interval=[(0, 999999), (0, 999999)],
+ charge_per_segment=[(0, 999999), (0, 999999)],
optimization_method="auto",
)
print(
- "Using auto mode with two unbounded intervals\n",
+ "Using auto mode with two unbounded segments\n",
pretty_format_usage(usage_plan),
)
print(usage_plan.sum())
@@ -293,19 +293,19 @@ def test_dp_two_intervals_unbounded(self):
# Check number of components
self.assertLessEqual(len(get_contiguity_info(usage_plan)), 2)
- def test_dp_two_intervals_flexible_length(self):
- """Test auto mode with two variable length intervals."""
+ def test_dp_two_segments_flexible_length(self):
+ """Test auto mode with two variable length segments."""
usage_plan = self.wt_opt.get_optimal_usage_plan(
region=self.region,
usage_window_start=self.window_start_test,
usage_window_end=self.window_end_test,
usage_time_required_minutes=160,
usage_power_kw=self.usage_power_kw,
- charge_per_interval=[(60, 100), (60, 100)],
+ charge_per_segment=[(60, 100), (60, 100)],
optimization_method="auto",
)
print(
- "Using auto mode with two flexible intervals\n",
+ "Using auto mode with two flexible segments\n",
pretty_format_usage(usage_plan),
)
print(usage_plan.sum())
@@ -335,19 +335,19 @@ def test_dp_two_intervals_flexible_length(self):
# Check combined component length
self.assertAlmostEqual(contiguity_info[0]["sum"], 160)
- def test_dp_two_intervals_one_sided_length(self):
- """Test auto mode with two variable length intervals."""
+ def test_dp_two_segments_one_sided_length(self):
+ """Test auto mode with two variable length segments."""
usage_plan = self.wt_opt.get_optimal_usage_plan(
region=self.region,
usage_window_start=self.window_start_test,
usage_window_end=self.window_end_test,
usage_time_required_minutes=160,
usage_power_kw=self.usage_power_kw,
- charge_per_interval=[(30, None), (30, None), (30, None), (30, None)],
+ charge_per_segment=[(30, None), (30, None), (30, None), (30, None)],
optimization_method="auto",
)
print(
- "Using auto mode with one-sided intervals\n",
+ "Using auto mode with one-sided segments\n",
pretty_format_usage(usage_plan),
)
print(usage_plan.sum())
@@ -370,20 +370,20 @@ def test_dp_two_intervals_one_sided_length(self):
# Check component length
self.assertGreaterEqual(contiguity_info[i]["sum"], 30)
- def test_dp_two_intervals_one_sided_length_use_all_false(self):
- """Test auto mode with two variable length intervals."""
+ def test_dp_two_segments_one_sided_length_use_all_false(self):
+ """Test auto mode with two variable length segments."""
usage_plan = self.wt_opt.get_optimal_usage_plan(
region=self.region,
usage_window_start=self.window_start_test,
usage_window_end=self.window_end_test,
usage_time_required_minutes=160,
usage_power_kw=self.usage_power_kw,
- charge_per_interval=[(40, None), (40, None), (40, None), (40, None)],
- use_all_intervals=False,
+ charge_per_segment=[(40, None), (40, None), (40, None), (40, None)],
+ use_all_segments=False,
optimization_method="auto",
)
print(
- "Using auto mode with one-sided intervals\n",
+ "Using auto mode with one-sided segments\n",
pretty_format_usage(usage_plan),
)
print(usage_plan.sum())
@@ -406,19 +406,19 @@ def test_dp_two_intervals_one_sided_length_use_all_false(self):
# Check component length
self.assertGreaterEqual(contiguity_info[i]["sum"], 40)
- def test_dp_two_intervals_exact_input_a(self):
- """Test auto mode with two intervals."""
+ def test_dp_two_segments_exact_input_a(self):
+ """Test auto mode with two segments."""
usage_plan = self.wt_opt.get_optimal_usage_plan(
region=self.region,
usage_window_start=self.window_start_test,
usage_window_end=self.window_end_test,
usage_time_required_minutes=160,
usage_power_kw=self.usage_power_kw,
- charge_per_interval=[(60, 60), (100, 100)],
+ charge_per_segment=[(60, 60), (100, 100)],
optimization_method="auto",
)
print(
- "Using auto mode with two exact intervals\n",
+ "Using auto mode with two exact segments\n",
pretty_format_usage(usage_plan),
)
print(usage_plan.sum())
@@ -446,18 +446,18 @@ def test_dp_two_intervals_exact_input_a(self):
# Check combined component length
self.assertAlmostEqual(contiguity_info[0]["sum"], 160)
- def test_dp_two_intervals_exact_input_b(self):
- """Test auto mode with two intervals."""
+ def test_dp_two_segments_exact_input_b(self):
+ """Test auto mode with two segments."""
usage_plan = self.wt_opt.get_optimal_usage_plan(
region=self.region,
usage_window_start=self.window_start_test,
usage_window_end=self.window_end_test,
usage_time_required_minutes=160,
usage_power_kw=self.usage_power_kw,
- charge_per_interval=[60, 100],
+ charge_per_segment=[60, 100],
optimization_method="auto",
)
- print("Using auto mode, but with two intervals")
+ print("Using auto mode, but with two segments")
print(pretty_format_usage(usage_plan))
print(usage_plan.sum())
@@ -484,19 +484,19 @@ def test_dp_two_intervals_exact_input_b(self):
# Check combined component length
self.assertAlmostEqual(contiguity_info[0]["sum"], 160)
- def test_dp_two_intervals_exact_unround(self):
- """Test auto mode with two intervals, specified via list of tuple."""
+ def test_dp_two_segments_exact_unround(self):
+ """Test auto mode with two segments, specified via list of tuple."""
usage_plan = self.wt_opt.get_optimal_usage_plan(
region=self.region,
usage_window_start=self.window_start_test,
usage_window_end=self.window_end_test,
usage_time_required_minutes=160,
usage_power_kw=self.usage_power_kw,
- charge_per_interval=[(67, 67), (93, 93)],
+ charge_per_segment=[(67, 67), (93, 93)],
optimization_method="auto",
)
print(
- "Using auto mode with two exact unround intervals\n",
+ "Using auto mode with two exact unround segments\n",
pretty_format_usage(usage_plan),
)
print(usage_plan.sum())
@@ -524,19 +524,19 @@ def test_dp_two_intervals_exact_unround(self):
# Check combined component length
self.assertAlmostEqual(contiguity_info[0]["sum"], 160)
- def test_dp_two_intervals_exact_unround_alternate_input(self):
- """Test auto mode with two intervals, specified via list of ints."""
+ def test_dp_two_segments_exact_unround_alternate_input(self):
+ """Test auto mode with two segments, specified via list of ints."""
usage_plan = self.wt_opt.get_optimal_usage_plan(
region=self.region,
usage_window_start=self.window_start_test,
usage_window_end=self.window_end_test,
usage_time_required_minutes=160,
usage_power_kw=self.usage_power_kw,
- charge_per_interval=[67, 93],
+ charge_per_segment=[67, 93],
optimization_method="auto",
)
print(
- "Using auto mode with two exact unround intervals\n",
+ "Using auto mode with two exact unround segments\n",
pretty_format_usage(usage_plan),
)
print(usage_plan.sum())
@@ -564,18 +564,18 @@ def test_dp_two_intervals_exact_unround_alternate_input(self):
# Check combined component length
self.assertAlmostEqual(contiguity_info[0]["sum"], 160)
- def test_dp_two_intervals_exact_inconsistent_b(self):
- """Test auto mode with one interval that is inconsistent with usage_time_required."""
+ def test_dp_two_segments_exact_inconsistent_b(self):
+ """Test auto mode with one segment that is inconsistent with usage_time_required."""
usage_plan = self.wt_opt.get_optimal_usage_plan(
region=self.region,
usage_window_start=self.window_start_test,
usage_window_end=self.window_end_test,
usage_time_required_minutes=160,
usage_power_kw=self.usage_power_kw,
- charge_per_interval=[(65, 65)],
+ charge_per_segment=[(65, 65)],
optimization_method="auto",
)
- print("Using auto mode, but with two intervals")
+ print("Using auto mode, but with two segments")
print(pretty_format_usage(usage_plan))
print(usage_plan.sum())
diff --git a/watttime/api.py b/watttime/api.py
index 6b74937c..bafaa150 100644
--- a/watttime/api.py
+++ b/watttime/api.py
@@ -10,9 +10,6 @@
import requests
from dateutil.parser import parse
from pytz import UTC, timezone
-from watttime.optimizer.alg import optCharger, moer
-from itertools import accumulate
-import bisect
class WattTimeBase:
@@ -545,415 +542,6 @@ def get_historical_forecast_pandas(
out = pd.concat([out, _df])
return out
-
-OPT_INTERVAL = 5
-MAX_PREDICTION_HOURS = 72
-
-
-class WattTimeOptimizer(WattTimeForecast):
- """
- This class inherits from WattTimeForecast, with additional methods to generate
- optimal usage plans for energy consumption based on various parameters and
- constraints.
-
- Additional Methods:
- --------
- get_optimal_usage_plan(region, usage_window_start, usage_window_end,
- usage_time_required_minutes, usage_power_kw,
- usage_time_uncertainty_minutes, optimization_method,
- moer_data_override)
- Generates an optimal usage plan for energy consumption.
- """
-
- OPT_INTERVAL = 5
- MAX_PREDICTION_HOURS = 72
- MAX_INT = 99999999999999999
-
- def get_optimal_usage_plan(
- self,
- region: str,
- usage_window_start: datetime,
- usage_window_end: datetime,
- usage_time_required_minutes: Optional[Union[int, float]] = None,
- usage_power_kw: Optional[Union[int, float, pd.DataFrame]] = None,
- energy_required_kwh: Optional[Union[int, float]] = None,
- usage_time_uncertainty_minutes: Optional[Union[int, float]] = 0,
- charge_per_interval: Optional[list] = None,
- use_all_intervals: bool = True,
- constraints: Optional[dict] = None,
- optimization_method: Optional[
- Literal["baseline", "simple", "sophisticated", "auto"]
- ] = "baseline",
- moer_data_override: Optional[pd.DataFrame] = None,
- verbose = True
- ) -> pd.DataFrame:
- """
- Generates an optimal usage plan for energy consumption based on given parameters.
-
- This method calculates the most efficient energy usage schedule within a specified
- time window, considering factors such as regional data, power requirements, and
- optimization methods.
-
- You should pass in exactly 2 of 3 parameters of (usage_time_required_minutes, usage_power_kw, energy_required_kwh)
-
- Parameters:
- -----------
- region : str
- The region for which forecast data is requested.
- usage_window_start : datetime
- Start time of the window when power consumption is allowed.
- usage_window_end : datetime
- End time of the window when power consumption is allowed.
- usage_time_required_minutes : Optional[Union[int, float]], default=None
- Required usage time in minutes.
- usage_power_kw : Optional[Union[int, float, pd.DataFrame]], default=None
- Power usage in kilowatts. Can be a constant value or a DataFrame for variable power.
- energy_required_kwh : Optional[Union[int, float]], default=None
- Energy required in kwh
- usage_time_uncertainty_minutes : Optional[Union[int, float]], default=0
- Uncertainty in usage time, in minutes.
- charge_per_interval : Optional[list], default=None
- Either a list of length-2 tuples representing minimium and maximum (inclusive) charging minutes per interval,
- or a list of ints representing both the min and max.
- use_all_intervals : Optional[bool], default=False
- If true, use all intervals provided by charge_per_interval; if false, can use the first few intervals and skip the rest.
- constraints : Optional[dict], default=None
- A dictionary containing contraints on how much usage must be used before the given time point
- optimization_method : Optional[Literal["baseline", "simple", "sophisticated", "auto"]], default="baseline"
- The method used for optimization.
- moer_data_override : Optional[pd.DataFrame], default=None
- Pre-generated MOER (Marginal Operating Emissions Rate) DataFrame, if available.
- verbose : default = True
- If false, suppresses print statements in the opt charger class.
-
- Returns:
- --------
- pd.DataFrame
- A DataFrame representing the optimal usage plan, including columns for
- predicted MOER, usage, CO2 emissions, and energy usage.
-
- Raises:
- -------
- AssertionError
- If input parameters do not meet specified conditions (e.g., timezone awareness,
- valid time ranges, supported optimization methods).
-
- Notes:
- ------
- - The method uses WattTime forecast data unless overridden by moer_data_override.
- - It supports various optimization methods and can handle both constant and variable power usage.
- - The resulting plan aims to minimize emissions while meeting the specified energy requirements.
- """
-
- def is_tz_aware(dt):
- return dt.tzinfo is not None and dt.tzinfo.utcoffset(dt) is not None
-
- def minutes_to_units(x, floor=False):
- '''Converts minutes to forecase intervals. Rounds UP by default.'''
- if x:
- if floor:
- return int(x // self.OPT_INTERVAL)
- else:
- return int(math.ceil(x / self.OPT_INTERVAL))
- return x
-
- assert is_tz_aware(usage_window_start), "Start time is not tz-aware"
- assert is_tz_aware(usage_window_end), "End time is not tz-aware"
-
- if constraints is None:
- constraints = {}
- else:
- # Convert constraints to a standardized format
- raw_constraints = constraints.copy()
- constraints = {}
-
- for (
- constraint_time_clock,
- constraint_usage_minutes,
- ) in raw_constraints.items():
- constraint_time_minutes = (
- constraint_time_clock - usage_window_start
- ).total_seconds() / 60
- constraint_time_units = minutes_to_units(constraint_time_minutes)
- constraint_usage_units = minutes_to_units(constraint_usage_minutes)
-
- constraints.update(
- {constraint_time_units: (constraint_usage_units, None)}
- )
-
- num_inputs = 0
- for input in (usage_time_required_minutes, usage_power_kw, energy_required_kwh):
- if input is not None:
- num_inputs += 1
- assert (
- num_inputs == 2
- ), "Exactly 2 of 3 inputs in (usage_time_required_minutes, usage_power_kw, energy_required_kwh) required"
- if usage_power_kw is None:
- usage_power_kw = energy_required_kwh / usage_time_required_minutes * 60
- print("Implied usage_power_kw =", usage_power_kw)
- if usage_time_required_minutes is None:
- if type(usage_power_kw) in (float, int) and type(energy_required_kwh) in (
- float,
- int,
- ):
- usage_time_required_minutes = energy_required_kwh / usage_power_kw * 60
- print("Implied usage time required =", usage_time_required_minutes)
- else:
- # TODO: Implement and test
- raise NotImplementedError(
- "When usage_time_required_minutes is None, only float or int usage_power_kw and energy_required_kwh is supported."
- )
-
- # Perform these checks if we are using live data
- if moer_data_override is None:
- datetime_now = datetime.now(UTC)
- assert (
- usage_window_end > datetime_now
- ), "Error, Window end is before current datetime"
- assert usage_window_end - datetime_now < timedelta(
- hours=self.MAX_PREDICTION_HOURS
- ), "End time is too far in the future"
- assert optimization_method in ("baseline", "simple", "sophisticated", "auto"), (
- "Unsupported optimization method:" + optimization_method
- )
- if moer_data_override is None:
- forecast_df = self.get_forecast_pandas(
- region=region,
- signal_type="co2_moer",
- horizon_hours=self.MAX_PREDICTION_HOURS,
- )
- else:
- forecast_df = moer_data_override.copy()
- forecast_df = forecast_df.set_index("point_time")
- forecast_df.index = pd.to_datetime(forecast_df.index)
-
- # relevant_forecast_df = forecast_df[usage_window_start:usage_window_end]
- relevant_forecast_df = forecast_df[forecast_df.index >= usage_window_start]
- relevant_forecast_df = relevant_forecast_df[
- relevant_forecast_df.index < usage_window_end
- ]
- relevant_forecast_df = relevant_forecast_df.rename(
- columns={"value": "pred_moer"}
- )
- result_df = relevant_forecast_df[["pred_moer"]]
- moer_values = relevant_forecast_df["pred_moer"].values
-
- m = moer.Moer(mu=moer_values)
-
- model = optCharger.OptCharger(verbose=verbose)
-
- total_charge_units = minutes_to_units(usage_time_required_minutes)
- if optimization_method in ("sophisticated", "auto"):
- # Give a buffer time equal to the uncertainty
- buffer_time = usage_time_uncertainty_minutes
- buffer_periods = minutes_to_units(buffer_time) if buffer_time else 0
- buffer_enforce_time = max(
- total_charge_units, len(moer_values) - buffer_periods
- )
- constraints.update({buffer_enforce_time: (total_charge_units, None)})
- else:
- assert (
- usage_time_uncertainty_minutes == 0
- ), "usage_time_uncertainty_minutes is only supported in optimization_method='sophisticated' or 'auto'"
-
- if type(usage_power_kw) in (int, float):
- # Convert to the MWh used in an optimization interval
- # expressed as a function to meet the parameter requirements for OptC function
- emission_multiplier_fn = (
- lambda sc, ec: float(usage_power_kw) * 0.001 * self.OPT_INTERVAL / 60.0
- )
- else:
- usage_power_kw = usage_power_kw.copy()
- # Resample usage power dataframe to an OPT_INTERVAL frequency
- usage_power_kw["time_step"] = usage_power_kw["time"] / self.OPT_INTERVAL
- usage_power_kw_new_index = pd.DataFrame(
- index=[float(x) for x in range(total_charge_units + 1)]
- )
- usage_power_kw = pd.merge_asof(
- usage_power_kw_new_index,
- usage_power_kw.set_index("time_step"),
- left_index=True,
- right_index=True,
- direction="backward",
- allow_exact_matches=True,
- )
-
- def emission_multiplier_fn(sc: float, ec: float) -> float:
- """
- Calculate the approximate mean power in the given time range,
- in units of MWh used per optimizer time unit.
-
- sc and ec are float values representing the start and end time of
- the time range, in optimizer time units.
- """
- value = (
- usage_power_kw[sc : max(sc, ec - 1e-12)]["power_kw"].mean()
- * 0.001
- * self.OPT_INTERVAL
- / 60.0
- )
- return value
-
- if charge_per_interval:
- # Handle the charge_per_interval input by converting it from minutes to units, rounding up
- converted_charge_per_interval = []
- for c in charge_per_interval:
- if isinstance(c, int):
- converted_charge_per_interval.append(minutes_to_units(c))
- else:
- assert (
- len(c) == 2
- ), "Length of tuples in charge_per_interval is not 2"
- interval_start_units = minutes_to_units(c[0]) if c[0] else 0
- interval_end_units = (
- minutes_to_units(c[1]) if c[1] else self.MAX_INT
- )
- converted_charge_per_interval.append(
- (interval_start_units, interval_end_units)
- )
- else:
- converted_charge_per_interval = None
- model.fit(
- total_charge=total_charge_units,
- total_time=len(moer_values),
- moer=m,
- constraints=constraints,
- charge_per_interval=converted_charge_per_interval,
- use_all_intervals=use_all_intervals,
- emission_multiplier_fn=emission_multiplier_fn,
- optimization_method=optimization_method,
- )
-
- optimizer_result = model.get_schedule()
- result_df = self._reconcile_constraints(
- optimizer_result,
- result_df,
- model,
- usage_time_required_minutes,
- charge_per_interval,
- )
-
- return result_df
-
- def _reconcile_constraints(
- self,
- optimizer_result,
- result_df,
- model,
- usage_time_required_minutes,
- charge_per_interval,
- ):
- # Make a copy of charge_per_interval if necessary
- if charge_per_interval is not None:
- charge_per_interval = charge_per_interval[::]
- for i in range(len(charge_per_interval)):
- if type(charge_per_interval[i]) == int:
- charge_per_interval[i] = (
- charge_per_interval[i],
- charge_per_interval[i],
- )
- assert len(charge_per_interval[i]) == 2
- processed_start = (
- charge_per_interval[i][0]
- if charge_per_interval[i][0] is not None
- else 0
- )
- processed_end = (
- charge_per_interval[i][1]
- if charge_per_interval[i][1] is not None
- else self.MAX_INT
- )
-
- charge_per_interval[i] = (processed_start, processed_end)
-
- if not charge_per_interval:
- # Handle case without charge_per_interval constraints
- total_usage_intervals = sum(optimizer_result)
- current_usage_intervals = 0
- usage_list = []
- for to_charge_binary in optimizer_result:
- current_usage_intervals += to_charge_binary
- if current_usage_intervals < total_usage_intervals:
- usage_list.append(to_charge_binary * float(self.OPT_INTERVAL))
- else:
- # Partial interval
- minutes_to_trim = (
- total_usage_intervals * self.OPT_INTERVAL
- - usage_time_required_minutes
- )
- usage_list.append(
- to_charge_binary * float(self.OPT_INTERVAL - minutes_to_trim)
- )
- result_df["usage"] = usage_list
- else:
- # Process charge_per_interval constraints
- result_df["usage"] = [
- x * float(self.OPT_INTERVAL) for x in optimizer_result
- ]
- usage = result_df["usage"].values
- sections = []
- interval_ids = model.get_interval_ids()
-
- def get_min_max_indices(lst, x):
- # Find the first occurrence of x
- min_index = lst.index(x)
- # Find the last occurrence of x
- max_index = len(lst) - 1 - lst[::-1].index(x)
- return min_index, max_index
-
- for interval_id in range(0, max(interval_ids) + 1):
- assert (
- interval_id in interval_ids
- ), "interval_id not found in interval_ids"
- sections.append(get_min_max_indices(interval_ids, interval_id))
-
- # Adjust sections to satisfy charge_per_interval constraints
- for i, (start, end) in enumerate(sections):
- section_usage = usage[start : end + 1]
- total_minutes = section_usage.sum()
-
- # Get the constraints for this section
- if isinstance(charge_per_interval[i], int):
- min_minutes, max_minutes = (
- charge_per_interval[i],
- charge_per_interval[i],
- )
- else:
- min_minutes, max_minutes = charge_per_interval[i]
-
- # Adjust the section to fit the constraints
- if total_minutes < min_minutes:
- raise ValueError(
- f"Cannot meet the minimum charging constraint of {min_minutes} minutes for section {i}."
- )
- elif total_minutes > max_minutes:
- # Reduce usage to fit within the max_minutes
- excess_minutes = total_minutes - max_minutes
- for j in range(len(section_usage)):
- if section_usage[j] > 0:
- reduction = min(section_usage[j], excess_minutes)
- section_usage[j] -= reduction
- excess_minutes -= reduction
- if excess_minutes <= 0:
- break
- usage[start : end + 1] = section_usage
- result_df["usage"] = usage
-
- # Recalculate these values approximately, based on the new "usage" column
- # Note: This is approximate since it assumes that
- # the charging emissions over time of the unrounded values are similar to the rounded values
- result_df["emissions_co2e_lb"] = (
- model.get_charging_emissions_over_time()
- * result_df["usage"]
- / self.OPT_INTERVAL
- )
- result_df["energy_usage_mwh"] = (
- model.get_energy_usage_over_time() * result_df["usage"] / self.OPT_INTERVAL
- )
-
- return result_df
-
-
class WattTimeMaps(WattTimeBase):
def get_maps_json(
self,
@@ -979,296 +567,4 @@ def get_maps_json(
params = {"signal_type": signal_type}
rsp = requests.get(url, headers=headers, params=params)
rsp.raise_for_status()
- return rsp.json()
-
-class WattTimeRecalculator:
- """A class to manage and update charging schedules over time.
-
- This class maintains a list of charging schedules and their associated time contexts,
- allowing for updates and recalculations of remaining charging time required.
-
- Attributes:
- all_schedules (list): List of tuples containing (schedule, time_context) pairs
- total_time_required (int): Total charging time needed in minutes
- end_time (datetime): Final deadline for the charging schedule
- charge_per_interval (list): List of charging durations per interval
- is_contiguous (bool): Flag indicating if charging must be contiguous
- sleep_delay(bool): Flag indicating if next query time must be delayed
- contiguity_values_dict (dict): Dictionary storing contiguity-related values
- """
-
- def __init__(
- self,
- initial_schedule: pd.DataFrame,
- start_time: datetime,
- end_time: datetime,
- total_time_required: int,
- contiguous = False,
- charge_per_interval: Optional[list] = None
- ) -> None:
- """Initialize the Recalculator with an initial schedule.
-
- Args:
- initial_schedule (pd.DataFrame): Starting charging schedule
- start_time (datetime): Start time for the schedule
- end_time (datetime): End time for the schedule
- total_time_required (int): Total charging time needed in minutes
- charge_per_interval (list): List of charging durations per interval
- """
- self.OPT_INTERVAL = 5
- self.all_schedules = [(initial_schedule, (start_time, end_time))]
- self.end_time=end_time
- self.total_time_required = total_time_required
- self.charge_per_interval = charge_per_interval
- self.is_contiguous = contiguous
- self.sleep_delay = False
- self.contiguity_values_dict = {
- "delay_usage_window_start": None,
- "delay_in_minutes": None,
- "delay_in_intervals": None,
- "remaining_time_required": None,
- "remaining_units_required":None,
- "num_segments_complete": None
- }
-
- self.total_available_units = self.minutes_to_units(
- int(int((self.end_time - start_time).total_seconds()) / 60)
- )
-
- def is_tz_aware(dt):
- return dt.tzinfo is not None and dt.tzinfo.utcoffset(dt) is not None
-
- def minutes_to_units(self, x, floor=False):
- '''Converts minutes to forecase intervals. Rounds UP by default.'''
- if x:
- if floor:
- return int(x // self.OPT_INTERVAL)
- else:
- return int(math.ceil(x / self.OPT_INTERVAL))
- return x
-
- def get_remaining_units_required(self,next_query_time):
- _minutes = self.get_remaining_time_required(next_query_time)
- return self.minutes_to_units(_minutes)
-
- def get_remaining_time_required(self, next_query_time: datetime):
- """Calculate remaining charging time needed at a given query time.
-
- Args:
- next_query_time (datetime): Time from which to calculate remaining time
-
- Returns:
- int: Remaining charging time required in minutes
- """
- if len(self.all_schedules) == 0:
- return self.total_time_required
-
- combined_schedule = self.get_combined_schedule()
- t = next_query_time - timedelta(minutes=5)
-
- usage_in_minutes = combined_schedule.loc[:t]["usage"].sum()
-
- return self.total_time_required - usage_in_minutes
-
- def set_last_schedule_end_time(self, next_query_time: datetime):
- """Update the end time of the most recent schedule.
-
- Args:
- next_query_time (datetime): New end time for the last schedule
-
- Raises:
- AssertionError: If new end time is before start time
- """
- if len(self.all_schedules) > 0:
- schedule, ctx = self.all_schedules[-1]
- self.all_schedules[-1] = (schedule, (ctx[0], next_query_time))
- assert ctx[0] < next_query_time
-
- def update_charging_schedule(
- self,
- next_query_time: datetime,
- next_new_schedule_start_time = None,
- new_schedule: Optional[pd.DataFrame] = None
- ):
- """
- Update charging schedule and contiguity values.
-
- Args:
- next_query_time: Current query time
- next_new_schedule_start_time: Start time for next schedule
- new_schedule: New charging schedule to add
- """
-
- def _protocol_no_new_schedule(next_new_schedule_start_time):
- '''
- 1. Confirm that charging is not in progress and sleep delay is not required
- '''
- if self.is_contiguous is True:
- self.sleep_delay = self.check_if_contiguity_sleep_required(self.all_schedules[0][0], next_new_schedule_start_time)
- else:
- pass
-
- def _protocol_new_schedule(new_schedule, next_query_time, next_new_schedule_start_time):
- '''
- 1. Modify previous schedule to end at "next_query_time"
- 2. Append new schedule to record of existing schedules
- 3. Confirm that charging is not in progress and sleep delay is not required
- '''
- self.set_last_schedule_end_time(next_query_time)
- self.all_schedules.append((new_schedule, (next_query_time, self.end_time)))
- if self.is_contiguous is True:
- self.sleep_delay = self.check_if_contiguity_sleep_required(new_schedule, next_new_schedule_start_time)
-
- def _protocol_sleep_delay(next_new_schedule_start_time):
- print('sleep protocol activated...')
- assert next_new_schedule_start_time is not None, "Sleep delay next new time is None"
- s = self.get_combined_schedule().loc[next_new_schedule_start_time:]['usage'] == 0
- delay_time = self.end_time if s[s == True].empty == True else s[s == True].index.min()
- self.contiguity_values_dict = {
- "delay_usage_window_start": delay_time,
- "delay_in_minutes": len(s[s == False]) * 5,
- "delay_in_intervals": len(s[s == False]),
- "remaining_units_required": self.get_remaining_units_required(delay_time),
- "remaining_time_required": self.get_remaining_time_required(delay_time)
- }
-
- self.contiguity_values_dict["num_segments_complete"] = self.number_segments_complete(next_query_time=self.contiguity_values_dict["delay_usage_window_start"])
-
- if new_schedule is None:
- _protocol_no_new_schedule(next_new_schedule_start_time)
- else:
- _protocol_new_schedule(new_schedule, next_query_time, next_new_schedule_start_time)
-
- if self.sleep_delay is True:
- _protocol_sleep_delay(next_new_schedule_start_time)
- else:
- self.contiguity_values_dict = {
- "delay_usage_window_start": None,
- "delay_in_minutes": None,
- "delay_in_intervals": None,
- "remaining_units_required": self.get_remaining_units_required(next_query_time),
- "remaining_time_required": self.get_remaining_time_required(next_query_time),
- "num_segments_complete": self.number_segments_complete(next_query_time=next_query_time)
- }
-
- def get_combined_schedule(self, end_time: datetime = None) -> pd.DataFrame:
- """Combine all schedules into a single DataFrame.
-
- Args:
- end_time (datetime, optional): Optional cutoff time for the combined schedule
-
- Returns:
- pd.DataFrame: Combined schedule of all charging segments
- """
- schedule_segments = []
- for s, ctx in self.all_schedules:
- schedule_segments.append(s[s.index < ctx[1]])
- combined_schedule = pd.concat(schedule_segments)
-
- if end_time:
- last_segment_start_time = end_time
- combined_schedule = combined_schedule.loc[:last_segment_start_time]
-
- return combined_schedule
-
- def check_if_contiguity_sleep_required(self, usage_plan, next_query_time):
- """Check if charging needs to be paused for contiguity.
-
- Args:
- usage_plan (pd.DataFrame): Planned charging schedule
- next_query_time (datetime): Time of next schedule update
-
- Returns:
- bool: True if charging needs to be paused
- """
- return bool(usage_plan.loc[(next_query_time - timedelta(minutes=5))]['usage'] > 0)
-
- def number_segments_complete(self, next_query_time: datetime = None):
- """Calculate number of completed charging segments.
-
- Args:
- next_query_time (datetime, optional): Time to check completion status
-
- Returns:
- int: Number of completed charging segments
- """
- if self.is_contiguous is True:
- combined_schedule = self.get_combined_schedule()
- completed_schedule = combined_schedule.loc[:next_query_time]
- charging_indicator = completed_schedule["usage"].astype(bool).sum()
- return bisect.bisect_right(
- list(accumulate(self.charge_per_interval)),
- (charging_indicator * 5)
- )
- else:
- return None
-
-class RequerySimulator:
- def __init__(self,
- moers_list,
- requery_dates,
- region="CAISO_NORTH",
- window_start=datetime(2025, 1, 1, hour=21, second=1, tzinfo=UTC),
- window_end=datetime(2025, 1, 2, hour=8, second=1, tzinfo=UTC),
- usage_time_required_minutes=240,
- usage_power_kw=2,
- charge_per_interval=None):
- self.moers_list = moers_list
- self.requery_dates = requery_dates
- self.region = region
- self.window_start = window_start
- self.window_end = window_end
- self.usage_time_required_minutes = usage_time_required_minutes
- self.usage_power_kw = usage_power_kw
- self.charge_per_interval = charge_per_interval
-
- self.username = os.getenv("WATTTIME_USER")
- self.password = os.getenv("WATTTIME_PASSWORD")
- self.wt_opt = WattTimeOptimizer(self.username, self.password)
-
- def _get_initial_plan(self):
- return self.wt_opt.get_optimal_usage_plan(
- region=self.region,
- usage_window_start=self.window_start,
- usage_window_end=self.window_end,
- usage_time_required_minutes=self.usage_time_required_minutes,
- usage_power_kw=self.usage_power_kw,
- charge_per_interval=self.charge_per_interval,
- optimization_method="simple",
- moer_data_override=self.moers_list[0][["point_time","value"]]
- )
-
- def simulate(self):
- initial_plan = self._get_initial_plan()
- recalculator = WattTimeRecalculator(
- initial_schedule=initial_plan,
- start_time=self.window_start,
- end_time=self.window_end,
- total_time_required=self.usage_time_required_minutes,
- charge_per_interval=self.charge_per_interval
- )
-
- # check to see the status of my segments to know if I should requery at all
- # if I do need to requery, then I need time required + segments remaining
- # if I don't then I store the state of my recalculator as is
-
- for i, new_window_start in enumerate(self.requery_dates[1:], 1):
- new_time_required = recalculator.get_remaining_time_required(new_window_start)
- if new_time_required > 0.0:
- next_plan = self.wt_opt.get_optimal_usage_plan(
- region=self.region,
- usage_window_start=new_window_start,
- usage_window_end=self.window_end,
- usage_time_required_minutes=new_time_required,
- usage_power_kw=self.usage_power_kw,
- charge_per_interval=self.charge_per_interval,
- optimization_method="simple",
- moer_data_override=self.moers_list[i][["point_time","value"]]
- )
- recalculator.update_charging_schedule(
- new_schedule=next_plan,
- next_query_time=new_window_start,
- next_new_schedule_start_time = None
- )
- else:
- return recalculator
\ No newline at end of file
+ return rsp.json()
\ No newline at end of file
diff --git a/watttime/optimizer/Optimizer README.md b/watttime/optimizer/Optimizer README.md
deleted file mode 100644
index 10d2356f..00000000
--- a/watttime/optimizer/Optimizer README.md
+++ /dev/null
@@ -1,89 +0,0 @@
-# Optimizer: basic principles and options
-
-The **basic intuition of the algorithm** is that when a device is plugged in for longer than the time required to fully charge it there exist ways to pick charging vs. non-charging time intervals such that the device draws power from the grid during cleaner intervals and thus minimizes emissions. **The algorithm takes as inputs** user and device parameters such as: the plug-in and plug-out times of the device, as well as the charging curve that determines the time it takes to charge it as well as the power that it needs to draw. **As an output, it produces** a charging schedule that divides the time between plug-in and plug-out time into charging and non-charging intervals such that emissions are minimized. Watttime’s forecast provides the basic building block for these algorithms as it forecasts when those relatively cleaner grid periods occur.
-
-There are **three different optimization algorithms** that are implemented in the API (alongside a baseline algorithm that just charges the device from the moment it’s plugged in to when it is fully charged, which is what devices do out of the box). We first start with **a simple algorithm** that, under full information about plug out time, uses the forecast to find the lowest possible emission interval that charges the device and outputs a charge schedule based on that. We then follow with a **sophisticated** version of the algorithm which takes into account variable charging curves and implements a dynamic optimization algorithm to adjust for the fact that device charging curves are non-linear. We provide additional functionality in the **fixed contiguous** and **contiguous** versions of the algorithms, which can enforce the charging schedule to be composed of several contiguous intervals; the length of each interval is either fixed or falls in a provided range.
-
-| optimization\_method | ASAP | Charging curve | Time constraint | Contiguous |
-| :---- | :---- | :---- | :---- | :---- |
-| baseline | Yes | Constant | No | No |
-| simple | No | Constant | No | No |
-| sophisticated | No | Variable | Yes | No |
-| contiguous | No | Variable | Yes | Intervals at fixed lengths |
-| Variable contiguous | No | Variable | Yes | Intervals at variable lengths |
-| auto | No | Chooses the fastest algorithm that can still process all inputs | | |
-
-### Raw Inputs
-
-*What we simulate for each use case*
-
-- Capacity C
- - Might also need init battery capacity if we don’t start from 0%
- - Unit: kWh
- - Type: energy
-- Power usage curve from capacity Q:cp
- - Marginal power usage to charge battery when it’s currently at capacity c
- - Unit: kW
- - Type: power
-- Marginal emission rate M:tm
- - Unit: lb/MWh
- - Type: emission per energy
- - We convert this to lb/kWh by multiplying M by 0.001
-- OPT\_INTERVAL
- - Smallest interval on which we have constant charging behavior and emissions.
- - Currently set to 5 minutes
- - We have now discretized time into L=T/ intervals of length . The l-th interval is lt\<(l+1).
-- Contiguity
-- Constraints
-
-### API Inputs
-
-*When calling the API*
-
-- usage\_time\_required\_minutes Tr
- - We compute this using C and Q. See example below.
- - Unit: mins
-- usage\_power\_kw P:tp
- - Marginal power usage to charge battery when it has been charged for t minutes. Converted from Q.
- - Unit: kW
-- usage\_window\_start, usage\_window\_end
- - These are timestamps to specify the charging window
-
-### Algorithm
-
-Find schedule s0,...,sL-1 that minimizes total emission 60l=0L-1slPl'=0l-1sl' Ml1000subject to
-
-* sl0,1
- * sl=1 if we charge on interval l
- * sl=0 if we do not charge on interval l
- * As an extension for future use cases, suppose we can supercharge the battery by consuming up to K times as much power. The DP algorithm will also be able to handle this optimization and output a schedule with sl0,1,...,K
-* l=0L-1sl=Tr
- * This just means that we charge for a total of Tr minutes according to this schedule.
-
-### API Output
-
- A data frame with \-spaced timestamp index and charging usage. For example,
-
-| time | usage (min) | energe\_use\_mwh | emissions\_co2e\_lb |
-| :---- | :---- | :---- | :---- |
-| 2024-7-26 15:00:00+00:00 | 5 | 0.0001 | 1.0 |
-| 2024-7-26 15:05:00+00:00 | 5 | 0.0001 | 1.0 |
-| 2024-7-26 15:10:00+00:00 | 0 | 0\. | 0.0 |
-| 2024-7-26 15:15:00+00:00 | 5 | 0.00005 | 0.5 |
-
-This would mean that we charge from 15:00-15:10 and then from 15:15-15:20. Note that the last column reflects **forecast emissions** based on forecast emission rate rather than the actuals. To compute actual emissions, you can take the dot product of energey\_use\_mwh and actual emission rates.
-
-In mathematical terms, we compute these three columns as follows. For the l-th interval tl,l+1, we have
-
-* usage: sl
-* energy\_use\_mwh: 1100060slPl'=0l-1sl', where 60 reflects interval length in hours and Pl'=0l-1sl' is the power. 11000 is a conversion factor from kWh to mWh
-* emissions\_co2e\_lb: energy\_us\_mwh \* Mt.
-
-# FAQs
-
-## How much co2 can we expect to avoid by using the optimizer?
-
-The amount of emission you can avoid will vary significantly based on a range of factors. For example:
-
-* The grid where the charging is occurring.
-* The amount of “slack time” available, that is, possible charging time beyond the minimum amount required for charging.
\ No newline at end of file
diff --git a/watttime/optimizer/test.py b/watttime/optimizer/test.py
deleted file mode 100644
index 02784b54..00000000
--- a/watttime/optimizer/test.py
+++ /dev/null
@@ -1,149 +0,0 @@
-from alg import moer, optCharger
-
-model = optCharger.OptCharger()
-
-m = moer.Moer(
- mu=[10, 10, 10, 1, 13, 3, 2, 3],
-)
-print("Length of schedule:", len(m))
-
-print("greedy algo")
-model.fit(total_charge=3, total_time=8, moer=m, optimization_method="baseline")
-model.summary()
-print("simple sorting algo")
-model.fit(
- total_charge=3,
- total_time=8,
- moer=m,
-)
-model.summary()
-print("sophisticated algo that produces same answer as simple")
-model.fit(total_charge=3, total_time=8, moer=m, optimization_method="sophisticated")
-model.summary()
-print("incorrect pairing of simple sorting algo + variable charge rate")
-model.fit(
- total_charge=3,
- total_time=8,
- moer=m,
- emission_multiplier_fn=lambda x, y: [1.0, 2.0, 1.0][x],
- optimization_method="simple",
-)
-model.summary()
-print("sophisticated algo + variable charge rate")
-model.fit(
- total_charge=3,
- total_time=8,
- moer=m,
- emission_multiplier_fn=lambda x, y: [1.0, 2.0, 1.0][x],
-)
-model.summary()
-print("sophisticated algo + constraints")
-model.fit(total_charge=3, total_time=8, moer=m, constraints={2: (2, None)})
-model.summary()
-
-m = moer.Moer(
- mu=[2, 1, 10, 10, 10, 1, 13, 3],
-)
-
-# Fixed Contiguous
-print("One contiguous interval")
-model.fit(total_charge=3, total_time=8, moer=m, charge_per_interval=[3])
-model.summary()
-print("Two contiguous intervals")
-model.fit(total_charge=3, total_time=8, moer=m, charge_per_interval=[2, 1])
-model.summary()
-print("Two contiguous intervals, one of which given as intervals + variable power rate")
-model.fit(
- total_charge=3,
- total_time=8,
- moer=m,
- charge_per_interval=[(2, 2), 1],
- emission_multiplier_fn=lambda x, y: [1.0, 0.1, 1.0][x],
-)
-model.summary()
-print("Two contiguous intervals, one of which given as intervals + variable power rate")
-model.fit(
- total_charge=3,
- total_time=8,
- moer=m,
- charge_per_interval=[2, (1, 1)],
- emission_multiplier_fn=lambda x, y: [1.0, 0.1, 1.0][x],
-)
-model.summary()
-print("Two contiguous intervals, one of which given as intervals + variable power rate")
-model.fit(
- total_charge=3,
- total_time=8,
- moer=m,
- charge_per_interval=[(2, 2), (1, 1)],
- emission_multiplier_fn=lambda x, y: [1.0, 0.1, 1.0][x],
-)
-model.summary()
-print("Two contiguous intervals + variable power rate + constraints")
-model.fit(
- total_charge=4,
- total_time=8,
- moer=m,
- charge_per_interval=[3, 1],
- constraints={2: (None, 1), 5: (3, None)},
-)
-model.summary()
-
-# Variable Contiguous
-print("One contiguous interval")
-model.fit(total_charge=3, total_time=8, moer=m, charge_per_interval=[(0, 3)])
-model.summary()
-print("Two contiguous intervals")
-model.fit(total_charge=3, total_time=8, moer=m, charge_per_interval=[(1, 2), (0, 3)])
-model.summary()
-print("Two contiguous intervals + variable power rate")
-model.fit(
- total_charge=3,
- total_time=8,
- moer=m,
- charge_per_interval=[(1, 2), (1, 2)],
- emission_multiplier_fn=lambda x, y: [1.0, 0.1, 1.0][x],
-)
-model.summary()
-print("Two contiguous intervals + variable power rate")
-model.fit(total_charge=4, total_time=8, moer=m, charge_per_interval=[(1, 3), (0, 3)])
-model.summary()
-print("Two contiguous intervals + variable power rate + constraints")
-model.fit(
- total_charge=4,
- total_time=8,
- moer=m,
- charge_per_interval=[(1, 3), (0, 3)],
- constraints={2: (None, 1)},
-)
-model.summary()
-print("Two contiguous intervals + variable power rate + constraints")
-model.fit(
- total_charge=4,
- total_time=8,
- moer=m,
- charge_per_interval=[(1, 3), (0, 3)],
- constraints={2: (None, 1), 5: (3, None)},
-)
-model.summary()
-
-m = moer.Moer(
- mu=[10, 1, 1, 1, 10, 1, 1, 1],
-)
-print("Three contiguous intervals of fixed lengths")
-model.fit(total_charge=6, total_time=8, moer=m, charge_per_interval=[2] * 3)
-model.summary()
-print("Three contiguous intervals of variable lengths")
-model.fit(total_charge=6, total_time=8, moer=m, charge_per_interval=[(2, 6)] * 3)
-model.summary()
-print(
- "Three contiguous intervals of variable lengths, but doesnt need to charge all intervals"
-)
-model.fit(
- total_charge=6,
- total_time=8,
- moer=m,
- charge_per_interval=[(2, 6)] * 3,
- use_all_intervals=False,
-)
-model.summary()
diff --git a/watttime_optimizer/Optimizer README.md b/watttime_optimizer/Optimizer README.md
new file mode 100644
index 00000000..085c78af
--- /dev/null
+++ b/watttime_optimizer/Optimizer README.md
@@ -0,0 +1,231 @@
+# Using the Optimizer Class
+`WattTime.Optimizer` produces a power consumption schedule that minimizes carbon emissions subject to user and device constraints.
+
+The `WattTime.Optimizer` class requires 4 things:
+
+- Watttime’s forecast of marginal emissions
+- device capacity and energy needs
+- region
+- window start
+- window end
+
+
+| optimization\_method | ASAP | Charging curve | Time constraint | Contiguous |
+| :---- | :---- | :---- | :---- | :---- |
+| baseline | Yes | Constant | No | No |
+| simple | No | Constant | No | No |
+| sophisticated | No | Variable | Yes | No |
+| contiguous | No | Variable | Yes | Intervals at fixed lengths |
+| Variable contiguous | No | Variable | Yes | Intervals at variable lengths |
+| auto | No | Chooses the fastest algorithm that can still process all inputs | | |
+
+Click any of the thumbnails below to see the notebook that generated it.
+
+1. Naive Smart device charging: needs 30 minutes to reach full charge, expected plug out time within the next 4 hours. Simple use case.
+2. Requery: update usage plan every 20 minutes using new forecast for the next 4 hours. Simple use case with recalculation
+3. Partial charging guarantee: charge 75% by 8am. User constraint
+4. Data center workloads: estimated runtime is 2 hours and it needs to complete by 12pm Contiguous (single period, fixed length)
+5. Dishwasher: needs to run over two usage intervals of lengths 80 min and 40 min. They must complete in that order. Contiguous (multiple periods, fixed length)
+6. Compressor: needs to run 120 minutes over the next 12 hours; each cycle needs to be at least 20 minutes long, and any number of contiguous intervals (from one to six) is okay. Contiguous (multiple periods, variable length)
+
+**Naive Smart Device Charging [EV or pluggable battery-powered device]**
+
+```py
+from datetime import datetime, timedelta
+import pandas as pd
+from pytz import UTC
+from watttime_optimizer import WattTimeOptimizer
+import os
+
+username = os.getenv("WATTTIME_USER")
+password = os.getenv("WATTTIME_PASSWORD")
+wt_opt = WattTimeOptimizer(username, password)
+
+# 12 hour charge window (720/60 = 12)
+now = datetime.now(UTC)
+window_start = now
+window_end = now + timedelta(minutes=720)
+
+usage_plan = wt_opt.get_optimal_usage_plan(
+ region="CAISO_NORTH",
+ usage_window_start=window_start,
+ usage_window_end=window_end,
+ usage_time_required_minutes=240,
+ usage_power_kw=12,
+ optimization_method="auto",
+)
+
+print(usage_plan.head())
+print(usage_plan["usage"].tolist())
+print(usage_plan.sum())
+```
+
+**Partial Charging Guarantee - Introducing Constraints**
+ * Sophisticated - total charge window 12 hours long, 75% charged by hour 8.
+
+```py
+from datetime import datetime, timedelta
+import pandas as pd
+from pytz import UTC
+from watttime_optimizer import WattTimeOptimizer
+import os
+
+username = os.getenv("WATTTIME_USER")
+password = os.getenv("WATTTIME_PASSWORD")
+wt_opt = WattTimeOptimizer(username, password)
+
+# 12 hour charge window (720/60 = 12)
+# Minute 480 is time context when the constraint, i.e. 75% charge, must be satisfied
+# 75% of 240 (required charge expressed in minutes) is 180
+
+now = datetime.now(UTC)
+window_start = now
+window_end = now + timedelta(minutes=720)
+usage_time_required_minutes = 240
+constraint_time = now + timedelta(minutes=480)
+constraint_usage_time_required_minutes = 180
+usage_power_kw = 12.0
+
+# map the constraint to the time context
+constraints = {constraint_time:constraint_usage_time_required_minutes}
+
+usage_plan = wt_opt.get_optimal_usage_plan(
+ region="CAISO_NORTH",
+ usage_window_start=window_start,
+ usage_window_end=window_end,
+ usage_time_required_minutes=240,
+ usage_power_kw=12,
+ constraints=constraints,
+ optimization_method="auto",
+)
+
+print(usage_plan.head())
+print(usage_plan["usage"].tolist())
+print(usage_plan.sum())
+```
+
+**Variable Charging Curve - EV**
+I know the model of my vehicle and want to match device characteristics. If we have a 10 kWh battery which initially charges at 20kW, the charge rate then linearly decreases to 10kW as the battery is 50%
+charged, and then remains at 10kW for the rest of the charging. This is the charging curve.
+
+```py
+from datetime import datetime, timedelta
+import pandas as pd
+from pytz import UTC
+from watttime_optimizer import WattTimeOptimizer
+from watttime_optimizer.battery import Battery
+import os
+
+username = os.getenv("WATTTIME_USER")
+password = os.getenv("WATTTIME_PASSWORD")
+wt_opt = WattTimeOptimizer(username, password)
+
+now = datetime.now(UTC)
+window_start = now
+window_end = now + timedelta(minutes=720)
+
+battery = Battery(
+ initial_soc=0.0,
+ charging_curve=pd.DataFrame(
+ columns=["SoC", "kW"],
+ data=[
+ [0.0, 20.0],
+ [0.5, 10.0],
+ [1.0, 10.0],
+ ]
+ ),
+ capacity_kWh=10.0,
+)
+
+variable_usage_power = battery.get_usage_power_kw_df()
+
+usage_plan = wt_opt.get_optimal_usage_plan(
+ region="CAISO_NORTH",
+ usage_window_start=window_start,
+ usage_window_end=window_end,
+ usage_time_required_minutes=240,
+ usage_power_kw=variable_usage_power,
+ optimization_method="auto",
+)
+
+print(usage_plan.head())
+print(usage_plan["usage"].tolist())
+print(usage_plan.sum())
+```
+
+* **Data Center Workload 1**:
+ * (single segment, fixed length) - charging schedule to be composed of a single contiguous, i.e. "block" segment of fixed length
+
+```py
+## AI model training - estimated runtime is 2 hours and it needs to complete within 12 hours
+
+from datetime import datetime, timedelta
+import pandas as pd
+from pytz import UTC
+from watttime_optimizer import WattTimeOptimizer
+import os
+
+username = os.getenv("WATTTIME_USER")
+password = os.getenv("WATTTIME_PASSWORD")
+wt_opt = WattTimeOptimizer(username, password)
+
+now = datetime.now(UTC)
+window_start = now
+window_end = now + timedelta(minutes=720)
+
+usage_power_kw = 12.0
+region = "CAISO_NORTH"
+
+# by passing a single interval of 120 minutes to charge_per_segment, the Optimizer will know to fit call the fixed contigous modeling function.
+usage_plan = wt_opt.get_optimal_usage_plan(
+ region=region,
+ usage_window_start=window_start,
+ usage_window_end=window_end,
+ usage_time_required_minutes=120,
+ usage_power_kw=12,
+ charge_per_segment=[120],
+ optimization_method="auto",
+ verbose = False
+)
+
+print(usage_plan.head())
+print(usage_plan["usage"].tolist())
+print(usage_plan.sum())
+```
+
+**Data Center Workload 2**:
+ * (multiple segments, fixed length) - runs over two usage periods of lengths 80 min and 40 min. The order of the segments is immutable.
+
+```py
+## there are two cycles of length 80 min and 40 min each, and they must be completed in that order.
+
+from datetime import datetime, timedelta
+import pandas as pd
+from pytz import UTC
+from watttime_optimizer import WattTimeOptimizer
+import os
+
+username = os.getenv("WATTTIME_USER")
+password = os.getenv("WATTTIME_PASSWORD")
+wt_opt = WattTimeOptimizer(username, password)
+
+# Suppose that the time now is 12 midnight
+now = datetime.now(UTC)
+window_start = now
+window_end = now + timedelta(minutes=720)
+
+# Pass two values to charge_per_segment instead of one.
+usage_plan = wt_opt.get_optimal_usage_plan(
+ region="CAISO_NORTH",
+ usage_window_start=window_start,
+ usage_window_end=window_end,
+ usage_time_required_minutes=120, # 80 + 40
+ usage_power_kw=12,
+ charge_per_segment=[80,40],
+ optimization_method="auto",
+)
+
+print(usage_plan.head())
+print(usage_plan["usage"].tolist())
+print(usage_plan.sum())
+```
\ No newline at end of file
diff --git a/watttime_optimizer/__init__.py b/watttime_optimizer/__init__.py
new file mode 100644
index 00000000..3f5ce00d
--- /dev/null
+++ b/watttime_optimizer/__init__.py
@@ -0,0 +1,4 @@
+from watttime.api import *
+from watttime_optimizer.api_opt import *
+from watttime_optimizer.api_opt import *
+from watttime_optimizer.evaluator import *
\ No newline at end of file
diff --git a/watttime/optimizer/alg/__init__.py b/watttime_optimizer/alg/__init__.py
similarity index 100%
rename from watttime/optimizer/alg/__init__.py
rename to watttime_optimizer/alg/__init__.py
diff --git a/watttime/optimizer/alg/moer.py b/watttime_optimizer/alg/moer.py
similarity index 100%
rename from watttime/optimizer/alg/moer.py
rename to watttime_optimizer/alg/moer.py
diff --git a/watttime/optimizer/alg/optCharger.py b/watttime_optimizer/alg/optCharger.py
similarity index 94%
rename from watttime/optimizer/alg/optCharger.py
rename to watttime_optimizer/alg/optCharger.py
index d2b76016..51b128dd 100644
--- a/watttime/optimizer/alg/optCharger.py
+++ b/watttime_optimizer/alg/optCharger.py
@@ -284,7 +284,7 @@ def __contiguous_fit(
total_time: int,
moer: Moer,
emission_multiplier_fn,
- charge_per_interval: list = [],
+ charge_per_segment: list = [],
constraints: dict = {},
):
"""
@@ -304,7 +304,7 @@ def __contiguous_fit(
An object representing Marginal Operating Emissions Rate.
emission_multiplier_fn : callable
A function that calculates emission multipliers.
- charge_per_interval : list of int
+ charge_per_segment : list of int
The exact charging amount per interval.
constraints : dict, optional
A dictionary of charging constraints for specific time steps. Constraints are one-indexed: t:(a,b) means that after t minutes, we have to have charged for between a and b minutes inclusive, so that 1<=t<=total_time
@@ -321,13 +321,13 @@ def __contiguous_fit(
This is the __diagonal_fit() algorithm with further constraint on contiguous charging intervals and their respective length
"""
self.verbose_on("== Fixed contiguous fit! ==")
- total_interval = len(charge_per_interval)
+ total_interval = len(charge_per_segment)
# This is a matrix with size = number of time states x number of intervals charged so far
max_util = np.full((total_time + 1, total_interval + 1), np.nan)
max_util[0, 0] = 0.0
path_history = np.full((total_time, total_interval + 1), False, dtype=bool)
cum_charge = [0]
- for c in charge_per_interval:
+ for c in charge_per_segment:
cum_charge.append(cum_charge[-1] + c)
charge_array_cache = [
@@ -354,8 +354,8 @@ def __contiguous_fit(
max_util[t, k] = max_util[t - 1, k]
init_val = False
## charging
- if (k > 0) and (charge_per_interval[k - 1] <= t):
- dc = charge_per_interval[k - 1]
+ if (k > 0) and (charge_per_segment[k - 1] <= t):
+ dc = charge_per_segment[k - 1]
if not np.isnan(
max_util[t - dc, k - 1]
) and OptCharger.__check_constraint(
@@ -389,7 +389,7 @@ def __contiguous_fit(
t_curr -= 1
else:
## charge
- dc = charge_per_interval[curr_state - 1]
+ dc = charge_per_segment[curr_state - 1]
t_curr -= dc
curr_state -= 1
if dc > 0:
@@ -406,8 +406,8 @@ def __variable_contiguous_fit(
total_time: int,
moer: Moer,
emission_multiplier_fn,
- charge_per_interval: list = [],
- use_all_intervals: bool = True,
+ charge_per_segment: list = [],
+ use_all_segments: bool = True,
constraints: dict = {},
):
"""
@@ -427,10 +427,10 @@ def __variable_contiguous_fit(
An object representing Marginal Operating Emissions Rate.
emission_multiplier_fn : callable
A function that calculates emission multipliers.
- charge_per_interval : list of (int, int)
+ charge_per_segment : list of (int, int)
The minimium and maximum (inclusive) charging amount per interval.
- use_all_intervals : bool
- If true, use all intervals provided by charge_per_interval; if false, can use the first few intervals and skip the rest.
+ use_all_segments : bool
+ If true, use all intervals provided by charge_per_segment; if false, can use the first few intervals and skip the rest.
constraints : dict, optional
A dictionary of charging constraints for specific time steps. Constraints are one-indexed: t:(a,b) means that after t minutes, we have to have charged for between a and b minutes inclusive, so that 1<=t<=total_time
@@ -446,7 +446,7 @@ def __variable_contiguous_fit(
This is the __diagonal_fit() algorithm with further constraint on contiguous charging intervals and their respective length
"""
self.verbose_on("== Variable contiguous fit! ==")
- total_interval = len(charge_per_interval)
+ total_interval = len(charge_per_segment)
# This is a matrix with size = number of time states x number of charge states x number of intervals charged so far
max_util = np.full(
(total_time + 1, total_charge + 1, total_interval + 1), np.nan
@@ -483,8 +483,8 @@ def __variable_contiguous_fit(
## charging
if k > 0:
for dc in range(
- charge_per_interval[k - 1][0],
- min(charge_per_interval[k - 1][1], t, c) + 1,
+ charge_per_segment[k - 1][0],
+ min(charge_per_segment[k - 1][1], t, c) + 1,
):
if not np.isnan(
max_util[t - dc, c - dc, k - 1]
@@ -505,7 +505,7 @@ def __variable_contiguous_fit(
total_interval,
max_util[total_time, total_charge, total_interval],
)
- if not use_all_intervals:
+ if not use_all_segments:
for k in range(0, total_interval):
if np.isnan(max_util[total_time, total_charge, optimal_interval]) or (
not np.isnan(max_util[total_time, total_charge, k])
@@ -547,8 +547,8 @@ def fit(
total_charge: int,
total_time: int,
moer: Moer,
- charge_per_interval=None,
- use_all_intervals: bool = True,
+ charge_per_segment=None,
+ use_all_segments: bool = True,
constraints: dict = {},
emission_multiplier_fn=None,
optimization_method: str = "auto",
@@ -568,10 +568,10 @@ def fit(
The total time available for charging.
moer : Moer
An object representing Marginal Operating Emissions Rate.
- charge_per_interval : list of int or (int,int), optional
+ charge_per_segment : list of int or (int,int), optional
The minimium and maximum (inclusive) charging amount per interval. If int instead of tuple, interpret as both min and max.
- use_all_intervals : bool
- If true, use all intervals provided by charge_per_interval; if false, can use the first few intervals and skip the rest. This can only be false if charge_per_interval is provided as a range.
+ use_all_segments : bool
+ If true, use all intervals provided by charge_per_segment; if false, can use the first few intervals and skip the rest. This can only be false if charge_per_segment is provided as a range.
constraints : dict, optional
A dictionary of charging constraints for specific time steps.
emission_multiplier_fn : callable, optional
@@ -620,7 +620,7 @@ def fit(
self.__greedy_fit(total_charge, total_time, moer)
elif (
not constraints
- and not charge_per_interval
+ and not charge_per_segment
and constant_emission_multiplier
and optimization_method == "auto"
) or (optimization_method == "simple"):
@@ -629,7 +629,7 @@ def fit(
"Warning: Emissions function is non-constant. Using the simple algorithm is suboptimal."
)
self.__simple_fit(total_charge, total_time, moer)
- elif not charge_per_interval:
+ elif not charge_per_segment:
self.__diagonal_fit(
total_charge,
total_time,
@@ -651,7 +651,7 @@ def convert_input(c):
return c[0], c, True
return None, c, False
- for c in charge_per_interval:
+ for c in charge_per_segment:
if use_fixed_alg:
sc, tc, use_fixed_alg = convert_input(c)
single_cpi.append(sc)
@@ -660,7 +660,7 @@ def convert_input(c):
tuple_cpi.append(convert_input(c)[1])
if use_fixed_alg:
assert (
- use_all_intervals
+ use_all_segments
), "Must use all intervals when interval lengths are fixed!"
self.__contiguous_fit(
total_charge,
@@ -681,7 +681,7 @@ def convert_input(c):
emission_multiplier_fn, total_charge
),
tuple_cpi,
- use_all_intervals,
+ use_all_segments,
constraints,
)
@@ -714,7 +714,7 @@ def get_interval_ids(self) -> list:
Returns list of the interval ids for each interval. Has a value of -1 for non-charging intervals.
Intervals are labeled starting from 0 to n-1 when there are n intervals
- Only defined when charge_per_interval variable is given to some fit function
+ Only defined when charge_per_segment variable is given to some fit function
"""
return self.__interval_ids
diff --git a/watttime/api_convert.py b/watttime_optimizer/api_convert.py
similarity index 97%
rename from watttime/api_convert.py
rename to watttime_optimizer/api_convert.py
index 5109d056..048c6a64 100644
--- a/watttime/api_convert.py
+++ b/watttime_optimizer/api_convert.py
@@ -22,9 +22,9 @@ def convert_soc_to_soe(soc_power_df, voltage_curve, battery_capacity_coulombs):
# Calculate differential SoC for numerical integration
delta_soc = np.diff(soc, prepend=0)
- charge_per_interval = delta_soc * battery_capacity_coulombs
+ charge_per_segment = delta_soc * battery_capacity_coulombs
# Energy is voltage * charge
- energy_kwh = np.cumsum(voltage * charge_per_interval * 0.001 / 3600)
+ energy_kwh = np.cumsum(voltage * charge_per_segment * 0.001 / 3600)
# Normalize so that State of energy goes from 0 to 1
soe_array = energy_kwh / energy_kwh.iloc[-1]
diff --git a/watttime_optimizer/api_opt.py b/watttime_optimizer/api_opt.py
new file mode 100644
index 00000000..2007074b
--- /dev/null
+++ b/watttime_optimizer/api_opt.py
@@ -0,0 +1,751 @@
+import os
+import math
+from datetime import datetime, timedelta
+from typing import Any, Literal, Optional, Union
+
+import pandas as pd
+from dateutil.parser import parse
+from pytz import UTC, timezone
+from watttime_optimizer.alg import optCharger, moer
+from itertools import accumulate
+import bisect
+
+from watttime.api import WattTimeForecast
+
+
+OPT_INTERVAL = 5
+MAX_PREDICTION_HOURS = 72
+
+
+class WattTimeOptimizer(WattTimeForecast):
+ """
+ This class inherits from WattTimeForecast, with additional methods to generate
+ optimal usage plans for energy consumption based on various parameters and
+ constraints.
+
+ Additional Methods:
+ --------
+ get_optimal_usage_plan(region, usage_window_start, usage_window_end,
+ usage_time_required_minutes, usage_power_kw,
+ usage_time_uncertainty_minutes, optimization_method,
+ moer_data_override)
+ Generates an optimal usage plan for energy consumption.
+ """
+
+ OPT_INTERVAL = 5
+ MAX_PREDICTION_HOURS = 72
+ MAX_INT = 99999999999999999
+
+ def get_optimal_usage_plan(
+ self,
+ region: str,
+ usage_window_start: datetime,
+ usage_window_end: datetime,
+ usage_time_required_minutes: Optional[Union[int, float]] = None,
+ usage_power_kw: Optional[Union[int, float, pd.DataFrame]] = None,
+ energy_required_kwh: Optional[Union[int, float]] = None,
+ usage_time_uncertainty_minutes: Optional[Union[int, float]] = 0,
+ charge_per_segment: Optional[list] = None,
+ use_all_segments: bool = True,
+ constraints: Optional[dict] = None,
+ optimization_method: Optional[
+ Literal["baseline", "simple", "sophisticated", "auto"]
+ ] = "baseline",
+ moer_data_override: Optional[pd.DataFrame] = None,
+ verbose=True,
+ ) -> pd.DataFrame:
+ """
+ Generates an optimal usage plan for energy consumption based on given parameters.
+
+ This method calculates the most efficient energy usage schedule within a specified
+ time window, considering factors such as regional data, power requirements, and
+ optimization methods.
+
+ You should pass in exactly 2 of 3 parameters of (usage_time_required_minutes, usage_power_kw, energy_required_kwh)
+
+ Parameters:
+ -----------
+ region : str
+ The region for which forecast data is requested.
+ usage_window_start : datetime
+ Start time of the window when power consumption is allowed.
+ usage_window_end : datetime
+ End time of the window when power consumption is allowed.
+ usage_time_required_minutes : Optional[Union[int, float]], default=None
+ Required usage time in minutes.
+ usage_power_kw : Optional[Union[int, float, pd.DataFrame]], default=None
+ Power usage in kilowatts. Can be a constant value or a DataFrame for variable power.
+ energy_required_kwh : Optional[Union[int, float]], default=None
+ Energy required in kwh
+ usage_time_uncertainty_minutes : Optional[Union[int, float]], default=0
+ Uncertainty in usage time, in minutes.
+ charge_per_segment : Optional[list], default=None
+ Either a list of length-2 tuples representing minimium and maximum (inclusive) charging minutes per interval,
+ or a list of ints representing both the min and max. [180] OR [(180,180)]
+ use_all_segments : Optional[bool], default=False
+ If true, use all intervals provided by charge_per_segment; if false, can use the first few intervals and skip the rest.
+ constraints : Optional[dict], default=None
+ A dictionary containing contraints on how much usage must be used before the given time point
+ optimization_method : Optional[Literal["baseline", "simple", "sophisticated", "auto"]], default="baseline"
+ The method used for optimization.
+ moer_data_override : Optional[pd.DataFrame], default=None
+ Pre-generated MOER (Marginal Operating Emissions Rate) DataFrame, if available.
+ verbose : default = True
+ If false, suppresses print statements in the opt charger class.
+
+ Returns:
+ --------
+ pd.DataFrame
+ A DataFrame representing the optimal usage plan, including columns for
+ predicted MOER, usage, CO2 emissions, and energy usage.
+
+ Raises:
+ -------
+ AssertionError
+ If input parameters do not meet specified conditions (e.g., timezone awareness,
+ valid time ranges, supported optimization methods).
+
+ Notes:
+ ------
+ - The method uses WattTime forecast data unless overridden by moer_data_override.
+ - It supports various optimization methods and can handle both constant and variable power usage.
+ - The resulting plan aims to minimize emissions while meeting the specified energy requirements.
+ """
+
+ def is_tz_aware(dt):
+ return dt.tzinfo is not None and dt.tzinfo.utcoffset(dt) is not None
+
+ def minutes_to_units(x, floor=False):
+ """Converts minutes to forecase intervals. Rounds UP by default."""
+ if x:
+ if floor:
+ return int(x // self.OPT_INTERVAL)
+ else:
+ return int(math.ceil(x / self.OPT_INTERVAL))
+ return x
+
+ assert is_tz_aware(usage_window_start), "Start time is not tz-aware"
+ assert is_tz_aware(usage_window_end), "End time is not tz-aware"
+
+ if constraints is None:
+ constraints = {}
+ else:
+ # Convert constraints to a standardized format
+ raw_constraints = constraints.copy()
+ constraints = {}
+
+ for (
+ constraint_time_clock,
+ constraint_usage_minutes,
+ ) in raw_constraints.items():
+ constraint_time_minutes = (
+ constraint_time_clock - usage_window_start
+ ).total_seconds() / 60
+ constraint_time_units = minutes_to_units(constraint_time_minutes)
+ constraint_usage_units = minutes_to_units(constraint_usage_minutes)
+
+ constraints.update(
+ {constraint_time_units: (constraint_usage_units, None)}
+ )
+
+ num_inputs = 0
+ for input in (usage_time_required_minutes, usage_power_kw, energy_required_kwh):
+ if input is not None:
+ num_inputs += 1
+ assert (
+ num_inputs == 2
+ ), "Exactly 2 of 3 inputs in (usage_time_required_minutes, usage_power_kw, energy_required_kwh) required"
+ if usage_power_kw is None:
+ usage_power_kw = energy_required_kwh / usage_time_required_minutes * 60
+ print("Implied usage_power_kw =", usage_power_kw)
+ if usage_time_required_minutes is None:
+ if type(usage_power_kw) in (float, int) and type(energy_required_kwh) in (
+ float,
+ int,
+ ):
+ usage_time_required_minutes = energy_required_kwh / usage_power_kw * 60
+ print("Implied usage time required =", usage_time_required_minutes)
+ else:
+ # TODO: Implement and test
+ raise NotImplementedError(
+ "When usage_time_required_minutes is None, only float or int usage_power_kw and energy_required_kwh is supported."
+ )
+
+ # Perform these checks if we are using live data
+ if moer_data_override is None:
+ datetime_now = datetime.now(UTC)
+ assert (
+ usage_window_end > datetime_now
+ ), "Error, Window end is before current datetime"
+ assert usage_window_end - datetime_now < timedelta(
+ hours=self.MAX_PREDICTION_HOURS
+ ), "End time is too far in the future"
+ assert optimization_method in ("baseline", "simple", "sophisticated", "auto"), (
+ "Unsupported optimization method:" + optimization_method
+ )
+ if moer_data_override is None:
+ forecast_df = self.get_forecast_pandas(
+ region=region,
+ signal_type="co2_moer",
+ horizon_hours=self.MAX_PREDICTION_HOURS,
+ )
+ else:
+ forecast_df = moer_data_override.copy()
+ forecast_df = forecast_df.set_index("point_time")
+ forecast_df.index = pd.to_datetime(forecast_df.index)
+
+ relevant_forecast_df = forecast_df[forecast_df.index >= usage_window_start]
+ relevant_forecast_df = relevant_forecast_df[
+ relevant_forecast_df.index < usage_window_end
+ ]
+ relevant_forecast_df = relevant_forecast_df.rename(
+ columns={"value": "pred_moer"}
+ )
+ result_df = relevant_forecast_df[["pred_moer"]]
+ moer_values = relevant_forecast_df["pred_moer"].values
+
+ m = moer.Moer(mu=moer_values)
+
+ model = optCharger.OptCharger(verbose=verbose)
+
+ total_charge_units = minutes_to_units(usage_time_required_minutes)
+ if optimization_method in ("sophisticated", "auto"):
+ # Give a buffer time equal to the uncertainty
+ buffer_time = usage_time_uncertainty_minutes
+ if buffer_time > 0:
+ buffer_periods = minutes_to_units(buffer_time) if buffer_time else 0
+ buffer_enforce_time = max(
+ total_charge_units, len(moer_values) - buffer_periods
+ )
+ constraints.update({buffer_enforce_time: (total_charge_units, None)})
+ else:
+ assert (
+ usage_time_uncertainty_minutes == 0
+ ), "usage_time_uncertainty_minutes is only supported in optimization_method='sophisticated' or 'auto'"
+
+ if type(usage_power_kw) in (int, float):
+ # Convert to the MWh used in an optimization interval
+ # expressed as a function to meet the parameter requirements for OptC function
+ emission_multiplier_fn = (
+ lambda sc, ec: float(usage_power_kw) * 0.001 * self.OPT_INTERVAL / 60.0
+ )
+ else:
+ usage_power_kw = usage_power_kw.copy()
+ # Resample usage power dataframe to an OPT_INTERVAL frequency
+ usage_power_kw["time_step"] = usage_power_kw["time"] / self.OPT_INTERVAL
+ usage_power_kw_new_index = pd.DataFrame(
+ index=[float(x) for x in range(total_charge_units + 1)]
+ )
+ usage_power_kw = pd.merge_asof(
+ usage_power_kw_new_index,
+ usage_power_kw.set_index("time_step"),
+ left_index=True,
+ right_index=True,
+ direction="backward",
+ allow_exact_matches=True,
+ )
+
+ def emission_multiplier_fn(sc: float, ec: float) -> float:
+ """
+ Calculate the approximate mean power in the given time range,
+ in units of MWh used per optimizer time unit.
+
+ sc and ec are float values representing the start and end time of
+ the time range, in optimizer time units.
+ """
+ value = (
+ usage_power_kw[sc : max(sc, ec - 1e-12)]["power_kw"].mean()
+ * 0.001
+ * self.OPT_INTERVAL
+ / 60.0
+ )
+ return value
+
+ if charge_per_segment:
+ # Handle the charge_per_segment input by converting it from minutes to units, rounding up
+ converted_charge_per_segment = []
+ for c in charge_per_segment:
+ if isinstance(c, int):
+ converted_charge_per_segment.append(minutes_to_units(c))
+ else:
+ assert (
+ len(c) == 2
+ ), "Length of tuples in charge_per_segment is not 2"
+ interval_start_units = minutes_to_units(c[0]) if c[0] else 0
+ interval_end_units = (
+ minutes_to_units(c[1]) if c[1] else self.MAX_INT
+ )
+ converted_charge_per_segment.append(
+ (interval_start_units, interval_end_units)
+ )
+ else:
+ converted_charge_per_segment = None
+ model.fit(
+ total_charge=total_charge_units,
+ total_time=len(moer_values),
+ moer=m,
+ constraints=constraints,
+ charge_per_segment=converted_charge_per_segment,
+ use_all_segments=use_all_segments,
+ emission_multiplier_fn=emission_multiplier_fn,
+ optimization_method=optimization_method,
+ )
+
+ optimizer_result = model.get_schedule()
+ result_df = self._reconcile_constraints(
+ optimizer_result,
+ result_df,
+ model,
+ usage_time_required_minutes,
+ charge_per_segment,
+ )
+
+ return result_df
+
+ def _reconcile_constraints(
+ self,
+ optimizer_result,
+ result_df,
+ model,
+ usage_time_required_minutes,
+ charge_per_segment,
+ ):
+ # Make a copy of charge_per_segment if necessary
+ if charge_per_segment is not None:
+ charge_per_segment = charge_per_segment[::]
+ for i in range(len(charge_per_segment)):
+ if type(charge_per_segment[i]) == int:
+ charge_per_segment[i] = (
+ charge_per_segment[i],
+ charge_per_segment[i],
+ )
+ assert len(charge_per_segment[i]) == 2
+ processed_start = (
+ charge_per_segment[i][0]
+ if charge_per_segment[i][0] is not None
+ else 0
+ )
+ processed_end = (
+ charge_per_segment[i][1]
+ if charge_per_segment[i][1] is not None
+ else self.MAX_INT
+ )
+
+ charge_per_segment[i] = (processed_start, processed_end)
+
+ if not charge_per_segment:
+ # Handle case without charge_per_segment constraints
+ total_usage_intervals = sum(optimizer_result)
+ current_usage_intervals = 0
+ usage_list = []
+ for to_charge_binary in optimizer_result:
+ current_usage_intervals += to_charge_binary
+ if current_usage_intervals < total_usage_intervals:
+ usage_list.append(to_charge_binary * float(self.OPT_INTERVAL))
+ else:
+ # Partial interval
+ minutes_to_trim = (
+ total_usage_intervals * self.OPT_INTERVAL
+ - usage_time_required_minutes
+ )
+ usage_list.append(
+ to_charge_binary * float(self.OPT_INTERVAL - minutes_to_trim)
+ )
+ result_df["usage"] = usage_list
+ else:
+ # Process charge_per_segment constraints
+ result_df["usage"] = [
+ x * float(self.OPT_INTERVAL) for x in optimizer_result
+ ]
+ usage = result_df["usage"].values
+ sections = []
+ interval_ids = model.get_interval_ids()
+
+ def get_min_max_indices(lst, x):
+ # Find the first occurrence of x
+ min_index = lst.index(x)
+ # Find the last occurrence of x
+ max_index = len(lst) - 1 - lst[::-1].index(x)
+ return min_index, max_index
+
+ for interval_id in range(0, max(interval_ids) + 1):
+ assert (
+ interval_id in interval_ids
+ ), "interval_id not found in interval_ids"
+ sections.append(get_min_max_indices(interval_ids, interval_id))
+
+ # Adjust sections to satisfy charge_per_segment constraints
+ for i, (start, end) in enumerate(sections):
+ section_usage = usage[start : end + 1]
+ total_minutes = section_usage.sum()
+
+ # Get the constraints for this section
+ if isinstance(charge_per_segment[i], int):
+ min_minutes, max_minutes = (
+ charge_per_segment[i],
+ charge_per_segment[i],
+ )
+ else:
+ min_minutes, max_minutes = charge_per_segment[i]
+
+ # Adjust the section to fit the constraints
+ if total_minutes < min_minutes:
+ raise ValueError(
+ f"Cannot meet the minimum charging constraint of {min_minutes} minutes for section {i}."
+ )
+ elif total_minutes > max_minutes:
+ # Reduce usage to fit within the max_minutes
+ excess_minutes = total_minutes - max_minutes
+ for j in range(len(section_usage)):
+ if section_usage[j] > 0:
+ reduction = min(section_usage[j], excess_minutes)
+ section_usage[j] -= reduction
+ excess_minutes -= reduction
+ if excess_minutes <= 0:
+ break
+ usage[start : end + 1] = section_usage
+ result_df["usage"] = usage
+
+ # Recalculate these values approximately, based on the new "usage" column
+ # Note: This is approximate since it assumes that
+ # the charging emissions over time of the unrounded values are similar to the rounded values
+ result_df["emissions_co2_lb"] = (
+ model.get_charging_emissions_over_time()
+ * result_df["usage"]
+ / self.OPT_INTERVAL
+ )
+ result_df["energy_usage_mwh"] = (
+ model.get_energy_usage_over_time() * result_df["usage"] / self.OPT_INTERVAL
+ )
+
+ return result_df
+
+
+class WattTimeRecalculator:
+ """A class to manage and update charging schedules over time.
+
+ This class maintains a list of charging schedules and their associated time contexts,
+ allowing for updates and recalculations of remaining charging time required.
+
+ Attributes:
+ all_schedules (list): List of tuples containing (schedule, time_context) pairs
+ total_time_required (int): Total charging time needed in minutes
+ end_time (datetime): Final deadline for the charging schedule
+ charge_per_segment (list): List of charging durations per interval
+ is_contiguous (bool): Flag indicating if charging must be contiguous
+ sleep_delay(bool): Flag indicating if next query time must be delayed
+ contiguity_values_dict (dict): Dictionary storing contiguity-related values
+ """
+
+ def __init__(
+ self,
+ initial_schedule: pd.DataFrame,
+ start_time: datetime,
+ end_time: datetime,
+ total_time_required: int,
+ contiguous=False,
+ charge_per_segment: Optional[list] = None,
+ ) -> None:
+ """Initialize the Recalculator with an initial schedule.
+
+ Args:
+ initial_schedule (pd.DataFrame): Starting charging schedule
+ start_time (datetime): Start time for the schedule
+ end_time (datetime): End time for the schedule
+ total_time_required (int): Total charging time needed in minutes
+ charge_per_segment (list): List of charging durations per interval
+ """
+ self.OPT_INTERVAL = 5
+ self.all_schedules = [(initial_schedule, (start_time, end_time))]
+ self.end_time = end_time
+ self.total_time_required = total_time_required
+ self.charge_per_segment = charge_per_segment
+ self.is_contiguous = contiguous
+ self.sleep_delay = False
+ self.contiguity_values_dict = {
+ "delay_usage_window_start": None,
+ "delay_in_minutes": None,
+ "delay_in_intervals": None,
+ "remaining_time_required": None,
+ "remaining_units_required": None,
+ "num_segments_complete": None,
+ }
+
+ self.total_available_units = self.minutes_to_units(
+ int(int((self.end_time - start_time).total_seconds()) / 60)
+ )
+
+ def is_tz_aware(dt):
+ return dt.tzinfo is not None and dt.tzinfo.utcoffset(dt) is not None
+
+ def minutes_to_units(self, x, floor=False):
+ """Converts minutes to forecase intervals. Rounds UP by default."""
+ if x:
+ if floor:
+ return int(x // self.OPT_INTERVAL)
+ else:
+ return int(math.ceil(x / self.OPT_INTERVAL))
+ return x
+
+ def get_remaining_units_required(self, next_query_time):
+ _minutes = self.get_remaining_time_required(next_query_time)
+ return self.minutes_to_units(_minutes)
+
+ def get_remaining_time_required(self, next_query_time: datetime):
+ """Calculate remaining charging time needed at a given query time.
+
+ Args:
+ next_query_time (datetime): Time from which to calculate remaining time
+
+ Returns:
+ int: Remaining charging time required in minutes
+ """
+ if len(self.all_schedules) == 0:
+ return self.total_time_required
+
+ combined_schedule = self.get_combined_schedule()
+ t = next_query_time - timedelta(minutes=5)
+
+ usage_in_minutes = combined_schedule.loc[:t]["usage"].sum()
+
+ return self.total_time_required - usage_in_minutes
+
+ def set_last_schedule_end_time(self, next_query_time: datetime):
+ """Update the end time of the most recent schedule.
+
+ Args:
+ next_query_time (datetime): New end time for the last schedule
+
+ Raises:
+ AssertionError: If new end time is before start time
+ """
+ if len(self.all_schedules) > 0:
+ schedule, ctx = self.all_schedules[-1]
+ self.all_schedules[-1] = (schedule, (ctx[0], next_query_time))
+ assert ctx[0] < next_query_time
+
+ def update_charging_schedule(
+ self,
+ next_query_time: datetime,
+ next_new_schedule_start_time=None,
+ new_schedule: Optional[pd.DataFrame] = None,
+ ):
+ """
+ Update charging schedule and contiguity values.
+
+ Args:
+ next_query_time: Current query time
+ next_new_schedule_start_time: Start time for next schedule
+ new_schedule: New charging schedule to add
+ """
+
+ def _protocol_no_new_schedule(next_new_schedule_start_time):
+ """
+ 1. Confirm that charging is not in progress and sleep delay is not required
+ """
+ if self.is_contiguous is True:
+ self.sleep_delay = self.check_if_contiguity_sleep_required(
+ self.all_schedules[0][0], next_new_schedule_start_time
+ )
+ else:
+ pass
+
+ def _protocol_new_schedule(
+ new_schedule, next_query_time, next_new_schedule_start_time
+ ):
+ """
+ 1. Modify previous schedule to end at "next_query_time"
+ 2. Append new schedule to record of existing schedules
+ 3. Confirm that charging is not in progress and sleep delay is not required
+ """
+ self.set_last_schedule_end_time(next_query_time)
+ self.all_schedules.append((new_schedule, (next_query_time, self.end_time)))
+ if self.is_contiguous is True:
+ self.sleep_delay = self.check_if_contiguity_sleep_required(
+ new_schedule, next_new_schedule_start_time
+ )
+
+ def _protocol_sleep_delay(next_new_schedule_start_time):
+ print("sleep protocol activated...")
+ assert (
+ next_new_schedule_start_time is not None
+ ), "Sleep delay next new time is None"
+ s = (
+ self.get_combined_schedule().loc[next_new_schedule_start_time:]["usage"]
+ == 0
+ )
+ delay_time = (
+ self.end_time
+ if s[s == True].empty == True
+ else s[s == True].index.min()
+ )
+ self.contiguity_values_dict = {
+ "delay_usage_window_start": delay_time,
+ "delay_in_minutes": len(s[s == False]) * 5,
+ "delay_in_intervals": len(s[s == False]),
+ "remaining_units_required": self.get_remaining_units_required(
+ delay_time
+ ),
+ "remaining_time_required": self.get_remaining_time_required(delay_time),
+ }
+
+ self.contiguity_values_dict["num_segments_complete"] = (
+ self.number_segments_complete(
+ next_query_time=self.contiguity_values_dict[
+ "delay_usage_window_start"
+ ]
+ )
+ )
+
+ if new_schedule is None:
+ _protocol_no_new_schedule(next_new_schedule_start_time)
+ else:
+ _protocol_new_schedule(
+ new_schedule, next_query_time, next_new_schedule_start_time
+ )
+
+ if self.sleep_delay is True:
+ _protocol_sleep_delay(next_new_schedule_start_time)
+ else:
+ self.contiguity_values_dict = {
+ "delay_usage_window_start": None,
+ "delay_in_minutes": None,
+ "delay_in_intervals": None,
+ "remaining_units_required": self.get_remaining_units_required(
+ next_query_time
+ ),
+ "remaining_time_required": self.get_remaining_time_required(
+ next_query_time
+ ),
+ "num_segments_complete": self.number_segments_complete(
+ next_query_time=next_query_time
+ ),
+ }
+
+ def get_combined_schedule(self, end_time: datetime = None) -> pd.DataFrame:
+ """Combine all schedules into a single DataFrame.
+
+ Args:
+ end_time (datetime, optional): Optional cutoff time for the combined schedule
+
+ Returns:
+ pd.DataFrame: Combined schedule of all charging segments
+ """
+ schedule_segments = []
+ for s, ctx in self.all_schedules:
+ schedule_segments.append(s[s.index < ctx[1]])
+ combined_schedule = pd.concat(schedule_segments)
+
+ if end_time:
+ last_segment_start_time = end_time
+ combined_schedule = combined_schedule.loc[:last_segment_start_time]
+
+ return combined_schedule
+
+ def check_if_contiguity_sleep_required(self, usage_plan, next_query_time):
+ """Check if charging needs to be paused for contiguity.
+
+ Args:
+ usage_plan (pd.DataFrame): Planned charging schedule
+ next_query_time (datetime): Time of next schedule update
+
+ Returns:
+ bool: True if charging needs to be paused
+ """
+ return bool(
+ usage_plan.loc[(next_query_time - timedelta(minutes=5))]["usage"] > 0
+ )
+
+ def number_segments_complete(self, next_query_time: datetime = None):
+ """Calculate number of completed charging segments.
+
+ Args:
+ next_query_time (datetime, optional): Time to check completion status
+
+ Returns:
+ int: Number of completed charging segments
+ """
+ if self.is_contiguous is True:
+ combined_schedule = self.get_combined_schedule()
+ completed_schedule = combined_schedule.loc[:next_query_time]
+ charging_indicator = completed_schedule["usage"].astype(bool).sum()
+ return bisect.bisect_right(
+ list(accumulate(self.charge_per_segment)), (charging_indicator * 5)
+ )
+ else:
+ return None
+
+
+class RequerySimulator:
+ def __init__(
+ self,
+ moers_list,
+ requery_dates,
+ region="CAISO_NORTH",
+ window_start=datetime(2025, 1, 1, hour=21, second=1, tzinfo=UTC),
+ window_end=datetime(2025, 1, 2, hour=8, second=1, tzinfo=UTC),
+ usage_time_required_minutes=240,
+ usage_power_kw=2,
+ charge_per_segment=None,
+ ):
+ self.moers_list = moers_list
+ self.requery_dates = requery_dates
+ self.region = region
+ self.window_start = window_start
+ self.window_end = window_end
+ self.usage_time_required_minutes = usage_time_required_minutes
+ self.usage_power_kw = usage_power_kw
+ self.charge_per_segment = charge_per_segment
+
+ self.username = os.getenv("WATTTIME_USER")
+ self.password = os.getenv("WATTTIME_PASSWORD")
+ self.wt_opt = WattTimeOptimizer(self.username, self.password)
+
+ def _get_initial_plan(self):
+ return self.wt_opt.get_optimal_usage_plan(
+ region=self.region,
+ usage_window_start=self.window_start,
+ usage_window_end=self.window_end,
+ usage_time_required_minutes=self.usage_time_required_minutes,
+ usage_power_kw=self.usage_power_kw,
+ charge_per_segment=self.charge_per_segment,
+ optimization_method="simple",
+ moer_data_override=self.moers_list[0][["point_time", "value"]],
+ )
+
+ def simulate(self):
+ initial_plan = self._get_initial_plan()
+ recalculator = WattTimeRecalculator(
+ initial_schedule=initial_plan,
+ start_time=self.window_start,
+ end_time=self.window_end,
+ total_time_required=self.usage_time_required_minutes,
+ charge_per_segment=self.charge_per_segment,
+ )
+
+ # check to see the status of my segments to know if I should requery at all
+ # if I do need to requery, then I need time required + segments remaining
+ # if I don't then I store the state of my recalculator as is
+
+ for i, new_window_start in enumerate(self.requery_dates[1:], 1):
+ new_time_required = recalculator.get_remaining_time_required(
+ new_window_start
+ )
+ if new_time_required > 0.0:
+ next_plan = self.wt_opt.get_optimal_usage_plan(
+ region=self.region,
+ usage_window_start=new_window_start,
+ usage_window_end=self.window_end,
+ usage_time_required_minutes=new_time_required,
+ usage_power_kw=self.usage_power_kw,
+ charge_per_segment=self.charge_per_segment,
+ optimization_method="simple",
+ moer_data_override=self.moers_list[i][["point_time", "value"]],
+ )
+ recalculator.update_charging_schedule(
+ new_schedule=next_plan,
+ next_query_time=new_window_start,
+ next_new_schedule_start_time=None,
+ )
+ else:
+ return recalculator
diff --git a/watttime_optimizer/battery.py b/watttime_optimizer/battery.py
new file mode 100644
index 00000000..eaa8c536
--- /dev/null
+++ b/watttime_optimizer/battery.py
@@ -0,0 +1,314 @@
+# encode the variable power curves
+from dataclasses import dataclass
+import pandas as pd
+import numpy as np
+import matplotlib.pyplot as plt
+
+@dataclass
+class Battery:
+ capacity_kWh: float
+ charging_curve: pd.DataFrame # columns SoC and kW
+ initial_soc: float = 0.2
+
+ def plot_charging_curve(self, ax=None):
+ """Plot the variabel charging curve of the battery"""
+ ax = self.charging_curve.set_index("SoC").plot(
+ ax=ax,
+ grid=True,
+ ylabel="kW",
+ legend=False,
+ title=f"Charging curve \nBattery capacity: {self.capacity_kWh} kWh"
+ )
+ if ax is None:
+ plt.show()
+
+ def get_usage_power_kw_df(self, max_capacity_fraction=0.95):
+ """
+ Output the variable charging curve in the format that optimizer accepts.
+ That is, dataframe with index "time" in minutes and "power_kw" which
+ tells us the average power consumption in a five minute interval
+ after an elapsed amount of time of charging.
+ """
+ capacity_kWh = self.capacity_kWh
+ initial_soc = self.initial_soc
+ # convert SoC column to numpy array for faster access
+ soc_array = self.charging_curve["SoC"].values
+ kW_array = self.charging_curve["kW"].values
+
+ def get_kW_at_SoC(soc):
+ """Linear interpolation to get charging rate at any SoC."""
+ idx = np.searchsorted(soc_array, soc)
+ if idx == 0:
+ return kW_array[0]
+ elif idx >= len(soc_array):
+ return kW_array[-1]
+ m1, m2 = soc_array[idx - 1], soc_array[idx]
+ p1, p2 = kW_array[idx - 1], kW_array[idx]
+ return p1 + (soc - m1) / (m2 - m1) * (p2 - p1)
+
+ # iterate over seconds
+ result = []
+ secs_elapsed = 0
+ charged_kWh = capacity_kWh * initial_soc
+ kW_by_second = []
+ while charged_kWh < capacity_kWh * max_capacity_fraction:
+ secs_elapsed += 1
+ curr_soc = charged_kWh / capacity_kWh
+ curr_kW = get_kW_at_SoC(curr_soc)
+ kW_by_second.append(curr_kW)
+ charged_kWh += curr_kW / 3600
+
+ if secs_elapsed % 300 == 0:
+ result.append((int(secs_elapsed / 60 - 5), pd.Series(kW_by_second).mean()))
+ kW_by_second = []
+
+ return pd.DataFrame(columns=["time", "power_kw"], data=result)
+
+
+CARS = {
+ # pulled data from https://www.fastnedcharging.com/en/brands-overview
+ # this is a subset of the cars
+ "audi": [ # 71kWh, https://www.fastnedcharging.com/en/brands-overview/audi
+ [0.0, 120.0],
+ [0.6, 120.0],
+ [1.0, 30.0],
+ ],
+ "bmw": [ # 42.2kWh, https://www.fastnedcharging.com/en/brands-overview/bmw
+ [0.0, 40.0],
+ [0.85, 50.0],
+ [1.0, 5.0],
+ ],
+ 'bolt':[
+ [0.0, 50.0],
+ [0.5, 50.0],
+ [0.93, 20.0],
+ [1.0, 0.5],
+ ],
+ "honda": [ # 35.5kWh, https://www.fastnedcharging.com/en/brands-overview/honda
+ [0.0, 40.0],
+ [0.4, 40.0],
+ [0.41, 30.0],
+ [0.70, 30.0],
+ [0.71, 20.0],
+ [0.95, 20.0],
+ [1.0, 10.0],
+ ],
+ "lucid": [ # 112kWh https://www.fastnedcharging.com/en/brands-overview/lucid
+ [0.0, 300.0],
+ [1.0, 50.0],
+ ],
+ "mazda": [ #35.5kWh https://www.fastnedcharging.com/en/brands-overview/mazda
+ [0.0, 50.0],
+ [0.2, 50.0],
+ [0.21, 40.0],
+ [1.0, 10.0],
+ ],
+ "subaru": [ # 75kWh https://www.fastnedcharging.com/en/brands-overview/subaru
+ [0.0, 150.0],
+ [0.25, 150.0],
+ [0.85, 30.0],
+ [1.00, 30.0],
+ ],
+ "tesla": [ # ??kWh https://www.fastnedcharging.com/en/brands-overview/tesla
+ [0.0, 180.0],
+ [0.4, 190.0],
+ [0.9, 40.0],
+ [1.0, 40.0],
+ ],
+ "volkswagen": [ # 24.2kWh https://www.fastnedcharging.com/en/brands-overview/volkswagen?model=e-Golf
+ [0.0, 40.0],
+ [0.1, 40.0],
+ [0.75, 45.0],
+ [0.81, 23.0],
+ [0.92, 17.0],
+ [0.95, 9.0],
+ [1.0, 9.0],
+ ]
+}
+
+TZ_DICTIONARY = {
+ "AECI": "America/Chicago",
+ "AVA": "America/Los_Angeles",
+ "AZPS": "America/Phoenix",
+ "BANC": "America/Los_Angeles",
+ "BPA": "America/Los_Angeles",
+ "CAISO_ESCONDIDO": "America/Los_Angeles",
+ "CAISO_LONGBEACH": "America/Los_Angeles",
+ "CAISO_NORTH": "America/Los_Angeles",
+ "CAISO_PALMSPRINGS": "America/Los_Angeles",
+ "CAISO_REDDING": "America/Los_Angeles",
+ "CAISO_SANBERNARDINO": "America/Los_Angeles",
+ "CAISO_SANDIEGO": "America/Los_Angeles",
+ "CHPD": "America/Los_Angeles",
+ "CPLE": "America/New_York",
+ "CPLW": "America/New_York",
+ "DOPD": "America/Los_Angeles",
+ "DUK": "America/New_York",
+ "ELE": "America/Denver",
+ "ERCOT_AUSTIN": "America/Chicago",
+ "ERCOT_COAST": "America/Chicago",
+ "ERCOT_EASTTX": "America/Chicago",
+ "ERCOT_HIDALGO": "America/Chicago",
+ "ERCOT_NORTHCENTRAL": "America/Chicago",
+ "ERCOT_PANHANDLE": "America/Chicago",
+ "ERCOT_SANANTONIO": "America/Chicago",
+ "ERCOT_SECOAST": "America/Chicago",
+ "ERCOT_SOUTHTX": "America/Chicago",
+ "ERCOT_WESTTX": "America/Chicago",
+ "FMPP": "America/New_York",
+ "FPC": "America/New_York",
+ "FPL": "America/New_York",
+ "GVL": "America/New_York",
+ "IID": "America/Los_Angeles",
+ "IPCO": "America/Boise",
+ "ISONE_CT": "America/New_York",
+ "ISONE_ME": "America/New_York",
+ "ISONE_NEMA": "America/New_York",
+ "ISONE_NH": "America/New_York",
+ "ISONE_RI": "America/New_York",
+ "ISONE_SEMA": "America/New_York",
+ "ISONE_VT": "America/New_York",
+ "ISONE_WCMA": "America/New_York",
+ "JEA": "America/New_York",
+ "LDWP": "America/Los_Angeles",
+ "LGEE": "America/New_York",
+ "MISO_INDIANAPOLIS": "America/Indiana/Indianapolis",
+ "MISO_N_DAKOTA": "America/North_Dakota/Center",
+ "MPCO": "America/Denver",
+ "NEVP": "America/Los_Angeles",
+ "NYISO_NYC": "America/New_York",
+ "PACE": "America/Denver",
+ "PACW": "America/Los_Angeles",
+ "PGE": "America/Los_Angeles",
+ "PJM_CHICAGO": "America/Chicago",
+ "PJM_DC": "America/New_York",
+ "PJM_EASTERN_KY": "America/New_York",
+ "PJM_EASTERN_OH": "America/New_York",
+ "PJM_ROANOKE": "America/New_York",
+ "PJM_NJ": "America/New_York",
+ "PJM_SOUTHWEST_OH": "America/New_York",
+ "PJM_WESTERN_KY": "America/New_York",
+ "PNM": "America/Denver",
+ "PSCO": "America/Denver",
+ "PSEI": "America/Los_Angeles",
+ "SC": "America/New_York",
+ "SCEG": "America/New_York",
+ "SCL": "America/Los_Angeles",
+ "SEC": "America/New_York",
+ "SOCO": "America/Chicago",
+ "SPA": "America/Chicago",
+ "SPP_FORTPECK": "America/Denver",
+ "SPP_KANSAS": "America/Chicago",
+ "SPP_KC": "America/Chicago",
+ "SPP_MEMPHIS": "America/Chicago",
+ "SPP_ND": "America/North_Dakota/Beulah",
+ "SPP_OKCTY": "America/Chicago",
+ "SPP_SIOUX": "America/Chicago",
+ "SPP_SPRINGFIELD": "America/Chicago",
+ "SPP_SWOK": "America/Chicago",
+ "SPP_TX": "America/Chicago",
+ "SPP_WESTNE": "America/Chicago",
+ "SRP": "America/Phoenix",
+ "TAL": "America/New_York",
+ "TEC": "America/New_York",
+ "TEPC": "America/Phoenix",
+ "TID": "America/Los_Angeles",
+ "TPWR": "America/Los_Angeles",
+ "TVA": "America/Chicago",
+ "WACM": "America/Denver",
+ "WALC": "America/Phoenix",
+ "WAUW": "America/Denver",
+}
+
+MOER_REGION_LIST = [
+ "AECI",
+ "AVA",
+ "AZPS",
+ "BANC",
+ "BPA",
+ "CAISO_ESCONDIDO",
+ "CAISO_LONGBEACH",
+ "CAISO_NORTH",
+ "CAISO_PALMSPRINGS",
+ "CAISO_REDDING",
+ "CAISO_SANBERNARDINO",
+ "CAISO_SANDIEGO",
+ "CHPD",
+ "CPLE",
+ "CPLW",
+ "DOPD",
+ "DUK",
+ "ELE",
+ "ERCOT_AUSTIN",
+ "ERCOT_COAST",
+ "ERCOT_EASTTX",
+ "ERCOT_HIDALGO",
+ "ERCOT_NORTHCENTRAL",
+ "ERCOT_PANHANDLE",
+ "ERCOT_SANANTONIO",
+ "ERCOT_SECOAST",
+ "ERCOT_SOUTHTX",
+ "ERCOT_WESTTX",
+ "FMPP",
+ "FPC",
+ "FPL",
+ "GVL",
+ "IID",
+ "IPCO",
+ "ISONE_CT",
+ "ISONE_ME",
+ "ISONE_NEMA",
+ "ISONE_NH",
+ "ISONE_RI",
+ "ISONE_SEMA",
+ "ISONE_VT",
+ "ISONE_WCMA",
+ "JEA",
+ "LDWP",
+ "LGEE",
+ "MISO_INDIANAPOLIS",
+ "MISO_N_DAKOTA",
+ "MPCO",
+ "NEVP",
+ "NYISO_NYC",
+ "PACE",
+ "PACW",
+ "PGE",
+ "PJM_CHICAGO",
+ "PJM_DC",
+ "PJM_EASTERN_KY",
+ "PJM_EASTERN_OH",
+ "PJM_NJ",
+ "PJM_SOUTHWEST_OH",
+ "PJM_WESTERN_KY",
+ "PNM",
+ "PSCO",
+ "PSEI",
+ "SC",
+ "SCEG",
+ "SCL",
+ "SEC",
+ "SOCO",
+ "SPA",
+ "SPP_FORTPECK",
+ "SPP_KANSAS",
+ "SPP_KC",
+ "SPP_MEMPHIS",
+ "SPP_ND",
+ "SPP_OKCTY",
+ "SPP_SIOUX",
+ "SPP_SPRINGFIELD",
+ "SPP_SWOK",
+ "SPP_TX",
+ "SPP_WESTNE",
+ "SRP",
+ "TAL",
+ "TEC",
+ "TEPC",
+ "TID",
+ "TPWR",
+ "TVA",
+ "WACM",
+ "WALC",
+ "WAUW",
+]
\ No newline at end of file
diff --git a/watttime_optimizer/evaluator/__init__.py b/watttime_optimizer/evaluator/__init__.py
new file mode 100644
index 00000000..faf2b222
--- /dev/null
+++ b/watttime_optimizer/evaluator/__init__.py
@@ -0,0 +1 @@
+from watttime.api import *
\ No newline at end of file
diff --git a/watttime_optimizer/evaluator/analysis.py b/watttime_optimizer/evaluator/analysis.py
new file mode 100644
index 00000000..9b4607dc
--- /dev/null
+++ b/watttime_optimizer/evaluator/analysis.py
@@ -0,0 +1,68 @@
+
+from watttime_optimizer.evaluator.evaluator import RecalculationOptChargeEvaluator
+from watttime_optimizer.evaluator.evaluator import OptChargeEvaluator
+from watttime_optimizer.evaluator.evaluator import ImpactEvaluator
+import numpy as np
+import tqdm
+
+
+# 4 seconds per row, mostly API call
+def analysis_loop(region, input_dict,username,password):
+ oce = OptChargeEvaluator(username=username,password=password)
+ results = {}
+ for key,value in tqdm.tqdm(input_dict.items()):
+ value.update({'region':region,'tz_convert':True, "verbose":False})
+ df = oce.get_schedule_and_cost_api(**value)
+ m, b = np.polyfit(np.arange(len(df.pred_moer.values)),df.pred_moer.values, 1)
+ stddev = df.pred_moer.std()
+ r = ImpactEvaluator(username,password,df).get_all_emissions_values(region=region)
+ r.update({'m':m,'b':b,'stddev':stddev})
+ results.update({key:r})
+ return results
+
+# 4 seconds per row, mostly API call
+def analysis_loop_requery(region, input_dict, interval,username,password):
+ roce = RecalculationOptChargeEvaluator(username,password)
+ results = {}
+ for key,value in tqdm.tqdm(input_dict.items()):
+ value.update(
+ {'region':region,
+ 'tz_convert':True,
+ "optimization_method": "auto",
+ "verbose":False,
+ "interval":interval,
+ "charge_per_segment":None}
+ )
+ df = roce.fit_recalculator(**value).get_combined_schedule()
+ m, b = np.polyfit(np.arange(len(df.pred_moer.values)),df.pred_moer.values, 1)
+ stddev = df.pred_moer.std()
+ r = ImpactEvaluator(username,password,df).get_all_emissions_values(region=region)
+ r.update({'m':m,'b':b,'stddev':stddev})
+ results.update({key:r})
+ return results
+
+# 4 seconds per row, mostly API call
+def analysis_loop_requery_contiguous(region, input_dict, interval,username,password):
+ roce = RecalculationOptChargeEvaluator(username,password)
+ results = {}
+ for key,value in tqdm.tqdm(input_dict.items()):
+ try:
+ value.update(
+ {'region':region,
+ 'tz_convert':True,
+ "optimization_method": "auto",
+ "verbose":False,
+ "interval":interval,
+ "contiguous":True
+ }
+ )
+ df = roce.fit_recalculator(**value).get_combined_schedule()
+ m, b = np.polyfit(np.arange(len(df.pred_moer.values)),df.pred_moer.values, 1)
+ stddev = df.pred_moer.std()
+ r = ImpactEvaluator(username,password,df).get_all_emissions_values(region=region)
+ r.update({'m':m,'b':b,'stddev':stddev})
+ results.update({key:r})
+ except:
+ print('error')
+ pass
+ return results
\ No newline at end of file
diff --git a/watttime/evaluator/evaluator.py b/watttime_optimizer/evaluator/evaluator.py
similarity index 91%
rename from watttime/evaluator/evaluator.py
rename to watttime_optimizer/evaluator/evaluator.py
index 1b4bbb8f..a877f78c 100644
--- a/watttime/evaluator/evaluator.py
+++ b/watttime_optimizer/evaluator/evaluator.py
@@ -1,6 +1,7 @@
-from watttime.api import WattTimeOptimizer, WattTimeForecast, WattTimeHistorical, WattTimeRecalculator
+from watttime.api import WattTimeForecast, WattTimeHistorical
+from watttime_optimizer.api_opt import WattTimeOptimizer, WattTimeRecalculator
import pandas as pd
-from watttime.evaluator.utils import convert_to_utc, get_timezone_from_dict
+from watttime_optimizer.evaluator.utils import convert_to_utc, get_timezone_from_dict
import numpy as np
from typing import Optional
from datetime import timedelta
@@ -87,11 +88,11 @@ def get_forecast_emissions(self):
"""
Calculate total CO2 emissions in pounds
Args:
- x: Input dictionary containing 'emissions_co2e_lb' key
+ x: Input dictionary containing 'emissions_co2_lb' key
Returns:
Sum of CO2 emissions
"""
- return self.obj["emissions_co2e_lb"].sum()
+ return self.obj["emissions_co2_lb"].sum()
def get_baseline_emissions(self,region:str):
"""
@@ -111,6 +112,9 @@ def get_all_emissions_values(self,region:str):
'actual':self.get_actual_emissions(region)
}
+ def plot_predicated_moer(self):
+ self.obj["pred_moer"].plot()
+
def get_timeseries_stats(self,df: pd.DataFrame, col:str = "pred_moer"):
''' Dispersion, slope, and intercept of the moer forecast'''
m, b = np.polyfit(np.arange(len(df[col].values)),df[col].values, 1)
@@ -157,7 +161,7 @@ def get_schedule_and_cost_api(
region:str = 'CAISO_NORTH',
optimization_method: str = "auto",
constraints: Optional[dict] = None,
- charge_per_interval: Optional[list] = None,
+ charge_per_segment: Optional[list] = None,
tz_convert: bool = False,
verbose:bool=False
) -> pd.DataFrame:
@@ -176,7 +180,7 @@ def get_schedule_and_cost_api(
MOER forecast data
optimization_method : str, optional
Optimization method (default: "auto")
- charge_per_interval : list, optional
+ charge_per_segment : list, optional
List of charging constraints per interval
Returns:
@@ -198,13 +202,13 @@ def get_schedule_and_cost_api(
usage_power_kw=usage_power_kw,
optimization_method=optimization_method,
moer_data_override=self.moer_data_override(start_time = usage_window_start, end_time = usage_window_end, region=region),
- charge_per_interval=charge_per_interval,
+ charge_per_segment=charge_per_segment,
constraints=constraints,
verbose=verbose
)
# Validate emissions data
- if schedule["emissions_co2e_lb"].sum() == 0.0:
+ if schedule["emissions_co2_lb"].sum() == 0.0:
self._log_zero_emissions_warning(
usage_power_kw,
time_needed,
@@ -244,7 +248,7 @@ def fit_recalculator(
region:str = 'CAISO_NORTH',
optimization_method: str = "auto",
constraints: Optional[dict] = None,
- charge_per_interval: Optional[list] = None,
+ charge_per_segment: Optional[list] = None,
tz_convert: bool = False,
verbose:bool=False,
contiguous:bool=False
@@ -260,7 +264,7 @@ def fit_recalculator(
usage_window_end=usage_window_end,
time_needed=time_needed,
usage_power_kw=usage_power_kw,
- charge_per_interval=charge_per_interval,
+ charge_per_segment=charge_per_segment,
optimization_method=optimization_method,
constraints=constraints,
verbose=verbose,
@@ -272,7 +276,7 @@ def fit_recalculator(
start_time=usage_window_start,
end_time=usage_window_end,
total_time_required=time_needed,
- charge_per_interval=charge_per_interval,
+ charge_per_segment=charge_per_segment,
contiguous=contiguous
)
@@ -290,7 +294,7 @@ def fit_recalculator(
usage_window_end=usage_window_end,
usage_time_required_minutes=optimization_outcomes["remaining_time_required"],
usage_power_kw=usage_power_kw,
- charge_per_interval=[int(optimization_outcomes["remaining_time_required"])] if recalculator.is_contiguous else None,
+ charge_per_segment=[int(optimization_outcomes["remaining_time_required"])] if recalculator.is_contiguous else None,
optimization_method=optimization_method,
moer_data_override=self.moer_data_override(start_time,usage_window_end,region),
verbose=verbose
diff --git a/watttime/evaluator/sessions.py b/watttime_optimizer/evaluator/sessions.py
similarity index 98%
rename from watttime/evaluator/sessions.py
rename to watttime_optimizer/evaluator/sessions.py
index 9c7736d3..98226a58 100644
--- a/watttime/evaluator/sessions.py
+++ b/watttime_optimizer/evaluator/sessions.py
@@ -1,6 +1,5 @@
-from typing import List, Any, Optional
+from typing import List, Any
from datetime import datetime, timedelta, date
-import pytz
import pandas as pd
import random
import numpy as np
@@ -163,7 +162,7 @@ def synthetic_user_data(self, distinct_date_list, **kwargs) -> pd.DataFrame:
user_df["initial_charge"] = user_df.apply(
lambda _: random.uniform(self.minimum_battery_starting_capacity, 0.8), axis=1
)
- user_df["usage_time_required_in_minutes"] = user_df["initial_charge"].apply(
+ user_df["time_needed"] = user_df["initial_charge"].apply(
lambda x: total_capacity
* (self.max_percent_capacity - x)
/ power_output_max_rate
@@ -172,7 +171,7 @@ def synthetic_user_data(self, distinct_date_list, **kwargs) -> pd.DataFrame:
# What time will the battery reach max capacity
user_df["expected_baseline_charge_complete_timestamp"] = user_df["usage_window_start"] + pd.to_timedelta(
- user_df["usage_time_required_in_minutes"], unit="m"
+ user_df["time_needed"], unit="m"
)
user_df["window_length_in_minutes"] = (
user_df.usage_window_end - user_df.usage_window_start
diff --git a/watttime/evaluator/utils.py b/watttime_optimizer/evaluator/utils.py
similarity index 100%
rename from watttime/evaluator/utils.py
rename to watttime_optimizer/evaluator/utils.py
diff --git a/watttime_optimizer/examples/ev.ipynb b/watttime_optimizer/examples/ev.ipynb
new file mode 100644
index 00000000..e4967d1e
--- /dev/null
+++ b/watttime_optimizer/examples/ev.ipynb
@@ -0,0 +1,199 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# EV Smart Scheduling"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 118,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import os\n",
+ "os.chdir(path=os.path.dirname(os.path.dirname(os.path.abspath(os.curdir))))"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 119,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "== Simple fit! ==\n"
+ ]
+ }
+ ],
+ "source": [
+ "from datetime import datetime, timedelta\n",
+ "import pandas as pd\n",
+ "from pytz import UTC\n",
+ "from watttime_optimizer import WattTimeOptimizer\n",
+ "import os\n",
+ "\n",
+ "username = os.getenv(\"WATTTIME_USER\")\n",
+ "password = os.getenv(\"WATTTIME_PASSWORD\")\n",
+ "wt_opt = WattTimeOptimizer(username, password)\n",
+ "\n",
+ "# 12 hour charge window (720/60 = 12)\n",
+ "now = datetime.now(UTC)\n",
+ "window_start = now\n",
+ "window_end = now + timedelta(minutes=720)\n",
+ "\n",
+ "usage_plan = wt_opt.get_optimal_usage_plan(\n",
+ " region=\"CAISO_NORTH\",\n",
+ " usage_window_start=window_start,\n",
+ " usage_window_end=window_end,\n",
+ " usage_time_required_minutes=240,\n",
+ " usage_power_kw=12,\n",
+ " optimization_method=\"auto\",\n",
+ ")"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 120,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "def plot_predicated_moer(df):\n",
+ " df.pred_moer.plot()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 121,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "def plot_charging_kw(df):\n",
+ " df.usage.plot(kind=\"bar\")"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 27,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "image/png": "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fvhhd12111l7jdOmTYtaZ901Tp482UaNGuWuq6E1Rn7n/fv3N5/PZ6tWrbJhw4aZz+dzD5tt27bNfdHPnz//S3+XoVDIxo8fbxMnTqz3u6y9xmnTplkgELALL7zQXRfPN883zzfPN8/38fF8Dx482M444wz3+Z42bZr9z//8z1ez2SguLrY33nij3nj16tXm8/nswgsvNJ/PZ6tXrzZJNmTIEPeBXHjhhRYOh61Fixb27LPP2vvvv+92lddff729+OKLJsm+853v2IsvvmjFxcXWv39/69Onj6WlpdnXv/51d9vMrFu3btaxY0e7+OKLo7Yj6/rtb39rPp8varv2Oh3n8JfWFBYWuk/86NGj7ZZbbjHHcY66Rsdx7Omnn7YXX3zRwuGwdenSxV1X7XXWXVftcd01vvHGG/bee++56zraGhcuXGgpKSnWokUL+9///V9LSUmJWlekE77rrru+9Hd5xhln2H333WfnnXdevd9l7TXW/r3WXiPPN883zzfPN8/3V//5NjP3O80i6o7j4WmzMXr0aPe9n7rjs88+29LS0tyF1x0/8cQT1q9fP1u9erV1797devfubY7jNPheVeT+2267zVq2bGmSrHPnzlGHgyRZMBi0Ll26mOM4lpuba6Wlpe66JkyYYIMHD47arruuumuMrLNHjx4xr1GSe4isR48eNnDgQHedkTUOHjzYevToEbXOumucPHmyrVu3zgYPHhzTGiO/y8jht9onC9Vd49F+l5GfS09Pr/e7rPu7qz3m+eb55vnm+eb5Pn6ebzNz1xRRdxwPT6+zsXLlSi1fvlzXXnttvfHOnTu1du1avfbaa7rrrrvqjf/2t78pOTlZgwcP1sGDB3XllVfq/fff12uvvaaWLVvqjjvu0Pz58/X888+rbdu2buZLL72klStX6tChQ1q8eLF27typlJQUpaamqqioSIWFherbt6+GDRsmn89Xb11111x7XTfeeGPUGiW56+zXr19CaywrK9PGjRt18OBBtWjRQqmpqXIcRwUFBVHrbIo1Dh48WC+99JJ+//vfa926dZo1a5Z+/etfN7jGRH6XPN883zzfPN883yfG893UuKgXAADwlKdfxAYAAHDMriDatm1btWvXTq+//ro73rNnjzIzM2VmateuncxM69evl5nFXVu4cKEGDx7cqDnIJptssskmm+zo/davXy9JGjp0qNavX++O43HMmo3x48erefPmUeM1a9aoS5cuMjO39vnnn8vM4q5VVVVp5MiRjZqDbLLJJptsssmO3i/ikksu0eeff65EcM4GAADwFOdsADiu1f73Ut1/OzV1LdE5gJPdf/XIRmlpqe666y797ne/qzdOtFZSUqKbb75ZM2fOVDAYdLc7d+6s8vJy/fSnP9XVV1+twsJCdzvRWtu2bbVo0SL961//0tSpU2Oe4+DBg3r44Yc1ZcoUdezYUS+88ILuvPNOderUSRdffLG2bNmilStXKiUlxT3PJTs7O6p28OBBZWdnq6Ki4qj7NaZ22mmn6ZlnntGWLVvk8/lUWVmp6upqSZLjODIz+f1+paenq6amRpLk8/m0d+9eVVdXe1Krm52dna2ioiL169dPEydOVHFxcYOvLy9ea4m8Jjt37hw19vq1Fk9txYoV7uvyiy++0D333KNAIHBMXmuxvs4HDhyoJUuWqLKyUmPGjNG5556rpKQk/fvf/1anTp2itiU1eS3eOd544w0tXrxYJSUlKiws1IgRIzRv3jyVlJQoKytLgUBAn3/++VFrse4XT+2LL75QamqqqqurZWbKzc3VvHnztG7dOqWkpCgpKUn79++X4zgKBAI6dOiQcnJy1LVrV61fv16bNm1q8lrd7L179yo9PV2SVF1dreuuu07Tpk2TJN1444264oorNGDAgKjtRGuJziFJv/jFL7Rs2TJddNFFuuKKKzR27Fj99a9/df8/tKKiQp988onS0tIUCAS0efPmerVDhw4pKSlJZnbU/RpTKy4u1ooVK7R//36FQiEFAgHV1NS4H3Otqalxv7E28v+vkuT3+9W2bVuNHDlSEyZMcO+Pm/0XHe1LXhKprV271v0GPsdx3C8C8vl81qtXr6jrzEcuapJoLbIdubBLPHNEfiYcDtvdd98ddRGWyP/6fD73WxMjl6Kt/fO1x0fa72hzfFmtXbt27hrz8vJMkmVlZVm7du3cfdu3b29ZWVnuRWHS09Pd/Xr06NHktbrZeXl5lpSUZKNGjbLOnTtbenq6vfXWW032eoqnFstrMvJaiFzO+Vi81mKtde3a1YLBoHshosglj4/Fay3W13nz5s1NkuXl5VlaWppJcl+nzZo1c1+nke2646aofdl+gUDAunXrZqeffrq79uTkZGvVqpV7Zcfc3Fw7++yz3f/u+/XrF1WLfOeI4zh2+umnH3G/o80RTy1yYaexY8dadna2+Xw+CwaD1qdPH/d5GTt2rPvf4pAhQ+yaa65p8lpD2ZH/H4hs5+fn23333ee+dtq3bx+1nWgt0TnOO+88S0tLs8suu8wKCgrsvPPOM8dxrEuXLlF/f9LS0tzXavPmzaNqSUlJ7nOQlJR0xP2ONseX1SK/z7S0NPf/f8LhsGVmZrrPR7Nmzdz/1jIzM901XXXVVXbllVdaVlaW9e3b1/bs2ZPQ33tPTxB96aWXosZLlixxtz/66CPt2rVLNTU1+v73vx81HjVqVEy1UaNGSZI7vvjiixUIHH5I3bp108cff6yDBw9q/vz5uvLKK7Vr1y5Jh8+ofe+991RZWZlwrX///iopKVEgENCnn36q0tLSmObo0aOHdu3apQ0bNmjmzJm6+eab1atXLy1fvlynnHKK1q9fr1atWunee+/VmDFjVFRUpE6dOmnjxo1uLRAIqLS0VKeffrr27t2rNWvWNLjf0eaIpVZYWKjTTjtN5eXlat26tbKyslReXq7JkydLkt566y1lZma6RyMkudtLly5Vr169mrQmKSr7Jz/5iR5//HG99957uuKKK/T6669r1KhRuvbaaxN6PcXzWqtbO9IctV+Tv/rVr3THHXeooqLimLzW4qmNHDlSycnJqqqqUps2bbRp0yZVVVWpXbt2x+S1FsvrfNWqVUpNTdWhQ4e0b98+BYNBbdy4UdLhCyFFXiORbcdxGlXbtWuX+6++WOeoqqrSxo0b5fP53LdRunXrppSUFG3atEnZ2dk6++yzFQ6HJUnnnnuumjVrppqaGrc2Z84c9enTR9nZ2Xr//fePuN/R5viyms/n08CBA5Wbmysz06mnnqoDBw5o9+7dmj59ulq1aqVvfetbSkpK0syZM/XrX/9aFRUVuvLKK7Vq1Srt3LmzyWt1sx3H0R133KFf//rXkg4f4dy5c6fuvfdemZn69Omj5s2bq6SkRD6fT9XV1QnXzCyhOV5++WU5jqNDhw7pzjvv1E033aTJkyfrZz/7mYqLi/Xpp5+qefPm+vvf/64ePXrooosu0tq1axUKhdxaMBjUZ599psmTJ+v555/XunXrGtzvaHPEUuvdu7d27Nih/Px8tWnTRitWrNBpp52m7Oxs/fOf/9SYMWP0yiuvaNOmTRo/frymTZumHj16aP78+SorK1N5ebnOPfdc/eAHP9BDDz2kuCXUosQo0gEe6ZKvx+rWqlUry8nJsREjRpgky8nJsfnz5zeq5vP5bO7cuTZhwgS3FsscaWlp9s4775h0+JsSpcNfPuTz+SwjI8PtMKurq02SPfTQQ+63AEZqke3Zs2e7/6JqaL+jzRFL7cEHH7T8/HwLh8P20ksvudsffvihffDBBxYOhy0cDtvcuXPrbZtZk9fqZkdeX/+t11W8t7r/DXj9WounlpaWZpdeeqn7Go0cZThWr7VYXuc+n8/y8vIsOzvb7r33XmvZsqVlZ2dbIBCwm2++2dq2bWs+n8/dnjdvXsI1n89nRUVFcc/hOI5t3brV7HCnUW88Y8YMKyoqsrZt25rjODZ37lwrKiqKqkmyP//5z7Z48WL3eWhov6PN8WW1jIwMe/LJJ62oqMgcx7Ef//jH7vazzz5r69atM+nwEcU5c+a436b64IMPWnJysuXl5TV5rW624zi2ZMkSt3bPPfeYz+dz/5tv3bq1+6/v4cOH24ABAxKuSbLMzEz72te+Fvccw4YNswEDBrjj0aNHW0lJiSUnJ1sgEHD/f02SPfPMM5aSkhJVi2y/9tpr7vecNLTf0eaIpfbUU0+5tQULFph0+KhbSUmJBQIBy8/Pt+TkZPP7/Zafn29mZq+++qpJsrKyMjMze+2116xFixYJ9QOeniBaWFiov/zlL6qpqVFNTY1atGihF154QWamFi1a6IEHHnD/BVB7HGstMt97770nn8+n9PR0/eUvf6m3/Y1vfEM7d+7UZZddJunwv0RKS0sbVfP7/br22mv1zW9+063FMkdFRYU2bNggn8+nESNGSJJ2797t/s6SkpK0b98+919UzZs3d+uRmiSFQiEVFhZqz549R9zvaHN8Wc1xHG3dulW7d+9WYWGhPvzwQ3d70aJFeuedd1RYWKjCwkI999xz9bYjz39T1hrK/s53vqO2bduqRYsWuuGGG9S2bduEX0/xvNZinaP267CmpsYdH4vXWjy1iooKXX755e5rNHI05li81mJ9naekpOjuu+/Wrl27tGPHDv385z/Xrl27VF1drQkTJmj27NmqqanRrl279Ic//EHXX399wjUz03333Rf3HGamqqoq1RY5AiJJX/va17R9+3ZVVlZKkvLy8rR9+/aoWuT+/Px89zylhvY72hxfVhs4cKDmzp2r7du3q7i4WLNmzXK3//d//1d/+tOflJmZqaKiIt1zzz1q166dW2vXrp0GDRrU5LW62WamF198Ue3atZMk7d+/X126dNGGDRskHT7iGTmva9WqVXr77bcTrjmOo29/+9vuUY5Y58jKytKKFSv09ttva968eZKkV155RaeeeqpycnJUXV2t9PR0lZSUSDp8tD8vLy+qFtn+5z//qezs7CPud7Q5Yqm9/vrrysvLU15enrvW5s2ba/Xq1aqurtauXbvcn4scAY2sJyMjQ5LUvn177dy5UwlJqEWJ0cUXX2w//OEPGxxffPHFdt1115njOPXGsdYi861YscIcx7FevXrZPffcU2/bzCwvL89SUlLcfx1FthOt9erVy4YOHWpZWVn1akebIycnx1JTU91aVlaWW+vWrZt997vftTZt2piZWYcOHez++++3tm3bRtW6detmjzzyiM2fP9/atm17xP2ONseX1b7//e9bMBi09PR0u+666ywQCFhKSor7lcY+n8/OP/9869Gjh0myLl26WOfOnU2S9ezZ06ZOndrktbrZhYWF5vP5bOLEidamTRvz+/32yCOPJPx6iue1FuscdV+Htcdev9biqdV+Xebk5Lj/mjwWr7VYX+erVq2y+fPnW+vWrW3cuHF2yimnuN+I2aFDB1u5cqUFAgH7xje+Yd26dbPFixc3uhbvHNLh98I7duzo/uu8a9eu1rNnT5MOn3MQDAYtLS3NPQcmcq5MpKb//Ms5cr7AkfY72hxfVnv88cetoKDAkpKS7LrrrnPXGll3ZA2Ro3HZ2dlWUFBgkiwlJcW6du3a5LW62ZHt888/36TD5xK98sorZnb4qHlZWZm99tprUduJ1iJHoGpqahqsHWmO73//+9a8eXMbPny4tW3b1j0/pkuXLu5/Q8Fg0HJyctxzkwoLC6NqycnJ7nk64XD4iPsdbY4vq0V+n927d7eePXua4xz+wrVLL73UUlNTrW3bttaiRQsrKiqycDhshYWF9s9//tO6d+/uHh0xO3yk45RTTvmyP/0N8vTTKG+++aYqKip0wQUX1Bu/+eab2rFjh7KzszVo0KCosc/ni6nm8/lUUVGhAQMGaNmyZVq0aJEWLFigO++8M2p70KBBmjFjhh599FGVlpZq+vTp7nZNTU1CtenTp+vNN99UmzZt9Oijj9arHWmOX/3qV/rrX/+qv/3tb6qpqdGjjz7qjn/1q19p/vz5Sk9P1+OPP65HH33UHZ955plR28XFxVq0aJG2bt1arxbLHF9Wkw5fwOXtt99WeXl5vX+tNcT5z6dEjlUtIhAIqH379ho5cqTuvfde97UW7+spntda3dqR5mjodVj7Nerlay2eWt3XYe1tr19rsb7OH3/8cX3/+993t0eOHKnXXntNlZWVuv/++/WTn/xE27dv16pVq7Ry5UrdfPPN2r59e5PUYt3v+uuvV1lZmV599VXt379fF110kc4880xJ0oIFC7R161b5/X5dfvnlWrdundavX689e/YoNzfXreXm5qqoqMit9ejRo8H9jjbHl9X69Omjl19+WfPmzZPjOFq7dq2qq6sVDAbdoyl+v181NTVyHEcpKSlyHEcHDhxQVVWVZ7Xa2YcOHVIgEFDLli31+eefa86cORo6dKikw1egXrZsmXJycqK2E60lOkd1dbXuu+8+LV68WGeffbamTJmiq6++Wi+99JIcx1HHjh1VU1OjjRs3yu/3KyMjQ2VlZVG1DRs26NChQ+45U4FAoMH9jjZHLDW/3+8e5QqFQqqurlZVVZVycnLcc8zOOussmZmWL1+umpoaderUSd/5znc0adIkSdJrr72m3bt3u+ewxYOLeuFLHTp0yL1qXGZmpnu4u/Z2bm6uJDW4nxe1uvsFg8Gmf+D4yistLdW7776roUOHaufOne52ampqk9cSnSM1NfW//WsCJElffPGFqqqqlJaWdsTtuvs1FZoNAADgrYTefInDZ599Zr///e/tlVdescrKyqjxmjVr7NJLL3Vr69atc8ex1jZu3GiPP/64XXPNNQnPQXbDtYULF1rXrl2tTZs27nuKgUDAPW8iEAiY3++3YDBoqamplpqaasFg0Px+v2c1sskmm2yyj012KBSy5ORka9OmjV199dW2dOnShHsBT5uNf/3rX5aVlWUZGRmWnJxsxcXFlpGRYRkZGRYKhdyPEUVqkQv1xFqLbKemppqkhOYgu+FaYWGhe8LbN7/5TfdjXWeddZZ7glfv3r2tTZs2JslatmxpLVu2NEnWpk0bGzlyZJPXyCabbLLJPjbZHTt2NL/fb7fccos9+OCDNnz4cEtKSrI5c+Yk1A94+jbKeeedp1atWumxxx5TRUWFOnfurO3bt+vtt9/Wbbfdps2bN6ukpERvvvmmRo8erW3btungwYM655xzYqqdeeaZ2rdvn7Zu3ary8nIVFRXFPQfZDddGjBihAwcO6IsvvlDnzp01cuRIzZgxQ506ddKYMWPkOI6efvppOY6j4uJi9zLEke3Vq1frtNNOa9Ka4zhkk0022WQfg+zVq1fr/vvv19NPP63Vq1dLUr1xXJr0UEYd2dnZtnbt2qjxlClTLDs72zIyMuztt982x3Hc8fXXX2+SYq45jmPPPfec/eAHP3Br8c5BdsO1pKQkt5aUlGRvv/22+Xw+C4VCtnbtWvvwww8tFApZKBSyv/3tb/W2zazJa2STTTbZZB+bbDNz74+oO46Hp5crlw6f1Vrb2LFjlZ+fr+9973tasmSJHMfRnXfeqe9973tq3769HMfR3r17Y6qFQiF985vf1FNPPeXW4p2D7IZrt99+u/bt2yfHcVRVVaVHH31UknTKKadozpw5kg5fJMhxHP385z+vtx3ZtylrjuOQTTbZZJN9DLIl6cUXX3S3GxrHJaEWJUYDBgxwL7JUd9y2bVv35JS641hrAwYMsEsuucRCoVC9WlPMfzJnjx071qTDF9U588wz3e2zzjorartt27YmyYqKitzLIrdt29YuueSSJq+RTTbZZJN9bLI7d+5sfr/fbr31VvvZz35mF110kQWDQfvLX/6SUD/gabPx2GOP2ZgxYxocP/bYY9azZ0/3KoK1x7HWIvPdf//9DdYaO//JnG1m9p3vfMdSUlKivrEy8l0NjuOY3+83n89nfr/fUlJSLCUlJeo+L2pkk0022WQfm+xgMGhJSUnWqlUru/LKK23RokUJ9wNcZwMAAHjK0y9iAwAAoNkAAACeotkAAACeotkAAACeotkAAACe8rTZ2Lx5s/v14JL0l7/8RZdddpkGDBigSy+9VDNnztQ111yjAQMGaODAgTrnnHPiqp111lkaPHiwzj///ITnILvhWocOHTRw4EANGDBAZ5xxhp5//nlJ0k9/+lN98skn7nNae+x1jWyyySab7GOT3eQS/tBsDPr27Wtz5841M7M5c+aYdPjLXm6//Xb3C8B69+5tl156qXthkcsuuyzmWrNmzUySOY5jffv2TWgOso9ey8vLc7d79Ojhfh576NChNmvWrKix1zWyySabbLKPTfasWbOssrKyyfoBT5uN9PR027Bhg5mZ9e7d20KhkDv2+Xw2dOhQ69mzp1ubNm2a9ezZM+aaz+ez733ve/aLX/zCUlJSEpqD7IZrjuPYNddcY5mZmSYd/n6UpKQkk2Spqan2ta99zf1W2PPOO8/OOeccz2tkk0022WQfm+xgMGg5OTk2efJkW7VqVaP7AU+bjczMTPv3v/9tZmZ5eXmWnp7ujh3Hcf9YRmovv/yypaSkxFxznMNfSLZu3TqTlNAcZDdccxzHlixZ4tYmTZpkjnP4q4gLCwutoKDAHTdv3ty9wuiFF15oHTp08KRGNtlkk032scnu0aOHXXrppdahQwfz+XzWq1cv+81vfmN79uxJqB/wtNn4xje+YXfccYeZmZ1//vl22mmnuePmzZtbp06drH379m5t+PDh1r59+5hrzZs3t6FDh9pjjz1mKSkpCc1BdsM1x3Hspz/9qbVv377e9hVXXGGpqakWDodNkqWkpERtp6am2htvvNHkNbLJJptsso9N9vjx4y01NdWdo/Y4EZ42G2vWrLGcnBwbN26c3X333ZaScviQf8+ePa1jx45uFzV06FDz+/0myVq1ahVzrfb5CpEvjYl3DrIbrkmHzwm57LLLzHEcS0pKsieeeMJ8Pp9t3brVdu/ebb/5zW/M5/PZunXr6m2bWZPXyCabbLLJPjbZZubeH1F3HA9Pmw0zs3Xr1tlVV11l6enp7mGayEkokT+YtW+J1ppiDrLr14LBoEmyJ554wswOv/21detW9/mtPfa6RjbZZJNN9rHJbmrH7IvYzEzbtm1TTU2NcnNzFQgE3HFOTo7Ky8sbVWuKOchuuBYMBo/FSwQAcILiW18BAICnAl4HbN68WY888ogWLVqksrIyVVVV6eDBg5Ikn8+nvXv3qrq6Wn6/X+np6aqpqYmrJkmO48jMEp6DbLLJJptsssmO3i8pKUnBYFD5+fnq16+fJk6cqOLiYiXEkzdn/uPNN9+0tLQ069Spk02ePNmuu+46S0pKsmbNmll6erpJsqysLOvRo4dlZWWZdPhjnLHW2rVr554H0r59+4TmIJtssskmm2yyo7ObNWtmSUlJdt1119nkyZOtc+fOlp6ebm+99VZC/YCnb6P06tVLZ599th588MF64169erkd19KlS6PGkmKqSdLZZ58tSXrrrbfq1Ro7P9lkk0022WSfjNlLly7VLbfcorfeektLly6VpHrjuDTpoYw6wuGwffjhhw2Ow+GwzZ0718LhcL1xrLXIfB988EGDtcbOTzbZZJNNNtknY7aZufdH1B3Hw9NzNgoLC7Vo0SKdeuqp9caFhYV67rnnVFhY6NZqj2OtLVq0yN2nbq0p5iebbLLJJpvskzH7nXfecbcbGscloRYlRr/85S8tKSnJJk2aZHPmzLEpU6ZYMBi0Cy+80Dp37mySrGfPnjZ16lTr0aOHSbIuXbrEXBs2bJj5fD7z+Xx2/vnnJzQH2WSTTTbZZJMdnT18+HALBoP2ve99z+bMmWOTJk2yUChkjzzySEL9gOcX9Zo1a5b17t3bAoGAeyKKpKjturdEa00xB9lkk0022WST7bi3QCBgvXv3ttmzZyfcCxyz62wcOnRIn3/+uSQpNzdXktxxZmamdu/e3ahaU8xBNtlkk0022WRH79cUF3f0Neqn4xAMBlVYWKgFCxbo4MGDUWMza3StKeYgm2yyySabbLKj94tcG6tR4j4W0kjp6en28ccfNzhuiprX85NNNtlkk032yZjdGMfsyEat5uaI46aoeT0/2WSTTTbZZJ+M2Y1xzJsNAABwkrFj7M0337QDBw40OG6Kmtfzk0022WSTTfbJmN0YfOsrAADwlKdXEP3oo4/Uvn17OY4jSXr22Wc1e/ZsrVu3TpmZmfr617+uf/3rXyopKZHjOEpJSVFFRUXMtffff1+ZmZkKBALat29fQnOQTTbZZJNNNtnR2YWFhRoyZIiWLFmikpISFRYW6sYbb9SIESMSawgafWzkKHw+n23dutXMzObPn2+SbNiwYTZ9+nSTDl84pHfv3vatb33LHU+YMCHmWu2Lk5x//vkJzUE22WSTTTbZZEdnDxw40N1v+vTpdtlll5nP57NXX301sX5AHrJa79Dcc889kqTf//73uvPOOyVJXbt2VSgU0saNGyVJN954o9avXx9zzcw0YcIE3XHHHVqyZElCc5BNNtlkk0022dHZwWBQp59+ukKhkO688079+c9/1ve+9z3de++9SkgjDlx8Kcdx3CMbhYWFUWNJ9uijj1pOTo5be+ONNywnJyfmmiSbO3eurV692r2sarxzkE022WSTTTbZ0dmFhYX2xz/+0XJycty/6atXr44ax8Pzj77u3btXe/bsUXJyctQ4Yv/+/W7t4MGD2r9/f8w1SaqqqlJlZaV7f7xzkE022WSTTTbZ0dnJyclKSUnRgQMH3FpSUlLUOC6NP35xZI7juN8oF+maImPp/74QpnZN/3nPKJZaZLv2fPHOQTbZZJNNNtlk19+eNGmStW/f3v2bPmfOnKhxPDz9NMr8+fOjxqWlpSouLpYkrVixQkuXLlVaWppGjx6t0tJSLVmyRIcOHVKnTp1iqnXq1Ek5OTlu7eyzz457DrLJJptssskmOzp79OjReu211/Ttb3/b/Ru+cePGqHE8uM4GAADwlKdHNmr75JNPVFZWJsdxlJ+fL0nu+NChQwoEAo2qNcUcZJNNNtlkk0129H75+flq3bq1GqVpzs44sgceeMCKiorc8zb0n/eJuHHjxo0bN25f3ZtT63yPoqIie/DBBxPuBTz9NMrdd9+tadOm6YYbbtDy5cs1ZcoUpaen66abblKfPn0kSQMHDtSsWbM0cOBASVKfPn1iro0aNUqhUEhJSUm64oorEpqDbLLJJptsssmOzr7pppuUlpamKVOmaPny5brhhhs0bdo095pZcWu6Yxj1FRUV2QsvvNDguKioyG6//XZr0aJFvXGstch8zz//fIO1xs5PNtlkk0022Sdjtpm590fUHcfD0yMbO3bs0KmnntrgeMeOHerXr5/Ky8vrjWOtRebr0KFDg7XGzk822WSTTTbZJ2O2JPf+iLrjeHj6aZTBgwerqKhITz75pAKBQNR4yJAh2rRpk1q1aqWFCxdq0KBB7lhSTDXHcdSiRQuZmbZs2SIzi3sOsskmm2yyySY7OnvevHkaP368Pv30Uy1YsEBVVVVR47gldDwkRitXrrSCggLLzs62kSNH2uWXX24pKSkWDAYtEAiYJEtKSrLCwkILBoMmyQKBQMy13Nxc90SW3NzchOYgm2yyySabbLKjs4PBoKWkpNgVV1xhI0eOtGbNmllhYaG9//77CfUDnl9nY+/evXrmmWe0ePFilZWVqaqqShUVFZKkUCik7du3q7KyUqFQSM2bN3cvmxprLRA4/OndqqqqhOcgm2yyySabbLKj90tNTVUgEFBBQYH69u2rq6++WhkZGUoEF/UCAACe8vyL2Oq66KKLtGXLlgbHTVHzen6yySabbLLJPhmzG6UJT9GISVpamn388ccNjpui5vX8ZJNNNtlkk30yZjfGMT+yAQAATi7HvNlo3bq1gsFgg+OmqHk9P9lkk0022WSfjNmNwQmiAADAU8fsW18jysvLtW7dOhUWFqqoqChqnJqa2uhaU8xBNtlkk0022WRH71dUVJT4H/9Gn/VxFFOnTrWKigozMzt48KD16NHD/RY5SdaqVSvz+XzuRUUkJVxrijnIJptssskmm+zDf6fbtGnjfmO7z+ezSy65xA4cOJBQP+Bps+Hz+Wzr1q1mZjZ9+nSTZL/73e/s008/dR/MVVddZbfffrtJsqysLJsyZUrMNcdxLD093Zo1a2Znn312QnOQTTbZZJNNNtnR2WPHjjWfz2dXXXWVffrpp/bXv/7VWrZsaT/+8Y8T6gc8bTYcx3GbjchRjchYko0fP946derk1n7zm99Yp06dYq5JsgcffNBmz55toVAooTnIJptssskmm+zo7B49etjEiROtU6dO7t/02bNnR43j4XmzsW3bNjMzy8nJiRpLslmzZllKSopbW7ZsmaWkpMRck2QLFy60DRs2mKSE5iCbbLLJJptssqOzc3Jy7NVXX7WUlBT3b/qGDRuixvHw/ATRxx57TGlpaQqFQlFjSfrTn/4kM3NrTz75pOw/H46JpSZJzzzzjLKzs+U4TkJzkE022WSTTTbZ0dmhUEifffaZ+/OStHv37qhxPDz96GubNm3cByAdXmhmZqYkqbS0VKFQSNXV1SosLNTu3bvlOI4qKircL4T5slpVVZU7X0VFhVJSUuKeg2yyySabbLLJjs4uLCxUjx49VFZWpnfeeUeS9LOf/UyzZ892x3Gx/6J33nnH3n333QbHTVHzen6yySabbLLJPhmz48VFvQAAgKeOyeXKa2pqGhzX1NRo48aNUfdHxvHUqqqqtGnTpkbNQTbZZJNNNtlkR+8Xub+hcVwSOh4So927d9uoUaMsHA5bXl6e3X777Xb55ZdbOBy23Nxc69Chg0lya1//+tdNUsy13Nxc69y5s4XDYZOU0Bxkk0022WSTTXZ0dl5ent16663mOI77N72srMx8Pl9C/YCnzcZNN91kHTp0sOeee84ee+wxS09Pt9TUVPvjH/9oQ4YMsUAgYJLskUcesfT0dPejOLHWzjnnHAsEAta1a1eTlNAcZJNNNtlkk012dPZjjz1mRUVFJskqKyvN7HCzUbv5iIenzUarVq1s/vz57rioqMg6depkw4YNs+LiYnvyySdNkg0bNsxatmxp7du3N0kx18LhsD333HN2+umnm6SE5iCbbLLJJptssqOzv/jiC1uzZo07xxdffPHVPbKRkpJi69evjxqvXLnS+vbtaz6fzxYuXGiO47jjuXPnmqSYaz6fz/r27WsrV650a/HOQTbZZJNNNtlkR2efe+65tn79eneOyNjn+wo2G6eeeqq98sor9cZ79+61cDhsbdu2NZ/PFzV2HCfmWvv27a1jx47WvXv3qFo8c5BNNtlkk0022dHZffv2te7du7tz1B4nwtNPowwbNkxPPPFEvXFaWprGjx+v3bt3S1LU2HGcmGsXXHCBOnbsqHA4HFWLZw6yySabbLLJJjs6++9//7vC4bA7R+1xIjy9zkZ5ebk+++wzdenSpd64vLxc69at0/79+zVo0KCocbdu3WKqdevWTZ999plat26t5cuX16s1dn6yySabbLLJPhmzBw0apH379mn58uUaNGiQJNUbx4OLegEAAE95/kVsFRUVevbZZ7Vo0SKVlZWpurpaFRUVkqRwOKxt27bpiy++UDgcVl5enr744ou4aklJSZKkgwcPJjwH2WSTTTbZZJMdvV9qaqoCgYDy8/PVv39/jR49WqmpqUpIU5wIeiSrV6+2Fi1aWFZWlo0YMcJGjRplKSkpFgwGze/3m+M4FgwGraCgwILBoDmOY4FAIOZabm6uSTLp8MVJEpmDbLLJJptsssmOzg4EApaSkmKXX365jRgxwrKysqxly5a2evXqhPoBT99GOeecc1RQUKCnnnpKSUlJUeNhw4Zpw4YNatOmjRYuXKjBgwe7Y8dxYqr5fD7l5eVJkrZt2yYzi3sOsskmm2yyySY7Ovv111/XhAkTtGXLFs2fP18HDx6MGsetUYcuvkRycnJUF1R7nJycbHPmzLHk5OR641hrkflWrVrVYK2x85NNNtlkk032yZhtZu79EXXH8fD0nI3s7GyVlJSoc+fO9cbZ2dlavHixsrOz3Vrtcay1kpISmVmDtaaYn2yyySabbLJPxux169a52w2N45JQixKju+66yzIzM23mzJm2YsUK++53v2sZGRl2yy23WJ8+fUySDRo0yGbPnm0DBw40Sda3b9+Ya5dffrmFQiELh8M2atSohOYgm2yyySabbLKjs2+55RbLzMy02267zVasWGEzZ8607Oxs+9GPfpRQP+Bps2Fmdt9991lhYaE5zuFLozqOY5K4cePGjRs3bl/hW+2/24WFhXb//fcn3Ascs+tsbNiwQWVlZZKkgoICSXLHhw4dUjAY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",
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "plot_charging_kw(usage_plan)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 14,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "image/png": "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",
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "plot_predicated_moer(usage_plan)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 117,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stderr",
+ "output_type": "stream",
+ "text": [
+ "/var/folders/kf/cqbq1hkx74v5lt73jq2w60x40000gn/T/ipykernel_51362/1120087637.py:29: UserWarning: set_ticklabels() should only be used with a fixed number of ticks, i.e. after set_ticks() or using a FixedLocator.\n",
+ " ax.set_xticklabels(ticks)\n"
+ ]
+ },
+ {
+ "data": {
+ "image/png": "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90R98yA9zyI5Th/0/dN28hBEE6mpqXjyySdx5MgR5OTk4L777kPv3r3x3nvvwe2mvyuiZUOCgGixlJSU4JlnnkHHjh2xdOlSvPHGG9i9ezemTZvWorLl0yzB8whKKWQQlcTFxeHee+/FwYMH8eijj2LevHno1q0bXn31VdhsNq3NI4gGQYKAaHGUl5djzpw56NixIzZu3IhPP/0UW7duxRVXXAGWbXn/pNPqyCMod3ggihSrjlb0ej1uuukm/P333/j3v/+NDz74AJ06dcJ//vMfOJ1Orc0jiLBoeU9PotXidDrxr3/9C126dMGmTZvw/fff4/vvv8eoUaNadK14XR4CQZRQHqAkkYguOI7DNddcg+3bt+P999/H+++/j549e2LRokUQBCodJVoGJAiIqEcQBLz33nvo0aMHPvzwQyxZsgQ//vgjBg8erLVpESHNYqwzsZDyCFoODMNg/Pjx2LlzJ+bOnYvnnnsO5557LlatWkVVCUTUQ4KAiFokScIXX3yBc845B//4xz8wb9487Ny5E5dcckmL9gjURM+zSDIHTyykPIKWB8uymDJlCvbu3Yvbb78dt956K4YNG4aNGzdqbRpBBIUEARGVbN26FRdccAFuu+023HHHHdizZw8mT57cInMEQqHOxEIqPWyx6PV63HPPPTh48CDGjBmDSy+9FBMnTsS+ffu0No0gahGbT1eixVJcXIwZM2Zg5MiRGDNmDA4ePIi7774ben3wHXQsUFdiIXkIWj4WiwVPP/00Dh48iA4dOuC8887D448/ThUJRFRBgoCICgRBwFtvvYUePXrgxIkT2L17N5599tmo7yMQKeryEFAOQeyQlpaGN954A5s2bcL69evRq1cvLF++nPILiKiABAGhOVu3bsXAgQPxz3/+Ex988AFWrVqFLl26aG1Ws5KWELxjIQ05ij369u2LjRs34tlnn8Udd9yBSy65hMIIhOaQICA049SpU7j11lsxcuRIXH755fjrr78wYcIErc3SBAPPoY0peDMlmmkQe7Asi5tuugm5ubno3r07zj33XAojEJpCgoBodiRJwuLFi9GzZ0+cOnUKf/31F5566ikYjXUP+ol16hp0RIIgdklKSsL8+fOxZcsW/PTTT+jduze+++47rc0iWiEkCIhmpaioCFdccQUefPBBvPvuu/jiiy/QuXNnrc2KCuoahVxK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zJnUybAFQoKgBfL000+joqICr7zyitamACBR0JykWWqHDaj0sHUSbWIAqJp58P333+O9997T2hwiTEgQtDDWrl2L119/HZ988klEOhFGChIFzUMgDwFVGrQ+olEMKGRkZGDJkiWYNWsW/vrrL63NIcKABEELorCwEDk5OXjttdfQu3dvrc2pBYmCpidQYiGFDFoX0SwGFEaPHo17770XkyZNgt1u19ocIkRIELQQBEHA1KlTMXbsWEyfPl1rc4JCoqBpiTfwiDNULy8tpdLDVkNLEAMKzz77LNq0aYN7771Xa1OIECFB0EL4z3/+g2PHjuHNN99s0Djj5oREQdNS00tAQ45aBy1JDAAAz/P4+OOPsXz5cqxYsUJrc4gQIEHQAtizZw+efvppLFq0CBaLRWtzQoJEQdMRqIUxDTmKbVqaGFBo3749Xn/9dcycORPFxcVam0PUAwmCKEcQBNx000244447MHToUK3NCQsSBU1DwDwCSiyMWVqqGFCYPHkyhgwZgnvuuUdrU4h6IEEQ5fzrX/9CWVkZnn/+ea1NaRAkCiJPoBbGlFgYm7R0MQDIz4C33noLa9euxeeff661OUQdkCCIYvbs2YNnn30W7733Hkwmk9bmNBgSBZHFYtTVSiyk0sPYIxbEgEJGRgZef/113HHHHRQ6iGJIEEQpXq8X06dPx5133okhQ4ZobU6jIVEQWWo2KKIhR7FFLIkBhRtuuAFDhw7F3XffrbUpRBBIEEQp//rXv1BeXo7nnntOa1MiBomCyFGzQRENOYodYlEMAPLv/3//+1989913WL58udbmEAEgQRCF7N27F8899xwWLVrUokMFgSBREBlqeghoyFFsEKtiQCEjIwPz58/HHXfcgZKSEq3NIWpAgiDKkCQJd911F2699VYMHjxYa3OaBBIFjSedWhjHHLEuBhSuv/56DBw4EI8//rjWphA1IEEQZSxbtgx//fVXTIUKAkGioHFYjDqY9dUTC8uo0qDF0lrEACD/7r/66qv48MMP8euvv2ptDuEHCYIowmq14oEHHsA///lPJCYmam1Ok0OioHHUzCMgD0HLpDWJAYWuXbvi4Ycfxl133QVRFLU2h/BBgiCKeP7559G5c2dMnTpVa1OaDRIFDSe9Rh4B9SJoebRGMaDw2GOP4dSpU1iwYIHWphA+SBBECXv27MFrr72GN954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",
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "import matplotlib.pyplot as plt\n",
+ "import numpy as np\n",
+ "\n",
+ "# Fixing random state for reproducibility\n",
+ "#np.random.seed(19680801)\n",
+ "\n",
+ "# Compute pie slices\n",
+ "N = len(usage_plan.index.values)\n",
+ "theta, width = np.linspace(0.0, 2 * np.pi, N, endpoint=False, retstep=True)\n",
+ "radii_1 = usage_plan.emissions_co2_lb.values\n",
+ "\n",
+ "l = int(240/5)\n",
+ "radii_2 = (usage_plan.pred_moer*0.001)[:l]\n",
+ "\n",
+ "offset = np.pi / 8\n",
+ "theta2 = theta + offset\n",
+ "\n",
+ "ax = plt.subplot(projection='polar')\n",
+ "ax.bar(theta, radii_1, width=width, bottom=0, alpha=0.5)\n",
+ "#ax.bar(theta2, radii_2, width=0.4, alpha=0.5)\n",
+ "\n",
+ "ax.set_theta_zero_location(\"N\")\n",
+ "ax.set_theta_direction(-1)\n",
+ "ax.grid(True)\n",
+ "ax.spines['polar'].set_visible(True)\n",
+ "ax.set_rticks([])\n",
+ "\n",
+ "ticks = usage_plan.index.strftime('%r').values[3-1::3]\n",
+ "ax.set_xticklabels(ticks)\n",
+ "\n",
+ "plt.show()"
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "watttime",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.9.21"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 2
+}
diff --git a/watttime_optimizer/examples/synthetic_data.ipynb b/watttime_optimizer/examples/synthetic_data.ipynb
new file mode 100644
index 00000000..0cc189c4
--- /dev/null
+++ b/watttime_optimizer/examples/synthetic_data.ipynb
@@ -0,0 +1,1015 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# Synthetic Data for Testing"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import os\n",
+ "os.chdir(path=os.path.dirname(os.path.dirname(os.path.abspath(os.curdir))))"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 2,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from watttime_optimizer.evaluator.sessions import SessionsGenerator"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Example: At home EV charging\n",
+ "\n",
+ "- Covers a 5.5 - 8.5 hour variable length window\n",
+ "- The vehicle has a BMW and has an average power draw of 42.5\n",
+ "- Battery is usually typically 50% charged at plug in time.\n",
+ "- Charging occurs during the workday"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 3,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "ev_kwargs = {\n",
+ " \"max_power_output_rates\": [42.5],\n",
+ " \"max_percent_capacity\": 0.95, # highest level of charge achieved by battery\n",
+ " \"power_output_efficiency\": 0.75, # power loss. 1 = no power loss.\n",
+ " \"minimum_battery_starting_capacity\": 0.2, # minimum starting percent charged\n",
+ " \"minimum_usage_window_start_time\": \"08:00:00\", # session can start as early as 8am\n",
+ " \"maximum_usage_window_start_time\": \"22:00:00\", # session can start as late as 9pm\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 4,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "s_ev = SessionsGenerator(**ev_kwargs)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Now we can generate synthetic data for users and devices with the attributes set above. The synthetic data creates one example per day."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 5,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# the class has a helper function to generate a random list of unique dates\n",
+ "distinct_date_list = s_ev.assign_random_dates(years=[2025])"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "You can generate data from a single user."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 6,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ "\n",
+ "\n",
+ "
\n",
+ " \n",
+ " \n",
+ " \n",
+ " 0 \n",
+ " \n",
+ " \n",
+ " \n",
+ " \n",
+ " distinct_dates \n",
+ " 2025-01-07 \n",
+ " \n",
+ " \n",
+ " user_type \n",
+ " r26.945_tc71_avglc28763_sdlc7299 \n",
+ " \n",
+ " \n",
+ " usage_window_start \n",
+ " 2025-01-07 20:30:00 \n",
+ " \n",
+ " \n",
+ " usage_window_end \n",
+ " 2025-01-08 09:15:00 \n",
+ " \n",
+ " \n",
+ " initial_charge \n",
+ " 0.340737 \n",
+ " \n",
+ " \n",
+ " time_needed \n",
+ " 96.324327 \n",
+ " \n",
+ " \n",
+ " expected_baseline_charge_complete_timestamp \n",
+ " 2025-01-07 22:06:19.459590312 \n",
+ " \n",
+ " \n",
+ " window_length_in_minutes \n",
+ " 765.0 \n",
+ " \n",
+ " \n",
+ " final_charge_time \n",
+ " 2025-01-07 22:06:19.459590312 \n",
+ " \n",
+ " \n",
+ " total_capacity \n",
+ " 71 \n",
+ " \n",
+ " \n",
+ " usage_power_kw \n",
+ " 26.945 \n",
+ " \n",
+ " \n",
+ " total_intervals_plugged_in \n",
+ " 153.0 \n",
+ " \n",
+ " \n",
+ " MWh_fraction \n",
+ " 0.002245 \n",
+ " \n",
+ " \n",
+ " early_session_stop \n",
+ " False \n",
+ " \n",
+ " \n",
+ "
\n",
+ "
"
+ ],
+ "text/plain": [
+ " 0\n",
+ "distinct_dates 2025-01-07\n",
+ "user_type r26.945_tc71_avglc28763_sdlc7299\n",
+ "usage_window_start 2025-01-07 20:30:00\n",
+ "usage_window_end 2025-01-08 09:15:00\n",
+ "initial_charge 0.340737\n",
+ "time_needed 96.324327\n",
+ "expected_baseline_charge_complete_timestamp 2025-01-07 22:06:19.459590312\n",
+ "window_length_in_minutes 765.0\n",
+ "final_charge_time 2025-01-07 22:06:19.459590312\n",
+ "total_capacity 71\n",
+ "usage_power_kw 26.945\n",
+ "total_intervals_plugged_in 153.0\n",
+ "MWh_fraction 0.002245\n",
+ "early_session_stop False"
+ ]
+ },
+ "execution_count": 6,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "s_ev.synthetic_user_data(distinct_date_list=[distinct_date_list[0]]).T"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Or for multiple users."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 7,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stderr",
+ "output_type": "stream",
+ "text": [
+ "100%|██████████| 10/10 [00:00<00:00, 345.29it/s]\n"
+ ]
+ },
+ {
+ "data": {
+ "text/html": [
+ "\n",
+ "\n",
+ "
\n",
+ " \n",
+ " \n",
+ " \n",
+ " index \n",
+ " distinct_dates \n",
+ " user_type \n",
+ " usage_window_start \n",
+ " usage_window_end \n",
+ " initial_charge \n",
+ " time_needed \n",
+ " expected_baseline_charge_complete_timestamp \n",
+ " window_length_in_minutes \n",
+ " final_charge_time \n",
+ " total_capacity \n",
+ " usage_power_kw \n",
+ " total_intervals_plugged_in \n",
+ " MWh_fraction \n",
+ " early_session_stop \n",
+ " \n",
+ " \n",
+ " \n",
+ " \n",
+ " 0 \n",
+ " 0 \n",
+ " 2025-01-07 \n",
+ " r31.2375_tc104_avglc29369_sdlc7128 \n",
+ " 2025-01-07 13:35:00 \n",
+ " 2025-01-07 20:10:00 \n",
+ " 0.476759 \n",
+ " 94.534494 \n",
+ " 2025-01-07 15:09:32.069628156 \n",
+ " 395.0 \n",
+ " 2025-01-07 15:09:32.069628156 \n",
+ " 104 \n",
+ " 31.2375 \n",
+ " 79.0 \n",
+ " 0.002603 \n",
+ " False \n",
+ " \n",
+ " \n",
+ " 1 \n",
+ " 0 \n",
+ " 2025-01-07 \n",
+ " r25.415_tc27_avglc22738_sdlc7758 \n",
+ " 2025-01-07 09:25:00 \n",
+ " 2025-01-07 12:50:00 \n",
+ " 0.262952 \n",
+ " 43.793704 \n",
+ " 2025-01-07 10:08:47.622239934 \n",
+ " 205.0 \n",
+ " 2025-01-07 10:08:47.622239934 \n",
+ " 27 \n",
+ " 25.4150 \n",
+ " 41.0 \n",
+ " 0.002118 \n",
+ " False \n",
+ " \n",
+ " \n",
+ " 2 \n",
+ " 0 \n",
+ " 2025-01-07 \n",
+ " r31.195_tc69_avglc28996_sdlc7437 \n",
+ " 2025-01-07 19:25:00 \n",
+ " 2025-01-08 02:15:00 \n",
+ " 0.268479 \n",
+ " 90.447105 \n",
+ " 2025-01-07 20:55:26.826279198 \n",
+ " 410.0 \n",
+ " 2025-01-07 20:55:26.826279198 \n",
+ " 69 \n",
+ " 31.1950 \n",
+ " 82.0 \n",
+ " 0.002600 \n",
+ " False \n",
+ " \n",
+ " \n",
+ " 3 \n",
+ " 0 \n",
+ " 2025-01-07 \n",
+ " r28.687500000000004_tc29_avglc20434_sdlc6948 \n",
+ " 2025-01-07 13:55:00 \n",
+ " 2025-01-07 16:55:00 \n",
+ " 0.402831 \n",
+ " 33.187754 \n",
+ " 2025-01-07 14:28:11.265212730 \n",
+ " 180.0 \n",
+ " 2025-01-07 14:28:11.265212730 \n",
+ " 29 \n",
+ " 28.6875 \n",
+ " 36.0 \n",
+ " 0.002391 \n",
+ " False \n",
+ " \n",
+ " \n",
+ " 4 \n",
+ " 0 \n",
+ " 2025-01-07 \n",
+ " r34.7225_tc102_avglc26861_sdlc7446 \n",
+ " 2025-01-07 19:40:00 \n",
+ " 2025-01-08 05:05:00 \n",
+ " 0.779481 \n",
+ " 30.054798 \n",
+ " 2025-01-07 20:10:03.287860500 \n",
+ " 565.0 \n",
+ " 2025-01-07 20:10:03.287860500 \n",
+ " 102 \n",
+ " 34.7225 \n",
+ " 113.0 \n",
+ " 0.002894 \n",
+ " False \n",
+ " \n",
+ " \n",
+ " 5 \n",
+ " 0 \n",
+ " 2025-01-07 \n",
+ " r25.84_tc84_avglc26756_sdlc7776 \n",
+ " 2025-01-07 16:20:00 \n",
+ " 2025-01-08 02:30:00 \n",
+ " 0.401695 \n",
+ " 106.944858 \n",
+ " 2025-01-07 18:06:56.691487974 \n",
+ " 610.0 \n",
+ " 2025-01-07 18:06:56.691487974 \n",
+ " 84 \n",
+ " 25.8400 \n",
+ " 122.0 \n",
+ " 0.002153 \n",
+ " False \n",
+ " \n",
+ " \n",
+ " 6 \n",
+ " 0 \n",
+ " 2025-01-07 \n",
+ " r32.1725_tc61_avglc25508_sdlc7508 \n",
+ " 2025-01-07 16:05:00 \n",
+ " 2025-01-08 01:25:00 \n",
+ " 0.461958 \n",
+ " 55.520505 \n",
+ " 2025-01-07 17:00:31.230272429 \n",
+ " 560.0 \n",
+ " 2025-01-07 17:00:31.230272429 \n",
+ " 61 \n",
+ " 32.1725 \n",
+ " 112.0 \n",
+ " 0.002681 \n",
+ " False \n",
+ " \n",
+ " \n",
+ " 7 \n",
+ " 0 \n",
+ " 2025-01-07 \n",
+ " r33.1925_tc116_avglc28830_sdlc7740 \n",
+ " 2025-01-07 16:45:00 \n",
+ " 2025-01-07 22:40:00 \n",
+ " 0.510567 \n",
+ " 92.142966 \n",
+ " 2025-01-07 18:17:08.577960305 \n",
+ " 355.0 \n",
+ " 2025-01-07 18:17:08.577960305 \n",
+ " 116 \n",
+ " 33.1925 \n",
+ " 71.0 \n",
+ " 0.002766 \n",
+ " False \n",
+ " \n",
+ " \n",
+ " 8 \n",
+ " 0 \n",
+ " 2025-01-07 \n",
+ " r29.2825_tc29_avglc23359_sdlc6996 \n",
+ " 2025-01-07 18:00:00 \n",
+ " 2025-01-07 22:35:00 \n",
+ " 0.314793 \n",
+ " 37.744748 \n",
+ " 2025-01-07 18:37:44.684857398 \n",
+ " 275.0 \n",
+ " 2025-01-07 18:37:44.684857398 \n",
+ " 29 \n",
+ " 29.2825 \n",
+ " 55.0 \n",
+ " 0.002440 \n",
+ " False \n",
+ " \n",
+ " \n",
+ " 9 \n",
+ " 0 \n",
+ " 2025-01-07 \n",
+ " r36.082499999999996_tc121_avglc27590_sdlc6912 \n",
+ " 2025-01-07 11:20:00 \n",
+ " 2025-01-07 18:20:00 \n",
+ " 0.703789 \n",
+ " 49.538951 \n",
+ " 2025-01-07 12:09:32.337083280 \n",
+ " 420.0 \n",
+ " 2025-01-07 12:09:32.337083280 \n",
+ " 121 \n",
+ " 36.0825 \n",
+ " 84.0 \n",
+ " 0.003007 \n",
+ " False \n",
+ " \n",
+ " \n",
+ "
\n",
+ "
"
+ ],
+ "text/plain": [
+ " index distinct_dates user_type \\\n",
+ "0 0 2025-01-07 r31.2375_tc104_avglc29369_sdlc7128 \n",
+ "1 0 2025-01-07 r25.415_tc27_avglc22738_sdlc7758 \n",
+ "2 0 2025-01-07 r31.195_tc69_avglc28996_sdlc7437 \n",
+ "3 0 2025-01-07 r28.687500000000004_tc29_avglc20434_sdlc6948 \n",
+ "4 0 2025-01-07 r34.7225_tc102_avglc26861_sdlc7446 \n",
+ "5 0 2025-01-07 r25.84_tc84_avglc26756_sdlc7776 \n",
+ "6 0 2025-01-07 r32.1725_tc61_avglc25508_sdlc7508 \n",
+ "7 0 2025-01-07 r33.1925_tc116_avglc28830_sdlc7740 \n",
+ "8 0 2025-01-07 r29.2825_tc29_avglc23359_sdlc6996 \n",
+ "9 0 2025-01-07 r36.082499999999996_tc121_avglc27590_sdlc6912 \n",
+ "\n",
+ " usage_window_start usage_window_end initial_charge time_needed \\\n",
+ "0 2025-01-07 13:35:00 2025-01-07 20:10:00 0.476759 94.534494 \n",
+ "1 2025-01-07 09:25:00 2025-01-07 12:50:00 0.262952 43.793704 \n",
+ "2 2025-01-07 19:25:00 2025-01-08 02:15:00 0.268479 90.447105 \n",
+ "3 2025-01-07 13:55:00 2025-01-07 16:55:00 0.402831 33.187754 \n",
+ "4 2025-01-07 19:40:00 2025-01-08 05:05:00 0.779481 30.054798 \n",
+ "5 2025-01-07 16:20:00 2025-01-08 02:30:00 0.401695 106.944858 \n",
+ "6 2025-01-07 16:05:00 2025-01-08 01:25:00 0.461958 55.520505 \n",
+ "7 2025-01-07 16:45:00 2025-01-07 22:40:00 0.510567 92.142966 \n",
+ "8 2025-01-07 18:00:00 2025-01-07 22:35:00 0.314793 37.744748 \n",
+ "9 2025-01-07 11:20:00 2025-01-07 18:20:00 0.703789 49.538951 \n",
+ "\n",
+ " expected_baseline_charge_complete_timestamp window_length_in_minutes \\\n",
+ "0 2025-01-07 15:09:32.069628156 395.0 \n",
+ "1 2025-01-07 10:08:47.622239934 205.0 \n",
+ "2 2025-01-07 20:55:26.826279198 410.0 \n",
+ "3 2025-01-07 14:28:11.265212730 180.0 \n",
+ "4 2025-01-07 20:10:03.287860500 565.0 \n",
+ "5 2025-01-07 18:06:56.691487974 610.0 \n",
+ "6 2025-01-07 17:00:31.230272429 560.0 \n",
+ "7 2025-01-07 18:17:08.577960305 355.0 \n",
+ "8 2025-01-07 18:37:44.684857398 275.0 \n",
+ "9 2025-01-07 12:09:32.337083280 420.0 \n",
+ "\n",
+ " final_charge_time total_capacity usage_power_kw \\\n",
+ "0 2025-01-07 15:09:32.069628156 104 31.2375 \n",
+ "1 2025-01-07 10:08:47.622239934 27 25.4150 \n",
+ "2 2025-01-07 20:55:26.826279198 69 31.1950 \n",
+ "3 2025-01-07 14:28:11.265212730 29 28.6875 \n",
+ "4 2025-01-07 20:10:03.287860500 102 34.7225 \n",
+ "5 2025-01-07 18:06:56.691487974 84 25.8400 \n",
+ "6 2025-01-07 17:00:31.230272429 61 32.1725 \n",
+ "7 2025-01-07 18:17:08.577960305 116 33.1925 \n",
+ "8 2025-01-07 18:37:44.684857398 29 29.2825 \n",
+ "9 2025-01-07 12:09:32.337083280 121 36.0825 \n",
+ "\n",
+ " total_intervals_plugged_in MWh_fraction early_session_stop \n",
+ "0 79.0 0.002603 False \n",
+ "1 41.0 0.002118 False \n",
+ "2 82.0 0.002600 False \n",
+ "3 36.0 0.002391 False \n",
+ "4 113.0 0.002894 False \n",
+ "5 122.0 0.002153 False \n",
+ "6 112.0 0.002681 False \n",
+ "7 71.0 0.002766 False \n",
+ "8 55.0 0.002440 False \n",
+ "9 84.0 0.003007 False "
+ ]
+ },
+ "execution_count": 7,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "s_ev.generate_synthetic_dataset(distinct_date_list=[distinct_date_list[0]], number_of_users=10)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Example: AI Model Training\n",
+ "- Model training can occur at any time of day\n",
+ "- There are 3 server models that consume 240, 310, and 640 watt-hour on average\n",
+ "- Early stopping is not an option"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 8,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "ai_kwargs = {\n",
+ " \"max_percent_capacity\":1.0, # job must run to completion\n",
+ " \"max_power_output_rates\": [24,31,64], # assuming a bare metal usecase, k8s or vm rescale to vCPU\n",
+ " \"minimum_usage_window_start_time\": \"00:00:00\", # earliest session can start\n",
+ " \"maximum_usage_window_start_time\": \"23:59:00\", # latest session can start\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 9,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "s_ai = SessionsGenerator(**ai_kwargs)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 10,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stderr",
+ "output_type": "stream",
+ "text": [
+ "100%|██████████| 10/10 [00:00<00:00, 259.96it/s]\n"
+ ]
+ }
+ ],
+ "source": [
+ "df_ai = s_ai.generate_synthetic_dataset(distinct_date_list=distinct_date_list, number_of_users=10)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# Optimization"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 11,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import os\n",
+ "import pandas as pd\n",
+ "from watttime_optimizer.evaluator.evaluator import OptChargeEvaluator\n",
+ "from watttime_optimizer.evaluator.evaluator import ImpactEvaluator\n",
+ "from watttime_optimizer.evaluator.evaluator import RecalculationOptChargeEvaluator"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 12,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "username = os.getenv(\"WATTTIME_USER\")\n",
+ "password = os.getenv(\"WATTTIME_PASSWORD\")\n",
+ "region = \"CAISO_NORTH\"\n",
+ "oce = OptChargeEvaluator(username=username,password=password)\n",
+ "roce = RecalculationOptChargeEvaluator(username,password)\n",
+ "\n",
+ "# single instance\n",
+ "df_ev_sample = s_ev.synthetic_user_data(distinct_date_list=[distinct_date_list[0]])\n",
+ "df_ev_sample = df_ev_sample.rename({\"usage_time_required_in_minutes\":\"time_needed\"},axis=1)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 20,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "%%capture\n",
+ "input_dict = df_ev_sample[['usage_window_start',\n",
+ " 'usage_window_end',\n",
+ " 'time_needed',\n",
+ " 'usage_power_kw'\n",
+ " ]].T.to_dict()\n",
+ "\n",
+ "value = input_dict[0]\n",
+ "value.update({'region':region,'tz_convert':True, \"verbose\":False})"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 21,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "{'usage_window_start': Timestamp('2025-01-07 12:30:00'),\n",
+ " 'usage_window_end': Timestamp('2025-01-07 22:45:00'),\n",
+ " 'time_needed': 87.57432605789094,\n",
+ " 'usage_power_kw': 24.862499999999997,\n",
+ " 'region': 'CAISO_NORTH',\n",
+ " 'tz_convert': True,\n",
+ " 'verbose': False}"
+ ]
+ },
+ "execution_count": 21,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "value"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 22,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "df = oce.get_schedule_and_cost_api(**value)\n",
+ "r = ImpactEvaluator(username,password,df).get_all_emissions_values(region=region)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 23,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "{'baseline': 24.462852599962222,\n",
+ " 'forecast': 19.10658972072091,\n",
+ " 'actual': 25.489088724775343}"
+ ]
+ },
+ "execution_count": 23,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "r"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# Requery"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 24,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "value.update({\"optimization_method\": \"simple\", \"interval\":15, \"charge_per_segment\":None})"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 37,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "tz converting...\n",
+ "tz converting...\n",
+ "tz converting...\n",
+ "tz converting...\n",
+ "tz converting...\n",
+ "tz converting...\n",
+ "tz converting...\n",
+ "tz converting...\n",
+ "7.92 s ± 2.39 s per loop (mean ± std. dev. of 7 runs, 1 loop each)\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%timeit\n",
+ "df_requery = roce.fit_recalculator(**value).get_combined_schedule()\n",
+ "r_requery = ImpactEvaluator(username,password,df_requery).get_all_emissions_values(region=region)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 38,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "{'baseline': 24.462852599962222,\n",
+ " 'forecast': 19.10658972072091,\n",
+ " 'actual': 25.489088724775343}"
+ ]
+ },
+ "execution_count": 38,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "r_requery"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# Iterate over multiple rows of data"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": []
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 39,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from watttime_optimizer.evaluator.analysis import analysis_loop"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 40,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stderr",
+ "output_type": "stream",
+ "text": [
+ "100%|██████████| 10/10 [00:00<00:00, 262.90it/s]\n"
+ ]
+ }
+ ],
+ "source": [
+ "df_ev_samples = s_ev.generate_synthetic_dataset(distinct_date_list=distinct_date_list, number_of_users=10).sample(10)\n",
+ "\n",
+ "input_dict = df_ev_samples[['usage_window_start',\n",
+ " 'usage_window_end',\n",
+ " 'time_needed',\n",
+ " 'usage_power_kw'\n",
+ " ]].T.to_dict()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 41,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stderr",
+ "output_type": "stream",
+ "text": [
+ "100%|██████████| 10/10 [00:48<00:00, 4.87s/it]\n"
+ ]
+ }
+ ],
+ "source": [
+ "results = analysis_loop(\n",
+ " region = \"CAISO_NORTH\",\n",
+ " input_dict = input_dict,\n",
+ " username=username,\n",
+ " password=password\n",
+ ")"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 43,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "results_loop = pd.DataFrame.from_dict(\n",
+ " results,\n",
+ " orient=\"index\"\n",
+ " )"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 49,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ "\n",
+ "\n",
+ "
\n",
+ " \n",
+ " \n",
+ " \n",
+ " baseline \n",
+ " forecast \n",
+ " actual \n",
+ " m \n",
+ " b \n",
+ " stddev \n",
+ " \n",
+ " \n",
+ " \n",
+ " \n",
+ " 74 \n",
+ " 18.785587 \n",
+ " 17.989601 \n",
+ " 18.131132 \n",
+ " -0.153361 \n",
+ " 958.348989 \n",
+ " 4.492026 \n",
+ " \n",
+ " \n",
+ " 87 \n",
+ " 7.239583 \n",
+ " 9.885909 \n",
+ " 7.149756 \n",
+ " 7.037200 \n",
+ " -64.524592 \n",
+ " 306.813320 \n",
+ " \n",
+ " \n",
+ " 164 \n",
+ " 3.531891 \n",
+ " 4.478827 \n",
+ " 27.828149 \n",
+ " -2.277382 \n",
+ " 350.811568 \n",
+ " 110.248641 \n",
+ " \n",
+ " \n",
+ " 36 \n",
+ " 51.959712 \n",
+ " 49.601364 \n",
+ " 51.277886 \n",
+ " -0.850466 \n",
+ " 987.026339 \n",
+ " 16.902915 \n",
+ " \n",
+ " \n",
+ " 98 \n",
+ " 18.336274 \n",
+ " 11.711657 \n",
+ " 17.373445 \n",
+ " 6.201609 \n",
+ " 22.024319 \n",
+ " 214.884382 \n",
+ " \n",
+ " \n",
+ " 249 \n",
+ " 67.523255 \n",
+ " 64.249195 \n",
+ " 66.979611 \n",
+ " -0.395036 \n",
+ " 978.947001 \n",
+ " 15.076282 \n",
+ " \n",
+ " \n",
+ " 81 \n",
+ " 55.829117 \n",
+ " 53.346816 \n",
+ " 53.685575 \n",
+ " -0.475130 \n",
+ " 961.037489 \n",
+ " 10.587760 \n",
+ " \n",
+ " \n",
+ " 10 \n",
+ " 32.056373 \n",
+ " 30.638461 \n",
+ " 30.998585 \n",
+ " -0.485953 \n",
+ " 964.179230 \n",
+ " 11.088391 \n",
+ " \n",
+ " \n",
+ " 27 \n",
+ " 6.885002 \n",
+ " 8.666217 \n",
+ " 7.030633 \n",
+ " 11.099877 \n",
+ " -47.994144 \n",
+ " 399.276077 \n",
+ " \n",
+ " \n",
+ " 241 \n",
+ " 13.175851 \n",
+ " 12.180933 \n",
+ " 13.041130 \n",
+ " -0.024084 \n",
+ " 947.862326 \n",
+ " 9.670335 \n",
+ " \n",
+ " \n",
+ "
\n",
+ "
"
+ ],
+ "text/plain": [
+ " baseline forecast actual m b stddev\n",
+ "74 18.785587 17.989601 18.131132 -0.153361 958.348989 4.492026\n",
+ "87 7.239583 9.885909 7.149756 7.037200 -64.524592 306.813320\n",
+ "164 3.531891 4.478827 27.828149 -2.277382 350.811568 110.248641\n",
+ "36 51.959712 49.601364 51.277886 -0.850466 987.026339 16.902915\n",
+ "98 18.336274 11.711657 17.373445 6.201609 22.024319 214.884382\n",
+ "249 67.523255 64.249195 66.979611 -0.395036 978.947001 15.076282\n",
+ "81 55.829117 53.346816 53.685575 -0.475130 961.037489 10.587760\n",
+ "10 32.056373 30.638461 30.998585 -0.485953 964.179230 11.088391\n",
+ "27 6.885002 8.666217 7.030633 11.099877 -47.994144 399.276077\n",
+ "241 13.175851 12.180933 13.041130 -0.024084 947.862326 9.670335"
+ ]
+ },
+ "execution_count": 49,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "results_loop"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Interate Over Multiple Rows of Data Using Requery"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 48,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "#from watttime_optimizer.evaluator.analysis import analysis_loop_requery_contiguous"
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "watttime",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.9.21"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 2
+}