From e8c2f70ba6d6676cbdcebd679493b16af5227bdd Mon Sep 17 00:00:00 2001 From: adfarth Date: Fri, 15 May 2026 15:05:07 -0700 Subject: [PATCH 01/45] add fixed soc inputs --- CHANGELOG.md | 10 +++++++--- reoptjl/models.py | 22 +++++++++++++++++++++- reoptjl/validators.py | 3 +++ 3 files changed, 31 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 67d6f3c78..2fc954f2e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,10 +26,16 @@ Classify the change according to the following categories: ##### Removed ### Patches +## fixed-soc +### Minor udpates +#### Added +- Add **ElectricStorage** inputs field **fixed_soc_series_fraction** and **fixed_soc_series_fraction_tolerance** to allow users to fix the SOC timeseries within a chosen absolute tolerance +### Changed +- **ElectricStorage** result key **state_of_health** to **state_of_health_series_fraction** ## Develop - 2026-05-14 ### Minor updates -##### Added +#### Added - New `CHP` fields **serve_absorption_chiller_only**, **months_serving_absorption_chiller_only**, **follow_electrical_load**, and **include_cooling_in_chp_size** - New output `thermal_to_absorption_chiller_series_mmbtu_per_hour` added to heating technologies `CHPOutputs`, `ElectricHeaterOutputs`, `CSTOutputs`, `BoilerOutputs`, `SteamTurbineOutputs`, and `ExistingBoilerOutputs`, and new output `storage_to_absorption_chiller_series_mmbtu_per_hour` for `HotThermalStorageOutputs` and `HighTempThermalStorageOutputs`. ### Changed @@ -40,8 +46,6 @@ Classify the change according to the following categories: - Fixed a bug in which the CHP system requires a **DomesticHotWater** load. - Fixed a bug in which the storage to steam turbine flow was included in the thermal heating load served. - - ## v3.18.0 ### Minor Updates ##### Changed diff --git a/reoptjl/models.py b/reoptjl/models.py index 56960d3b6..f2d20b5d9 100644 --- a/reoptjl/models.py +++ b/reoptjl/models.py @@ -3753,6 +3753,26 @@ class ElectricStorageInputs(BaseModel, models.Model): blank=True, help_text="Battery state of charge at first hour of optimization as fraction of energy capacity." ) + fixed_soc_series_fraction = ArrayField( + models.FloatField( + validators=[ + MinValueValidator(0), + MaxValueValidator(1) + ], + blank=True + ), + default=list, blank=True, + help_text=("If provided, SOC (as fraction of total energy capacity) will not be optimized and will instead be fixed to the values provided" + "here +- the absolute fixed_soc_series_fraction_tolerance. Must be an array of values 0-1 with length equal to 8760*time_steps_per_hour.") + ) + fixed_soc_series_fraction_tolerance = models.FloatField( + validators=[ + MinValueValidator(-1), + MaxValueValidator(1) + ], + null=True, blank=True, + help_text="Absolute tolerance on fixed_soc_series_fraction to avoid infeasible solutions when fixed_soc_series_fraction is provided." + ) can_grid_charge = models.BooleanField( blank=True, help_text="Flag to set whether the battery can be charged from the grid, or just onsite generation." @@ -3944,7 +3964,7 @@ class ElectricStorageOutputs(BaseModel, models.Model): ) initial_capital_cost = models.FloatField(null=True, blank=True) maintenance_cost = models.FloatField(null=True, blank=True) - state_of_health = ArrayField( + state_of_health_series_fraction = ArrayField( models.FloatField(null=True, blank=True), blank=True, default=list ) diff --git a/reoptjl/validators.py b/reoptjl/validators.py index a55301486..8e4f0621d 100644 --- a/reoptjl/validators.py +++ b/reoptjl/validators.py @@ -368,6 +368,9 @@ def update_pv_defaults_offgrid(self, pvmodel): self.models["ElectricStorage"].can_grid_charge = True else: self.models["ElectricStorage"].can_grid_charge = False + + if len(self.models["ElectricStorage"].__getattribute__("fixed_soc_series_fraction")) > 1: + self.clean_time_series("ElectricStorage", "fixed_soc_series_fraction") """ From ac53adfa7f634df177d38b938dd279f55f6fc94b Mon Sep 17 00:00:00 2001 From: adfarth Date: Fri, 15 May 2026 15:25:29 -0700 Subject: [PATCH 02/45] Create 0116_rename_state_of_health_electricstorageoutputs_state_of_health_series_fraction_and_more.py --- ...tate_of_health_series_fraction_and_more.py | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 reoptjl/migrations/0116_rename_state_of_health_electricstorageoutputs_state_of_health_series_fraction_and_more.py diff --git a/reoptjl/migrations/0116_rename_state_of_health_electricstorageoutputs_state_of_health_series_fraction_and_more.py b/reoptjl/migrations/0116_rename_state_of_health_electricstorageoutputs_state_of_health_series_fraction_and_more.py new file mode 100644 index 000000000..4945d667e --- /dev/null +++ b/reoptjl/migrations/0116_rename_state_of_health_electricstorageoutputs_state_of_health_series_fraction_and_more.py @@ -0,0 +1,30 @@ +# Generated by Django 4.2.26 on 2026-05-15 22:25 + +import django.contrib.postgres.fields +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('reoptjl', '0115_alter_coldthermalstorageinputs_installed_cost_per_gal_and_more'), + ] + + operations = [ + migrations.RenameField( + model_name='electricstorageoutputs', + old_name='state_of_health', + new_name='state_of_health_series_fraction', + ), + migrations.AddField( + model_name='electricstorageinputs', + name='fixed_soc_series_fraction', + field=django.contrib.postgres.fields.ArrayField(base_field=models.FloatField(blank=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(1)]), blank=True, default=list, help_text='If provided, SOC (as fraction of total energy capacity) will not be optimized and will instead be fixed to the values providedhere +- the absolute fixed_soc_series_fraction_tolerance. Must be an array of values 0-1 with length equal to 8760*time_steps_per_hour.', size=None), + ), + migrations.AddField( + model_name='electricstorageinputs', + name='fixed_soc_series_fraction_tolerance', + field=models.FloatField(blank=True, help_text='Absolute tolerance on fixed_soc_series_fraction to avoid infeasible solutions when fixed_soc_series_fraction is provided.', null=True, validators=[django.core.validators.MinValueValidator(-1), django.core.validators.MaxValueValidator(1)]), + ), + ] From 0cf8a0599dc11a322f3789ba8e0daddb09c4f6cb Mon Sep 17 00:00:00 2001 From: adfarth Date: Fri, 15 May 2026 15:30:27 -0700 Subject: [PATCH 03/45] Update all_inputs_test.json --- reoptjl/test/posts/all_inputs_test.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/reoptjl/test/posts/all_inputs_test.json b/reoptjl/test/posts/all_inputs_test.json index 36926c78e..d53def1ad 100644 --- a/reoptjl/test/posts/all_inputs_test.json +++ b/reoptjl/test/posts/all_inputs_test.json @@ -187,7 +187,9 @@ "total_itc_fraction": 0.0, "total_rebate_per_kw": 0.0, "total_rebate_per_kwh": 0.0, - "optimize_soc_init_fraction": false + "optimize_soc_init_fraction": false, + "fixed_soc_series_fraction": [], + "fixed_soc_series_fraction_tolerance": 0.05 }, "Generator": { "existing_kw": 0.0, From 55b3d1c77e75b31079096fdfacdd40df0a8575de Mon Sep 17 00:00:00 2001 From: lixiangk1 <72464565+lixiangk1@users.noreply.github.com> Date: Mon, 1 Jun 2026 12:21:25 -0600 Subject: [PATCH 04/45] Add initial MPC implementation --- julia_src/http.jl | 52 +- julia_src/mpc.jl | 655 ++++++++++++++++++ .../migrations/0117_merge_20260530_1640.py | 14 + 3 files changed, 720 insertions(+), 1 deletion(-) create mode 100644 julia_src/mpc.jl create mode 100644 reoptjl/migrations/0117_merge_20260530_1640.py diff --git a/julia_src/http.jl b/julia_src/http.jl index 9eab6396d..e6f94a42c 100644 --- a/julia_src/http.jl +++ b/julia_src/http.jl @@ -1,4 +1,4 @@ -using HTTP, JSON, JuMP +using HTTP, JSON, JuMP using HiGHS, Cbc, SCIP using GhpGhx import REopt as reoptjl # For REopt.jl, needed because we still have local REopt.jl module for V1/V2 @@ -8,6 +8,7 @@ DotEnv.load!() const test_nrel_developer_api_key = ENV["NREL_DEVELOPER_API_KEY"] ENV["NREL_DEVELOPER_EMAIL"] = "reopt@nlr.gov" +include("mpc.jl") include("os_solvers.jl") @@ -64,6 +65,27 @@ function reopt(req::HTTP.Request) ENV["NREL_DEVELOPER_API_KEY"] = test_nrel_developer_api_key delete!(d, "api_key") end + + # ---- API-only battery heuristic dispatch strategy: "daily_foresight_optimized" ---- + # When ElectricStorage.dispatch_options == "daily_foresight_optimized", first run the MPC rolling-horizon loop to get an SOC profile. + # Then set ElectricStorage.fixed_soc_series_fraction = MPC SOC before running the main REopt optimization. + electric_storage = get(d, "ElectricStorage", Dict()) + if get(electric_storage, "dispatch_options", nothing) == "daily_foresight_optimized" + try + @info "Running MPC to obtain daily foresight optimized battery dispatch profile." + mpc_results = get_mpc_results(d) + soc = mpc_results["dispatch"]["ElectricStorage"]["soc_series_fraction"] + d["ElectricStorage"]["fixed_soc_series_fraction"] = soc + d["ElectricStorage"]["dispatch_options"] = "custom" + catch e + @error "MPC pre-solve failed" exception=(e, catch_backtrace()) + return HTTP.Response(500, JSON.json(Dict( + "error" => "MPC pre-solve failed: " * sprint(showerror, e), + "reopt_version" => string(pkgversion(reoptjl)), + ))) + end + end + settings = d["Settings"] solver_name = get(settings, "solver_name", "HiGHS") if solver_name == "Xpress" && !(xpress_installed=="True") @@ -810,6 +832,33 @@ function job_no_xpress(req::HTTP.Request) return HTTP.Response(500, JSON.json(error_response)) end + +function mpc(req::HTTP.Request) + d = JSON.parse(String(req.body)) + error_response = Dict() + results = Dict() + try + if !isempty(get(d, "api_key", "")) + ENV["NREL_DEVELOPER_API_KEY"] = pop!(d, "api_key") + else + ENV["NREL_DEVELOPER_API_KEY"] = test_nrel_developer_api_key + delete!(d, "api_key") + end + results = get_mpc_results(d) + catch e + @error "MPC failed" exception=(e, catch_backtrace()) + error_response["error"] = sprint(showerror, e) + error_response["reopt_version"] = string(pkgversion(reoptjl)) + end + GC.gc() + if isempty(error_response) + @info "MPC ran successfully." + return HTTP.Response(200, JSON.json(results)) + else + return HTTP.Response(500, JSON.json(error_response)) + end +end + # define REST endpoints to dispatch to "service" functions const ROUTER = HTTP.Router() @@ -820,6 +869,7 @@ else end HTTP.register!(ROUTER, "POST", "/reopt", reopt) HTTP.register!(ROUTER, "POST", "/erp", erp) +HTTP.register!(ROUTER, "POST", "/mpc", mpc) HTTP.register!(ROUTER, "POST", "/ghpghx", ghpghx) HTTP.register!(ROUTER, "GET", "/chp_defaults", chp_defaults) HTTP.register!(ROUTER, "GET", "/avert_emissions_profile", avert_emissions_profile) diff --git a/julia_src/mpc.jl b/julia_src/mpc.jl new file mode 100644 index 000000000..21cb9510a --- /dev/null +++ b/julia_src/mpc.jl @@ -0,0 +1,655 @@ +# REopt®, Copyright (c) Alliance for Sustainable Energy, LLC. See also https://github.com/NatLabRockies/REopt_API/blob/master/LICENSE. +# ============================================================================ +# MPC (Model Predictive Control) endpoint +# ---------------------------------------------------------------------------- +# Rolling-horizon dispatch with one day look-ahead using REopt.run_mpc. Assumes: +# * PV and ElectricStorage are the only available technologies (perfect forecast) +# * Only perfect forecast scenarios are modeled (no forecast errors) +# * Sizing: Perform a sizing run first if PV or ElectricStorage sizes are not provided (storage sizes must be greater than zero) +# * PV: Use PVWatts if `PV.production_factor_series` is not provided +# * ElectricLoad: Use commercial reference profiles if `ElectricLoad.loads_kw` is not provided +# * Code currently wraps with Jan 1 data to determine Dec 31 dispatch (allows leap-year data, which avoids the need to wrap) +# * MPC settings are currently hard coded (e.g., forecast horizon, control horizon, optimization horizon) +# ============================================================================ + +""" + get_month_transition_timesteps(time_steps_per_hour) + +Return the 1-based timestep index marking the START of each month for a +non-leap year beginning Jan 1 00:00. Length 12; first element == 1. +The last "transition" (end of December) is implicitly `length_of_data + 1` +and is handled by the caller. +""" +function get_month_transition_timesteps(time_steps_per_hour::Int) + # hours in each month for a non-leap year + hours_per_month = [744, 672, 744, 720, 744, 720, 744, 744, 720, 744, 720, 744] + starts = Vector{Int}(undef, 12) + starts[1] = 1 + for m in 2:12 + starts[m] = starts[m-1] + hours_per_month[m-1] * time_steps_per_hour + end + return starts +end + +""" + _mpc_slice_with_wrap(arr, idx, end_idx) + +Return `arr[idx:end_idx]` but wrap around to the start of `arr` if +`end_idx > length(arr)`. The wrap threshold is the actual array length, +not `length_of_data`, so a leap-year-length input (8784*tsh) naturally +supplies the extra day for the year-end look-ahead window before any +wrap is needed. +""" +function _mpc_slice_with_wrap(arr::AbstractVector, idx::Int, end_idx::Int) + n = length(arr) + if end_idx <= n + return arr[idx:end_idx] + else + wrap_len = end_idx - n + return vcat(arr[idx:n], arr[1:wrap_len]) + end +end + +""" + _mpc_check_series_length(name, series, time_steps_per_hour, length_of_data) + +Validate that a user-supplied annual time series has either +`8760 * time_steps_per_hour` entries (standard year) or +`8784 * time_steps_per_hour` entries (leap year). Returns the series +unchanged. + +The MPC loop iterates over `length_of_data = 8760 * time_steps_per_hour` +timesteps; if a leap-year-length series is supplied, the extra day is +used as additional look-ahead data by `_mpc_slice_with_wrap` (which wraps +on the actual array length, not on `length_of_data`). Any other length is +a hard error — REopt.jl's own `check_and_adjust_load_length` would +silently repeat/pad, masking the mistake. +""" +function _mpc_check_series_length(name::String, series::AbstractVector, + time_steps_per_hour::Int, length_of_data::Int) + n = length(series) + n == length_of_data && return series + if n == 8784 * time_steps_per_hour + @info "MPC: $name has leap-year length ($n); the extra day will be used " * + "for year-end look-ahead instead of wrapping to Jan 1." + return series + end + # Diagnostic: if `n` is consistent with 8760 hours at a *different* + # sub-hourly resolution, the user probably mis-declared + # `Settings.time_steps_per_hour`. + tsh_hint = "" + for tsh_guess in (1, 2, 4) + tsh_guess == time_steps_per_hour && continue + if n == 8760 * tsh_guess || n == 8784 * tsh_guess + tsh_hint = " (length is consistent with time_steps_per_hour=$(tsh_guess); " * + "check Settings.time_steps_per_hour)." + break + end + end + error("MPC: $name length $n != " * + "8760 * time_steps_per_hour ($length_of_data) " * + "and != 8784 * time_steps_per_hour ($(8784 * time_steps_per_hour))." * + tsh_hint) +end + +""" + _mpc_build_tariff_arrays(et_input, year, time_steps_per_hour, length_of_data) + +Construct a `REopt.ElectricTariff` from the user's ElectricTariff input dict +(which uses the real REopt input schema: `urdb_label`, `urdb_response`, +`urdb_utility_name`+`urdb_rate_name`, `tou_energy_rates_per_kwh`, +`monthly_energy_rates`, `monthly_demand_rates`, `blended_annual_energy_rate`, +`blended_annual_demand_rate`, etc.) and extract the processed arrays MPC +needs for its per-window posts. + +Returns a NamedTuple with: + * `energy_rates::Vector{Float64}` (length `length_of_data`) + * `monthly_demand_rates::Vector{Float64}` (length 12, all zeros if not set) + * `tou_demand_rates::Vector{Float64}` (one per ratchet; empty for non-URDB) + * `tou_demand_time_steps::Vector{Vector{Int}}` (full-year indices per ratchet) + +Multi-tier rates are flattened to a single tier (MPC does not model tiers). +""" +function _mpc_build_tariff_arrays(et_input::Dict, year::Union{Int,Nothing}, + time_steps_per_hour::Int, length_of_data::Int) + # Only forward known ElectricTariff kwargs so unrelated keys (e.g. a + # parsed API artifact) don't trip the constructor. + known_kwargs = (:urdb_label, :urdb_response, :urdb_utility_name, + :urdb_rate_name, :urdb_metadata, + :wholesale_rate, :export_rate_beyond_net_metering_limit, + :monthly_energy_rates, :monthly_demand_rates, + :blended_annual_energy_rate, :blended_annual_demand_rate, + :add_monthly_rates_to_urdb_rate, + :tou_energy_rates_per_kwh, :add_tou_energy_rates_to_urdb_rate, + :remove_tiers, :demand_lookback_months, + :demand_lookback_percent, :demand_lookback_range, + :coincident_peak_load_active_time_steps, + :coincident_peak_load_charge_per_kw) + kwargs = Dict{Symbol,Any}(Symbol(k) => v for (k, v) in et_input + if Symbol(k) in known_kwargs) + # Force single-tier arrays so MPC sees a flat schedule; without this the + # constructor could return `length_of_data x n_tiers` and we'd silently + # only use one tier (and inconsistently across energy vs demand). + kwargs[:remove_tiers] = get(kwargs, :remove_tiers, true) + + tariff = reoptjl.ElectricTariff(; year = year, + time_steps_per_hour = time_steps_per_hour, + kwargs...) + + # All arrays are single-tier (we forced `remove_tiers=true`). + energy_rates = vec(Float64.(tariff.energy_rates[:, 1])) + length(energy_rates) == length_of_data || error( + "MPC: derived energy_rates length $(length(energy_rates)) != length_of_data $(length_of_data).") + + monthly_demand_rates = isempty(tariff.monthly_demand_rates) ? + zeros(Float64, 12) : vec(Float64.(tariff.monthly_demand_rates[:, 1])) + length(monthly_demand_rates) == 12 || error( + "MPC: derived monthly_demand_rates length $(length(monthly_demand_rates)) != 12.") + + tou_demand_rates = isempty(tariff.tou_demand_rates) ? + Float64[] : vec(Float64.(tariff.tou_demand_rates[:, 1])) + tou_demand_time_steps = [Int.(v) for v in tariff.tou_demand_ratchet_time_steps] + + return (energy_rates = energy_rates, + monthly_demand_rates = monthly_demand_rates, + tou_demand_rates = tou_demand_rates, + tou_demand_time_steps = tou_demand_time_steps) +end + +""" + _mpc_generate_pv_production_factor(d, time_steps_per_hour) + +Derive a PV production factor series via `REopt.get_production_factor` +(PVWatts) using `Site.latitude`/`Site.longitude` and the PV inputs in `d`. +Returns a `Vector{Float64}`. Throws if lat/lon are missing. +""" +function _mpc_generate_pv_production_factor(d::Dict, time_steps_per_hour::Int) + site = get(d, "Site", Dict()) + haskey(site, "latitude") || error( + "MPC: Site.latitude is required to derive PV.production_factor_series via PVWatts.") + haskey(site, "longitude") || error( + "MPC: Site.longitude is required to derive PV.production_factor_series via PVWatts.") + lat = Float64(site["latitude"]) + lon = Float64(site["longitude"]) + + @info "MPC: PV.production_factor_series not provided; deriving via REopt.jl PVWatts (lat=$(lat), lon=$(lon))." + + # Only forward known REopt.PV kwargs that affect the production factor. + pv = get(d, "PV", Dict()) + pv_pf_kwargs = (:array_type, :tilt, :module_type, :losses, :azimuth, :gcr, + :radius, :name, :location, :dc_ac_ratio, :inv_eff) + kwargs = Dict{Symbol,Any}(Symbol(k) => v for (k, v) in pv + if Symbol(k) in pv_pf_kwargs) + pv_struct = reoptjl.PV(; latitude = lat, kwargs...) + derived = reoptjl.get_production_factor(pv_struct, lat, lon; + time_steps_per_hour = time_steps_per_hour) + return collect(Float64.(derived)) +end + +""" + _mpc_generate_loads_kw(d, time_steps_per_hour) + +Derive an electric load profile via `REopt.ElectricLoad` from the user's +standard ElectricLoad inputs (`doe_reference_name`+`city` / +`blended_doe_reference_names`+`blended_doe_reference_percents`, +`annual_kwh`, `monthly_totals_kwh`, `monthly_peaks_kw`, `year`, etc.) — +the same DOE CRB derivation path the main REopt run would take. Returns a +`Vector{Float64}`. Requires `Site.latitude`/`Site.longitude`. +""" +function _mpc_generate_loads_kw(d::Dict, time_steps_per_hour::Int) + site = get(d, "Site", Dict()) + haskey(site, "latitude") || error( + "MPC: Site.latitude is required to derive ElectricLoad.loads_kw from a DOE CRB profile.") + haskey(site, "longitude") || error( + "MPC: Site.longitude is required to derive ElectricLoad.loads_kw from a DOE CRB profile.") + lat = Float64(site["latitude"]) + lon = Float64(site["longitude"]) + + @info "MPC: ElectricLoad.loads_kw not provided; deriving via REopt.jl ElectricLoad (lat=$(lat), lon=$(lon))." + + # Only forward known ElectricLoad kwargs. + eload = get(d, "ElectricLoad", Dict()) + known_kwargs = (:normalize_and_scale_load_profile_input, + :path_to_csv, :doe_reference_name, + :blended_doe_reference_names, :blended_doe_reference_percents, + :year, :city, :annual_kwh, :monthly_totals_kwh, + :monthly_peaks_kw, :critical_loads_kw, :loads_kw_is_net, + :critical_loads_kw_is_net, :critical_load_fraction, + :operating_reserve_required_fraction, + :min_load_met_annual_fraction, :off_grid_flag) + kwargs = Dict{Symbol,Any}(Symbol(k) => v for (k, v) in eload + if Symbol(k) in known_kwargs) + load_struct = reoptjl.ElectricLoad(; latitude = lat, longitude = lon, + time_steps_per_hour = time_steps_per_hour, + kwargs...) + return collect(Float64.(load_struct.loads_kw)) +end + +""" + _mpc_resolve_sizes!(d) + +Determines PV/ElectricStorage sizes for the MPC loop, using the actual +REopt input schema (`min_kw`/`max_kw`, `min_kwh`/`max_kwh`). + +Rules: + * If `min_kw == max_kw` for PV AND `min_kw == max_kw` AND + `min_kwh == max_kwh` for ElectricStorage → sizes are already fixed; use + those values directly. No sizing run. + * Otherwise → run a regular REopt sizing optimization, then mutate `d` to + set `min_kw = max_kw = sized_kw` (and similarly for kWh on storage). + Locking sizes in `d` ensures the downstream main REopt run uses the + same sizes the MPC SOC trajectory was computed against. + +Returns the tuple `(pv_kw, bess_kw, bess_kwh)` for the caller to use when +building per-window MPC posts. +""" +function _mpc_resolve_sizes!(d::Dict) + pv = get!(d, "PV", Dict()) + bess = get!(d, "ElectricStorage", Dict()) + + function _is_fixed(dct, lo_key, hi_key) + lo = get(dct, lo_key, nothing) + hi = get(dct, hi_key, nothing) + return lo !== nothing && hi !== nothing && Float64(lo) == Float64(hi) + end + + # Guard: daily_foresight_optimized requires a non-zero ElectricStorage. + # If the user has pinned kW or kWh to zero (min == max == 0), fail fast + # with a clear message instead of running a pointless sizing/MPC pass. + if (_is_fixed(bess, "min_kw", "max_kw") && Float64(get(bess, "min_kw", 0)) == 0.0) || + (_is_fixed(bess, "min_kwh", "max_kwh") && Float64(get(bess, "min_kwh", 0)) == 0.0) + error("ElectricStorage power (kW) and energy (kWh) must both be greater than zero to run the daily_foresight_optimized dispatch option. Got min_kw=$(get(bess, "min_kw", nothing)), max_kw=$(get(bess, "max_kw", nothing)), min_kwh=$(get(bess, "min_kwh", nothing)), max_kwh=$(get(bess, "max_kwh", nothing)).") + end + + pv_fixed = _is_fixed(pv, "min_kw", "max_kw") + bess_fixed = _is_fixed(bess, "min_kw", "max_kw") && + _is_fixed(bess, "min_kwh", "max_kwh") + + if pv_fixed && bess_fixed + pv_kw = Float64(pv["min_kw"]) + bess_kw = Float64(bess["min_kw"]) + bess_kwh = Float64(bess["min_kwh"]) + @info "MPC: sizes already fixed via min_kw==max_kw. PV=$(pv_kw) kW, BESS=$(bess_kw) kW / $(bess_kwh) kWh." + return (pv_kw = pv_kw, bess_kw = bess_kw, bess_kwh = bess_kwh) + end + + @info "MPC: PV and/or ElectricStorage sizes not fixed — running REopt sizing first." + sizing_post = deepcopy(d) + # Strip any API-only sentinels before sizing. + if haskey(sizing_post, "ElectricStorage") + delete!(sizing_post["ElectricStorage"], "dispatch_options") + delete!(sizing_post["ElectricStorage"], "fixed_soc_series_fraction") + end + settings = get!(sizing_post, "Settings", Dict()) + settings["run_bau"] = false + settings["solver_name"] = get(settings, "solver_name", "HiGHS") + settings["timeout_seconds"] = get(settings, "timeout_seconds", 420) + settings["optimality_tolerance"] = get(settings, "optimality_tolerance", 0.001) + + solver_attributes = SolverAttributes( + settings["timeout_seconds"], settings["optimality_tolerance"]) + m = get_solver_model(get_solver_model_type(settings["solver_name"]), solver_attributes) + + model_inputs = reoptjl.REoptInputs(sizing_post) + sizing_results = reoptjl.run_reopt(m, model_inputs) + + if get(sizing_results, "status", "") != "optimal" + error("MPC sizing pre-step did not solve to optimality (status = $(get(sizing_results, "status", "unknown"))).") + end + + pv_kw = Float64(get(get(sizing_results, "PV", Dict()), "size_kw", 0.0)) + bess_kw = Float64(get(get(sizing_results, "ElectricStorage", Dict()), "size_kw", 0.0)) + bess_kwh = Float64(get(get(sizing_results, "ElectricStorage", Dict()), "size_kwh", 0.0)) + + # Lock the resolved sizes in `d` so the downstream main REopt run uses + # the SAME sizes that the MPC SOC trajectory is consistent with. + pv["min_kw"] = pv_kw + pv["max_kw"] = pv_kw + bess["min_kw"] = bess_kw + bess["max_kw"] = bess_kw + bess["min_kwh"] = bess_kwh + bess["max_kwh"] = bess_kwh + + @info "MPC: sized PV=$(pv_kw) kW, BESS=$(bess_kw) kW / $(bess_kwh) kWh (locked into d via min==max)." + return (pv_kw = pv_kw, bess_kw = bess_kw, bess_kwh = bess_kwh) +end + +# Core MPC computation. Takes the already-parsed (and api-key-stripped) request +# dict `d` and returns the results dict (dispatch series, costs, etc.). +# May throw; callers handle error responses. +# Called by both /mpc (standalone) and /reopt (when dispatch_options = +# "daily_foresight_optimized"). +function get_mpc_results(d::Dict)::Dict + # ---- Shared settings (with sensible defaults) ---- + settings = get(d, "Settings", Dict()) + solver_name = get(settings, "solver_name", "HiGHS") + if solver_name == "Xpress" && xpress_installed != "True" + solver_name = "HiGHS" + @warn "Changing solver_name from Xpress to $solver_name because Xpress is not installed. " * + "Next time specify Settings.solver_name = 'HiGHS' or 'Cbc' or 'SCIP'." + end + + optimality_tolerance = Float64(get(settings, "optimality_tolerance", 0.001)) + + # ---- Hard-coded MPC config ---- + # Full-year, 24-h horizon, year-end wrap-around, 60 s/iter solver cap. + time_steps_per_hour = Int(get(settings, "time_steps_per_hour", 1)) + length_of_data = 8760 * time_steps_per_hour + horizon = 24 * time_steps_per_hour + per_iter_timeout_s = 60.0 + + # ---- Guarantee presence of the tech / load sub-dicts ---- + # Done up front so PV/load resolution and sizing can mutate them in + # place, and downstream code can use plain indexing. + pv = get!(d, "PV", Dict()) + bess = get!(d, "ElectricStorage", Dict()) + eload = get!(d, "ElectricLoad", Dict()) + et = get(d, "ElectricTariff", Dict()) + eutil = get(d, "ElectricUtility", Dict()) + + # ---- Resolve PV production factor series ---- + # If not user-provided, derive via REopt.jl PVWatts and cache into `d` + # so both the sizing pre-step and downstream main REopt run reuse it + # without a second PVWatts call. + pv_prod_factor = if haskey(pv, "production_factor_series") && !isempty(pv["production_factor_series"]) + # Broadcast-convert: handles Vector{Any} from JSON parsing + # (convert(Vector{Float64}, ::Vector{Any}) would throw). + Float64.(pv["production_factor_series"]) + else + series = _mpc_generate_pv_production_factor(d, time_steps_per_hour) + pv["production_factor_series"] = series + series + end + + # ---- Resolve electric load series ---- + # Mirror the PV pattern: if user-provided, use it; else derive via + # REopt.jl ElectricLoad (DOE CRB path) and cache into `d`. + load_series = if haskey(eload, "loads_kw") && !isempty(eload["loads_kw"]) + Float64.(eload["loads_kw"]) + else + series = _mpc_generate_loads_kw(d, time_steps_per_hour) + eload["loads_kw"] = series + series + end + + # ---- Length checks (8760 or 8784 * tsh) ---- + # No mutation of user-supplied series. A leap-year-length input + # (8784*tsh) is accepted as-is; `_mpc_slice_with_wrap` reads the extra + # day from it for year-end look-ahead instead of wrapping to Jan 1. + pv_prod_factor = _mpc_check_series_length("PV.production_factor_series", + pv_prod_factor, time_steps_per_hour, length_of_data) + load_series = _mpc_check_series_length("ElectricLoad.loads_kw", + load_series, time_steps_per_hour, length_of_data) + + # ---- Resolve PV and ElectricStorage sizes ---- + # Runs AFTER PV/load resolution so the (possibly internal) sizing + # REopt run reuses the cached series instead of re-deriving them. + # Either pulls sizes from min_kw==max_kw / min_kwh==max_kwh, or runs + # a sizing REopt and locks the resolved values into `d` so the + # downstream main REopt run uses the SAME sizes. + sizes = _mpc_resolve_sizes!(d) + pv_kw = sizes.pv_kw + bess_kw = sizes.bess_kw + bess_kwh = sizes.bess_kwh + + soc_init = Float64(get(bess, "soc_init_fraction", 0.5)) + soc_min = Float64(get(bess, "soc_min_fraction", 0.2)) + # REopt ElectricStorage exposes three component efficiencies; MPC's + # MPCElectricStorage uses combined charge/discharge round-trip halves. + # Match REopt's own composition: charge = rectifier * sqrt(internal), + # discharge = inverter * sqrt(internal). Defaults from REopt.jl. + rect_eff = Float64(get(bess, "rectifier_efficiency_fraction", 0.96)) + inv_eff = Float64(get(bess, "inverter_efficiency_fraction", 0.96)) + int_eff = Float64(get(bess, "internal_efficiency_fraction", 0.975)) + charge_eff = rect_eff * sqrt(int_eff) + discharge_eff = inv_eff * sqrt(int_eff) + + # Year for URDB schedule decoding is sourced from ElectricLoad.year + # (the canonical year input in REopt; ElectricTariff's `year` kwarg is + # an internal pass-through, not a user input). + tariff_year = haskey(eload, "year") ? Int(eload["year"]) : nothing + tariff = _mpc_build_tariff_arrays(et, tariff_year, time_steps_per_hour, length_of_data) + energy_rates = tariff.energy_rates + tou_demand_rates = tariff.tou_demand_rates # one per ratchet, $/kW + tou_demand_ts_all = tariff.tou_demand_time_steps # full-year indices per ratchet + monthly_demand_rates = tariff.monthly_demand_rates # length 12, $/kW per month + + # Optional emissions series — broadcast zero if not provided. + emissions = haskey(eutil, "emissions_factor_series_lb_CO2_per_kwh") ? + Float64.(eutil["emissions_factor_series_lb_CO2_per_kwh"]) : + zeros(Float64, length_of_data) + emissions = _mpc_check_series_length("ElectricUtility.emissions_factor_series_lb_CO2_per_kwh", + emissions, time_steps_per_hour, length_of_data) + + # ---- Demand tracking state ---- + # Reverse indices into the calendar: each timestep maps to its + # 1-based month (always defined) and 1-based TOU ratchet (0 if no + # ratchet applies). Both are vector lookups (O(1)) used inside the + # rolling-horizon loop to build window-local time-step groupings + # and to attribute realized peaks to the right month/tier. + month_starts = get_month_transition_timesteps(time_steps_per_hour) + ts_to_month = Vector{Int}(undef, length_of_data) + for m in 1:12 + s = month_starts[m] + e = m < 12 ? month_starts[m+1] - 1 : length_of_data + ts_to_month[s:e] .= m + end + + ts_to_tier = zeros(Int, length_of_data) + for (t, ratchet_ts) in enumerate(tou_demand_ts_all), g in ratchet_ts + if 1 <= g <= length_of_data + ts_to_tier[g] = t + end + end + + n_tou = length(tou_demand_rates) + # `monthly_previous_peak_demands` and `tou_previous_peak_demands` are + # passed into every MPC post AND mutated after each solve so that + # subsequent windows "see" the realized peaks so far. Both reset at + # month boundaries (matches REopt billing semantics; multi-month + # ratchet/lookback support would require a different reset cadence). + tou_previous_peak_demands = zeros(Float64, n_tou) + monthly_previous_peak_demands = zeros(Float64, 12) + monthly_demand_cost_total = 0.0 + tou_demand_cost_total = 0.0 + tou_peaks_by_month = Vector{Vector{Float64}}() + monthly_peaks = Float64[] + + # ---- Dispatch accumulators (1 value per executed timestep) ---- + dispatch = Dict( + "PV" => Dict( + "electric_to_load_series_kw" => Float64[], + "electric_to_storage_series_kw" => Float64[], + "electric_to_grid_series_kw" => Float64[], + "electric_curtailed_series_kw" => Float64[], + ), + "ElectricStorage" => Dict( + "storage_to_load_series_kw" => Float64[], + "soc_series_fraction" => Float64[], + ), + "ElectricUtility" => Dict( + "electric_to_load_series_kw" => Float64[], + "electric_to_storage_series_kw" => Float64[], + "emissions_series_lb_CO2" => Float64[], + ), + "ElectricLoad" => Dict( + "load_series_kw" => Float64[], + ), + ) + cost_series = Float64[] + total_energy_cost = 0.0 + bess_soc_init_fraction = soc_init + + # ---- Build the (reused) MPC post template ---- + # Note: no Settings block here; the solver is constructed below per + # window and passed directly to run_mpc(model, post). All four demand + # inputs (TOU + monthly rates and previous-peak vectors) are passed + # so the optimizer's objective accounts for *both* charges; omitting + # `monthly_*` would silently optimize without the monthly demand + # contribution and over-state savings under monthly-demand tariffs. + function build_mpc_post(window_pv, window_load, window_rates, window_emissions, + window_tou_ts, window_monthly_ts, + tou_prev_peaks, monthly_prev_peaks, soc_init_frac) + return Dict( + "PV" => Dict( + "size_kw" => pv_kw, + "production_factor_series" => window_pv, + ), + "ElectricStorage" => Dict( + "size_kw" => bess_kw, + "size_kwh" => bess_kwh, + "charge_efficiency" => charge_eff, + "discharge_efficiency" => discharge_eff, + "soc_init_fraction" => soc_init_frac, + "soc_min_fraction" => soc_min, + ), + "ElectricLoad" => Dict( + "loads_kw" => window_load, + ), + "ElectricTariff" => Dict( + "energy_rates" => window_rates, + "tou_demand_rates" => tou_demand_rates, + "tou_demand_time_steps" => window_tou_ts, + "tou_previous_peak_demands" => tou_prev_peaks, + "monthly_demand_rates" => monthly_demand_rates, + "time_steps_monthly" => window_monthly_ts, + "monthly_previous_peak_demands" => monthly_prev_peaks, + ), + "ElectricUtility" => Dict( + "emissions_factor_series_lb_CO2_per_kwh" => window_emissions, + ), + ) + end + + @info "MPC: starting rolling-horizon loop ($(length_of_data) iterations, horizon=$(horizon))" + for idx in 1:length_of_data + end_ts = idx + horizon - 1 + window_len = end_ts - idx + 1 + + window_pv = _mpc_slice_with_wrap(pv_prod_factor, idx, end_ts) + window_load = _mpc_slice_with_wrap(load_series, idx, end_ts) + window_rates = _mpc_slice_with_wrap(energy_rates, idx, end_ts) + window_emiss = _mpc_slice_with_wrap(emissions, idx, end_ts) + + # Window-local time-step groupings, both built from a single pass + # over the horizon. TOU buckets are 1 vector per ratchet (length + # n_tou); monthly buckets are 1 vector per month (length 12). + # Global indices wrap on `length_of_data` (8760·tsh) so the month + # / tier of a wrapped step matches its calendar position. + window_tou_ts = [Int[] for _ in 1:n_tou] + window_monthly_ts = [Int[] for _ in 1:12] + for k in 1:window_len + g = idx + k - 1 + if g > length_of_data + g -= length_of_data + end + push!(window_monthly_ts[ts_to_month[g]], k) + tier = ts_to_tier[g] + if tier > 0 + push!(window_tou_ts[tier], k) + end + end + + post = build_mpc_post(window_pv, window_load, window_rates, window_emiss, + window_tou_ts, window_monthly_ts, + tou_previous_peak_demands, monthly_previous_peak_demands, + bess_soc_init_fraction) + + model = get_solver_model(get_solver_model_type(solver_name), + SolverAttributes(per_iter_timeout_s, optimality_tolerance)) + result = reoptjl.run_mpc(model, post) + + # ---- Extract first-timestep dispatch (perfect-forecast) ---- + pv_res = result["PV"] + bess_res = result["ElectricStorage"] + util_res = result["ElectricUtility"] + + pv_to_load = pv_res["electric_to_load_series_kw"][1] + pv_to_bess = pv_res["electric_to_storage_series_kw"][1] + pv_to_grid = haskey(pv_res, "electric_to_grid_series_kw") ? pv_res["electric_to_grid_series_kw"][1] : 0.0 + pv_curtailed = haskey(pv_res, "electric_curtailed_series_kw") ? pv_res["electric_curtailed_series_kw"][1] : 0.0 + bess_to_load = bess_res["storage_to_load_series_kw"][1] + bess_soc = bess_res["soc_series_fraction"][1] + util_to_load = util_res["electric_to_load_series_kw"][1] + util_to_bess = util_res["electric_to_storage_series_kw"][1] + grid_power = max(util_to_load + util_to_bess, 0.0) + + push!(dispatch["PV"]["electric_to_load_series_kw"], pv_to_load) + push!(dispatch["PV"]["electric_to_storage_series_kw"], pv_to_bess) + push!(dispatch["PV"]["electric_to_grid_series_kw"], pv_to_grid) + push!(dispatch["PV"]["electric_curtailed_series_kw"], pv_curtailed) + push!(dispatch["ElectricStorage"]["storage_to_load_series_kw"], bess_to_load) + push!(dispatch["ElectricStorage"]["soc_series_fraction"], round(bess_soc, digits=6)) + push!(dispatch["ElectricUtility"]["electric_to_load_series_kw"], util_to_load) + push!(dispatch["ElectricUtility"]["electric_to_storage_series_kw"], util_to_bess) + push!(dispatch["ElectricUtility"]["emissions_series_lb_CO2"], + emissions[idx] * grid_power / time_steps_per_hour) + push!(dispatch["ElectricLoad"]["load_series_kw"], load_series[idx]) + + # ---- Energy cost ---- + step_energy_cost = grid_power * energy_rates[idx] / time_steps_per_hour + push!(cost_series, step_energy_cost) + total_energy_cost += step_energy_cost + + # ---- Carry state ---- + bess_soc_init_fraction = bess_soc + + # ---- Demand peak tracking (per-tier and per-month) ---- + # Attribute realized peak grid power to its TOU ratchet (if any) + # and to its calendar month. Identity comes from the reverse + # indices, NOT from rate values — so coincident-rate ratchets + # and equal-rate months remain separate billing pools. + m_now = ts_to_month[idx] + monthly_previous_peak_demands[m_now] = + max(grid_power, monthly_previous_peak_demands[m_now]) + + if n_tou > 0 + tier_now = ts_to_tier[idx] + if tier_now > 0 + tou_previous_peak_demands[tier_now] = + max(grid_power, tou_previous_peak_demands[tier_now]) + end + end + + # ---- Month transition: close out the just-ended month ---- + # Both monthly and TOU peaks reset at month boundaries (matches + # REopt billing semantics). The last timestep of month m is + # detected by m_now != ts_to_month[idx+1]; we handle Dec via + # idx == length_of_data. + is_month_end = (idx == length_of_data) || (ts_to_month[idx + 1] != m_now) + if is_month_end + push!(monthly_peaks, monthly_previous_peak_demands[m_now]) + monthly_demand_cost_total += + monthly_previous_peak_demands[m_now] * monthly_demand_rates[m_now] + monthly_previous_peak_demands[m_now] = 0.0 + + if n_tou > 0 + push!(tou_peaks_by_month, copy(tou_previous_peak_demands)) + tou_demand_cost_total += sum(tou_previous_peak_demands .* tou_demand_rates) + fill!(tou_previous_peak_demands, 0.0) + end + end + end + + @info "MPC: loop complete. total_energy_cost=\$$(round(total_energy_cost, digits=2))" + + return Dict( + "MPC" => Dict( + "time_steps_per_hour" => time_steps_per_hour, + "horizon" => horizon, + ), + "PV" => Dict("size_kw" => pv_kw), + "ElectricStorage" => Dict("size_kw" => bess_kw, "size_kwh" => bess_kwh), + "dispatch" => dispatch, + "ElectricTariff" => Dict( + "total_energy_cost" => total_energy_cost, + "energy_cost_series_per_timestep" => cost_series, + "total_tou_demand_cost" => tou_demand_cost_total, + "total_monthly_demand_cost" => monthly_demand_cost_total, + "tou_peaks_by_month_kw" => tou_peaks_by_month, + "monthly_peaks_kw" => monthly_peaks, + ), + "status" => "optimal", + "reopt_version" => string(pkgversion(reoptjl)), + ) +end diff --git a/reoptjl/migrations/0117_merge_20260530_1640.py b/reoptjl/migrations/0117_merge_20260530_1640.py new file mode 100644 index 000000000..0302ff8e6 --- /dev/null +++ b/reoptjl/migrations/0117_merge_20260530_1640.py @@ -0,0 +1,14 @@ +# Generated by Django 4.2.26 on 2026-05-30 16:40 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('reoptjl', '0116_alter_apimeta_api_key_alter_apimeta_job_type'), + ('reoptjl', '0116_rename_state_of_health_electricstorageoutputs_state_of_health_series_fraction_and_more'), + ] + + operations = [ + ] From 8087cc97ea2bb75433f613bdcdf3c99ee44fcabd Mon Sep 17 00:00:00 2001 From: adfarth Date: Mon, 1 Jun 2026 13:41:21 -0600 Subject: [PATCH 05/45] updt lower bound on fixed_soc tolerance --- reoptjl/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reoptjl/models.py b/reoptjl/models.py index be35002d1..cee7bfaa1 100644 --- a/reoptjl/models.py +++ b/reoptjl/models.py @@ -3767,7 +3767,7 @@ class ElectricStorageInputs(BaseModel, models.Model): ) fixed_soc_series_fraction_tolerance = models.FloatField( validators=[ - MinValueValidator(-1), + MinValueValidator(0), MaxValueValidator(1) ], null=True, blank=True, From 57c201d70ee026f93d658ec7f89e27eae1f04640 Mon Sep 17 00:00:00 2001 From: adfarth Date: Mon, 1 Jun 2026 14:56:12 -0600 Subject: [PATCH 06/45] Create 0117_merge_20260601_2056.py --- reoptjl/migrations/0117_merge_20260601_2056.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 reoptjl/migrations/0117_merge_20260601_2056.py diff --git a/reoptjl/migrations/0117_merge_20260601_2056.py b/reoptjl/migrations/0117_merge_20260601_2056.py new file mode 100644 index 000000000..6040e706e --- /dev/null +++ b/reoptjl/migrations/0117_merge_20260601_2056.py @@ -0,0 +1,14 @@ +# Generated by Django 4.2.26 on 2026-06-01 20:56 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('reoptjl', '0116_alter_apimeta_api_key_alter_apimeta_job_type'), + ('reoptjl', '0116_rename_state_of_health_electricstorageoutputs_state_of_health_series_fraction_and_more'), + ] + + operations = [ + ] From bfcef2a4c9faf5ae6ea8a2d10f8e0c65cdf1d8e2 Mon Sep 17 00:00:00 2001 From: lixiangk1 <72464565+lixiangk1@users.noreply.github.com> Date: Thu, 11 Jun 2026 12:45:04 -0600 Subject: [PATCH 07/45] Update MPC modeling --- julia_src/http.jl | 28 +- julia_src/mpc.jl | 688 ++++++++++++++++++---------------------------- 2 files changed, 294 insertions(+), 422 deletions(-) diff --git a/julia_src/http.jl b/julia_src/http.jl index e6f94a42c..b03a742f8 100644 --- a/julia_src/http.jl +++ b/julia_src/http.jl @@ -66,14 +66,23 @@ function reopt(req::HTTP.Request) delete!(d, "api_key") end + settings = d["Settings"] + solver_name = get(settings, "solver_name", "HiGHS") + if solver_name == "Xpress" && !(xpress_installed=="True") + solver_name = "HiGHS" + @warn "Changing solver_name from Xpress to $solver_name because Xpress is not installed. Next time + Specify Settings.solver_name = 'HiGHS' or 'Cbc' or 'SCIP'" + end + # ---- API-only battery heuristic dispatch strategy: "daily_foresight_optimized" ---- # When ElectricStorage.dispatch_options == "daily_foresight_optimized", first run the MPC rolling-horizon loop to get an SOC profile. # Then set ElectricStorage.fixed_soc_series_fraction = MPC SOC before running the main REopt optimization. + # Runs after the Xpress availability check so the resolved solver_name is passed in. electric_storage = get(d, "ElectricStorage", Dict()) if get(electric_storage, "dispatch_options", nothing) == "daily_foresight_optimized" try @info "Running MPC to obtain daily foresight optimized battery dispatch profile." - mpc_results = get_mpc_results(d) + mpc_results = get_mpc_results(d; solver_name=solver_name) soc = mpc_results["dispatch"]["ElectricStorage"]["soc_series_fraction"] d["ElectricStorage"]["fixed_soc_series_fraction"] = soc d["ElectricStorage"]["dispatch_options"] = "custom" @@ -86,13 +95,7 @@ function reopt(req::HTTP.Request) end end - settings = d["Settings"] - solver_name = get(settings, "solver_name", "HiGHS") - if solver_name == "Xpress" && !(xpress_installed=="True") - solver_name = "HiGHS" - @warn "Changing solver_name from Xpress to $solver_name because Xpress is not installed. Next time - Specify Settings.solver_name = 'HiGHS' or 'Cbc' or 'SCIP'" - end + #TODO: What timeout and optimality tolerance should MPC use? timeout_seconds = pop!(settings, "timeout_seconds") optimality_tolerance = pop!(settings, "optimality_tolerance") solver_attributes = SolverAttributes(timeout_seconds, optimality_tolerance) @@ -844,7 +847,14 @@ function mpc(req::HTTP.Request) ENV["NREL_DEVELOPER_API_KEY"] = test_nrel_developer_api_key delete!(d, "api_key") end - results = get_mpc_results(d) + settings = get(d, "Settings", Dict()) + solver_name = get(settings, "solver_name", "HiGHS") + if solver_name == "Xpress" && !(xpress_installed=="True") + solver_name = "HiGHS" + @warn "Changing solver_name from Xpress to $solver_name because Xpress is not installed. + Next time specify Settings.solver_name = 'HiGHS' or 'Cbc' or 'SCIP'." + end + results = get_mpc_results(d; solver_name=solver_name) catch e @error "MPC failed" exception=(e, catch_backtrace()) error_response["error"] = sprint(showerror, e) diff --git a/julia_src/mpc.jl b/julia_src/mpc.jl index 21cb9510a..af376cdbe 100644 --- a/julia_src/mpc.jl +++ b/julia_src/mpc.jl @@ -3,22 +3,19 @@ # MPC (Model Predictive Control) endpoint # ---------------------------------------------------------------------------- # Rolling-horizon dispatch with one day look-ahead using REopt.run_mpc. Assumes: -# * PV and ElectricStorage are the only available technologies (perfect forecast) +# * PV and ElectricStorage are the only available technologies # * Only perfect forecast scenarios are modeled (no forecast errors) -# * Sizing: Perform a sizing run first if PV or ElectricStorage sizes are not provided (storage sizes must be greater than zero) +# * Sizing: Perform a sizing run first if PV or ElectricStorage sizes are not provided (storage sizes must be > 0) # * PV: Use PVWatts if `PV.production_factor_series` is not provided # * ElectricLoad: Use commercial reference profiles if `ElectricLoad.loads_kw` is not provided -# * Code currently wraps with Jan 1 data to determine Dec 31 dispatch (allows leap-year data, which avoids the need to wrap) +# * Code wraps with Jan 1 data to determine Dec 31 dispatch (leap-year inputs are not supported) # * MPC settings are currently hard coded (e.g., forecast horizon, control horizon, optimization horizon) # ============================================================================ """ get_month_transition_timesteps(time_steps_per_hour) -Return the 1-based timestep index marking the START of each month for a -non-leap year beginning Jan 1 00:00. Length 12; first element == 1. -The last "transition" (end of December) is implicitly `length_of_data + 1` -and is handled by the caller. +Return an array of length 12 specifying the index marking the start of each month in a non-leap year """ function get_month_transition_timesteps(time_steps_per_hour::Int) # hours in each month for a non-leap year @@ -32,15 +29,11 @@ function get_month_transition_timesteps(time_steps_per_hour::Int) end """ - _mpc_slice_with_wrap(arr, idx, end_idx) + slice_data(arr, idx, end_idx) -Return `arr[idx:end_idx]` but wrap around to the start of `arr` if -`end_idx > length(arr)`. The wrap threshold is the actual array length, -not `length_of_data`, so a leap-year-length input (8784*tsh) naturally -supplies the extra day for the year-end look-ahead window before any -wrap is needed. +Returns arr[idx:end_idx] but wrap around to the start of arr if end_idx > length(arr). """ -function _mpc_slice_with_wrap(arr::AbstractVector, idx::Int, end_idx::Int) +function slice_data(arr::AbstractVector, idx::Int, end_idx::Int) n = length(arr) if end_idx <= n return arr[idx:end_idx] @@ -51,383 +44,286 @@ function _mpc_slice_with_wrap(arr::AbstractVector, idx::Int, end_idx::Int) end """ - _mpc_check_series_length(name, series, time_steps_per_hour, length_of_data) - -Validate that a user-supplied annual time series has either -`8760 * time_steps_per_hour` entries (standard year) or -`8784 * time_steps_per_hour` entries (leap year). Returns the series -unchanged. - -The MPC loop iterates over `length_of_data = 8760 * time_steps_per_hour` -timesteps; if a leap-year-length series is supplied, the extra day is -used as additional look-ahead data by `_mpc_slice_with_wrap` (which wraps -on the actual array length, not on `length_of_data`). Any other length is -a hard error — REopt.jl's own `check_and_adjust_load_length` would -silently repeat/pad, masking the mistake. + check_series_length(name, series, time_steps_per_hour, length_of_data) + +Validate that a user-entered time series is of length 8760 * time_steps_per_hour (cannot be a leap year). """ -function _mpc_check_series_length(name::String, series::AbstractVector, - time_steps_per_hour::Int, length_of_data::Int) +function check_series_length(name::String, series::AbstractVector, length_of_data::Int) n = length(series) - n == length_of_data && return series - if n == 8784 * time_steps_per_hour - @info "MPC: $name has leap-year length ($n); the extra day will be used " * - "for year-end look-ahead instead of wrapping to Jan 1." + if n == length_of_data return series end - # Diagnostic: if `n` is consistent with 8760 hours at a *different* - # sub-hourly resolution, the user probably mis-declared - # `Settings.time_steps_per_hour`. - tsh_hint = "" - for tsh_guess in (1, 2, 4) - tsh_guess == time_steps_per_hour && continue - if n == 8760 * tsh_guess || n == 8784 * tsh_guess - tsh_hint = " (length is consistent with time_steps_per_hour=$(tsh_guess); " * - "check Settings.time_steps_per_hour)." - break - end - end - error("MPC: $name length $n != " * - "8760 * time_steps_per_hour ($length_of_data) " * - "and != 8784 * time_steps_per_hour ($(8784 * time_steps_per_hour))." * - tsh_hint) + error("MPC: $name length $n != 8760 * time_steps_per_hour ($length_of_data).") end """ - _mpc_build_tariff_arrays(et_input, year, time_steps_per_hour, length_of_data) - -Construct a `REopt.ElectricTariff` from the user's ElectricTariff input dict -(which uses the real REopt input schema: `urdb_label`, `urdb_response`, -`urdb_utility_name`+`urdb_rate_name`, `tou_energy_rates_per_kwh`, -`monthly_energy_rates`, `monthly_demand_rates`, `blended_annual_energy_rate`, -`blended_annual_demand_rate`, etc.) and extract the processed arrays MPC -needs for its per-window posts. - -Returns a NamedTuple with: - * `energy_rates::Vector{Float64}` (length `length_of_data`) - * `monthly_demand_rates::Vector{Float64}` (length 12, all zeros if not set) - * `tou_demand_rates::Vector{Float64}` (one per ratchet; empty for non-URDB) - * `tou_demand_time_steps::Vector{Vector{Int}}` (full-year indices per ratchet) - -Multi-tier rates are flattened to a single tier (MPC does not model tiers). + get_tariff_inputs(electric_tariff, year, time_steps_per_hour) + +Call REopt.ElectricTariff to get energy_rates, monthly_demand_rates, tou_demand_rates, tou_demand_ratchet_time_steps +MPC does not currently model tiers (tiers are flattened), coincident peak charges, or demand lookback. """ -function _mpc_build_tariff_arrays(et_input::Dict, year::Union{Int,Nothing}, - time_steps_per_hour::Int, length_of_data::Int) - # Only forward known ElectricTariff kwargs so unrelated keys (e.g. a - # parsed API artifact) don't trip the constructor. - known_kwargs = (:urdb_label, :urdb_response, :urdb_utility_name, +function get_tariff_inputs(electric_tariff::Dict, year::Union{Int,Nothing}, time_steps_per_hour::Int) + + # TODO: Are all of these relevant? Any missing inputs? + # TODO: Implement lookback? (demand_lookback_months, demand_lookback_percent, demand_lookback_range) Ignoring coincident peak charges for now + # TODO: Need to think through NEM or passing back export values (wholesale_rate, export_rate_beyond_net_metering_limit) + tariff_kwargs = (:urdb_label, :urdb_response, :urdb_utility_name, :urdb_rate_name, :urdb_metadata, :wholesale_rate, :export_rate_beyond_net_metering_limit, :monthly_energy_rates, :monthly_demand_rates, :blended_annual_energy_rate, :blended_annual_demand_rate, :add_monthly_rates_to_urdb_rate, :tou_energy_rates_per_kwh, :add_tou_energy_rates_to_urdb_rate, - :remove_tiers, :demand_lookback_months, - :demand_lookback_percent, :demand_lookback_range, - :coincident_peak_load_active_time_steps, - :coincident_peak_load_charge_per_kw) - kwargs = Dict{Symbol,Any}(Symbol(k) => v for (k, v) in et_input - if Symbol(k) in known_kwargs) - # Force single-tier arrays so MPC sees a flat schedule; without this the - # constructor could return `length_of_data x n_tiers` and we'd silently - # only use one tier (and inconsistently across energy vs demand). - kwargs[:remove_tiers] = get(kwargs, :remove_tiers, true) - - tariff = reoptjl.ElectricTariff(; year = year, - time_steps_per_hour = time_steps_per_hour, - kwargs...) - - # All arrays are single-tier (we forced `remove_tiers=true`). - energy_rates = vec(Float64.(tariff.energy_rates[:, 1])) - length(energy_rates) == length_of_data || error( - "MPC: derived energy_rates length $(length(energy_rates)) != length_of_data $(length_of_data).") + :demand_lookback_months, :demand_lookback_percent, :demand_lookback_range) + kwargs = Dict{Symbol,Any}(Symbol(k) => v for (k, v) in electric_tariff if Symbol(k) in tariff_kwargs) - monthly_demand_rates = isempty(tariff.monthly_demand_rates) ? - zeros(Float64, 12) : vec(Float64.(tariff.monthly_demand_rates[:, 1])) - length(monthly_demand_rates) == 12 || error( - "MPC: derived monthly_demand_rates length $(length(monthly_demand_rates)) != 12.") + # MPC does not currently model tiered rates + kwargs[:remove_tiers] = true + tariff = reoptjl.ElectricTariff(; year = year, time_steps_per_hour = time_steps_per_hour, kwargs...) - tou_demand_rates = isempty(tariff.tou_demand_rates) ? - Float64[] : vec(Float64.(tariff.tou_demand_rates[:, 1])) - tou_demand_time_steps = [Int.(v) for v in tariff.tou_demand_ratchet_time_steps] + energy_rates = Float64.(tariff.energy_rates[:, 1]) + monthly_demand_rates = isempty(tariff.monthly_demand_rates) ? + zeros(Float64, 12) : Float64.(tariff.monthly_demand_rates[:, 1]) + tou_demand_rates = isempty(tariff.tou_demand_rates) ? Float64[] : Float64.(tariff.tou_demand_rates[:, 1]) + tou_demand_ratchet_time_steps = [Int.(v) for v in tariff.tou_demand_ratchet_time_steps] + # TODO: Track lookback variables - demand_lookback_months, demand_lookback_percent, demand_lookback_range? return (energy_rates = energy_rates, monthly_demand_rates = monthly_demand_rates, tou_demand_rates = tou_demand_rates, - tou_demand_time_steps = tou_demand_time_steps) + tou_demand_ratchet_time_steps = tou_demand_ratchet_time_steps) end """ - _mpc_generate_pv_production_factor(d, time_steps_per_hour) + generate_pv_production_factors(d, time_steps_per_hour) -Derive a PV production factor series via `REopt.get_production_factor` -(PVWatts) using `Site.latitude`/`Site.longitude` and the PV inputs in `d`. -Returns a `Vector{Float64}`. Throws if lat/lon are missing. +Generate a PV production factor series using PVWatts by calling REopt.get_production_factor """ -function _mpc_generate_pv_production_factor(d::Dict, time_steps_per_hour::Int) +function generate_pv_production_factors(d::Dict, time_steps_per_hour::Int) site = get(d, "Site", Dict()) - haskey(site, "latitude") || error( - "MPC: Site.latitude is required to derive PV.production_factor_series via PVWatts.") - haskey(site, "longitude") || error( - "MPC: Site.longitude is required to derive PV.production_factor_series via PVWatts.") + if !haskey(site, "latitude") + error("MPC: Site.latitude is required to generate PV.production_factor_series using PVWatts.") + end + if !haskey(site, "longitude") + error("MPC: Site.longitude is required to generate PV.production_factor_series using PVWatts.") + end lat = Float64(site["latitude"]) lon = Float64(site["longitude"]) - @info "MPC: PV.production_factor_series not provided; deriving via REopt.jl PVWatts (lat=$(lat), lon=$(lon))." + @info "MPC: PV.production_factor_series not provided; generating using PVWatts through REopt.jl (lat=$(lat), lon=$(lon))." - # Only forward known REopt.PV kwargs that affect the production factor. pv = get(d, "PV", Dict()) pv_pf_kwargs = (:array_type, :tilt, :module_type, :losses, :azimuth, :gcr, :radius, :name, :location, :dc_ac_ratio, :inv_eff) - kwargs = Dict{Symbol,Any}(Symbol(k) => v for (k, v) in pv - if Symbol(k) in pv_pf_kwargs) + kwargs = Dict{Symbol,Any}(Symbol(k) => v for (k, v) in pv if Symbol(k) in pv_pf_kwargs) pv_struct = reoptjl.PV(; latitude = lat, kwargs...) - derived = reoptjl.get_production_factor(pv_struct, lat, lon; - time_steps_per_hour = time_steps_per_hour) - return collect(Float64.(derived)) + pv_production_factor_series = reoptjl.get_production_factor(pv_struct, lat, lon; + time_steps_per_hour = time_steps_per_hour) + return Vector{Float64}(pv_production_factor_series) end """ - _mpc_generate_loads_kw(d, time_steps_per_hour) - -Derive an electric load profile via `REopt.ElectricLoad` from the user's -standard ElectricLoad inputs (`doe_reference_name`+`city` / -`blended_doe_reference_names`+`blended_doe_reference_percents`, -`annual_kwh`, `monthly_totals_kwh`, `monthly_peaks_kw`, `year`, etc.) — -the same DOE CRB derivation path the main REopt run would take. Returns a -`Vector{Float64}`. Requires `Site.latitude`/`Site.longitude`. + generate_loads_kw(d, time_steps_per_hour) + +Generate an electric load profile using DOE Commercial Reference buildings by calling REopt.ElectricLoad """ -function _mpc_generate_loads_kw(d::Dict, time_steps_per_hour::Int) +function generate_loads_kw(d::Dict, time_steps_per_hour::Int) site = get(d, "Site", Dict()) - haskey(site, "latitude") || error( - "MPC: Site.latitude is required to derive ElectricLoad.loads_kw from a DOE CRB profile.") - haskey(site, "longitude") || error( - "MPC: Site.longitude is required to derive ElectricLoad.loads_kw from a DOE CRB profile.") + if !haskey(site, "latitude") + error("MPC: Site.latitude is required to generate ElectricLoad.loads_kw using a DOE CRB profile.") + end + if !haskey(site, "longitude") + error("MPC: Site.longitude is required to generate ElectricLoad.loads_kw using a DOE CRB profile.") + end lat = Float64(site["latitude"]) lon = Float64(site["longitude"]) - @info "MPC: ElectricLoad.loads_kw not provided; deriving via REopt.jl ElectricLoad (lat=$(lat), lon=$(lon))." + @info "MPC: ElectricLoad.loads_kw not provided; generating via REopt.jl ElectricLoad (lat=$(lat), lon=$(lon))." - # Only forward known ElectricLoad kwargs. - eload = get(d, "ElectricLoad", Dict()) - known_kwargs = (:normalize_and_scale_load_profile_input, + electric_load = get(d, "ElectricLoad", Dict()) + + # TODO: Double check this list for relevance/missing inputs + load_kwargs = (:normalize_and_scale_load_profile_input, :path_to_csv, :doe_reference_name, :blended_doe_reference_names, :blended_doe_reference_percents, :year, :city, :annual_kwh, :monthly_totals_kwh, - :monthly_peaks_kw, :critical_loads_kw, :loads_kw_is_net, - :critical_loads_kw_is_net, :critical_load_fraction, - :operating_reserve_required_fraction, - :min_load_met_annual_fraction, :off_grid_flag) - kwargs = Dict{Symbol,Any}(Symbol(k) => v for (k, v) in eload - if Symbol(k) in known_kwargs) - load_struct = reoptjl.ElectricLoad(; latitude = lat, longitude = lon, - time_steps_per_hour = time_steps_per_hour, - kwargs...) - return collect(Float64.(load_struct.loads_kw)) + :monthly_peaks_kw, :loads_kw_is_net) + kwargs = Dict{Symbol,Any}(Symbol(k) => v for (k, v) in electric_load if Symbol(k) in load_kwargs) + load = reoptjl.ElectricLoad(; latitude = lat, longitude = lon, + time_steps_per_hour = time_steps_per_hour, kwargs...) + return Vector{Float64}(load.loads_kw) end """ - _mpc_resolve_sizes!(d) - -Determines PV/ElectricStorage sizes for the MPC loop, using the actual -REopt input schema (`min_kw`/`max_kw`, `min_kwh`/`max_kwh`). - -Rules: - * If `min_kw == max_kw` for PV AND `min_kw == max_kw` AND - `min_kwh == max_kwh` for ElectricStorage → sizes are already fixed; use - those values directly. No sizing run. - * Otherwise → run a regular REopt sizing optimization, then mutate `d` to - set `min_kw = max_kw = sized_kw` (and similarly for kWh on storage). - Locking sizes in `d` ensures the downstream main REopt run uses the - same sizes the MPC SOC trajectory was computed against. - -Returns the tuple `(pv_kw, bess_kw, bess_kwh)` for the caller to use when -building per-window MPC posts. + get_technology_sizes!(d) + +Determine PV and ElectricStorage sizes for the MPC loop. If min_kw != max_kw and/or min_kwh != max_kwh, +call REopt to size technologies. User input battery sizes must also be greater than zero. """ -function _mpc_resolve_sizes!(d::Dict) +function get_technology_sizes!(d::Dict) pv = get!(d, "PV", Dict()) - bess = get!(d, "ElectricStorage", Dict()) + batt = get!(d, "ElectricStorage", Dict()) - function _is_fixed(dct, lo_key, hi_key) + function is_fixed(dct, lo_key, hi_key) lo = get(dct, lo_key, nothing) hi = get(dct, hi_key, nothing) return lo !== nothing && hi !== nothing && Float64(lo) == Float64(hi) end - # Guard: daily_foresight_optimized requires a non-zero ElectricStorage. - # If the user has pinned kW or kWh to zero (min == max == 0), fail fast - # with a clear message instead of running a pointless sizing/MPC pass. - if (_is_fixed(bess, "min_kw", "max_kw") && Float64(get(bess, "min_kw", 0)) == 0.0) || - (_is_fixed(bess, "min_kwh", "max_kwh") && Float64(get(bess, "min_kwh", 0)) == 0.0) - error("ElectricStorage power (kW) and energy (kWh) must both be greater than zero to run the daily_foresight_optimized dispatch option. Got min_kw=$(get(bess, "min_kw", nothing)), max_kw=$(get(bess, "max_kw", nothing)), min_kwh=$(get(bess, "min_kwh", nothing)), max_kwh=$(get(bess, "max_kwh", nothing)).") + # Check storage sizes are greater than zero + if Float64(get(batt, "max_kw", 1.0)) <= 0.0 || Float64(get(batt, "max_kwh", 1.0)) <= 0.0 + error("ElectricStorage max_kw and max_kwh must both be greater than zero " * + "to run the daily_foresight_optimized dispatch option.") end - pv_fixed = _is_fixed(pv, "min_kw", "max_kw") - bess_fixed = _is_fixed(bess, "min_kw", "max_kw") && - _is_fixed(bess, "min_kwh", "max_kwh") + pv_fixed = is_fixed(pv, "min_kw", "max_kw") + batt_fixed = is_fixed(batt, "min_kw", "max_kw") && + is_fixed(batt, "min_kwh", "max_kwh") - if pv_fixed && bess_fixed + if pv_fixed && batt_fixed pv_kw = Float64(pv["min_kw"]) - bess_kw = Float64(bess["min_kw"]) - bess_kwh = Float64(bess["min_kwh"]) - @info "MPC: sizes already fixed via min_kw==max_kw. PV=$(pv_kw) kW, BESS=$(bess_kw) kW / $(bess_kwh) kWh." - return (pv_kw = pv_kw, bess_kw = bess_kw, bess_kwh = bess_kwh) + batt_kw = Float64(batt["min_kw"]) + batt_kwh = Float64(batt["min_kwh"]) + @info "MPC: fixed technology sizes entered, PV = $(pv_kw) kW, battery = $(batt_kw) kW / $(batt_kwh) kWh." + return (pv_kw = pv_kw, batt_kw = batt_kw, batt_kwh = batt_kwh) end - @info "MPC: PV and/or ElectricStorage sizes not fixed — running REopt sizing first." + @info "MPC: PV and/or ElectricStorage sizes are not specified — running REopt sizing first." sizing_post = deepcopy(d) - # Strip any API-only sentinels before sizing. + + # Delete inputs specific to the heuristic battery dispatch run if haskey(sizing_post, "ElectricStorage") delete!(sizing_post["ElectricStorage"], "dispatch_options") delete!(sizing_post["ElectricStorage"], "fixed_soc_series_fraction") end + settings = get!(sizing_post, "Settings", Dict()) settings["run_bau"] = false + + # TODO: Okay to hard code defaults here? solver_name check is redundant if function is only called from get_mpc_results settings["solver_name"] = get(settings, "solver_name", "HiGHS") settings["timeout_seconds"] = get(settings, "timeout_seconds", 420) settings["optimality_tolerance"] = get(settings, "optimality_tolerance", 0.001) - solver_attributes = SolverAttributes( - settings["timeout_seconds"], settings["optimality_tolerance"]) + solver_attributes = SolverAttributes(settings["timeout_seconds"], settings["optimality_tolerance"]) m = get_solver_model(get_solver_model_type(settings["solver_name"]), solver_attributes) model_inputs = reoptjl.REoptInputs(sizing_post) sizing_results = reoptjl.run_reopt(m, model_inputs) if get(sizing_results, "status", "") != "optimal" - error("MPC sizing pre-step did not solve to optimality (status = $(get(sizing_results, "status", "unknown"))).") + error("MPC sizing pre-step did not solve (status = $(get(sizing_results, "status", "unknown"))).") end pv_kw = Float64(get(get(sizing_results, "PV", Dict()), "size_kw", 0.0)) - bess_kw = Float64(get(get(sizing_results, "ElectricStorage", Dict()), "size_kw", 0.0)) - bess_kwh = Float64(get(get(sizing_results, "ElectricStorage", Dict()), "size_kwh", 0.0)) + batt_kw = Float64(get(get(sizing_results, "ElectricStorage", Dict()), "size_kw", 0.0)) + batt_kwh = Float64(get(get(sizing_results, "ElectricStorage", Dict()), "size_kwh", 0.0)) + + if batt_kw <= 0.0 || batt_kwh <= 0.0 + error("MPC: Optimal REopt sizing does not include ElectricStorage (size_kw=$(batt_kw), size_kwh=$(batt_kwh)). " * + "The daily_foresight_optimized dispatch option requires a non-zero battery size. " * + "Set ElectricStorage.min_kw == ElectricStorage.max_kw and " * + "ElectricStorage.min_kwh == ElectricStorage.max_kwh to fix sizes manually.") + end - # Lock the resolved sizes in `d` so the downstream main REopt run uses - # the SAME sizes that the MPC SOC trajectory is consistent with. pv["min_kw"] = pv_kw pv["max_kw"] = pv_kw - bess["min_kw"] = bess_kw - bess["max_kw"] = bess_kw - bess["min_kwh"] = bess_kwh - bess["max_kwh"] = bess_kwh + batt["min_kw"] = batt_kw + batt["max_kw"] = batt_kw + batt["min_kwh"] = batt_kwh + batt["max_kwh"] = batt_kwh - @info "MPC: sized PV=$(pv_kw) kW, BESS=$(bess_kw) kW / $(bess_kwh) kWh (locked into d via min==max)." - return (pv_kw = pv_kw, bess_kw = bess_kw, bess_kwh = bess_kwh) + @info "MPC: REopt sizing solved with PV = $(pv_kw) kW and battery = $(batt_kw) kW / $(batt_kwh) kWh." + return (pv_kw = pv_kw, batt_kw = batt_kw, batt_kwh = batt_kwh) end -# Core MPC computation. Takes the already-parsed (and api-key-stripped) request -# dict `d` and returns the results dict (dispatch series, costs, etc.). -# May throw; callers handle error responses. -# Called by both /mpc (standalone) and /reopt (when dispatch_options = -# "daily_foresight_optimized"). -function get_mpc_results(d::Dict)::Dict - # ---- Shared settings (with sensible defaults) ---- - settings = get(d, "Settings", Dict()) - solver_name = get(settings, "solver_name", "HiGHS") - if solver_name == "Xpress" && xpress_installed != "True" - solver_name = "HiGHS" - @warn "Changing solver_name from Xpress to $solver_name because Xpress is not installed. " * - "Next time specify Settings.solver_name = 'HiGHS' or 'Cbc' or 'SCIP'." - end +function get_mpc_results(d::Dict; solver_name::String="HiGHS")::Dict + """ + Run a full-year rolling-horizon MPC dispatch for PV + ElectricStorage by + calling `REopt.run_mpc` once per timestep with a 24-hour look-ahead. + + Inputs: + d::Dict, REopt inputs dictionary + solver_name::String, solver to use ("HiGHS", "Cbc", "SCIP", or "Xpress") + Returns a Dict with PV and BATT sizes, dispatch time series, and cost metrics + """ + + settings = get!(d, "Settings", Dict()) + settings["solver_name"] = solver_name + + # TODO: MPC timeout and optimality tolerance optimality_tolerance = Float64(get(settings, "optimality_tolerance", 0.001)) - # ---- Hard-coded MPC config ---- - # Full-year, 24-h horizon, year-end wrap-around, 60 s/iter solver cap. + # TODO: MPC horizons and timeout are currently hard coded time_steps_per_hour = Int(get(settings, "time_steps_per_hour", 1)) length_of_data = 8760 * time_steps_per_hour horizon = 24 * time_steps_per_hour - per_iter_timeout_s = 60.0 - - # ---- Guarantee presence of the tech / load sub-dicts ---- - # Done up front so PV/load resolution and sizing can mutate them in - # place, and downstream code can use plain indexing. - pv = get!(d, "PV", Dict()) - bess = get!(d, "ElectricStorage", Dict()) - eload = get!(d, "ElectricLoad", Dict()) - et = get(d, "ElectricTariff", Dict()) - eutil = get(d, "ElectricUtility", Dict()) - - # ---- Resolve PV production factor series ---- - # If not user-provided, derive via REopt.jl PVWatts and cache into `d` - # so both the sizing pre-step and downstream main REopt run reuse it - # without a second PVWatts call. - pv_prod_factor = if haskey(pv, "production_factor_series") && !isempty(pv["production_factor_series"]) - # Broadcast-convert: handles Vector{Any} from JSON parsing - # (convert(Vector{Float64}, ::Vector{Any}) would throw). - Float64.(pv["production_factor_series"]) + per_iter_timeout_s = 30.0 + + # TODO: Should MPC handle multiple PVs? + pv = get!(d, "PV", Dict()) + electric_storage = get!(d, "ElectricStorage", Dict()) + electric_load = get!(d, "ElectricLoad", Dict()) + electric_tariff = get(d, "ElectricTariff", Dict()) + electric_utility = get(d, "ElectricUtility", Dict()) + + # Read timeseries PV production factors or generate using PVWatts, save generated values to reduce PVWatts calls + if haskey(pv, "production_factor_series") && !isempty(pv["production_factor_series"]) + pv_prod_factor = Float64.(pv["production_factor_series"]) else - series = _mpc_generate_pv_production_factor(d, time_steps_per_hour) - pv["production_factor_series"] = series - series + # TODO: These production factors don't consider degradation, problem? + # Does MPCPV need a degradation input to calculate the levelization factor used in the optimization? + pv_prod_factor = generate_pv_production_factors(d, time_steps_per_hour) + pv["production_factor_series"] = pv_prod_factor end - # ---- Resolve electric load series ---- - # Mirror the PV pattern: if user-provided, use it; else derive via - # REopt.jl ElectricLoad (DOE CRB path) and cache into `d`. - load_series = if haskey(eload, "loads_kw") && !isempty(eload["loads_kw"]) - Float64.(eload["loads_kw"]) + # Read timeseries electric load inputs or generate using CRBs + if haskey(electric_load, "loads_kw") && !isempty(electric_load["loads_kw"]) + loads_kw = Float64.(electric_load["loads_kw"]) else - series = _mpc_generate_loads_kw(d, time_steps_per_hour) - eload["loads_kw"] = series - series + loads_kw = generate_loads_kw(d, time_steps_per_hour) + electric_load["loads_kw"] = loads_kw end - # ---- Length checks (8760 or 8784 * tsh) ---- - # No mutation of user-supplied series. A leap-year-length input - # (8784*tsh) is accepted as-is; `_mpc_slice_with_wrap` reads the extra - # day from it for year-end look-ahead instead of wrapping to Jan 1. - pv_prod_factor = _mpc_check_series_length("PV.production_factor_series", - pv_prod_factor, time_steps_per_hour, length_of_data) - load_series = _mpc_check_series_length("ElectricLoad.loads_kw", - load_series, time_steps_per_hour, length_of_data) - - # ---- Resolve PV and ElectricStorage sizes ---- - # Runs AFTER PV/load resolution so the (possibly internal) sizing - # REopt run reuses the cached series instead of re-deriving them. - # Either pulls sizes from min_kw==max_kw / min_kwh==max_kwh, or runs - # a sizing REopt and locks the resolved values into `d` so the - # downstream main REopt run uses the SAME sizes. - sizes = _mpc_resolve_sizes!(d) - pv_kw = sizes.pv_kw - bess_kw = sizes.bess_kw - bess_kwh = sizes.bess_kwh - - soc_init = Float64(get(bess, "soc_init_fraction", 0.5)) - soc_min = Float64(get(bess, "soc_min_fraction", 0.2)) - # REopt ElectricStorage exposes three component efficiencies; MPC's - # MPCElectricStorage uses combined charge/discharge round-trip halves. - # Match REopt's own composition: charge = rectifier * sqrt(internal), - # discharge = inverter * sqrt(internal). Defaults from REopt.jl. - rect_eff = Float64(get(bess, "rectifier_efficiency_fraction", 0.96)) - inv_eff = Float64(get(bess, "inverter_efficiency_fraction", 0.96)) - int_eff = Float64(get(bess, "internal_efficiency_fraction", 0.975)) + # Check data series lengths + # TODO: Do API inputs validation when calling the MPC endpoint directly? + # Should we even have an MPC endpoint? + pv_prod_factor = check_series_length("PV.production_factor_series", pv_prod_factor, length_of_data) + loads_kw = check_series_length("ElectricLoad.loads_kw", loads_kw, length_of_data) + + # MPC requires fixed PV and battery sizes. If not provided, call REopt first in a sizing run. + technology_sizes = get_technology_sizes!(d) + pv_kw = technology_sizes.pv_kw + batt_kw = technology_sizes.batt_kw + batt_kwh = technology_sizes.batt_kwh + + # TODO: Should these defaults be read from somewhere so that they don't have to be updated in various places? + soc_0 = Float64(get(electric_storage, "soc_init_fraction", 0.5)) + soc_min = Float64(get(electric_storage, "soc_min_fraction", 0.2)) + rect_eff = Float64(get(electric_storage, "rectifier_efficiency_fraction", 0.96)) + inv_eff = Float64(get(electric_storage, "inverter_efficiency_fraction", 0.96)) + int_eff = Float64(get(electric_storage, "internal_efficiency_fraction", 0.975)) charge_eff = rect_eff * sqrt(int_eff) discharge_eff = inv_eff * sqrt(int_eff) - # Year for URDB schedule decoding is sourced from ElectricLoad.year - # (the canonical year input in REopt; ElectricTariff's `year` kwarg is - # an internal pass-through, not a user input). - tariff_year = haskey(eload, "year") ? Int(eload["year"]) : nothing - tariff = _mpc_build_tariff_arrays(et, tariff_year, time_steps_per_hour, length_of_data) - energy_rates = tariff.energy_rates - tou_demand_rates = tariff.tou_demand_rates # one per ratchet, $/kW - tou_demand_ts_all = tariff.tou_demand_time_steps # full-year indices per ratchet - monthly_demand_rates = tariff.monthly_demand_rates # length 12, $/kW per month - - # Optional emissions series — broadcast zero if not provided. - emissions = haskey(eutil, "emissions_factor_series_lb_CO2_per_kwh") ? - Float64.(eutil["emissions_factor_series_lb_CO2_per_kwh"]) : - zeros(Float64, length_of_data) - emissions = _mpc_check_series_length("ElectricUtility.emissions_factor_series_lb_CO2_per_kwh", - emissions, time_steps_per_hour, length_of_data) - - # ---- Demand tracking state ---- - # Reverse indices into the calendar: each timestep maps to its - # 1-based month (always defined) and 1-based TOU ratchet (0 if no - # ratchet applies). Both are vector lookups (O(1)) used inside the - # rolling-horizon loop to build window-local time-step groupings - # and to attribute realized peaks to the right month/tier. + # Process utility rate + year = haskey(electric_load, "year") ? Int(electric_load["year"]) : nothing + tariff = get_tariff_inputs(electric_tariff, year, time_steps_per_hour) + energy_rates = tariff.energy_rates + tou_demand_rates = tariff.tou_demand_rates + tou_demand_ratchet_time_steps = tariff.tou_demand_ratchet_time_steps + monthly_demand_rates = tariff.monthly_demand_rates + + # TODO: MPC loop cherry picked one specific emissions type - remove or keep and add others? + # TODO: If keep, add function to pull defaults? Currently only allowing user upload + emissions = haskey(electric_utility, "emissions_factor_series_lb_CO2_per_kwh") ? + Float64.(electric_utility["emissions_factor_series_lb_CO2_per_kwh"]) : zeros(Float64, length_of_data) + emissions = check_series_length("ElectricUtility.emissions_factor_series_lb_CO2_per_kwh", emissions, length_of_data) + month_starts = get_month_transition_timesteps(time_steps_per_hour) + + # ts_to_month = 8760 array specifying which month each timestep falls in (1-12) ts_to_month = Vector{Int}(undef, length_of_data) for m in 1:12 s = month_starts[m] @@ -435,28 +331,21 @@ function get_mpc_results(d::Dict)::Dict ts_to_month[s:e] .= m end - ts_to_tier = zeros(Int, length_of_data) - for (t, ratchet_ts) in enumerate(tou_demand_ts_all), g in ratchet_ts + # ts_to_ratchet = 8760 array specifying which ratchet each timestep falls in + ts_to_ratchet = zeros(Int, length_of_data) + for (t, ratchet_ts) in enumerate(tou_demand_ratchet_time_steps), g in ratchet_ts if 1 <= g <= length_of_data - ts_to_tier[g] = t + ts_to_ratchet[g] = t end end - n_tou = length(tou_demand_rates) - # `monthly_previous_peak_demands` and `tou_previous_peak_demands` are - # passed into every MPC post AND mutated after each solve so that - # subsequent windows "see" the realized peaks so far. Both reset at - # month boundaries (matches REopt billing semantics; multi-month - # ratchet/lookback support would require a different reset cadence). - tou_previous_peak_demands = zeros(Float64, n_tou) - monthly_previous_peak_demands = zeros(Float64, 12) - monthly_demand_cost_total = 0.0 - tou_demand_cost_total = 0.0 - tou_peaks_by_month = Vector{Vector{Float64}}() - monthly_peaks = Float64[] - - # ---- Dispatch accumulators (1 value per executed timestep) ---- - dispatch = Dict( + # TODO: Monthly demand has never been tested + n_tou_ratchets = length(tou_demand_rates) # Number of TOU ratchets + tou_previous_peak_demands = zeros(Float64, n_tou_ratchets) # Tracks past TOU peak demand per ratchet + monthly_previous_peak_demands = zeros(Float64, 12) # Tracks past monthly peak demand + + # Saved dispatch series (first timestep of each MPC loop) + dispatch_series = Dict( "PV" => Dict( "electric_to_load_series_kw" => Float64[], "electric_to_storage_series_kw" => Float64[], @@ -476,178 +365,151 @@ function get_mpc_results(d::Dict)::Dict "load_series_kw" => Float64[], ), ) - cost_series = Float64[] + energy_cost_series = Float64[] total_energy_cost = 0.0 - bess_soc_init_fraction = soc_init - - # ---- Build the (reused) MPC post template ---- - # Note: no Settings block here; the solver is constructed below per - # window and passed directly to run_mpc(model, post). All four demand - # inputs (TOU + monthly rates and previous-peak vectors) are passed - # so the optimizer's objective accounts for *both* charges; omitting - # `monthly_*` would silently optimize without the monthly demand - # contribution and over-state savings under monthly-demand tariffs. - function build_mpc_post(window_pv, window_load, window_rates, window_emissions, - window_tou_ts, window_monthly_ts, - tou_prev_peaks, monthly_prev_peaks, soc_init_frac) + soc_init_frac = soc_0 + + # Build MPC post + function build_mpc_post(current_horizon_pv, current_horizon_load, current_horizon_energy_rates, + current_horizon_emissions, current_horizon_tou_ts, current_horizon_monthly_ts, + tou_previous_peak_demands, monthly_previous_peak_demands, soc_init_frac) return Dict( "PV" => Dict( "size_kw" => pv_kw, - "production_factor_series" => window_pv, + "production_factor_series" => current_horizon_pv, ), "ElectricStorage" => Dict( - "size_kw" => bess_kw, - "size_kwh" => bess_kwh, + "size_kw" => batt_kw, + "size_kwh" => batt_kwh, "charge_efficiency" => charge_eff, "discharge_efficiency" => discharge_eff, "soc_init_fraction" => soc_init_frac, "soc_min_fraction" => soc_min, ), "ElectricLoad" => Dict( - "loads_kw" => window_load, + "loads_kw" => current_horizon_load, ), "ElectricTariff" => Dict( - "energy_rates" => window_rates, + "energy_rates" => current_horizon_energy_rates, "tou_demand_rates" => tou_demand_rates, - "tou_demand_time_steps" => window_tou_ts, - "tou_previous_peak_demands" => tou_prev_peaks, + "tou_demand_ratchet_time_steps" => current_horizon_tou_ts, + "tou_previous_peak_demands" => tou_previous_peak_demands, "monthly_demand_rates" => monthly_demand_rates, - "time_steps_monthly" => window_monthly_ts, - "monthly_previous_peak_demands" => monthly_prev_peaks, + "time_steps_monthly" => current_horizon_monthly_ts, + "monthly_previous_peak_demands" => monthly_previous_peak_demands, ), "ElectricUtility" => Dict( - "emissions_factor_series_lb_CO2_per_kwh" => window_emissions, + "emissions_factor_series_lb_CO2_per_kwh" => current_horizon_emissions, ), ) end - @info "MPC: starting rolling-horizon loop ($(length_of_data) iterations, horizon=$(horizon))" + @info "MPC: starting rolling-horizon optimization ($(length_of_data) iterations, horizon = $(horizon) timesteps)" for idx in 1:length_of_data end_ts = idx + horizon - 1 - window_len = end_ts - idx + 1 - - window_pv = _mpc_slice_with_wrap(pv_prod_factor, idx, end_ts) - window_load = _mpc_slice_with_wrap(load_series, idx, end_ts) - window_rates = _mpc_slice_with_wrap(energy_rates, idx, end_ts) - window_emiss = _mpc_slice_with_wrap(emissions, idx, end_ts) - - # Window-local time-step groupings, both built from a single pass - # over the horizon. TOU buckets are 1 vector per ratchet (length - # n_tou); monthly buckets are 1 vector per month (length 12). - # Global indices wrap on `length_of_data` (8760·tsh) so the month - # / tier of a wrapped step matches its calendar position. - window_tou_ts = [Int[] for _ in 1:n_tou] - window_monthly_ts = [Int[] for _ in 1:12] - for k in 1:window_len + + current_horizon_pv = slice_data(pv_prod_factor, idx, end_ts) + current_horizon_load = slice_data(loads_kw, idx, end_ts) + current_horizon_energy_rates = slice_data(energy_rates, idx, end_ts) + current_horizon_emissions = slice_data(emissions, idx, end_ts) + + # List of length n_tou_ratchets, specifies which ts of the current horizon are in each TOU ratchet + # by placing values 1 to horizon into the corresponding element of the array based on ratchet number + current_horizon_tou_ts = [Int[] for _ in 1:n_tou_ratchets] + + # 12 element list, each element for one month of the year. Specifies which timesteps of the current horizon are + # in each month by placing values 1 - horizon into the corresponding element of the array based on month number + current_horizon_monthly_ts = [Int[] for _ in 1:12] + for k in 1:horizon g = idx + k - 1 if g > length_of_data g -= length_of_data end - push!(window_monthly_ts[ts_to_month[g]], k) - tier = ts_to_tier[g] - if tier > 0 - push!(window_tou_ts[tier], k) + push!(current_horizon_monthly_ts[ts_to_month[g]], k) + ratchet = ts_to_ratchet[g] + if ratchet > 0 + push!(current_horizon_tou_ts[ratchet], k) end end - post = build_mpc_post(window_pv, window_load, window_rates, window_emiss, - window_tou_ts, window_monthly_ts, - tou_previous_peak_demands, monthly_previous_peak_demands, - bess_soc_init_fraction) + post = build_mpc_post(current_horizon_pv, current_horizon_load, current_horizon_energy_rates, + current_horizon_emissions, current_horizon_tou_ts, current_horizon_monthly_ts, + tou_previous_peak_demands, monthly_previous_peak_demands, soc_init_frac) model = get_solver_model(get_solver_model_type(solver_name), SolverAttributes(per_iter_timeout_s, optimality_tolerance)) result = reoptjl.run_mpc(model, post) - # ---- Extract first-timestep dispatch (perfect-forecast) ---- + # Assume perfect forecast; save first timestep of results as the executed state pv_res = result["PV"] - bess_res = result["ElectricStorage"] + batt_res = result["ElectricStorage"] util_res = result["ElectricUtility"] pv_to_load = pv_res["electric_to_load_series_kw"][1] - pv_to_bess = pv_res["electric_to_storage_series_kw"][1] + pv_to_batt = pv_res["electric_to_storage_series_kw"][1] pv_to_grid = haskey(pv_res, "electric_to_grid_series_kw") ? pv_res["electric_to_grid_series_kw"][1] : 0.0 pv_curtailed = haskey(pv_res, "electric_curtailed_series_kw") ? pv_res["electric_curtailed_series_kw"][1] : 0.0 - bess_to_load = bess_res["storage_to_load_series_kw"][1] - bess_soc = bess_res["soc_series_fraction"][1] + batt_to_load = batt_res["storage_to_load_series_kw"][1] + batt_soc = batt_res["soc_series_fraction"][1] util_to_load = util_res["electric_to_load_series_kw"][1] - util_to_bess = util_res["electric_to_storage_series_kw"][1] - grid_power = max(util_to_load + util_to_bess, 0.0) - - push!(dispatch["PV"]["electric_to_load_series_kw"], pv_to_load) - push!(dispatch["PV"]["electric_to_storage_series_kw"], pv_to_bess) - push!(dispatch["PV"]["electric_to_grid_series_kw"], pv_to_grid) - push!(dispatch["PV"]["electric_curtailed_series_kw"], pv_curtailed) - push!(dispatch["ElectricStorage"]["storage_to_load_series_kw"], bess_to_load) - push!(dispatch["ElectricStorage"]["soc_series_fraction"], round(bess_soc, digits=6)) - push!(dispatch["ElectricUtility"]["electric_to_load_series_kw"], util_to_load) - push!(dispatch["ElectricUtility"]["electric_to_storage_series_kw"], util_to_bess) - push!(dispatch["ElectricUtility"]["emissions_series_lb_CO2"], + util_to_batt = util_res["electric_to_storage_series_kw"][1] + grid_power = max(util_to_load + util_to_batt, 0.0) + + push!(dispatch_series["PV"]["electric_to_load_series_kw"], pv_to_load) + push!(dispatch_series["PV"]["electric_to_storage_series_kw"], pv_to_batt) + push!(dispatch_series["PV"]["electric_to_grid_series_kw"], pv_to_grid) + push!(dispatch_series["PV"]["electric_curtailed_series_kw"], pv_curtailed) + push!(dispatch_series["ElectricStorage"]["storage_to_load_series_kw"], batt_to_load) + push!(dispatch_series["ElectricStorage"]["soc_series_fraction"], batt_soc) + push!(dispatch_series["ElectricUtility"]["electric_to_load_series_kw"], util_to_load) + push!(dispatch_series["ElectricUtility"]["electric_to_storage_series_kw"], util_to_batt) + push!(dispatch_series["ElectricUtility"]["emissions_series_lb_CO2"], emissions[idx] * grid_power / time_steps_per_hour) - push!(dispatch["ElectricLoad"]["load_series_kw"], load_series[idx]) + push!(dispatch_series["ElectricLoad"]["load_series_kw"], loads_kw[idx]) - # ---- Energy cost ---- + # Running energy costs step_energy_cost = grid_power * energy_rates[idx] / time_steps_per_hour - push!(cost_series, step_energy_cost) + push!(energy_cost_series, step_energy_cost) total_energy_cost += step_energy_cost - # ---- Carry state ---- - bess_soc_init_fraction = bess_soc - - # ---- Demand peak tracking (per-tier and per-month) ---- - # Attribute realized peak grid power to its TOU ratchet (if any) - # and to its calendar month. Identity comes from the reverse - # indices, NOT from rate values — so coincident-rate ratchets - # and equal-rate months remain separate billing pools. - m_now = ts_to_month[idx] - monthly_previous_peak_demands[m_now] = - max(grid_power, monthly_previous_peak_demands[m_now]) - - if n_tou > 0 - tier_now = ts_to_tier[idx] - if tier_now > 0 - tou_previous_peak_demands[tier_now] = - max(grid_power, tou_previous_peak_demands[tier_now]) - end - end + soc_init_frac = batt_soc + + # Update monthly and TOU peak demand as max(current ts grid_power, previous max) + current_month = ts_to_month[idx] + monthly_previous_peak_demands[current_month] = max(grid_power, monthly_previous_peak_demands[current_month]) - # ---- Month transition: close out the just-ended month ---- - # Both monthly and TOU peaks reset at month boundaries (matches - # REopt billing semantics). The last timestep of month m is - # detected by m_now != ts_to_month[idx+1]; we handle Dec via - # idx == length_of_data. - is_month_end = (idx == length_of_data) || (ts_to_month[idx + 1] != m_now) - if is_month_end - push!(monthly_peaks, monthly_previous_peak_demands[m_now]) - monthly_demand_cost_total += - monthly_previous_peak_demands[m_now] * monthly_demand_rates[m_now] - monthly_previous_peak_demands[m_now] = 0.0 - - if n_tou > 0 - push!(tou_peaks_by_month, copy(tou_previous_peak_demands)) - tou_demand_cost_total += sum(tou_previous_peak_demands .* tou_demand_rates) - fill!(tou_previous_peak_demands, 0.0) + if n_tou_ratchets > 0 + current_ratchet = ts_to_ratchet[idx] + if current_ratchet > 0 + tou_previous_peak_demands[current_ratchet] = max(grid_power, tou_previous_peak_demands[current_ratchet]) end end + end - @info "MPC: loop complete. total_energy_cost=\$$(round(total_energy_cost, digits=2))" + @info "MPC looping completed." + + # Calculate final demand costs + monthly_demand_cost_total = sum(monthly_previous_peak_demands .* monthly_demand_rates) + tou_demand_cost_total = n_tou_ratchets > 0 ? + sum(tou_previous_peak_demands .* tou_demand_rates) : 0.0 return Dict( "MPC" => Dict( "time_steps_per_hour" => time_steps_per_hour, "horizon" => horizon, ), - "PV" => Dict("size_kw" => pv_kw), - "ElectricStorage" => Dict("size_kw" => bess_kw, "size_kwh" => bess_kwh), - "dispatch" => dispatch, + "PV" => merge(Dict("size_kw" => pv_kw), dispatch_series["PV"]), + "ElectricStorage" => merge(Dict("size_kw" => batt_kw, "size_kwh" => batt_kwh), dispatch_series["ElectricStorage"]), + "ElectricUtility" => dispatch_series["ElectricUtility"], + "ElectricLoad" => dispatch_series["ElectricLoad"], "ElectricTariff" => Dict( "total_energy_cost" => total_energy_cost, - "energy_cost_series_per_timestep" => cost_series, + "energy_cost_series_per_timestep" => energy_cost_series, "total_tou_demand_cost" => tou_demand_cost_total, "total_monthly_demand_cost" => monthly_demand_cost_total, - "tou_peaks_by_month_kw" => tou_peaks_by_month, - "monthly_peaks_kw" => monthly_peaks, + "tou_peaks_by_ratchet_kw" => tou_previous_peak_demands, + "monthly_peaks_kw" => monthly_previous_peak_demands, ), "status" => "optimal", "reopt_version" => string(pkgversion(reoptjl)), From 3e695b30806950c7c7f7cdb7859e9d1b7b78da29 Mon Sep 17 00:00:00 2001 From: lixiangk1 <72464565+lixiangk1@users.noreply.github.com> Date: Thu, 11 Jun 2026 20:29:54 -0600 Subject: [PATCH 08/45] Update MPC variable names --- julia_src/http.jl | 9 +++++---- julia_src/mpc.jl | 7 +++++-- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/julia_src/http.jl b/julia_src/http.jl index b03a742f8..3f83c18d4 100644 --- a/julia_src/http.jl +++ b/julia_src/http.jl @@ -74,18 +74,19 @@ function reopt(req::HTTP.Request) Specify Settings.solver_name = 'HiGHS' or 'Cbc' or 'SCIP'" end + @info "HERE1" # ---- API-only battery heuristic dispatch strategy: "daily_foresight_optimized" ---- - # When ElectricStorage.dispatch_options == "daily_foresight_optimized", first run the MPC rolling-horizon loop to get an SOC profile. + # When ElectricStorage.dispatch_strategy == "daily_foresight_optimized", first run the MPC rolling-horizon loop to get an SOC profile. # Then set ElectricStorage.fixed_soc_series_fraction = MPC SOC before running the main REopt optimization. # Runs after the Xpress availability check so the resolved solver_name is passed in. electric_storage = get(d, "ElectricStorage", Dict()) - if get(electric_storage, "dispatch_options", nothing) == "daily_foresight_optimized" + if get(electric_storage, "dispatch_strategy", nothing) == "daily_foresight_optimized" try @info "Running MPC to obtain daily foresight optimized battery dispatch profile." mpc_results = get_mpc_results(d; solver_name=solver_name) - soc = mpc_results["dispatch"]["ElectricStorage"]["soc_series_fraction"] + soc = mpc_results["ElectricStorage"]["soc_series_fraction"] d["ElectricStorage"]["fixed_soc_series_fraction"] = soc - d["ElectricStorage"]["dispatch_options"] = "custom" + d["ElectricStorage"]["dispatch_strategy"] = "custom_soc" catch e @error "MPC pre-solve failed" exception=(e, catch_backtrace()) return HTTP.Response(500, JSON.json(Dict( diff --git a/julia_src/mpc.jl b/julia_src/mpc.jl index af376cdbe..f9916b955 100644 --- a/julia_src/mpc.jl +++ b/julia_src/mpc.jl @@ -193,7 +193,7 @@ function get_technology_sizes!(d::Dict) # Delete inputs specific to the heuristic battery dispatch run if haskey(sizing_post, "ElectricStorage") - delete!(sizing_post["ElectricStorage"], "dispatch_options") + delete!(sizing_post["ElectricStorage"], "dispatch_strategy") delete!(sizing_post["ElectricStorage"], "fixed_soc_series_fraction") end @@ -219,6 +219,7 @@ function get_technology_sizes!(d::Dict) batt_kw = Float64(get(get(sizing_results, "ElectricStorage", Dict()), "size_kw", 0.0)) batt_kwh = Float64(get(get(sizing_results, "ElectricStorage", Dict()), "size_kwh", 0.0)) + # TODO: If no battery is sized, just return the optimal REopt result if batt_kw <= 0.0 || batt_kwh <= 0.0 error("MPC: Optimal REopt sizing does not include ElectricStorage (size_kw=$(batt_kw), size_kwh=$(batt_kwh)). " * "The daily_foresight_optimized dispatch option requires a non-zero battery size. " * @@ -283,7 +284,9 @@ function get_mpc_results(d::Dict; solver_name::String="HiGHS")::Dict loads_kw = Float64.(electric_load["loads_kw"]) else loads_kw = generate_loads_kw(d, time_steps_per_hour) - electric_load["loads_kw"] = loads_kw + + # Previously caching to save an extra CRB call but loads_kw conflicts with other load inputs + # electric_load["loads_kw"] = loads_kw end # Check data series lengths From 40d32dba8e85ba60200d162785ae390bdc829aa2 Mon Sep 17 00:00:00 2001 From: lixiangk1 <72464565+lixiangk1@users.noreply.github.com> Date: Thu, 11 Jun 2026 20:51:24 -0600 Subject: [PATCH 09/45] Remove print statement --- julia_src/http.jl | 1 - 1 file changed, 1 deletion(-) diff --git a/julia_src/http.jl b/julia_src/http.jl index 3f83c18d4..e13d04679 100644 --- a/julia_src/http.jl +++ b/julia_src/http.jl @@ -74,7 +74,6 @@ function reopt(req::HTTP.Request) Specify Settings.solver_name = 'HiGHS' or 'Cbc' or 'SCIP'" end - @info "HERE1" # ---- API-only battery heuristic dispatch strategy: "daily_foresight_optimized" ---- # When ElectricStorage.dispatch_strategy == "daily_foresight_optimized", first run the MPC rolling-horizon loop to get an SOC profile. # Then set ElectricStorage.fixed_soc_series_fraction = MPC SOC before running the main REopt optimization. From dd45a05b17379f9b69efdf3faf25937096071ed4 Mon Sep 17 00:00:00 2001 From: lixiangk1 <72464565+lixiangk1@users.noreply.github.com> Date: Sat, 13 Jun 2026 13:55:20 -0600 Subject: [PATCH 10/45] Skip MPC dispatch if battery is not optimally sized --- julia_src/http.jl | 16 +++++++++++----- julia_src/mpc.jl | 49 +++++++++++++++++++++++++++++++---------------- 2 files changed, 44 insertions(+), 21 deletions(-) diff --git a/julia_src/http.jl b/julia_src/http.jl index e13d04679..ee5b99cc4 100644 --- a/julia_src/http.jl +++ b/julia_src/http.jl @@ -76,16 +76,22 @@ function reopt(req::HTTP.Request) # ---- API-only battery heuristic dispatch strategy: "daily_foresight_optimized" ---- # When ElectricStorage.dispatch_strategy == "daily_foresight_optimized", first run the MPC rolling-horizon loop to get an SOC profile. - # Then set ElectricStorage.fixed_soc_series_fraction = MPC SOC before running the main REopt optimization. - # Runs after the Xpress availability check so the resolved solver_name is passed in. + # Then set ElectricStorage.fixed_soc_series_fraction = MPC SOC before running the main REopt optimization. If the user does not + # specify a fixed PV/battery size, optimally size the technologies using REopt (skip MPC dispatch if optimal battery size is 0). electric_storage = get(d, "ElectricStorage", Dict()) if get(electric_storage, "dispatch_strategy", nothing) == "daily_foresight_optimized" try @info "Running MPC to obtain daily foresight optimized battery dispatch profile." mpc_results = get_mpc_results(d; solver_name=solver_name) - soc = mpc_results["ElectricStorage"]["soc_series_fraction"] - d["ElectricStorage"]["fixed_soc_series_fraction"] = soc - d["ElectricStorage"]["dispatch_strategy"] = "custom_soc" + # TODO: Cache sizing run results and avoid a second call to REopt? Are those the same results? + if get(mpc_results, "skip_mpc", false) == true + @info "Cannot execute daily_foresight_optimized battery dispatch: optimal battery size is 0." + delete!(d["ElectricStorage"], "dispatch_strategy") + else + soc = mpc_results["ElectricStorage"]["soc_series_fraction"] + d["ElectricStorage"]["fixed_soc_series_fraction"] = soc + d["ElectricStorage"]["dispatch_strategy"] = "custom_soc" + end catch e @error "MPC pre-solve failed" exception=(e, catch_backtrace()) return HTTP.Response(500, JSON.json(Dict( diff --git a/julia_src/mpc.jl b/julia_src/mpc.jl index f9916b955..3798ee226 100644 --- a/julia_src/mpc.jl +++ b/julia_src/mpc.jl @@ -185,7 +185,7 @@ function get_technology_sizes!(d::Dict) batt_kw = Float64(batt["min_kw"]) batt_kwh = Float64(batt["min_kwh"]) @info "MPC: fixed technology sizes entered, PV = $(pv_kw) kW, battery = $(batt_kw) kW / $(batt_kwh) kWh." - return (pv_kw = pv_kw, batt_kw = batt_kw, batt_kwh = batt_kwh) + return (pv_kw = pv_kw, batt_kw = batt_kw, batt_kwh = batt_kwh, skip_mpc = false) end @info "MPC: PV and/or ElectricStorage sizes are not specified — running REopt sizing first." @@ -197,34 +197,45 @@ function get_technology_sizes!(d::Dict) delete!(sizing_post["ElectricStorage"], "fixed_soc_series_fraction") end - settings = get!(sizing_post, "Settings", Dict()) - settings["run_bau"] = false + # TODO: Avoid redefining defaults here? + settings = get(sizing_post, "Settings", Dict()) + sizing_solver_name = get(settings, "solver_name", "HiGHS") + sizing_timeout = Float64(get(settings, "timeout_seconds", 420)) + sizing_opt_tol = Float64(get(settings, "optimality_tolerance", 0.001)) - # TODO: Okay to hard code defaults here? solver_name check is redundant if function is only called from get_mpc_results - settings["solver_name"] = get(settings, "solver_name", "HiGHS") - settings["timeout_seconds"] = get(settings, "timeout_seconds", 420) - settings["optimality_tolerance"] = get(settings, "optimality_tolerance", 0.001) + solver_attributes = SolverAttributes(sizing_timeout, sizing_opt_tol) + m = get_solver_model(get_solver_model_type(sizing_solver_name), solver_attributes) - solver_attributes = SolverAttributes(settings["timeout_seconds"], settings["optimality_tolerance"]) - m = get_solver_model(get_solver_model_type(settings["solver_name"]), solver_attributes) + # Delete Settings inputs specific to the API + api_only_settings_keys = ("timeout_seconds", "optimality_tolerance", "run_bau") + if haskey(sizing_post, "Settings") + for k in api_only_settings_keys + delete!(sizing_post["Settings"], k) + end + if isempty(sizing_post["Settings"]) + delete!(sizing_post, "Settings") + end + end model_inputs = reoptjl.REoptInputs(sizing_post) sizing_results = reoptjl.run_reopt(m, model_inputs) if get(sizing_results, "status", "") != "optimal" - error("MPC sizing pre-step did not solve (status = $(get(sizing_results, "status", "unknown"))).") + status = get(sizing_results, "status", "unknown") + msgs = get(sizing_results, "Messages", Dict()) + errs = get(msgs, "errors", []) + warns = get(msgs, "warnings", []) + error("MPC sizing pre-step did not solve (status = $(status)). " * + "REopt errors: $(errs). REopt warnings: $(warns).") end pv_kw = Float64(get(get(sizing_results, "PV", Dict()), "size_kw", 0.0)) batt_kw = Float64(get(get(sizing_results, "ElectricStorage", Dict()), "size_kw", 0.0)) batt_kwh = Float64(get(get(sizing_results, "ElectricStorage", Dict()), "size_kwh", 0.0)) - # TODO: If no battery is sized, just return the optimal REopt result + # Skip the MPC loop if no battery is sized if batt_kw <= 0.0 || batt_kwh <= 0.0 - error("MPC: Optimal REopt sizing does not include ElectricStorage (size_kw=$(batt_kw), size_kwh=$(batt_kwh)). " * - "The daily_foresight_optimized dispatch option requires a non-zero battery size. " * - "Set ElectricStorage.min_kw == ElectricStorage.max_kw and " * - "ElectricStorage.min_kwh == ElectricStorage.max_kwh to fix sizes manually.") + return (pv_kw = pv_kw, batt_kw = 0.0, batt_kwh = 0.0, skip_mpc = true) end pv["min_kw"] = pv_kw @@ -235,7 +246,7 @@ function get_technology_sizes!(d::Dict) batt["max_kwh"] = batt_kwh @info "MPC: REopt sizing solved with PV = $(pv_kw) kW and battery = $(batt_kw) kW / $(batt_kwh) kWh." - return (pv_kw = pv_kw, batt_kw = batt_kw, batt_kwh = batt_kwh) + return (pv_kw = pv_kw, batt_kw = batt_kw, batt_kwh = batt_kwh, skip_mpc = false) end function get_mpc_results(d::Dict; solver_name::String="HiGHS")::Dict @@ -297,6 +308,12 @@ function get_mpc_results(d::Dict; solver_name::String="HiGHS")::Dict # MPC requires fixed PV and battery sizes. If not provided, call REopt first in a sizing run. technology_sizes = get_technology_sizes!(d) + + # Skip MPC if no battery is optimally sized + if technology_sizes.skip_mpc + return Dict("skip_mpc" => true) + end + pv_kw = technology_sizes.pv_kw batt_kw = technology_sizes.batt_kw batt_kwh = technology_sizes.batt_kwh From 3d259af522e92e2dcb3f8ec500c59052db2355f6 Mon Sep 17 00:00:00 2001 From: adfarth Date: Wed, 24 Jun 2026 09:40:48 -0600 Subject: [PATCH 11/45] Update Manifest.toml --- julia_src/Manifest.toml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/julia_src/Manifest.toml b/julia_src/Manifest.toml index dc825f89c..47ac83bda 100644 --- a/julia_src/Manifest.toml +++ b/julia_src/Manifest.toml @@ -948,7 +948,9 @@ version = "1.11.0" [[deps.REopt]] deps = ["ArchGDAL", "CSV", "CoolProp", "DataFrames", "Dates", "DelimitedFiles", "HTTP", "JLD", "JSON", "JuMP", "LinDistFlow", "LinearAlgebra", "Logging", "MathOptInterface", "Requires", "Roots", "Statistics", "TestEnv"] -git-tree-sha1 = "e807b62ab249fca7b59ac6ab7bb71725f504cc5f" +git-tree-sha1 = "1e54ef8f58ddcf9d66f392cb76fdd78123d7d2ad" +repo-rev = "dispatch-options" +repo-url = "https://github.com/NatLabRockies/REopt.jl.git" uuid = "d36ad4e8-d74a-4f7a-ace1-eaea049febf6" version = "0.59.2" From 855f18902f9189bd998f5da9664f924c44207002 Mon Sep 17 00:00:00 2001 From: adfarth Date: Mon, 29 Jun 2026 11:15:25 -0600 Subject: [PATCH 12/45] validation and help text --- julia_src/http.jl | 42 +++++++++++++++++++++++++++------------- julia_src/mpc.jl | 49 +++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 74 insertions(+), 17 deletions(-) diff --git a/julia_src/http.jl b/julia_src/http.jl index ee5b99cc4..04a4bfa5e 100644 --- a/julia_src/http.jl +++ b/julia_src/http.jl @@ -75,9 +75,9 @@ function reopt(req::HTTP.Request) end # ---- API-only battery heuristic dispatch strategy: "daily_foresight_optimized" ---- - # When ElectricStorage.dispatch_strategy == "daily_foresight_optimized", first run the MPC rolling-horizon loop to get an SOC profile. - # Then set ElectricStorage.fixed_soc_series_fraction = MPC SOC before running the main REopt optimization. If the user does not - # specify a fixed PV/battery size, optimally size the technologies using REopt (skip MPC dispatch if optimal battery size is 0). + # When ElectricStorage.dispatch_strategy == "daily_foresight_optimized", if needed, first run REopt to get optimal sizing of PV and battery. + # Then run the MPC rolling-horizon loop to get a SOC profile (skip MPC dispatch if optimal battery size is 0). + # Then set ElectricStorage.fixed_soc_series_fraction = MPC SOC before running the main REopt optimization. electric_storage = get(d, "ElectricStorage", Dict()) if get(electric_storage, "dispatch_strategy", nothing) == "daily_foresight_optimized" try @@ -85,8 +85,8 @@ function reopt(req::HTTP.Request) mpc_results = get_mpc_results(d; solver_name=solver_name) # TODO: Cache sizing run results and avoid a second call to REopt? Are those the same results? if get(mpc_results, "skip_mpc", false) == true - @info "Cannot execute daily_foresight_optimized battery dispatch: optimal battery size is 0." - delete!(d["ElectricStorage"], "dispatch_strategy") + @info "Cannot execute daily_foresight_optimized battery dispatch because optimal battery size is 0. Setting dispatch strategy to 'optimized'." + d["ElectricStorage"]["dispatch_strategy"] = "optimized" else soc = mpc_results["ElectricStorage"]["soc_series_fraction"] d["ElectricStorage"]["fixed_soc_series_fraction"] = soc @@ -841,7 +841,29 @@ function job_no_xpress(req::HTTP.Request) return HTTP.Response(500, JSON.json(error_response)) end +""" + mpc(req::HTTP.Request) +HTTP endpoint for rolling-horizon Model Predictive Control (MPC) dispatch optimization. + +This endpoint performs a full-year rolling-horizon MPC dispatch for PV + ElectricStorage systems, +optimizing daily dispatch using a 24-hour look-ahead window. Runs the MPC dispatch loop via +`get_mpc_results` and returns dispatch results and cost metrics as JSON. + +Arguments: + req::HTTP.Request: REopt inputs dictionary + +Returns JSON dictionary containing: + - MPC: Metadata (time_steps_per_hour, horizon) + - PV: Size and dispatch series (to load, storage, grid, curtailed) + - ElectricStorage: Sizes and state-of-charge series + - ElectricUtility: Grid dispatch series and emissions + - ElectricLoad: Load profile used + - ElectricTariff: Energy and demand costs, peak demands by month/ratchet + - status: "optimal" + - reopt_version: Version of REopt.jl used + +""" function mpc(req::HTTP.Request) d = JSON.parse(String(req.body)) error_response = Dict() @@ -853,14 +875,8 @@ function mpc(req::HTTP.Request) ENV["NREL_DEVELOPER_API_KEY"] = test_nrel_developer_api_key delete!(d, "api_key") end - settings = get(d, "Settings", Dict()) - solver_name = get(settings, "solver_name", "HiGHS") - if solver_name == "Xpress" && !(xpress_installed=="True") - solver_name = "HiGHS" - @warn "Changing solver_name from Xpress to $solver_name because Xpress is not installed. - Next time specify Settings.solver_name = 'HiGHS' or 'Cbc' or 'SCIP'." - end - results = get_mpc_results(d; solver_name=solver_name) + + results = get_mpc_results(d) catch e @error "MPC failed" exception=(e, catch_backtrace()) error_response["error"] = sprint(showerror, e) diff --git a/julia_src/mpc.jl b/julia_src/mpc.jl index 3798ee226..d1b0a12e4 100644 --- a/julia_src/mpc.jl +++ b/julia_src/mpc.jl @@ -249,19 +249,60 @@ function get_technology_sizes!(d::Dict) return (pv_kw = pv_kw, batt_kw = batt_kw, batt_kwh = batt_kwh, skip_mpc = false) end -function get_mpc_results(d::Dict; solver_name::String="HiGHS")::Dict +function get_mpc_results(d::Dict)::Dict """ Run a full-year rolling-horizon MPC dispatch for PV + ElectricStorage by calling `REopt.run_mpc` once per timestep with a 24-hour look-ahead. Inputs: d::Dict, REopt inputs dictionary - solver_name::String, solver to use ("HiGHS", "Cbc", "SCIP", or "Xpress") - Returns a Dict with PV and BATT sizes, dispatch time series, and cost metrics + Returns a Dict with PV and ElectricStorage sizes, dispatch time series, and cost metrics """ - + + ## Validation on allowable inputs for MPC ## + # Error if any techs other than PV and ElectricStorage are provided + mpc_allowed_keys = Set(["PV", "ElectricStorage", "ElectricLoad", "ElectricTariff", "ElectricUtility", "Site", "Settings", "Financial"]) + unsupported_keys = setdiff(keys(d), mpc_allowed_keys) + if !isempty(unsupported_keys) + error("When using MPC (daily_foresight_optimized dispatch), only PV and ElectricStorage are supported technologies. " * + "Unsupported inputs found: $(join(unsupported_keys, ", ")).") + end + + # Error if unsupported CO2/renewable-fraction constraints are set + _site_input = get(d, "Site", Dict()) + if !isnothing(get(_site_input, "CO2_emissions_reduction_min_fraction", nothing)) + error("MPC: Site.CO2_emissions_reduction_min_fraction is not supported in MPC runs.") + end + if get(_site_input, "include_grid_renewable_fraction_in_RE_constraints", false) == true + error("MPC: Site.include_grid_renewable_fraction_in_RE_constraints is not supported in MPC runs.") + end + if get(_site_input, "include_exported_elec_emissions_in_total", true) == false + error("MPC: Site.include_exported_elec_emissions_in_total = false is not supported in MPC runs.") + end + if get(_site_input, "include_exported_renewable_electricity_in_total", true) == false + error("MPC: Site.include_exported_renewable_electricity_in_total = false is not supported in MPC runs.") + end + + # TODO: Add warnings for REopt inputs and scenarios that are not modeled in MPC (e.g., coincident peak charges, demand lookback, etc.) + @warn "Using MPC to determine dispatch. MPC does not model: tiered electricity rates; rates will be flattened to the first tier." + + # TODO: Test with outage inputs before enabling this warning. + # # Warning for outage inputs (MPC does not model outages) + # _utility_input = get(d, "ElectricUtility", Dict()) + # if any(k -> haskey(_utility_input, k), ("outage_start_time_step", "outage_start_time_steps", "outage_durations")) + # @warn "MPC: Outage inputs detected (outage_start_time_step, outage_start_time_steps, outage_durations). " * + # "MPC does not model outages; these inputs will be ignored." + # end + + ## Set up MPC inputs ## settings = get!(d, "Settings", Dict()) + solver_name = get(settings, "solver_name", "HiGHS") + if solver_name == "Xpress" && !(xpress_installed=="True") + solver_name = "HiGHS" + @warn "Changing solver_name from Xpress to $solver_name because Xpress is not installed. + Next time specify Settings.solver_name = 'HiGHS' or 'Cbc' or 'SCIP'." + end settings["solver_name"] = solver_name # TODO: MPC timeout and optimality tolerance From e25fadd875fd60eef108055911f468630b149d8d Mon Sep 17 00:00:00 2001 From: adfarth Date: Mon, 29 Jun 2026 11:16:54 -0600 Subject: [PATCH 13/45] rm solver_name from get_mpc_results inputs --- julia_src/http.jl | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/julia_src/http.jl b/julia_src/http.jl index 04a4bfa5e..617ef89b7 100644 --- a/julia_src/http.jl +++ b/julia_src/http.jl @@ -66,14 +66,6 @@ function reopt(req::HTTP.Request) delete!(d, "api_key") end - settings = d["Settings"] - solver_name = get(settings, "solver_name", "HiGHS") - if solver_name == "Xpress" && !(xpress_installed=="True") - solver_name = "HiGHS" - @warn "Changing solver_name from Xpress to $solver_name because Xpress is not installed. Next time - Specify Settings.solver_name = 'HiGHS' or 'Cbc' or 'SCIP'" - end - # ---- API-only battery heuristic dispatch strategy: "daily_foresight_optimized" ---- # When ElectricStorage.dispatch_strategy == "daily_foresight_optimized", if needed, first run REopt to get optimal sizing of PV and battery. # Then run the MPC rolling-horizon loop to get a SOC profile (skip MPC dispatch if optimal battery size is 0). @@ -82,7 +74,7 @@ function reopt(req::HTTP.Request) if get(electric_storage, "dispatch_strategy", nothing) == "daily_foresight_optimized" try @info "Running MPC to obtain daily foresight optimized battery dispatch profile." - mpc_results = get_mpc_results(d; solver_name=solver_name) + mpc_results = get_mpc_results(d) # TODO: Cache sizing run results and avoid a second call to REopt? Are those the same results? if get(mpc_results, "skip_mpc", false) == true @info "Cannot execute daily_foresight_optimized battery dispatch because optimal battery size is 0. Setting dispatch strategy to 'optimized'." From 7d4cfaa7dd8d42664964d7c8c0b590d01f4a1ad9 Mon Sep 17 00:00:00 2001 From: adfarth Date: Mon, 29 Jun 2026 16:37:25 -0600 Subject: [PATCH 14/45] condense input processing undo solver_name change --- julia_src/http.jl | 22 ++++- julia_src/mpc.jl | 239 +++++++++++++--------------------------------- 2 files changed, 87 insertions(+), 174 deletions(-) diff --git a/julia_src/http.jl b/julia_src/http.jl index 617ef89b7..7d6c2bd40 100644 --- a/julia_src/http.jl +++ b/julia_src/http.jl @@ -66,6 +66,14 @@ function reopt(req::HTTP.Request) delete!(d, "api_key") end + settings = d["Settings"] + solver_name = get(settings, "solver_name", "HiGHS") + if solver_name == "Xpress" && !(xpress_installed=="True") + solver_name = "HiGHS" + @warn "Changing solver_name from Xpress to $solver_name because Xpress is not installed. Next time + Specify Settings.solver_name = 'HiGHS' or 'Cbc' or 'SCIP'" + end + # ---- API-only battery heuristic dispatch strategy: "daily_foresight_optimized" ---- # When ElectricStorage.dispatch_strategy == "daily_foresight_optimized", if needed, first run REopt to get optimal sizing of PV and battery. # Then run the MPC rolling-horizon loop to get a SOC profile (skip MPC dispatch if optimal battery size is 0). @@ -74,7 +82,7 @@ function reopt(req::HTTP.Request) if get(electric_storage, "dispatch_strategy", nothing) == "daily_foresight_optimized" try @info "Running MPC to obtain daily foresight optimized battery dispatch profile." - mpc_results = get_mpc_results(d) + mpc_results = get_mpc_results(d; solver_name=solver_name) # TODO: Cache sizing run results and avoid a second call to REopt? Are those the same results? if get(mpc_results, "skip_mpc", false) == true @info "Cannot execute daily_foresight_optimized battery dispatch because optimal battery size is 0. Setting dispatch strategy to 'optimized'." @@ -846,7 +854,7 @@ Arguments: req::HTTP.Request: REopt inputs dictionary Returns JSON dictionary containing: - - MPC: Metadata (time_steps_per_hour, horizon) + - MPC: Metadata (time_steps_per_hour, horizon_time_steps) - PV: Size and dispatch series (to load, storage, grid, curtailed) - ElectricStorage: Sizes and state-of-charge series - ElectricUtility: Grid dispatch series and emissions @@ -868,7 +876,15 @@ function mpc(req::HTTP.Request) delete!(d, "api_key") end - results = get_mpc_results(d) + settings = d["Settings"] + solver_name = get(settings, "solver_name", "HiGHS") + if solver_name == "Xpress" && !(xpress_installed=="True") + solver_name = "HiGHS" + @warn "Changing solver_name from Xpress to $solver_name because Xpress is not installed. Next time + Specify Settings.solver_name = 'HiGHS' or 'Cbc' or 'SCIP'" + end + + results = get_mpc_results(d; solver_name=solver_name) catch e @error "MPC failed" exception=(e, catch_backtrace()) error_response["error"] = sprint(showerror, e) diff --git a/julia_src/mpc.jl b/julia_src/mpc.jl index d1b0a12e4..86b4952a2 100644 --- a/julia_src/mpc.jl +++ b/julia_src/mpc.jl @@ -43,61 +43,12 @@ function slice_data(arr::AbstractVector, idx::Int, end_idx::Int) end end -""" - check_series_length(name, series, time_steps_per_hour, length_of_data) - -Validate that a user-entered time series is of length 8760 * time_steps_per_hour (cannot be a leap year). -""" -function check_series_length(name::String, series::AbstractVector, length_of_data::Int) - n = length(series) - if n == length_of_data - return series - end - error("MPC: $name length $n != 8760 * time_steps_per_hour ($length_of_data).") -end - -""" - get_tariff_inputs(electric_tariff, year, time_steps_per_hour) - -Call REopt.ElectricTariff to get energy_rates, monthly_demand_rates, tou_demand_rates, tou_demand_ratchet_time_steps -MPC does not currently model tiers (tiers are flattened), coincident peak charges, or demand lookback. -""" -function get_tariff_inputs(electric_tariff::Dict, year::Union{Int,Nothing}, time_steps_per_hour::Int) - - # TODO: Are all of these relevant? Any missing inputs? - # TODO: Implement lookback? (demand_lookback_months, demand_lookback_percent, demand_lookback_range) Ignoring coincident peak charges for now - # TODO: Need to think through NEM or passing back export values (wholesale_rate, export_rate_beyond_net_metering_limit) - tariff_kwargs = (:urdb_label, :urdb_response, :urdb_utility_name, - :urdb_rate_name, :urdb_metadata, - :wholesale_rate, :export_rate_beyond_net_metering_limit, - :monthly_energy_rates, :monthly_demand_rates, - :blended_annual_energy_rate, :blended_annual_demand_rate, - :add_monthly_rates_to_urdb_rate, - :tou_energy_rates_per_kwh, :add_tou_energy_rates_to_urdb_rate, - :demand_lookback_months, :demand_lookback_percent, :demand_lookback_range) - kwargs = Dict{Symbol,Any}(Symbol(k) => v for (k, v) in electric_tariff if Symbol(k) in tariff_kwargs) - - # MPC does not currently model tiered rates - kwargs[:remove_tiers] = true - tariff = reoptjl.ElectricTariff(; year = year, time_steps_per_hour = time_steps_per_hour, kwargs...) - - energy_rates = Float64.(tariff.energy_rates[:, 1]) - monthly_demand_rates = isempty(tariff.monthly_demand_rates) ? - zeros(Float64, 12) : Float64.(tariff.monthly_demand_rates[:, 1]) - tou_demand_rates = isempty(tariff.tou_demand_rates) ? Float64[] : Float64.(tariff.tou_demand_rates[:, 1]) - tou_demand_ratchet_time_steps = [Int.(v) for v in tariff.tou_demand_ratchet_time_steps] - - # TODO: Track lookback variables - demand_lookback_months, demand_lookback_percent, demand_lookback_range? - return (energy_rates = energy_rates, - monthly_demand_rates = monthly_demand_rates, - tou_demand_rates = tou_demand_rates, - tou_demand_ratchet_time_steps = tou_demand_ratchet_time_steps) -end """ generate_pv_production_factors(d, time_steps_per_hour) -Generate a PV production factor series using PVWatts by calling REopt.get_production_factor +Generate a PV production factor series using PVWatts by calling REopt.get_production_factor. +This is called by get_mpc_results only when the user does not provide a custom production_factor_series. """ function generate_pv_production_factors(d::Dict, time_steps_per_hour::Int) site = get(d, "Site", Dict()) @@ -122,45 +73,13 @@ function generate_pv_production_factors(d::Dict, time_steps_per_hour::Int) return Vector{Float64}(pv_production_factor_series) end -""" - generate_loads_kw(d, time_steps_per_hour) - -Generate an electric load profile using DOE Commercial Reference buildings by calling REopt.ElectricLoad -""" -function generate_loads_kw(d::Dict, time_steps_per_hour::Int) - site = get(d, "Site", Dict()) - if !haskey(site, "latitude") - error("MPC: Site.latitude is required to generate ElectricLoad.loads_kw using a DOE CRB profile.") - end - if !haskey(site, "longitude") - error("MPC: Site.longitude is required to generate ElectricLoad.loads_kw using a DOE CRB profile.") - end - lat = Float64(site["latitude"]) - lon = Float64(site["longitude"]) - - @info "MPC: ElectricLoad.loads_kw not provided; generating via REopt.jl ElectricLoad (lat=$(lat), lon=$(lon))." - - electric_load = get(d, "ElectricLoad", Dict()) - - # TODO: Double check this list for relevance/missing inputs - load_kwargs = (:normalize_and_scale_load_profile_input, - :path_to_csv, :doe_reference_name, - :blended_doe_reference_names, :blended_doe_reference_percents, - :year, :city, :annual_kwh, :monthly_totals_kwh, - :monthly_peaks_kw, :loads_kw_is_net) - kwargs = Dict{Symbol,Any}(Symbol(k) => v for (k, v) in electric_load if Symbol(k) in load_kwargs) - load = reoptjl.ElectricLoad(; latitude = lat, longitude = lon, - time_steps_per_hour = time_steps_per_hour, kwargs...) - return Vector{Float64}(load.loads_kw) -end - """ get_technology_sizes!(d) Determine PV and ElectricStorage sizes for the MPC loop. If min_kw != max_kw and/or min_kwh != max_kwh, call REopt to size technologies. User input battery sizes must also be greater than zero. """ -function get_technology_sizes!(d::Dict) +function get_technology_sizes!(d::Dict; solver_name::String="HiGHS") pv = get!(d, "PV", Dict()) batt = get!(d, "ElectricStorage", Dict()) @@ -176,6 +95,7 @@ function get_technology_sizes!(d::Dict) "to run the daily_foresight_optimized dispatch option.") end + # Check if both PV and BESS sizes fixed pv_fixed = is_fixed(pv, "min_kw", "max_kw") batt_fixed = is_fixed(batt, "min_kw", "max_kw") && is_fixed(batt, "min_kwh", "max_kwh") @@ -184,7 +104,6 @@ function get_technology_sizes!(d::Dict) pv_kw = Float64(pv["min_kw"]) batt_kw = Float64(batt["min_kw"]) batt_kwh = Float64(batt["min_kwh"]) - @info "MPC: fixed technology sizes entered, PV = $(pv_kw) kW, battery = $(batt_kw) kW / $(batt_kwh) kWh." return (pv_kw = pv_kw, batt_kw = batt_kw, batt_kwh = batt_kwh, skip_mpc = false) end @@ -197,25 +116,15 @@ function get_technology_sizes!(d::Dict) delete!(sizing_post["ElectricStorage"], "fixed_soc_series_fraction") end - # TODO: Avoid redefining defaults here? + # TODO: Should we "remove tiers" here for sizing or allow for optimizing with tiers? + settings = get(sizing_post, "Settings", Dict()) - sizing_solver_name = get(settings, "solver_name", "HiGHS") - sizing_timeout = Float64(get(settings, "timeout_seconds", 420)) - sizing_opt_tol = Float64(get(settings, "optimality_tolerance", 0.001)) - - solver_attributes = SolverAttributes(sizing_timeout, sizing_opt_tol) - m = get_solver_model(get_solver_model_type(sizing_solver_name), solver_attributes) - - # Delete Settings inputs specific to the API - api_only_settings_keys = ("timeout_seconds", "optimality_tolerance", "run_bau") - if haskey(sizing_post, "Settings") - for k in api_only_settings_keys - delete!(sizing_post["Settings"], k) - end - if isempty(sizing_post["Settings"]) - delete!(sizing_post, "Settings") - end - end + delete!(settings, "run_bau") # Remove run_bau from sizing run + timeout_seconds = pop!(settings, "timeout_seconds", 420) + optimality_tolerance = pop!(settings, "optimality_tolerance", 0.001) + solver_attributes = SolverAttributes(timeout_seconds, optimality_tolerance) + + m = get_solver_model(get_solver_model_type(solver_name), solver_attributes) model_inputs = reoptjl.REoptInputs(sizing_post) sizing_results = reoptjl.run_reopt(m, model_inputs) @@ -249,15 +158,24 @@ function get_technology_sizes!(d::Dict) return (pv_kw = pv_kw, batt_kw = batt_kw, batt_kwh = batt_kwh, skip_mpc = false) end -function get_mpc_results(d::Dict)::Dict +function get_mpc_results(d::Dict; solver_name::String="HiGHS")::Dict """ Run a full-year rolling-horizon MPC dispatch for PV + ElectricStorage by calling `REopt.run_mpc` once per timestep with a 24-hour look-ahead. Inputs: - d::Dict, REopt inputs dictionary + d::Dict, REopt inputs dictionary + + Returns JSON dictionary containing: + - MPC: Metadata (time_steps_per_hour, horizon_time_steps) + - PV: Size and dispatch series (to load, storage, grid, curtailed) + - ElectricStorage: Sizes and state-of-charge series + - ElectricUtility: Grid dispatch series and emissions + - ElectricLoad: Load profile used + - ElectricTariff: Energy and demand costs, peak demands by month/ratchet + - status: "optimal" + - reopt_version: Version of REopt.jl used - Returns a Dict with PV and ElectricStorage sizes, dispatch time series, and cost metrics """ ## Validation on allowable inputs for MPC ## @@ -297,12 +215,6 @@ function get_mpc_results(d::Dict)::Dict ## Set up MPC inputs ## settings = get!(d, "Settings", Dict()) - solver_name = get(settings, "solver_name", "HiGHS") - if solver_name == "Xpress" && !(xpress_installed=="True") - solver_name = "HiGHS" - @warn "Changing solver_name from Xpress to $solver_name because Xpress is not installed. - Next time specify Settings.solver_name = 'HiGHS' or 'Cbc' or 'SCIP'." - end settings["solver_name"] = solver_name # TODO: MPC timeout and optimality tolerance @@ -314,38 +226,10 @@ function get_mpc_results(d::Dict)::Dict horizon = 24 * time_steps_per_hour per_iter_timeout_s = 30.0 - # TODO: Should MPC handle multiple PVs? - pv = get!(d, "PV", Dict()) - electric_storage = get!(d, "ElectricStorage", Dict()) - electric_load = get!(d, "ElectricLoad", Dict()) - electric_tariff = get(d, "ElectricTariff", Dict()) - electric_utility = get(d, "ElectricUtility", Dict()) - - # Read timeseries PV production factors or generate using PVWatts, save generated values to reduce PVWatts calls - if haskey(pv, "production_factor_series") && !isempty(pv["production_factor_series"]) - pv_prod_factor = Float64.(pv["production_factor_series"]) - else - # TODO: These production factors don't consider degradation, problem? - # Does MPCPV need a degradation input to calculate the levelization factor used in the optimization? - pv_prod_factor = generate_pv_production_factors(d, time_steps_per_hour) - pv["production_factor_series"] = pv_prod_factor - end - - # Read timeseries electric load inputs or generate using CRBs - if haskey(electric_load, "loads_kw") && !isempty(electric_load["loads_kw"]) - loads_kw = Float64.(electric_load["loads_kw"]) - else - loads_kw = generate_loads_kw(d, time_steps_per_hour) - - # Previously caching to save an extra CRB call but loads_kw conflicts with other load inputs - # electric_load["loads_kw"] = loads_kw - end - - # Check data series lengths - # TODO: Do API inputs validation when calling the MPC endpoint directly? - # Should we even have an MPC endpoint? - pv_prod_factor = check_series_length("PV.production_factor_series", pv_prod_factor, length_of_data) - loads_kw = check_series_length("ElectricLoad.loads_kw", loads_kw, length_of_data) + # Call REoptInputs once upfront to process and validate inputs + # This handles: load profile generation, tariff processing (including tier removal), emissions defaults, storage efficiency defaults + i = reoptjl.REoptInputs(d) + s = i.s # Access the processed Scenario struct # MPC requires fixed PV and battery sizes. If not provided, call REopt first in a sizing run. technology_sizes = get_technology_sizes!(d) @@ -355,41 +239,54 @@ function get_mpc_results(d::Dict)::Dict return Dict("skip_mpc" => true) end - pv_kw = technology_sizes.pv_kw - batt_kw = technology_sizes.batt_kw - batt_kwh = technology_sizes.batt_kwh - - # TODO: Should these defaults be read from somewhere so that they don't have to be updated in various places? - soc_0 = Float64(get(electric_storage, "soc_init_fraction", 0.5)) - soc_min = Float64(get(electric_storage, "soc_min_fraction", 0.2)) - rect_eff = Float64(get(electric_storage, "rectifier_efficiency_fraction", 0.96)) - inv_eff = Float64(get(electric_storage, "inverter_efficiency_fraction", 0.96)) - int_eff = Float64(get(electric_storage, "internal_efficiency_fraction", 0.975)) + # Note: REoptInputs does not provide PV production factors if user doesn't specify custom values + if !isempty(s.pvs[1].production_factor_series) + pv_prod_factor = Float64.(s.pvs[1].production_factor_series) + else + # TODO: These production factors don't consider degradation, problem? + # Does MPCPV need a degradation input to calculate the levelization factor used in the optimization? + pv_prod_factor = generate_pv_production_factors(d, time_steps_per_hour) + end + + loads_kw = Float64.(s.electric_load.loads_kw) + + # Extract tariff inputs relevant to MPC (use first tier only if tiered rates) + # TODO: Are all of these relevant? Any missing inputs? + # TODO: Implement lookback? (demand_lookback_months, demand_lookback_percent, demand_lookback_range) Ignoring coincident peak charges for now + # TODO: Need to think through NEM or passing back export values (wholesale_rate, export_rate_beyond_net_metering_limit) + # TODO: Track lookback variables - demand_lookback_months, demand_lookback_percent, demand_lookback_range? + energy_rates = Float64.(s.electric_tariff.energy_rates[:, 1]) + monthly_demand_rates = isempty(s.electric_tariff.monthly_demand_rates) ? + zeros(Float64, 12) : Float64.(s.electric_tariff.monthly_demand_rates[:, 1]) + tou_demand_rates = isempty(s.electric_tariff.tou_demand_rates) ? Float64[] : Float64.(s.electric_tariff.tou_demand_rates[:, 1]) + tou_demand_ratchet_time_steps = [Int.(v) for v in s.electric_tariff.tou_demand_ratchet_time_steps] + + # Extract storage efficiency and SOC defaults from processed inputs + rect_eff = Float64(s.storage.attr["ElectricStorage"].rectifier_efficiency_fraction) + inv_eff = Float64(s.storage.attr["ElectricStorage"].inverter_efficiency_fraction) + int_eff = Float64(s.storage.attr["ElectricStorage"].internal_efficiency_fraction) charge_eff = rect_eff * sqrt(int_eff) discharge_eff = inv_eff * sqrt(int_eff) + soc_0 = Float64(s.storage.attr["ElectricStorage"].soc_init_fraction) + soc_min = Float64(s.storage.attr["ElectricStorage"].soc_min_fraction) + + # Extract emissions defaults (or use user input if provided) + co2_grid_emissions_series = Float64.(s.electric_utility.emissions_factor_series_lb_CO2_per_kwh) - # Process utility rate - year = haskey(electric_load, "year") ? Int(electric_load["year"]) : nothing - tariff = get_tariff_inputs(electric_tariff, year, time_steps_per_hour) - energy_rates = tariff.energy_rates - tou_demand_rates = tariff.tou_demand_rates - tou_demand_ratchet_time_steps = tariff.tou_demand_ratchet_time_steps - monthly_demand_rates = tariff.monthly_demand_rates - # TODO: MPC loop cherry picked one specific emissions type - remove or keep and add others? - # TODO: If keep, add function to pull defaults? Currently only allowing user upload - emissions = haskey(electric_utility, "emissions_factor_series_lb_CO2_per_kwh") ? - Float64.(electric_utility["emissions_factor_series_lb_CO2_per_kwh"]) : zeros(Float64, length_of_data) - emissions = check_series_length("ElectricUtility.emissions_factor_series_lb_CO2_per_kwh", emissions, length_of_data) + + pv_kw = technology_sizes.pv_kw + batt_kw = technology_sizes.batt_kw + batt_kwh = technology_sizes.batt_kwh month_starts = get_month_transition_timesteps(time_steps_per_hour) # ts_to_month = 8760 array specifying which month each timestep falls in (1-12) ts_to_month = Vector{Int}(undef, length_of_data) for m in 1:12 - s = month_starts[m] + s_idx = month_starts[m] e = m < 12 ? month_starts[m+1] - 1 : length_of_data - ts_to_month[s:e] .= m + ts_to_month[s_idx:e] .= m end # ts_to_ratchet = 8760 array specifying which ratchet each timestep falls in @@ -472,7 +369,7 @@ function get_mpc_results(d::Dict)::Dict current_horizon_pv = slice_data(pv_prod_factor, idx, end_ts) current_horizon_load = slice_data(loads_kw, idx, end_ts) current_horizon_energy_rates = slice_data(energy_rates, idx, end_ts) - current_horizon_emissions = slice_data(emissions, idx, end_ts) + current_horizon_emissions = slice_data(co2_grid_emissions_series, idx, end_ts) # List of length n_tou_ratchets, specifies which ts of the current horizon are in each TOU ratchet # by placing values 1 to horizon into the corresponding element of the array based on ratchet number @@ -525,7 +422,7 @@ function get_mpc_results(d::Dict)::Dict push!(dispatch_series["ElectricUtility"]["electric_to_load_series_kw"], util_to_load) push!(dispatch_series["ElectricUtility"]["electric_to_storage_series_kw"], util_to_batt) push!(dispatch_series["ElectricUtility"]["emissions_series_lb_CO2"], - emissions[idx] * grid_power / time_steps_per_hour) + co2_grid_emissions_series[idx] * grid_power / time_steps_per_hour) push!(dispatch_series["ElectricLoad"]["load_series_kw"], loads_kw[idx]) # Running energy costs @@ -558,7 +455,7 @@ function get_mpc_results(d::Dict)::Dict return Dict( "MPC" => Dict( "time_steps_per_hour" => time_steps_per_hour, - "horizon" => horizon, + "horizon_time_steps" => horizon, ), "PV" => merge(Dict("size_kw" => pv_kw), dispatch_series["PV"]), "ElectricStorage" => merge(Dict("size_kw" => batt_kw, "size_kwh" => batt_kwh), dispatch_series["ElectricStorage"]), From 9fb665df0918273a27b894378ca32ff608aff3f3 Mon Sep 17 00:00:00 2001 From: adfarth Date: Mon, 29 Jun 2026 16:39:39 -0600 Subject: [PATCH 15/45] rmv lat long validation --- julia_src/mpc.jl | 6 ------ 1 file changed, 6 deletions(-) diff --git a/julia_src/mpc.jl b/julia_src/mpc.jl index 86b4952a2..f9ec0a8e8 100644 --- a/julia_src/mpc.jl +++ b/julia_src/mpc.jl @@ -52,12 +52,6 @@ This is called by get_mpc_results only when the user does not provide a custom p """ function generate_pv_production_factors(d::Dict, time_steps_per_hour::Int) site = get(d, "Site", Dict()) - if !haskey(site, "latitude") - error("MPC: Site.latitude is required to generate PV.production_factor_series using PVWatts.") - end - if !haskey(site, "longitude") - error("MPC: Site.longitude is required to generate PV.production_factor_series using PVWatts.") - end lat = Float64(site["latitude"]) lon = Float64(site["longitude"]) From 7518e3606a79d971c8424b709a22fda334d5ee01 Mon Sep 17 00:00:00 2001 From: adfarth Date: Mon, 29 Jun 2026 17:04:54 -0600 Subject: [PATCH 16/45] get pv prod from REopt run too --- julia_src/mpc.jl | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/julia_src/mpc.jl b/julia_src/mpc.jl index f9ec0a8e8..c3d55f49f 100644 --- a/julia_src/mpc.jl +++ b/julia_src/mpc.jl @@ -135,6 +135,7 @@ function get_technology_sizes!(d::Dict; solver_name::String="HiGHS") pv_kw = Float64(get(get(sizing_results, "PV", Dict()), "size_kw", 0.0)) batt_kw = Float64(get(get(sizing_results, "ElectricStorage", Dict()), "size_kw", 0.0)) batt_kwh = Float64(get(get(sizing_results, "ElectricStorage", Dict()), "size_kwh", 0.0)) + pv_production_factor_series = get(get(sizing_results, "PV", Dict()), "production_factor_series", nothing) # Skip the MPC loop if no battery is sized if batt_kw <= 0.0 || batt_kwh <= 0.0 @@ -149,7 +150,7 @@ function get_technology_sizes!(d::Dict; solver_name::String="HiGHS") batt["max_kwh"] = batt_kwh @info "MPC: REopt sizing solved with PV = $(pv_kw) kW and battery = $(batt_kw) kW / $(batt_kwh) kWh." - return (pv_kw = pv_kw, batt_kw = batt_kw, batt_kwh = batt_kwh, skip_mpc = false) + return (pv_kw = pv_kw, batt_kw = batt_kw, batt_kwh = batt_kwh, skip_mpc = false, pv_production_factor_series = pv_production_factor_series) end function get_mpc_results(d::Dict; solver_name::String="HiGHS")::Dict @@ -220,10 +221,16 @@ function get_mpc_results(d::Dict; solver_name::String="HiGHS")::Dict horizon = 24 * time_steps_per_hour per_iter_timeout_s = 30.0 - # Call REoptInputs once upfront to process and validate inputs - # This handles: load profile generation, tariff processing (including tier removal), emissions defaults, storage efficiency defaults - i = reoptjl.REoptInputs(d) - s = i.s # Access the processed Scenario struct + # Process and validate inputs using REoptInputs + try + model_inputs = reoptjl.REoptInputs(d) + @info "Successfully processed REopt inputs." + catch e + @error "Something went wrong during REopt inputs processing!" exception=(e, catch_backtrace()) + error_response["error"] = sprint(showerror, e) + end + + s = model_inputs.s # Access the processed Scenario struct # MPC requires fixed PV and battery sizes. If not provided, call REopt first in a sizing run. technology_sizes = get_technology_sizes!(d) From 9c255efb083447d314bba3353d339717af714891 Mon Sep 17 00:00:00 2001 From: adfarth Date: Mon, 29 Jun 2026 17:26:13 -0600 Subject: [PATCH 17/45] set pv prod for final run --- julia_src/http.jl | 2 +- julia_src/mpc.jl | 12 ++++++++---- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/julia_src/http.jl b/julia_src/http.jl index 7d6c2bd40..1d7094ff8 100644 --- a/julia_src/http.jl +++ b/julia_src/http.jl @@ -77,7 +77,7 @@ function reopt(req::HTTP.Request) # ---- API-only battery heuristic dispatch strategy: "daily_foresight_optimized" ---- # When ElectricStorage.dispatch_strategy == "daily_foresight_optimized", if needed, first run REopt to get optimal sizing of PV and battery. # Then run the MPC rolling-horizon loop to get a SOC profile (skip MPC dispatch if optimal battery size is 0). - # Then set ElectricStorage.fixed_soc_series_fraction = MPC SOC before running the main REopt optimization. + # Then set ElectricStorage.fixed_soc_series_fraction = MPC SOC and fix PV and BESS sizing before running the main REopt optimization. electric_storage = get(d, "ElectricStorage", Dict()) if get(electric_storage, "dispatch_strategy", nothing) == "daily_foresight_optimized" try diff --git a/julia_src/mpc.jl b/julia_src/mpc.jl index c3d55f49f..d5c9c1dfe 100644 --- a/julia_src/mpc.jl +++ b/julia_src/mpc.jl @@ -139,18 +139,22 @@ function get_technology_sizes!(d::Dict; solver_name::String="HiGHS") # Skip the MPC loop if no battery is sized if batt_kw <= 0.0 || batt_kwh <= 0.0 - return (pv_kw = pv_kw, batt_kw = 0.0, batt_kwh = 0.0, skip_mpc = true) + return (pv_kw = pv_kw, batt_kw = 0.0, batt_kwh = 0.0, skip_mpc = true, pv_production_factor_series = pv_production_factor_series) end + println(pv_kw, batt_kw, batt_kwh) + + # Fix inputs for final REopt run in http.jl pv["min_kw"] = pv_kw pv["max_kw"] = pv_kw + pv["production_factor_series"] = pv_production_factor_series batt["min_kw"] = batt_kw batt["max_kw"] = batt_kw batt["min_kwh"] = batt_kwh batt["max_kwh"] = batt_kwh @info "MPC: REopt sizing solved with PV = $(pv_kw) kW and battery = $(batt_kw) kW / $(batt_kwh) kWh." - return (pv_kw = pv_kw, batt_kw = batt_kw, batt_kwh = batt_kwh, skip_mpc = false, pv_production_factor_series = pv_production_factor_series) + return (; pv_kw, batt_kw, batt_kwh, skip_mpc = false, pv_production_factor_series) end function get_mpc_results(d::Dict; solver_name::String="HiGHS")::Dict @@ -243,6 +247,8 @@ function get_mpc_results(d::Dict; solver_name::String="HiGHS")::Dict # Note: REoptInputs does not provide PV production factors if user doesn't specify custom values if !isempty(s.pvs[1].production_factor_series) pv_prod_factor = Float64.(s.pvs[1].production_factor_series) + elseif technology_sizes.pv_production_factor_series !== nothing + pv_prod_factor = Float64.(technology_sizes.pv_production_factor_series) else # TODO: These production factors don't consider degradation, problem? # Does MPCPV need a degradation input to calculate the levelization factor used in the optimization? @@ -274,8 +280,6 @@ function get_mpc_results(d::Dict; solver_name::String="HiGHS")::Dict # Extract emissions defaults (or use user input if provided) co2_grid_emissions_series = Float64.(s.electric_utility.emissions_factor_series_lb_CO2_per_kwh) - - pv_kw = technology_sizes.pv_kw batt_kw = technology_sizes.batt_kw batt_kwh = technology_sizes.batt_kwh From 01c0140b9f4d525e8e266464bc1981b07f919322 Mon Sep 17 00:00:00 2001 From: adfarth Date: Mon, 29 Jun 2026 17:43:21 -0600 Subject: [PATCH 18/45] update pv prod in final REopt run --- julia_src/http.jl | 4 ++-- julia_src/mpc.jl | 27 +++++++++++++-------------- 2 files changed, 15 insertions(+), 16 deletions(-) diff --git a/julia_src/http.jl b/julia_src/http.jl index 1d7094ff8..c4a73dbd6 100644 --- a/julia_src/http.jl +++ b/julia_src/http.jl @@ -82,7 +82,7 @@ function reopt(req::HTTP.Request) if get(electric_storage, "dispatch_strategy", nothing) == "daily_foresight_optimized" try @info "Running MPC to obtain daily foresight optimized battery dispatch profile." - mpc_results = get_mpc_results(d; solver_name=solver_name) + mpc_results = get_mpc_results!(d; solver_name=solver_name) # TODO: Cache sizing run results and avoid a second call to REopt? Are those the same results? if get(mpc_results, "skip_mpc", false) == true @info "Cannot execute daily_foresight_optimized battery dispatch because optimal battery size is 0. Setting dispatch strategy to 'optimized'." @@ -884,7 +884,7 @@ function mpc(req::HTTP.Request) Specify Settings.solver_name = 'HiGHS' or 'Cbc' or 'SCIP'" end - results = get_mpc_results(d; solver_name=solver_name) + results = get_mpc_results!(d; solver_name=solver_name) catch e @error "MPC failed" exception=(e, catch_backtrace()) error_response["error"] = sprint(showerror, e) diff --git a/julia_src/mpc.jl b/julia_src/mpc.jl index d5c9c1dfe..bb3229a7a 100644 --- a/julia_src/mpc.jl +++ b/julia_src/mpc.jl @@ -48,7 +48,7 @@ end generate_pv_production_factors(d, time_steps_per_hour) Generate a PV production factor series using PVWatts by calling REopt.get_production_factor. -This is called by get_mpc_results only when the user does not provide a custom production_factor_series. +This is called by get_mpc_results only when the user does not provide a custom production_factor_series and REopt is not called for sizing. """ function generate_pv_production_factors(d::Dict, time_steps_per_hour::Int) site = get(d, "Site", Dict()) @@ -142,12 +142,9 @@ function get_technology_sizes!(d::Dict; solver_name::String="HiGHS") return (pv_kw = pv_kw, batt_kw = 0.0, batt_kwh = 0.0, skip_mpc = true, pv_production_factor_series = pv_production_factor_series) end - println(pv_kw, batt_kw, batt_kwh) - # Fix inputs for final REopt run in http.jl pv["min_kw"] = pv_kw pv["max_kw"] = pv_kw - pv["production_factor_series"] = pv_production_factor_series batt["min_kw"] = batt_kw batt["max_kw"] = batt_kw batt["min_kwh"] = batt_kwh @@ -157,7 +154,7 @@ function get_technology_sizes!(d::Dict; solver_name::String="HiGHS") return (; pv_kw, batt_kw, batt_kwh, skip_mpc = false, pv_production_factor_series) end -function get_mpc_results(d::Dict; solver_name::String="HiGHS")::Dict +function get_mpc_results!(d::Dict; solver_name::String="HiGHS")::Dict """ Run a full-year rolling-horizon MPC dispatch for PV + ElectricStorage by calling `REopt.run_mpc` once per timestep with a 24-hour look-ahead. @@ -244,6 +241,11 @@ function get_mpc_results(d::Dict; solver_name::String="HiGHS")::Dict return Dict("skip_mpc" => true) end + # Sizes from user or initial REopt run + pv_kw = technology_sizes.pv_kw + batt_kw = technology_sizes.batt_kw + batt_kwh = technology_sizes.batt_kwh + # Note: REoptInputs does not provide PV production factors if user doesn't specify custom values if !isempty(s.pvs[1].production_factor_series) pv_prod_factor = Float64.(s.pvs[1].production_factor_series) @@ -254,6 +256,8 @@ function get_mpc_results(d::Dict; solver_name::String="HiGHS")::Dict # Does MPCPV need a degradation input to calculate the levelization factor used in the optimization? pv_prod_factor = generate_pv_production_factors(d, time_steps_per_hour) end + # Avoid another PVWatts call in the final REopt run. + d["PV"]["production_factor_series"] = pv_prod_factor loads_kw = Float64.(s.electric_load.loads_kw) @@ -267,6 +271,10 @@ function get_mpc_results(d::Dict; solver_name::String="HiGHS")::Dict zeros(Float64, 12) : Float64.(s.electric_tariff.monthly_demand_rates[:, 1]) tou_demand_rates = isempty(s.electric_tariff.tou_demand_rates) ? Float64[] : Float64.(s.electric_tariff.tou_demand_rates[:, 1]) tou_demand_ratchet_time_steps = [Int.(v) for v in s.electric_tariff.tou_demand_ratchet_time_steps] + # TODO: Monthly demand has never been tested + n_tou_ratchets = length(tou_demand_rates) # Number of TOU ratchets + tou_previous_peak_demands = zeros(Float64, n_tou_ratchets) # Tracks past TOU peak demand per ratchet + monthly_previous_peak_demands = zeros(Float64, 12) # Tracks past monthly peak demand # Extract storage efficiency and SOC defaults from processed inputs rect_eff = Float64(s.storage.attr["ElectricStorage"].rectifier_efficiency_fraction) @@ -280,10 +288,6 @@ function get_mpc_results(d::Dict; solver_name::String="HiGHS")::Dict # Extract emissions defaults (or use user input if provided) co2_grid_emissions_series = Float64.(s.electric_utility.emissions_factor_series_lb_CO2_per_kwh) - pv_kw = technology_sizes.pv_kw - batt_kw = technology_sizes.batt_kw - batt_kwh = technology_sizes.batt_kwh - month_starts = get_month_transition_timesteps(time_steps_per_hour) # ts_to_month = 8760 array specifying which month each timestep falls in (1-12) @@ -302,11 +306,6 @@ function get_mpc_results(d::Dict; solver_name::String="HiGHS")::Dict end end - # TODO: Monthly demand has never been tested - n_tou_ratchets = length(tou_demand_rates) # Number of TOU ratchets - tou_previous_peak_demands = zeros(Float64, n_tou_ratchets) # Tracks past TOU peak demand per ratchet - monthly_previous_peak_demands = zeros(Float64, 12) # Tracks past monthly peak demand - # Saved dispatch series (first timestep of each MPC loop) dispatch_series = Dict( "PV" => Dict( From 569b894271a0fadcc3cc8eace7eb749d77c55df0 Mon Sep 17 00:00:00 2001 From: adfarth Date: Mon, 29 Jun 2026 17:47:53 -0600 Subject: [PATCH 19/45] Create 0118_merge_20260629_2347.py --- reoptjl/migrations/0118_merge_20260629_2347.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 reoptjl/migrations/0118_merge_20260629_2347.py diff --git a/reoptjl/migrations/0118_merge_20260629_2347.py b/reoptjl/migrations/0118_merge_20260629_2347.py new file mode 100644 index 000000000..e55228f28 --- /dev/null +++ b/reoptjl/migrations/0118_merge_20260629_2347.py @@ -0,0 +1,14 @@ +# Generated by Django 4.2.26 on 2026-06-29 23:47 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('reoptjl', '0117_merge_20260530_1640'), + ('reoptjl', '0117_merge_20260601_2056'), + ] + + operations = [ + ] From 13bbbed630fbd3be1443da6be8232c767cf89ff2 Mon Sep 17 00:00:00 2001 From: adfarth Date: Mon, 29 Jun 2026 17:52:15 -0600 Subject: [PATCH 20/45] add back in TODO --- julia_src/mpc.jl | 2 ++ 1 file changed, 2 insertions(+) diff --git a/julia_src/mpc.jl b/julia_src/mpc.jl index bb3229a7a..c1e1f9cc2 100644 --- a/julia_src/mpc.jl +++ b/julia_src/mpc.jl @@ -222,6 +222,8 @@ function get_mpc_results!(d::Dict; solver_name::String="HiGHS")::Dict horizon = 24 * time_steps_per_hour per_iter_timeout_s = 30.0 + # TODO: Should MPC handle multiple PVs? + # Process and validate inputs using REoptInputs try model_inputs = reoptjl.REoptInputs(d) From 2fc5065788ccc6d5226b249b8032f9348ea9416f Mon Sep 17 00:00:00 2001 From: adfarth Date: Wed, 1 Jul 2026 08:17:12 -0600 Subject: [PATCH 21/45] pv prod and solver --- julia_src/mpc.jl | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/julia_src/mpc.jl b/julia_src/mpc.jl index c1e1f9cc2..fad40726c 100644 --- a/julia_src/mpc.jl +++ b/julia_src/mpc.jl @@ -68,7 +68,7 @@ function generate_pv_production_factors(d::Dict, time_steps_per_hour::Int) end """ - get_technology_sizes!(d) + get_technology_sizes!(d, solver_name="HiGHS") Determine PV and ElectricStorage sizes for the MPC loop. If min_kw != max_kw and/or min_kwh != max_kwh, call REopt to size technologies. User input battery sizes must also be greater than zero. @@ -236,7 +236,7 @@ function get_mpc_results!(d::Dict; solver_name::String="HiGHS")::Dict s = model_inputs.s # Access the processed Scenario struct # MPC requires fixed PV and battery sizes. If not provided, call REopt first in a sizing run. - technology_sizes = get_technology_sizes!(d) + technology_sizes = get_technology_sizes!(d, solver_name=solver_name) # Skip MPC if no battery is optimally sized if technology_sizes.skip_mpc @@ -249,17 +249,22 @@ function get_mpc_results!(d::Dict; solver_name::String="HiGHS")::Dict batt_kwh = technology_sizes.batt_kwh # Note: REoptInputs does not provide PV production factors if user doesn't specify custom values - if !isempty(s.pvs[1].production_factor_series) - pv_prod_factor = Float64.(s.pvs[1].production_factor_series) - elseif technology_sizes.pv_production_factor_series !== nothing - pv_prod_factor = Float64.(technology_sizes.pv_production_factor_series) + if !isempty(s.pvs) # Get prod factors if PV considered. + if !isempty(s.pvs[1].production_factor_series) + pv_prod_factor = Float64.(s.pvs[1].production_factor_series) + elseif technology_sizes.pv_production_factor_series !== nothing + pv_prod_factor = Float64.(technology_sizes.pv_production_factor_series) + else + # TODO: These production factors don't consider degradation, problem? + # Does MPCPV need a degradation input to calculate the levelization factor used in the optimization? + # AF: none of these production factors consider degradation and I don't think it's a problem? + pv_prod_factor = generate_pv_production_factors(d, time_steps_per_hour) + end + # Avoid another PVWatts call in the final REopt run. + d["PV"]["production_factor_series"] = pv_prod_factor else - # TODO: These production factors don't consider degradation, problem? - # Does MPCPV need a degradation input to calculate the levelization factor used in the optimization? - pv_prod_factor = generate_pv_production_factors(d, time_steps_per_hour) + pv_prod_factor = zeros(Float64, length_of_data) end - # Avoid another PVWatts call in the final REopt run. - d["PV"]["production_factor_series"] = pv_prod_factor loads_kw = Float64.(s.electric_load.loads_kw) From 919d30cb7c5026f5c4e71556277e7d122e8a5985 Mon Sep 17 00:00:00 2001 From: adfarth Date: Wed, 1 Jul 2026 08:23:27 -0600 Subject: [PATCH 22/45] utf-8 --- julia_src/http.jl | 2 +- julia_src/mpc.jl | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/julia_src/http.jl b/julia_src/http.jl index c4a73dbd6..8c23f8ef6 100644 --- a/julia_src/http.jl +++ b/julia_src/http.jl @@ -1,4 +1,4 @@ -using HTTP, JSON, JuMP +using HTTP, JSON, JuMP using HiGHS, Cbc, SCIP using GhpGhx import REopt as reoptjl # For REopt.jl, needed because we still have local REopt.jl module for V1/V2 diff --git a/julia_src/mpc.jl b/julia_src/mpc.jl index fad40726c..0ca203d6e 100644 --- a/julia_src/mpc.jl +++ b/julia_src/mpc.jl @@ -1,4 +1,4 @@ -# REopt®, Copyright (c) Alliance for Sustainable Energy, LLC. See also https://github.com/NatLabRockies/REopt_API/blob/master/LICENSE. +# REopt®, Copyright (c) Alliance for Sustainable Energy, LLC. See also https://github.com/NatLabRockies/REopt_API/blob/master/LICENSE. # ============================================================================ # MPC (Model Predictive Control) endpoint # ---------------------------------------------------------------------------- From 0a557b234c71b1a93bfa8ec37a96951c55bcd41f Mon Sep 17 00:00:00 2001 From: adfarth Date: Wed, 1 Jul 2026 08:35:42 -0600 Subject: [PATCH 23/45] consistent response --- julia_src/http.jl | 2 +- julia_src/mpc.jl | 79 ++++++++++++++++++++++++++++++++--------------- 2 files changed, 55 insertions(+), 26 deletions(-) diff --git a/julia_src/http.jl b/julia_src/http.jl index 8c23f8ef6..e857ba30f 100644 --- a/julia_src/http.jl +++ b/julia_src/http.jl @@ -848,7 +848,7 @@ HTTP endpoint for rolling-horizon Model Predictive Control (MPC) dispatch optimi This endpoint performs a full-year rolling-horizon MPC dispatch for PV + ElectricStorage systems, optimizing daily dispatch using a 24-hour look-ahead window. Runs the MPC dispatch loop via -`get_mpc_results` and returns dispatch results and cost metrics as JSON. +`get_mpc_results!` and returns dispatch results and cost metrics as JSON. Arguments: req::HTTP.Request: REopt inputs dictionary diff --git a/julia_src/mpc.jl b/julia_src/mpc.jl index 0ca203d6e..5bf0e58c2 100644 --- a/julia_src/mpc.jl +++ b/julia_src/mpc.jl @@ -48,7 +48,7 @@ end generate_pv_production_factors(d, time_steps_per_hour) Generate a PV production factor series using PVWatts by calling REopt.get_production_factor. -This is called by get_mpc_results only when the user does not provide a custom production_factor_series and REopt is not called for sizing. +This is called by get_mpc_results! only when the user does not provide a custom production_factor_series and REopt is not called for sizing. """ function generate_pv_production_factors(d::Dict, time_steps_per_hour::Int) site = get(d, "Site", Dict()) @@ -67,6 +67,23 @@ function generate_pv_production_factors(d::Dict, time_steps_per_hour::Int) return Vector{Float64}(pv_production_factor_series) end +""" + build_mpc_response(status; skip_mpc=false, messages=Dict(), result_dict=Dict()) + +Build a consistent MPC response envelope with status, version info, and messages. +Ensures all response paths (success, error, skip) have uniform structure. +""" +function build_mpc_response(status::String; skip_mpc=false, messages=Dict(), result_dict=Dict()) + response = Dict( + "status" => status, + "reopt_version" => string(pkgversion(reoptjl)), + "Messages" => messages, + "skip_mpc" => skip_mpc, + ) + # Merge result data if provided (for success case) + return merge(response, result_dict) +end + """ get_technology_sizes!(d, solver_name="HiGHS") @@ -160,17 +177,21 @@ function get_mpc_results!(d::Dict; solver_name::String="HiGHS")::Dict calling `REopt.run_mpc` once per timestep with a 24-hour look-ahead. Inputs: - d::Dict, REopt inputs dictionary + d::Dict, REopt inputs dictionary (will be modified in-place to store computed fixed sizes and PV production factors.) Returns JSON dictionary containing: + - status: "optimal", "error", or "skipped" + - reopt_version: Version of REopt.jl used + - Messages: Dict with optional errors, warnings, or info + - skip_mpc: Boolean indicating if MPC was skipped + + For "optimal" status, also includes: - MPC: Metadata (time_steps_per_hour, horizon_time_steps) - PV: Size and dispatch series (to load, storage, grid, curtailed) - ElectricStorage: Sizes and state-of-charge series - ElectricUtility: Grid dispatch series and emissions - ElectricLoad: Load profile used - ElectricTariff: Energy and demand costs, peak demands by month/ratchet - - status: "optimal" - - reopt_version: Version of REopt.jl used """ @@ -230,7 +251,10 @@ function get_mpc_results!(d::Dict; solver_name::String="HiGHS")::Dict @info "Successfully processed REopt inputs." catch e @error "Something went wrong during REopt inputs processing!" exception=(e, catch_backtrace()) - error_response["error"] = sprint(showerror, e) + return build_mpc_response( + "error", + messages = Dict("errors" => [sprint(showerror, e)]) + ) end s = model_inputs.s # Access the processed Scenario struct @@ -240,7 +264,11 @@ function get_mpc_results!(d::Dict; solver_name::String="HiGHS")::Dict # Skip MPC if no battery is optimally sized if technology_sizes.skip_mpc - return Dict("skip_mpc" => true) + return build_mpc_response( + "skipped", + skip_mpc = true, + messages = Dict("info" => ["No battery was optimally sized in REopt pre-step; MPC dispatch not needed."]) + ) end # Sizes from user or initial REopt run @@ -463,24 +491,25 @@ function get_mpc_results!(d::Dict; solver_name::String="HiGHS")::Dict tou_demand_cost_total = n_tou_ratchets > 0 ? sum(tou_previous_peak_demands .* tou_demand_rates) : 0.0 - return Dict( - "MPC" => Dict( - "time_steps_per_hour" => time_steps_per_hour, - "horizon_time_steps" => horizon, - ), - "PV" => merge(Dict("size_kw" => pv_kw), dispatch_series["PV"]), - "ElectricStorage" => merge(Dict("size_kw" => batt_kw, "size_kwh" => batt_kwh), dispatch_series["ElectricStorage"]), - "ElectricUtility" => dispatch_series["ElectricUtility"], - "ElectricLoad" => dispatch_series["ElectricLoad"], - "ElectricTariff" => Dict( - "total_energy_cost" => total_energy_cost, - "energy_cost_series_per_timestep" => energy_cost_series, - "total_tou_demand_cost" => tou_demand_cost_total, - "total_monthly_demand_cost" => monthly_demand_cost_total, - "tou_peaks_by_ratchet_kw" => tou_previous_peak_demands, - "monthly_peaks_kw" => monthly_previous_peak_demands, - ), - "status" => "optimal", - "reopt_version" => string(pkgversion(reoptjl)), + return build_mpc_response( + "optimal", + result_dict = Dict( + "MPC" => Dict( + "time_steps_per_hour" => time_steps_per_hour, + "horizon_time_steps" => horizon, + ), + "PV" => merge(Dict("size_kw" => pv_kw), dispatch_series["PV"]), + "ElectricStorage" => merge(Dict("size_kw" => batt_kw, "size_kwh" => batt_kwh), dispatch_series["ElectricStorage"]), + "ElectricUtility" => dispatch_series["ElectricUtility"], + "ElectricLoad" => dispatch_series["ElectricLoad"], + "ElectricTariff" => Dict( + "total_energy_cost" => total_energy_cost, + "energy_cost_series_per_timestep" => energy_cost_series, + "total_tou_demand_cost" => tou_demand_cost_total, + "total_monthly_demand_cost" => monthly_demand_cost_total, + "tou_peaks_by_ratchet_kw" => tou_previous_peak_demands, + "monthly_peaks_kw" => monthly_previous_peak_demands, + ), + ) ) end From 0e2efc04b00c687a254ab8ae64006abb58745983 Mon Sep 17 00:00:00 2001 From: adfarth Date: Thu, 9 Jul 2026 13:02:33 -0600 Subject: [PATCH 24/45] fix inputs processing --- julia_src/mpc.jl | 57 ++++++++++--------- ...uts_fixed_soc_series_fraction_tolerance.py | 19 +++++++ 2 files changed, 48 insertions(+), 28 deletions(-) create mode 100644 reoptjl/migrations/0119_alter_electricstorageinputs_fixed_soc_series_fraction_tolerance.py diff --git a/julia_src/mpc.jl b/julia_src/mpc.jl index 5bf0e58c2..3e7845717 100644 --- a/julia_src/mpc.jl +++ b/julia_src/mpc.jl @@ -85,12 +85,15 @@ function build_mpc_response(status::String; skip_mpc=false, messages=Dict(), res end """ - get_technology_sizes!(d, solver_name="HiGHS") + get_technology_sizes!(d::Dict, model_inputs::reoptjl.REoptInputs, solver_settings::Dict) Determine PV and ElectricStorage sizes for the MPC loop. If min_kw != max_kw and/or min_kwh != max_kwh, call REopt to size technologies. User input battery sizes must also be greater than zero. + d is mutated in-place to update PV and ElectricStorage sizes. + model_inputs has already been processed to remove inputs not used in REopt.jl """ -function get_technology_sizes!(d::Dict; solver_name::String="HiGHS") +function get_technology_sizes!(d::Dict, model_inputs::reoptjl.REoptInputs, solver_settings::Dict) + # pv and batt are updated in-place from the dictionary `d` and used in the final REopt run pv = get!(d, "PV", Dict()) batt = get!(d, "ElectricStorage", Dict()) @@ -119,25 +122,11 @@ function get_technology_sizes!(d::Dict; solver_name::String="HiGHS") end @info "MPC: PV and/or ElectricStorage sizes are not specified — running REopt sizing first." - sizing_post = deepcopy(d) - - # Delete inputs specific to the heuristic battery dispatch run - if haskey(sizing_post, "ElectricStorage") - delete!(sizing_post["ElectricStorage"], "dispatch_strategy") - delete!(sizing_post["ElectricStorage"], "fixed_soc_series_fraction") - end - # TODO: Should we "remove tiers" here for sizing or allow for optimizing with tiers? - settings = get(sizing_post, "Settings", Dict()) - delete!(settings, "run_bau") # Remove run_bau from sizing run - timeout_seconds = pop!(settings, "timeout_seconds", 420) - optimality_tolerance = pop!(settings, "optimality_tolerance", 0.001) - solver_attributes = SolverAttributes(timeout_seconds, optimality_tolerance) - - m = get_solver_model(get_solver_model_type(solver_name), solver_attributes) + m = get_solver_model(get_solver_model_type(solver_settings["solver_name"]), solver_settings["solver_attributes"]) - model_inputs = reoptjl.REoptInputs(sizing_post) + # model_inputs = reoptjl.REoptInputs(sizing_post) sizing_results = reoptjl.run_reopt(m, model_inputs) if get(sizing_results, "status", "") != "optimal" @@ -232,10 +221,7 @@ function get_mpc_results!(d::Dict; solver_name::String="HiGHS")::Dict ## Set up MPC inputs ## settings = get!(d, "Settings", Dict()) - settings["solver_name"] = solver_name - - # TODO: MPC timeout and optimality tolerance - optimality_tolerance = Float64(get(settings, "optimality_tolerance", 0.001)) + settings["solver_name"] = solver_name # TODO: remove? # TODO: MPC horizons and timeout are currently hard coded time_steps_per_hour = Int(get(settings, "time_steps_per_hour", 1)) @@ -244,10 +230,26 @@ function get_mpc_results!(d::Dict; solver_name::String="HiGHS")::Dict per_iter_timeout_s = 30.0 # TODO: Should MPC handle multiple PVs? + # TODO: MPC timeout and optimality tolerance + # Update sizing_post to remove solver settings and dispatch inputs, to be able to validate inputs using REoptInputs + sizing_post = deepcopy(d) + settings = get(sizing_post, "Settings", Dict()) + solver_settings=Dict() + delete!(settings, "run_bau") # Remove run_bau from sizing run + solver_settings["timeout_seconds"] = pop!(settings, "timeout_seconds", 420) + solver_settings["optimality_tolerance"] = pop!(settings, "optimality_tolerance", 0.001) + solver_settings["solver_attributes"] = SolverAttributes(solver_settings["timeout_seconds"], solver_settings["optimality_tolerance"]) + solver_settings["solver_name"] = solver_name + # Delete inputs specific to the heuristic battery dispatch run + if haskey(sizing_post, "ElectricStorage") + delete!(sizing_post["ElectricStorage"], "dispatch_strategy") + delete!(sizing_post["ElectricStorage"], "fixed_soc_series_fraction") + end # Process and validate inputs using REoptInputs + model_inputs = nothing try - model_inputs = reoptjl.REoptInputs(d) + model_inputs = reoptjl.REoptInputs(sizing_post) @info "Successfully processed REopt inputs." catch e @error "Something went wrong during REopt inputs processing!" exception=(e, catch_backtrace()) @@ -256,11 +258,10 @@ function get_mpc_results!(d::Dict; solver_name::String="HiGHS")::Dict messages = Dict("errors" => [sprint(showerror, e)]) ) end - - s = model_inputs.s # Access the processed Scenario struct - + # MPC requires fixed PV and battery sizes. If not provided, call REopt first in a sizing run. - technology_sizes = get_technology_sizes!(d, solver_name=solver_name) + technology_sizes = get_technology_sizes!(d, model_inputs, solver_settings) + s = model_inputs.s # Access the processed Scenario struct # Skip MPC if no battery is optimally sized if technology_sizes.skip_mpc @@ -434,7 +435,7 @@ function get_mpc_results!(d::Dict; solver_name::String="HiGHS")::Dict tou_previous_peak_demands, monthly_previous_peak_demands, soc_init_frac) model = get_solver_model(get_solver_model_type(solver_name), - SolverAttributes(per_iter_timeout_s, optimality_tolerance)) + SolverAttributes(per_iter_timeout_s, solver_settings["optimality_tolerance"])) result = reoptjl.run_mpc(model, post) # Assume perfect forecast; save first timestep of results as the executed state diff --git a/reoptjl/migrations/0119_alter_electricstorageinputs_fixed_soc_series_fraction_tolerance.py b/reoptjl/migrations/0119_alter_electricstorageinputs_fixed_soc_series_fraction_tolerance.py new file mode 100644 index 000000000..2378b63f3 --- /dev/null +++ b/reoptjl/migrations/0119_alter_electricstorageinputs_fixed_soc_series_fraction_tolerance.py @@ -0,0 +1,19 @@ +# Generated by Django 4.2.26 on 2026-07-09 18:09 + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('reoptjl', '0118_merge_20260629_2347'), + ] + + operations = [ + migrations.AlterField( + model_name='electricstorageinputs', + name='fixed_soc_series_fraction_tolerance', + field=models.FloatField(blank=True, help_text='Absolute tolerance on fixed_soc_series_fraction to avoid infeasible solutions when fixed_soc_series_fraction is provided.', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(1)]), + ), + ] From 41a68ab2cd3815fe9162f42dc8c0480e82c97035 Mon Sep 17 00:00:00 2001 From: adfarth Date: Thu, 9 Jul 2026 14:33:13 -0600 Subject: [PATCH 25/45] add inputs to models.py --- CHANGELOG.md | 5 ++++ ...torageinputs_dispatch_strategy_and_more.py | 24 +++++++++++++++++++ reoptjl/models.py | 18 +++++++++++++- reoptjl/test/posts/all_inputs_test.json | 1 + reoptjl/validators.py | 6 +++++ 5 files changed, 53 insertions(+), 1 deletion(-) create mode 100644 reoptjl/migrations/0120_electricstorageinputs_dispatch_strategy_and_more.py diff --git a/CHANGELOG.md b/CHANGELOG.md index a2e0d1944..8a9c2292c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,11 @@ Classify the change according to the following categories: ##### Removed ### Patches +## heuristic-dispatch-option +### Minor udpates +#### Added +- Add **ElectricStorage** inputs field **dispatch_options** with heuristic options + ## fixed-soc ### Minor udpates #### Added diff --git a/reoptjl/migrations/0120_electricstorageinputs_dispatch_strategy_and_more.py b/reoptjl/migrations/0120_electricstorageinputs_dispatch_strategy_and_more.py new file mode 100644 index 000000000..486adb62d --- /dev/null +++ b/reoptjl/migrations/0120_electricstorageinputs_dispatch_strategy_and_more.py @@ -0,0 +1,24 @@ +# Generated by Django 4.2.26 on 2026-07-09 20:32 + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('reoptjl', '0119_alter_electricstorageinputs_fixed_soc_series_fraction_tolerance'), + ] + + operations = [ + migrations.AddField( + model_name='electricstorageinputs', + name='dispatch_strategy', + field=models.TextField(blank=True, choices=[('optimized', 'Optimized'), ('peak_shaving_look_ahead', 'Peak Shaving Look Ahead'), ('peak_shaving_look_behind', 'Peak Shaving Look Behind'), ('self_consumption', 'Self Consumption'), ('backup', 'Backup'), ('custom_soc', 'Custom Soc'), ('daily_foresight_optimized', 'Daily Foresight Optimized')], default='optimized', help_text='Electric storage dispatch strategy can be one of: optimized, peak_shaving_look_ahead, peak_shaving_look_behind, self_consumption, backup, custom_soc, daily_foresight_optimized', null=True), + ), + migrations.AlterField( + model_name='electricstorageinputs', + name='soc_min_fraction', + field=models.FloatField(blank=True, help_text='Minimum allowable battery state of charge as fraction of energy capacity.', validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(1.0)]), + ), + ] diff --git a/reoptjl/models.py b/reoptjl/models.py index cee7bfaa1..3efe51998 100644 --- a/reoptjl/models.py +++ b/reoptjl/models.py @@ -3668,6 +3668,16 @@ class ElectricStorageInputs(BaseModel, models.Model): primary_key=True ) + ELECTRICSTORAGE_DISPATCH_STRATEGY = models.TextChoices('ELECTRICSTORAGE_DISPATCH_STRATEGY', ( + "optimized", + "peak_shaving_look_ahead", + "peak_shaving_look_behind", + "self_consumption", + "backup", + "custom_soc", + "daily_foresight_optimized" + )) + min_kw = models.FloatField( default=0, validators=[ @@ -3731,8 +3741,14 @@ class ElectricStorageInputs(BaseModel, models.Model): blank=True, help_text="Battery rectifier efficiency" ) + dispatch_strategy = models.TextField( + default=ELECTRICSTORAGE_DISPATCH_STRATEGY.optimized, + choices=ELECTRICSTORAGE_DISPATCH_STRATEGY.choices, + null=True, + blank=True, + help_text="Electric storage dispatch strategy can be one of: optimized, peak_shaving_look_ahead, peak_shaving_look_behind, self_consumption, backup, custom_soc, daily_foresight_optimized" + ) soc_min_fraction = models.FloatField( - default=0.2, validators=[ MinValueValidator(0), MaxValueValidator(1.0) diff --git a/reoptjl/test/posts/all_inputs_test.json b/reoptjl/test/posts/all_inputs_test.json index d53def1ad..3cc26956b 100644 --- a/reoptjl/test/posts/all_inputs_test.json +++ b/reoptjl/test/posts/all_inputs_test.json @@ -167,6 +167,7 @@ "internal_efficiency_fraction": 0.975, "inverter_efficiency_fraction": 0.96, "rectifier_efficiency_fraction": 0.96, + "dispatch_strategy": "optimized", "soc_min_fraction": 0.2, "soc_min_applies_during_outages": true, "soc_init_fraction": 0.5, diff --git a/reoptjl/validators.py b/reoptjl/validators.py index 99cdc456a..76dbd852a 100644 --- a/reoptjl/validators.py +++ b/reoptjl/validators.py @@ -363,6 +363,12 @@ def update_pv_defaults_offgrid(self, pvmodel): else: self.models["ElectricStorage"].soc_init_fraction = 1.0 + if self.models["ElectricStorage"].__getattribute__("soc_min_fraction") == None: + if self.models["ElectricStorage"].dispatch_strategy=="backup": + self.models["ElectricStorage"].soc_min_fraction = 0.8 + else: + self.models["ElectricStorage"].soc_min_fraction = 0.2 + if self.models["ElectricStorage"].__getattribute__("can_grid_charge") == None: if self.models["Settings"].off_grid_flag==False: self.models["ElectricStorage"].can_grid_charge = True From e2eb18a1d1f2276d561f1222f0b6caff869358e2 Mon Sep 17 00:00:00 2001 From: adfarth Date: Wed, 15 Jul 2026 13:47:51 -0600 Subject: [PATCH 26/45] add bess export io --- ...ts_can_export_beyond_nem_limit_and_more.py | 34 +++++++++++++++++++ reoptjl/models.py | 22 ++++++++++++ 2 files changed, 56 insertions(+) create mode 100644 reoptjl/migrations/0121_electricstorageinputs_can_export_beyond_nem_limit_and_more.py diff --git a/reoptjl/migrations/0121_electricstorageinputs_can_export_beyond_nem_limit_and_more.py b/reoptjl/migrations/0121_electricstorageinputs_can_export_beyond_nem_limit_and_more.py new file mode 100644 index 000000000..d1c39e257 --- /dev/null +++ b/reoptjl/migrations/0121_electricstorageinputs_can_export_beyond_nem_limit_and_more.py @@ -0,0 +1,34 @@ +# Generated by Django 4.2.26 on 2026-07-15 19:47 + +import django.contrib.postgres.fields +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('reoptjl', '0120_electricstorageinputs_dispatch_strategy_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='electricstorageinputs', + name='can_export_beyond_nem_limit', + field=models.BooleanField(blank=True, default=False, help_text='True/False for if technology can export energy beyond the annual site load (and be compensated for that energy at the export_rate_beyond_net_metering_limit).'), + ), + migrations.AddField( + model_name='electricstorageinputs', + name='can_net_meter', + field=models.BooleanField(blank=True, default=False, help_text='True/False for if technology has option to participate in net metering agreement with utility. Note that a technology can only participate in either net metering or wholesale rates (not both).'), + ), + migrations.AddField( + model_name='electricstorageinputs', + name='can_wholesale', + field=models.BooleanField(blank=True, default=False, help_text='True/False for if technology has option to export energy that is compensated at the wholesale_rate. Note that a technology can only participate in either net metering or wholesale rates (not both).'), + ), + migrations.AddField( + model_name='electricstorageoutputs', + name='storage_to_grid_series_kw', + field=django.contrib.postgres.fields.ArrayField(base_field=models.FloatField(blank=True, null=True), blank=True, default=list, size=None), + ), + ] diff --git a/reoptjl/models.py b/reoptjl/models.py index 3efe51998..b3d0e14f0 100644 --- a/reoptjl/models.py +++ b/reoptjl/models.py @@ -3793,6 +3793,24 @@ class ElectricStorageInputs(BaseModel, models.Model): blank=True, help_text="Flag to set whether the battery can be charged from the grid, or just onsite generation." ) + can_net_meter = models.BooleanField( + default=False, + blank=True, + help_text=("True/False for if technology has option to participate in net metering agreement with utility. " + "Note that a technology can only participate in either net metering or wholesale rates (not both).") + ) + can_wholesale = models.BooleanField( + default=False, + blank=True, + help_text=("True/False for if technology has option to export energy that is compensated at the wholesale_rate. " + "Note that a technology can only participate in either net metering or wholesale rates (not both).") + ) + can_export_beyond_nem_limit = models.BooleanField( + default=False, + blank=True, + help_text=("True/False for if technology can export energy beyond the annual site load (and be compensated for " + "that energy at the export_rate_beyond_net_metering_limit).") + ) installed_cost_per_kw = models.FloatField( default=968.0, validators=[ @@ -3978,6 +3996,10 @@ class ElectricStorageOutputs(BaseModel, models.Model): models.FloatField(null=True, blank=True), blank=True, default=list ) + storage_to_grid_series_kw = ArrayField( + models.FloatField(null=True, blank=True), + blank=True, default=list + ) initial_capital_cost = models.FloatField(null=True, blank=True) maintenance_cost = models.FloatField(null=True, blank=True) state_of_health_series_fraction = ArrayField( From 3f6d57ea255c656fa8d5260fe3d1e4b9d287e230 Mon Sep 17 00:00:00 2001 From: adfarth Date: Wed, 15 Jul 2026 13:59:12 -0600 Subject: [PATCH 27/45] Update custom_table_config.py --- reoptjl/custom_table_config.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/reoptjl/custom_table_config.py b/reoptjl/custom_table_config.py index 4cd3ec479..4d52f13cc 100644 --- a/reoptjl/custom_table_config.py +++ b/reoptjl/custom_table_config.py @@ -804,6 +804,12 @@ "bau_value" : lambda df: safe_get(df, "outputs.ElectricStorage.storage_to_load_series_kw_bau"), "scenario_value": lambda df: safe_get(df, "outputs.ElectricStorage.storage_to_load_series_kw") }, + { + "label" : "Battery Exported to Grid (kWh/yr)", + "key" : "battery_exported_to_grid", + "bau_value" : lambda df: safe_get(df, "outputs.ElectricStorage.storage_to_grid_series_kw_bau"), + "scenario_value": lambda df: safe_get(df, "outputs.ElectricStorage.storage_to_grid_series_kw") + }, { "label" : "Generator Serving Load (kWh/yr)", "key" : "generator_serving_load", From 756df197e2df32191987deb0be36995c58347cc0 Mon Sep 17 00:00:00 2001 From: adfarth Date: Thu, 16 Jul 2026 13:19:34 -0600 Subject: [PATCH 28/45] fix settings and pv processing --- julia_src/http.jl | 5 ++--- julia_src/mpc.jl | 9 +++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/julia_src/http.jl b/julia_src/http.jl index e857ba30f..3326447df 100644 --- a/julia_src/http.jl +++ b/julia_src/http.jl @@ -875,9 +875,8 @@ function mpc(req::HTTP.Request) ENV["NREL_DEVELOPER_API_KEY"] = test_nrel_developer_api_key delete!(d, "api_key") end - - settings = d["Settings"] - solver_name = get(settings, "solver_name", "HiGHS") + + solver_name = get(get(d, "Settings", Dict()), "solver_name", "HiGHS") if solver_name == "Xpress" && !(xpress_installed=="True") solver_name = "HiGHS" @warn "Changing solver_name from Xpress to $solver_name because Xpress is not installed. Next time diff --git a/julia_src/mpc.jl b/julia_src/mpc.jl index 3e7845717..cb8d4204f 100644 --- a/julia_src/mpc.jl +++ b/julia_src/mpc.jl @@ -118,7 +118,7 @@ function get_technology_sizes!(d::Dict, model_inputs::reoptjl.REoptInputs, solve pv_kw = Float64(pv["min_kw"]) batt_kw = Float64(batt["min_kw"]) batt_kwh = Float64(batt["min_kwh"]) - return (pv_kw = pv_kw, batt_kw = batt_kw, batt_kwh = batt_kwh, skip_mpc = false) + return (pv_kw = pv_kw, batt_kw = batt_kw, batt_kwh = batt_kwh, skip_mpc = false, pv_production_factor_series = nothing) end @info "MPC: PV and/or ElectricStorage sizes are not specified — running REopt sizing first." @@ -250,7 +250,7 @@ function get_mpc_results!(d::Dict; solver_name::String="HiGHS")::Dict model_inputs = nothing try model_inputs = reoptjl.REoptInputs(sizing_post) - @info "Successfully processed REopt inputs." + @info "Successfully processed REopt inputs." catch e @error "Something went wrong during REopt inputs processing!" exception=(e, catch_backtrace()) return build_mpc_response( @@ -279,7 +279,7 @@ function get_mpc_results!(d::Dict; solver_name::String="HiGHS")::Dict # Note: REoptInputs does not provide PV production factors if user doesn't specify custom values if !isempty(s.pvs) # Get prod factors if PV considered. - if !isempty(s.pvs[1].production_factor_series) + if !isnothing(s.pvs[1].production_factor_series) pv_prod_factor = Float64.(s.pvs[1].production_factor_series) elseif technology_sizes.pv_production_factor_series !== nothing pv_prod_factor = Float64.(technology_sizes.pv_production_factor_series) @@ -432,7 +432,8 @@ function get_mpc_results!(d::Dict; solver_name::String="HiGHS")::Dict post = build_mpc_post(current_horizon_pv, current_horizon_load, current_horizon_energy_rates, current_horizon_emissions, current_horizon_tou_ts, current_horizon_monthly_ts, - tou_previous_peak_demands, monthly_previous_peak_demands, soc_init_frac) + tou_previous_peak_demands, monthly_previous_peak_demands, soc_init_frac + ) model = get_solver_model(get_solver_model_type(solver_name), SolverAttributes(per_iter_timeout_s, solver_settings["optimality_tolerance"])) From e470e9ce00c4efb12d624d45ad986081920e8987 Mon Sep 17 00:00:00 2001 From: adfarth Date: Thu, 16 Jul 2026 14:41:58 -0600 Subject: [PATCH 29/45] update todos --- julia_src/http.jl | 1 - julia_src/mpc.jl | 23 ++++++++++++----------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/julia_src/http.jl b/julia_src/http.jl index 3326447df..c89172a49 100644 --- a/julia_src/http.jl +++ b/julia_src/http.jl @@ -101,7 +101,6 @@ function reopt(req::HTTP.Request) end end - #TODO: What timeout and optimality tolerance should MPC use? timeout_seconds = pop!(settings, "timeout_seconds") optimality_tolerance = pop!(settings, "optimality_tolerance") solver_attributes = SolverAttributes(timeout_seconds, optimality_tolerance) diff --git a/julia_src/mpc.jl b/julia_src/mpc.jl index cb8d4204f..675e42fdb 100644 --- a/julia_src/mpc.jl +++ b/julia_src/mpc.jl @@ -192,6 +192,10 @@ function get_mpc_results!(d::Dict; solver_name::String="HiGHS")::Dict error("When using MPC (daily_foresight_optimized dispatch), only PV and ElectricStorage are supported technologies. " * "Unsupported inputs found: $(join(unsupported_keys, ", ")).") end + # Error if multiple PVs + if haskey(d, "PV") && length(d["PV"]) > 1 + error("MPC: Multiple PV systems are not supported.") + end # Error if unsupported CO2/renewable-fraction constraints are set _site_input = get(d, "Site", Dict()) @@ -211,6 +215,8 @@ function get_mpc_results!(d::Dict; solver_name::String="HiGHS")::Dict # TODO: Add warnings for REopt inputs and scenarios that are not modeled in MPC (e.g., coincident peak charges, demand lookback, etc.) @warn "Using MPC to determine dispatch. MPC does not model: tiered electricity rates; rates will be flattened to the first tier." + # TODO: Error if rate tariff contains lookbacks. + # TODO: Test with outage inputs before enabling this warning. # # Warning for outage inputs (MPC does not model outages) # _utility_input = get(d, "ElectricUtility", Dict()) @@ -220,24 +226,22 @@ function get_mpc_results!(d::Dict; solver_name::String="HiGHS")::Dict # end ## Set up MPC inputs ## - settings = get!(d, "Settings", Dict()) - settings["solver_name"] = solver_name # TODO: remove? # TODO: MPC horizons and timeout are currently hard coded + settings = get!(d, "Settings", Dict()) time_steps_per_hour = Int(get(settings, "time_steps_per_hour", 1)) length_of_data = 8760 * time_steps_per_hour horizon = 24 * time_steps_per_hour per_iter_timeout_s = 30.0 - # TODO: Should MPC handle multiple PVs? - # TODO: MPC timeout and optimality tolerance + # TODO: Handle multiple PVs # Update sizing_post to remove solver settings and dispatch inputs, to be able to validate inputs using REoptInputs sizing_post = deepcopy(d) settings = get(sizing_post, "Settings", Dict()) solver_settings=Dict() delete!(settings, "run_bau") # Remove run_bau from sizing run - solver_settings["timeout_seconds"] = pop!(settings, "timeout_seconds", 420) - solver_settings["optimality_tolerance"] = pop!(settings, "optimality_tolerance", 0.001) + solver_settings["timeout_seconds"] = pop!(settings, "timeout_seconds", 600) # only gets used in sizing run. + solver_settings["optimality_tolerance"] = pop!(settings, "optimality_tolerance", 0.001) # Update to a higher value if solve time becomes an issue solver_settings["solver_attributes"] = SolverAttributes(solver_settings["timeout_seconds"], solver_settings["optimality_tolerance"]) solver_settings["solver_name"] = solver_name # Delete inputs specific to the heuristic battery dispatch run @@ -278,15 +282,13 @@ function get_mpc_results!(d::Dict; solver_name::String="HiGHS")::Dict batt_kwh = technology_sizes.batt_kwh # Note: REoptInputs does not provide PV production factors if user doesn't specify custom values + # PV production is NOT levelized in MPC but IS levelized in REopt. This will result in a slight mis-match. if !isempty(s.pvs) # Get prod factors if PV considered. if !isnothing(s.pvs[1].production_factor_series) pv_prod_factor = Float64.(s.pvs[1].production_factor_series) elseif technology_sizes.pv_production_factor_series !== nothing pv_prod_factor = Float64.(technology_sizes.pv_production_factor_series) else - # TODO: These production factors don't consider degradation, problem? - # Does MPCPV need a degradation input to calculate the levelization factor used in the optimization? - # AF: none of these production factors consider degradation and I don't think it's a problem? pv_prod_factor = generate_pv_production_factors(d, time_steps_per_hour) end # Avoid another PVWatts call in the final REopt run. @@ -299,9 +301,8 @@ function get_mpc_results!(d::Dict; solver_name::String="HiGHS")::Dict # Extract tariff inputs relevant to MPC (use first tier only if tiered rates) # TODO: Are all of these relevant? Any missing inputs? - # TODO: Implement lookback? (demand_lookback_months, demand_lookback_percent, demand_lookback_range) Ignoring coincident peak charges for now + # TODO: Implement lookback (demand_lookback_months, demand_lookback_percent, demand_lookback_range) Ignoring coincident peak charges for now # TODO: Need to think through NEM or passing back export values (wholesale_rate, export_rate_beyond_net_metering_limit) - # TODO: Track lookback variables - demand_lookback_months, demand_lookback_percent, demand_lookback_range? energy_rates = Float64.(s.electric_tariff.energy_rates[:, 1]) monthly_demand_rates = isempty(s.electric_tariff.monthly_demand_rates) ? zeros(Float64, 12) : Float64.(s.electric_tariff.monthly_demand_rates[:, 1]) From 41514227f47250f02ed0cf3cfe6bedb2bfa900d4 Mon Sep 17 00:00:00 2001 From: adfarth Date: Fri, 17 Jul 2026 13:27:52 -0700 Subject: [PATCH 30/45] account for nem and whl --- julia_src/mpc.jl | 110 ++++++++++++++++++++++++++++++++++++----------- 1 file changed, 86 insertions(+), 24 deletions(-) diff --git a/julia_src/mpc.jl b/julia_src/mpc.jl index 675e42fdb..a85616e04 100644 --- a/julia_src/mpc.jl +++ b/julia_src/mpc.jl @@ -180,7 +180,9 @@ function get_mpc_results!(d::Dict; solver_name::String="HiGHS")::Dict - ElectricStorage: Sizes and state-of-charge series - ElectricUtility: Grid dispatch series and emissions - ElectricLoad: Load profile used - - ElectricTariff: Energy and demand costs, peak demands by month/ratchet + - ElectricTariff: Separate cost components (total_energy_cost, total_export_benefit, + total_tou_demand_cost, total_monthly_demand_cost), a combined total_electricity_bill, + per-timestep energy/export series, and peak demands by month/ratchet """ @@ -193,6 +195,7 @@ function get_mpc_results!(d::Dict; solver_name::String="HiGHS")::Dict "Unsupported inputs found: $(join(unsupported_keys, ", ")).") end # Error if multiple PVs + # TODO: Handle multiple PVs if haskey(d, "PV") && length(d["PV"]) > 1 error("MPC: Multiple PV systems are not supported.") end @@ -234,7 +237,6 @@ function get_mpc_results!(d::Dict; solver_name::String="HiGHS")::Dict horizon = 24 * time_steps_per_hour per_iter_timeout_s = 30.0 - # TODO: Handle multiple PVs # Update sizing_post to remove solver settings and dispatch inputs, to be able to validate inputs using REoptInputs sizing_post = deepcopy(d) settings = get(sizing_post, "Settings", Dict()) @@ -325,6 +327,27 @@ function get_mpc_results!(d::Dict; solver_name::String="HiGHS")::Dict # Extract emissions defaults (or use user input if provided) co2_grid_emissions_series = Float64.(s.electric_utility.emissions_factor_series_lb_CO2_per_kwh) + # --- Export / net metering setup (mirror the sizing-run scenario) --- + # NEM is enabled in MPC when ElectricUtility.net_metering_limit_kw > 0. + nm_limit_kw = Float64(s.electric_utility.net_metering_limit_kw) + + # WHL (net billing) rate: export_rates[:WHL] in the processed scenario is a negative "cost"; + # MPCElectricTariff expects a positive wholesale_rate (it negates internally). + whl_rate_full = (:WHL in s.electric_tariff.export_bins) ? + -1.0 .* Float64.(s.electric_tariff.export_rates[:WHL]) : nothing + + # PV export capability comes from the processed PV struct. + pv_can_net_meter = !isempty(s.pvs) ? Bool(s.pvs[1].can_net_meter) : false + pv_can_wholesale = !isempty(s.pvs) ? Bool(s.pvs[1].can_wholesale) : false + + # Battery export capability comes from the processed ElectricStorage struct + _batt_attr = s.storage.attr["ElectricStorage"] + batt_can_net_meter = Bool(_batt_attr.can_net_meter) + batt_can_wholesale = Bool(_batt_attr.can_wholesale) + + # NEM export is credited (approximately) at the retail energy rate; used for cost reporting below. + nem_active = nm_limit_kw > 0 && (pv_can_net_meter || batt_can_net_meter) + month_starts = get_month_transition_timesteps(time_steps_per_hour) # ts_to_month = 8760 array specifying which month each timestep falls in (1-12) @@ -353,6 +376,7 @@ function get_mpc_results!(d::Dict; solver_name::String="HiGHS")::Dict ), "ElectricStorage" => Dict( "storage_to_load_series_kw" => Float64[], + "storage_to_grid_series_kw" => Float64[], "soc_series_fraction" => Float64[], ), "ElectricUtility" => Dict( @@ -364,18 +388,37 @@ function get_mpc_results!(d::Dict; solver_name::String="HiGHS")::Dict "load_series_kw" => Float64[], ), ) - energy_cost_series = Float64[] + energy_charge_series = Float64[] # grid purchase (energy) charges per timestep + export_benefit_series = Float64[] # NEM/WHL export credits per timestep (positive = revenue) total_energy_cost = 0.0 + total_export_benefit = 0.0 soc_init_frac = soc_0 # Build MPC post function build_mpc_post(current_horizon_pv, current_horizon_load, current_horizon_energy_rates, current_horizon_emissions, current_horizon_tou_ts, current_horizon_monthly_ts, - tou_previous_peak_demands, monthly_previous_peak_demands, soc_init_frac) + tou_previous_peak_demands, monthly_previous_peak_demands, soc_init_frac, + current_horizon_whl_rate) + tariff = Dict( + "energy_rates" => current_horizon_energy_rates, + "tou_demand_rates" => tou_demand_rates, + "tou_demand_ratchet_time_steps" => current_horizon_tou_ts, + "tou_previous_peak_demands" => tou_previous_peak_demands, + "monthly_demand_rates" => monthly_demand_rates, + "time_steps_monthly" => current_horizon_monthly_ts, + "monthly_previous_peak_demands" => monthly_previous_peak_demands, + ) + # WHL (net billing) export: MPCElectricTariff reads a positive `wholesale_rate` and + # builds the :WHL export bin when it is provided. + if current_horizon_whl_rate !== nothing + tariff["wholesale_rate"] = current_horizon_whl_rate + end return Dict( "PV" => Dict( "size_kw" => pv_kw, "production_factor_series" => current_horizon_pv, + "can_net_meter" => pv_can_net_meter, + "can_wholesale" => pv_can_wholesale, ), "ElectricStorage" => Dict( "size_kw" => batt_kw, @@ -384,20 +427,16 @@ function get_mpc_results!(d::Dict; solver_name::String="HiGHS")::Dict "discharge_efficiency" => discharge_eff, "soc_init_fraction" => soc_init_frac, "soc_min_fraction" => soc_min, + "can_net_meter" => batt_can_net_meter, + "can_wholesale" => batt_can_wholesale, ), "ElectricLoad" => Dict( "loads_kw" => current_horizon_load, ), - "ElectricTariff" => Dict( - "energy_rates" => current_horizon_energy_rates, - "tou_demand_rates" => tou_demand_rates, - "tou_demand_ratchet_time_steps" => current_horizon_tou_ts, - "tou_previous_peak_demands" => tou_previous_peak_demands, - "monthly_demand_rates" => monthly_demand_rates, - "time_steps_monthly" => current_horizon_monthly_ts, - "monthly_previous_peak_demands" => monthly_previous_peak_demands, - ), + "ElectricTariff" => tariff, + # net_metering_limit_kw drives the NEM export bin in MPCElectricTariff (NEM on if > 0) "ElectricUtility" => Dict( + "net_metering_limit_kw" => nm_limit_kw, "emissions_factor_series_lb_CO2_per_kwh" => current_horizon_emissions, ), ) @@ -411,6 +450,7 @@ function get_mpc_results!(d::Dict; solver_name::String="HiGHS")::Dict current_horizon_load = slice_data(loads_kw, idx, end_ts) current_horizon_energy_rates = slice_data(energy_rates, idx, end_ts) current_horizon_emissions = slice_data(co2_grid_emissions_series, idx, end_ts) + current_horizon_whl_rate = whl_rate_full === nothing ? nothing : slice_data(whl_rate_full, idx, end_ts) # List of length n_tou_ratchets, specifies which ts of the current horizon are in each TOU ratchet # by placing values 1 to horizon into the corresponding element of the array based on ratchet number @@ -433,7 +473,8 @@ function get_mpc_results!(d::Dict; solver_name::String="HiGHS")::Dict post = build_mpc_post(current_horizon_pv, current_horizon_load, current_horizon_energy_rates, current_horizon_emissions, current_horizon_tou_ts, current_horizon_monthly_ts, - tou_previous_peak_demands, monthly_previous_peak_demands, soc_init_frac + tou_previous_peak_demands, monthly_previous_peak_demands, soc_init_frac, + current_horizon_whl_rate ) model = get_solver_model(get_solver_model_type(solver_name), @@ -450,6 +491,7 @@ function get_mpc_results!(d::Dict; solver_name::String="HiGHS")::Dict pv_to_grid = haskey(pv_res, "electric_to_grid_series_kw") ? pv_res["electric_to_grid_series_kw"][1] : 0.0 pv_curtailed = haskey(pv_res, "electric_curtailed_series_kw") ? pv_res["electric_curtailed_series_kw"][1] : 0.0 batt_to_load = batt_res["storage_to_load_series_kw"][1] + batt_to_grid = haskey(batt_res, "storage_to_grid_series_kw") ? batt_res["storage_to_grid_series_kw"][1] : 0.0 batt_soc = batt_res["soc_series_fraction"][1] util_to_load = util_res["electric_to_load_series_kw"][1] util_to_batt = util_res["electric_to_storage_series_kw"][1] @@ -460,6 +502,7 @@ function get_mpc_results!(d::Dict; solver_name::String="HiGHS")::Dict push!(dispatch_series["PV"]["electric_to_grid_series_kw"], pv_to_grid) push!(dispatch_series["PV"]["electric_curtailed_series_kw"], pv_curtailed) push!(dispatch_series["ElectricStorage"]["storage_to_load_series_kw"], batt_to_load) + push!(dispatch_series["ElectricStorage"]["storage_to_grid_series_kw"], batt_to_grid) push!(dispatch_series["ElectricStorage"]["soc_series_fraction"], batt_soc) push!(dispatch_series["ElectricUtility"]["electric_to_load_series_kw"], util_to_load) push!(dispatch_series["ElectricUtility"]["electric_to_storage_series_kw"], util_to_batt) @@ -467,10 +510,22 @@ function get_mpc_results!(d::Dict; solver_name::String="HiGHS")::Dict co2_grid_emissions_series[idx] * grid_power / time_steps_per_hour) push!(dispatch_series["ElectricLoad"]["load_series_kw"], loads_kw[idx]) - # Running energy costs - step_energy_cost = grid_power * energy_rates[idx] / time_steps_per_hour - push!(energy_cost_series, step_energy_cost) - total_energy_cost += step_energy_cost + # Running electricity costs (reported as separate components; see results below) + # Energy (grid purchase) charge for this timestep + step_energy_charge = grid_power * energy_rates[idx] / time_steps_per_hour + + # Export credit for this timestep. NEM credits at the retail energy rate; otherwise WHL credits + # at the wholesale rate. NOTE: when both NEM and WHL are available the model chooses per horizon + # and the executed-timestep bin split is not returned, so this can misprice such timesteps. + step_export_kw = pv_to_grid + batt_to_grid + export_rate_idx = nem_active ? energy_rates[idx] : + (whl_rate_full !== nothing ? whl_rate_full[idx] : 0.0) + step_export_benefit = step_export_kw * export_rate_idx / time_steps_per_hour + + push!(energy_charge_series, step_energy_charge) + push!(export_benefit_series, step_export_benefit) + total_energy_cost += step_energy_charge + total_export_benefit += step_export_benefit soc_init_frac = batt_soc @@ -506,12 +561,19 @@ function get_mpc_results!(d::Dict; solver_name::String="HiGHS")::Dict "ElectricUtility" => dispatch_series["ElectricUtility"], "ElectricLoad" => dispatch_series["ElectricLoad"], "ElectricTariff" => Dict( - "total_energy_cost" => total_energy_cost, - "energy_cost_series_per_timestep" => energy_cost_series, - "total_tou_demand_cost" => tou_demand_cost_total, - "total_monthly_demand_cost" => monthly_demand_cost_total, - "tou_peaks_by_ratchet_kw" => tou_previous_peak_demands, - "monthly_peaks_kw" => monthly_previous_peak_demands, + # --- Cost components (before tax); combine for the total bill below --- + "total_energy_cost" => total_energy_cost, # grid purchases (energy) only + "total_export_benefit" => total_export_benefit, # NEM/WHL credits (positive = revenue) + "total_tou_demand_cost" => tou_demand_cost_total, + "total_monthly_demand_cost" => monthly_demand_cost_total, + # --- Total electricity bill = energy charge - export benefit + demand charges --- + "total_electricity_bill" => total_energy_cost - total_export_benefit + + tou_demand_cost_total + monthly_demand_cost_total, + # --- Per-timestep series (energy/exports only; demand is a peak-based charge) --- + "energy_charge_series_per_timestep" => energy_charge_series, + "export_benefit_series_per_timestep" => export_benefit_series, + "tou_peaks_by_ratchet_kw" => tou_previous_peak_demands, + "monthly_peaks_kw" => monthly_previous_peak_demands, ), ) ) From 181a316ddeced82db04204c59f605c6177423bdd Mon Sep 17 00:00:00 2001 From: adfarth Date: Fri, 17 Jul 2026 14:42:38 -0700 Subject: [PATCH 31/45] updt branch --- julia_src/Manifest.toml | 4 ++-- julia_src/mpc.jl | 7 +++---- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/julia_src/Manifest.toml b/julia_src/Manifest.toml index 47ac83bda..1cb08ee62 100644 --- a/julia_src/Manifest.toml +++ b/julia_src/Manifest.toml @@ -948,11 +948,11 @@ version = "1.11.0" [[deps.REopt]] deps = ["ArchGDAL", "CSV", "CoolProp", "DataFrames", "Dates", "DelimitedFiles", "HTTP", "JLD", "JSON", "JuMP", "LinDistFlow", "LinearAlgebra", "Logging", "MathOptInterface", "Requires", "Roots", "Statistics", "TestEnv"] -git-tree-sha1 = "1e54ef8f58ddcf9d66f392cb76fdd78123d7d2ad" +git-tree-sha1 = "539750260abec1f7b965670e498ee2618548e148" repo-rev = "dispatch-options" repo-url = "https://github.com/NatLabRockies/REopt.jl.git" uuid = "d36ad4e8-d74a-4f7a-ace1-eaea049febf6" -version = "0.59.2" +version = "0.59.3" [[deps.Random]] deps = ["SHA"] diff --git a/julia_src/mpc.jl b/julia_src/mpc.jl index a85616e04..dcbea8fa8 100644 --- a/julia_src/mpc.jl +++ b/julia_src/mpc.jl @@ -304,7 +304,6 @@ function get_mpc_results!(d::Dict; solver_name::String="HiGHS")::Dict # Extract tariff inputs relevant to MPC (use first tier only if tiered rates) # TODO: Are all of these relevant? Any missing inputs? # TODO: Implement lookback (demand_lookback_months, demand_lookback_percent, demand_lookback_range) Ignoring coincident peak charges for now - # TODO: Need to think through NEM or passing back export values (wholesale_rate, export_rate_beyond_net_metering_limit) energy_rates = Float64.(s.electric_tariff.energy_rates[:, 1]) monthly_demand_rates = isempty(s.electric_tariff.monthly_demand_rates) ? zeros(Float64, 12) : Float64.(s.electric_tariff.monthly_demand_rates[:, 1]) @@ -388,7 +387,7 @@ function get_mpc_results!(d::Dict; solver_name::String="HiGHS")::Dict "load_series_kw" => Float64[], ), ) - energy_charge_series = Float64[] # grid purchase (energy) charges per timestep + energy_cost_series = Float64[] # grid purchase (energy) charges per timestep export_benefit_series = Float64[] # NEM/WHL export credits per timestep (positive = revenue) total_energy_cost = 0.0 total_export_benefit = 0.0 @@ -522,7 +521,7 @@ function get_mpc_results!(d::Dict; solver_name::String="HiGHS")::Dict (whl_rate_full !== nothing ? whl_rate_full[idx] : 0.0) step_export_benefit = step_export_kw * export_rate_idx / time_steps_per_hour - push!(energy_charge_series, step_energy_charge) + push!(energy_cost_series, step_energy_charge) push!(export_benefit_series, step_export_benefit) total_energy_cost += step_energy_charge total_export_benefit += step_export_benefit @@ -570,7 +569,7 @@ function get_mpc_results!(d::Dict; solver_name::String="HiGHS")::Dict "total_electricity_bill" => total_energy_cost - total_export_benefit + tou_demand_cost_total + monthly_demand_cost_total, # --- Per-timestep series (energy/exports only; demand is a peak-based charge) --- - "energy_charge_series_per_timestep" => energy_charge_series, + "energy_cost_series_per_timestep" => energy_cost_series, "export_benefit_series_per_timestep" => export_benefit_series, "tou_peaks_by_ratchet_kw" => tou_previous_peak_demands, "monthly_peaks_kw" => monthly_previous_peak_demands, From ac1b14954b01e2409a3708c763c4669972497c53 Mon Sep 17 00:00:00 2001 From: adfarth Date: Fri, 17 Jul 2026 15:47:58 -0700 Subject: [PATCH 32/45] update REopt --- julia_src/Manifest.toml | 2 +- julia_src/mpc.jl | 19 +++++++++++++++---- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/julia_src/Manifest.toml b/julia_src/Manifest.toml index 1cb08ee62..ceb0887ae 100644 --- a/julia_src/Manifest.toml +++ b/julia_src/Manifest.toml @@ -948,7 +948,7 @@ version = "1.11.0" [[deps.REopt]] deps = ["ArchGDAL", "CSV", "CoolProp", "DataFrames", "Dates", "DelimitedFiles", "HTTP", "JLD", "JSON", "JuMP", "LinDistFlow", "LinearAlgebra", "Logging", "MathOptInterface", "Requires", "Roots", "Statistics", "TestEnv"] -git-tree-sha1 = "539750260abec1f7b965670e498ee2618548e148" +git-tree-sha1 = "ec4dad03ea876407e69a384cecc62c29be858405" repo-rev = "dispatch-options" repo-url = "https://github.com/NatLabRockies/REopt.jl.git" uuid = "d36ad4e8-d74a-4f7a-ace1-eaea049febf6" diff --git a/julia_src/mpc.jl b/julia_src/mpc.jl index dcbea8fa8..22f524da5 100644 --- a/julia_src/mpc.jl +++ b/julia_src/mpc.jl @@ -194,10 +194,18 @@ function get_mpc_results!(d::Dict; solver_name::String="HiGHS")::Dict error("When using MPC (daily_foresight_optimized dispatch), only PV and ElectricStorage are supported technologies. " * "Unsupported inputs found: $(join(unsupported_keys, ", ")).") end - # Error if multiple PVs + # PV can be provided as a Dict (single system) or an array of Dicts. + # MPC only supports a single PV, so error on multiple PVs and normalize a + # one-element array down to a Dict so downstream code can treat d["PV"] as a Dict. # TODO: Handle multiple PVs - if haskey(d, "PV") && length(d["PV"]) > 1 - error("MPC: Multiple PV systems are not supported.") + if haskey(d, "PV") && isa(d["PV"], AbstractArray) + if length(d["PV"]) > 1 + error("MPC: Multiple PV systems are not supported.") + elseif length(d["PV"]) == 1 + d["PV"] = d["PV"][1] + else + delete!(d, "PV") # empty PV array -> treat as no PV + end end # Error if unsupported CO2/renewable-fraction constraints are set @@ -218,7 +226,7 @@ function get_mpc_results!(d::Dict; solver_name::String="HiGHS")::Dict # TODO: Add warnings for REopt inputs and scenarios that are not modeled in MPC (e.g., coincident peak charges, demand lookback, etc.) @warn "Using MPC to determine dispatch. MPC does not model: tiered electricity rates; rates will be flattened to the first tier." - # TODO: Error if rate tariff contains lookbacks. + # TODO: Error if rate tariff contains lookbacks or coincident peak charges. # TODO: Test with outage inputs before enabling this warning. # # Warning for outage inputs (MPC does not model outages) @@ -314,6 +322,9 @@ function get_mpc_results!(d::Dict; solver_name::String="HiGHS")::Dict tou_previous_peak_demands = zeros(Float64, n_tou_ratchets) # Tracks past TOU peak demand per ratchet monthly_previous_peak_demands = zeros(Float64, 12) # Tracks past monthly peak demand + println("monthly_demand_rates: ", monthly_demand_rates) + + # Extract storage efficiency and SOC defaults from processed inputs rect_eff = Float64(s.storage.attr["ElectricStorage"].rectifier_efficiency_fraction) inv_eff = Float64(s.storage.attr["ElectricStorage"].inverter_efficiency_fraction) From 41468b13ecad12ae46a8bc68faadd44539c13090 Mon Sep 17 00:00:00 2001 From: adfarth Date: Fri, 17 Jul 2026 16:56:30 -0700 Subject: [PATCH 33/45] update for demand rates --- julia_src/mpc.jl | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/julia_src/mpc.jl b/julia_src/mpc.jl index 22f524da5..e9f8b5c1d 100644 --- a/julia_src/mpc.jl +++ b/julia_src/mpc.jl @@ -181,7 +181,7 @@ function get_mpc_results!(d::Dict; solver_name::String="HiGHS")::Dict - ElectricUtility: Grid dispatch series and emissions - ElectricLoad: Load profile used - ElectricTariff: Separate cost components (total_energy_cost, total_export_benefit, - total_tou_demand_cost, total_monthly_demand_cost), a combined total_electricity_bill, + total_tou_demand_cost, total_non_tou_monthly_demand_cost), a combined total_electricity_bill, per-timestep energy/export series, and peak demands by month/ratchet """ @@ -264,7 +264,19 @@ function get_mpc_results!(d::Dict; solver_name::String="HiGHS")::Dict model_inputs = nothing try model_inputs = reoptjl.REoptInputs(sizing_post) - @info "Successfully processed REopt inputs." + + # REoptInputs returns an error Dict (rather than throwing) when input validation fails. + # Surface those messages instead of falling through to get_technology_sizes!, which expects + # a REoptInputs and would otherwise raise a confusing MethodError. + if isa(model_inputs, Dict) + @error "REopt input validation failed during MPC pre-solve." messages=get(model_inputs, "Messages", Dict()) + return build_mpc_response( + "error", + messages = get(model_inputs, "Messages", Dict("errors" => ["REopt input validation failed."])) + ) + else + @info "Successfully processed REopt inputs." + end catch e @error "Something went wrong during REopt inputs processing!" exception=(e, catch_backtrace()) return build_mpc_response( @@ -272,7 +284,9 @@ function get_mpc_results!(d::Dict; solver_name::String="HiGHS")::Dict messages = Dict("errors" => [sprint(showerror, e)]) ) end - + + + # MPC requires fixed PV and battery sizes. If not provided, call REopt first in a sizing run. technology_sizes = get_technology_sizes!(d, model_inputs, solver_settings) s = model_inputs.s # Access the processed Scenario struct @@ -412,7 +426,7 @@ function get_mpc_results!(d::Dict; solver_name::String="HiGHS")::Dict tariff = Dict( "energy_rates" => current_horizon_energy_rates, "tou_demand_rates" => tou_demand_rates, - "tou_demand_ratchet_time_steps" => current_horizon_tou_ts, + "tou_demand_time_steps" => current_horizon_tou_ts, "tou_previous_peak_demands" => tou_previous_peak_demands, "monthly_demand_rates" => monthly_demand_rates, "time_steps_monthly" => current_horizon_monthly_ts, @@ -575,7 +589,7 @@ function get_mpc_results!(d::Dict; solver_name::String="HiGHS")::Dict "total_energy_cost" => total_energy_cost, # grid purchases (energy) only "total_export_benefit" => total_export_benefit, # NEM/WHL credits (positive = revenue) "total_tou_demand_cost" => tou_demand_cost_total, - "total_monthly_demand_cost" => monthly_demand_cost_total, + "total_non_tou_monthly_demand_cost" => monthly_demand_cost_total, # --- Total electricity bill = energy charge - export benefit + demand charges --- "total_electricity_bill" => total_energy_cost - total_export_benefit + tou_demand_cost_total + monthly_demand_cost_total, From cf0ec36f7e7255e5ff2a6c35b35a395a8061fe9c Mon Sep 17 00:00:00 2001 From: lixiangk1 <72464565+lixiangk1@users.noreply.github.com> Date: Tue, 28 Jul 2026 12:28:10 -0600 Subject: [PATCH 34/45] Propagate REopt.jl changes to internal_efficiency_fraction in ElectricStorage back to the API (relevant for SAM dispatch strategies) --- julia_src/http.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/julia_src/http.jl b/julia_src/http.jl index c89172a49..16a2c5644 100644 --- a/julia_src/http.jl +++ b/julia_src/http.jl @@ -1,4 +1,4 @@ -using HTTP, JSON, JuMP +using HTTP, JSON, JuMP using HiGHS, Cbc, SCIP using GhpGhx import REopt as reoptjl # For REopt.jl, needed because we still have local REopt.jl module for V1/V2 @@ -238,7 +238,7 @@ function reopt(req::HTTP.Request) end if haskey(d, "ElectricStorage") inputs_with_defaults_from_julia_electric_storage = [ - :macrs_option_years, :macrs_bonus_fraction, :total_itc_fraction + :macrs_option_years, :macrs_bonus_fraction, :total_itc_fraction, :internal_efficiency_fraction ] electric_storage_dict = Dict(key=>getfield(model_inputs.s.storage.attr["ElectricStorage"], key) for key in inputs_with_defaults_from_julia_electric_storage) else From 11ce69cdc60fa97253915680ce5d722755ea6620 Mon Sep 17 00:00:00 2001 From: adfarth Date: Tue, 28 Jul 2026 14:34:24 -0600 Subject: [PATCH 35/45] point to latest branch --- julia_src/Manifest.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/julia_src/Manifest.toml b/julia_src/Manifest.toml index ceb0887ae..1f618bbcd 100644 --- a/julia_src/Manifest.toml +++ b/julia_src/Manifest.toml @@ -948,7 +948,7 @@ version = "1.11.0" [[deps.REopt]] deps = ["ArchGDAL", "CSV", "CoolProp", "DataFrames", "Dates", "DelimitedFiles", "HTTP", "JLD", "JSON", "JuMP", "LinDistFlow", "LinearAlgebra", "Logging", "MathOptInterface", "Requires", "Roots", "Statistics", "TestEnv"] -git-tree-sha1 = "ec4dad03ea876407e69a384cecc62c29be858405" +git-tree-sha1 = "d7d3e02b4e692858787aba765e67001bd080521b" repo-rev = "dispatch-options" repo-url = "https://github.com/NatLabRockies/REopt.jl.git" uuid = "d36ad4e8-d74a-4f7a-ace1-eaea049febf6" From f93cc1bfd6472f37ab7216cfbe5717c82906a144 Mon Sep 17 00:00:00 2001 From: adfarth Date: Tue, 28 Jul 2026 18:01:46 -0600 Subject: [PATCH 36/45] update input checks --- julia_src/mpc.jl | 38 ++++++++++++++++++++++---------------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/julia_src/mpc.jl b/julia_src/mpc.jl index e9f8b5c1d..e6084c0a1 100644 --- a/julia_src/mpc.jl +++ b/julia_src/mpc.jl @@ -194,13 +194,12 @@ function get_mpc_results!(d::Dict; solver_name::String="HiGHS")::Dict error("When using MPC (daily_foresight_optimized dispatch), only PV and ElectricStorage are supported technologies. " * "Unsupported inputs found: $(join(unsupported_keys, ", ")).") end - # PV can be provided as a Dict (single system) or an array of Dicts. - # MPC only supports a single PV, so error on multiple PVs and normalize a - # one-element array down to a Dict so downstream code can treat d["PV"] as a Dict. + + # MPC only supports a single PV, so error on multiple PVs and normalize a one-element array down to a Dict so downstream code can treat d["PV"] as a Dict. # TODO: Handle multiple PVs if haskey(d, "PV") && isa(d["PV"], AbstractArray) if length(d["PV"]) > 1 - error("MPC: Multiple PV systems are not supported.") + error("MPC: Multiple PV systems are not supported in MPC runs at this time.") elseif length(d["PV"]) == 1 d["PV"] = d["PV"][1] else @@ -223,10 +222,23 @@ function get_mpc_results!(d::Dict; solver_name::String="HiGHS")::Dict error("MPC: Site.include_exported_renewable_electricity_in_total = false is not supported in MPC runs.") end - # TODO: Add warnings for REopt inputs and scenarios that are not modeled in MPC (e.g., coincident peak charges, demand lookback, etc.) - @warn "Using MPC to determine dispatch. MPC does not model: tiered electricity rates; rates will be flattened to the first tier." + # Error for off-grid runs + if haskey(d, "Settings") && get(d["Settings"], "off_grid_flag", false) == true + error("MPC: Off-grid runs are not currently supported in MPC.") + end - # TODO: Error if rate tariff contains lookbacks or coincident peak charges. + # Error if rate tariff contains lookbacks or coincident peak charges. (These are not currently supported in MPC.) + if (haskey(d["ElectricTariff"], "demand_lookback_months") && length(d["ElectricTariff"]["demand_lookback_months"]) > 0 ) || + (haskey(d["ElectricTariff"], "demand_lookback_percent") && d["ElectricTariff"]["demand_lookback_percent"] > 0) || + (haskey(d["ElectricTariff"], "demand_lookback_range") && d["ElectricTariff"]["demand_lookback_range"] > 0) + error("MPC: ElectricTariff with demand lookbacks is not currently supported in MPC runs.") + end + if haskey(d["ElectricTariff"], "coincident_peak_load_active_time_steps") && d["ElectricTariff"]["coincident_peak_load_active_time_steps"] != [Int64[]] + error("MPC: ElectricTariff with coincident peak charges is not currently supported in MPC runs.") + end + + # TODO: show this warning only if tiered rates are detected in the tariff. + @warn "Using MPC to determine dispatch. MPC does not model: tiered electricity rates; rates will be flattened to the first tier." # TODO: Test with outage inputs before enabling this warning. # # Warning for outage inputs (MPC does not model outages) @@ -266,8 +278,7 @@ function get_mpc_results!(d::Dict; solver_name::String="HiGHS")::Dict model_inputs = reoptjl.REoptInputs(sizing_post) # REoptInputs returns an error Dict (rather than throwing) when input validation fails. - # Surface those messages instead of falling through to get_technology_sizes!, which expects - # a REoptInputs and would otherwise raise a confusing MethodError. + # Surface those messages instead of falling through to get_technology_sizes!, which expects a REoptInputs and would otherwise raise a confusing MethodError. if isa(model_inputs, Dict) @error "REopt input validation failed during MPC pre-solve." messages=get(model_inputs, "Messages", Dict()) return build_mpc_response( @@ -285,8 +296,6 @@ function get_mpc_results!(d::Dict; solver_name::String="HiGHS")::Dict ) end - - # MPC requires fixed PV and battery sizes. If not provided, call REopt first in a sizing run. technology_sizes = get_technology_sizes!(d, model_inputs, solver_settings) s = model_inputs.s # Access the processed Scenario struct @@ -324,14 +333,12 @@ function get_mpc_results!(d::Dict; solver_name::String="HiGHS")::Dict loads_kw = Float64.(s.electric_load.loads_kw) # Extract tariff inputs relevant to MPC (use first tier only if tiered rates) - # TODO: Are all of these relevant? Any missing inputs? - # TODO: Implement lookback (demand_lookback_months, demand_lookback_percent, demand_lookback_range) Ignoring coincident peak charges for now + # TODO: Implement lookbacks (demand_lookback_months, demand_lookback_percent, demand_lookback_range), coincident peak charges, and handling of tiered rates here and in REopt.jl. energy_rates = Float64.(s.electric_tariff.energy_rates[:, 1]) monthly_demand_rates = isempty(s.electric_tariff.monthly_demand_rates) ? zeros(Float64, 12) : Float64.(s.electric_tariff.monthly_demand_rates[:, 1]) tou_demand_rates = isempty(s.electric_tariff.tou_demand_rates) ? Float64[] : Float64.(s.electric_tariff.tou_demand_rates[:, 1]) tou_demand_ratchet_time_steps = [Int.(v) for v in s.electric_tariff.tou_demand_ratchet_time_steps] - # TODO: Monthly demand has never been tested n_tou_ratchets = length(tou_demand_rates) # Number of TOU ratchets tou_previous_peak_demands = zeros(Float64, n_tou_ratchets) # Tracks past TOU peak demand per ratchet monthly_previous_peak_demands = zeros(Float64, 12) # Tracks past monthly peak demand @@ -593,11 +600,10 @@ function get_mpc_results!(d::Dict; solver_name::String="HiGHS")::Dict # --- Total electricity bill = energy charge - export benefit + demand charges --- "total_electricity_bill" => total_energy_cost - total_export_benefit + tou_demand_cost_total + monthly_demand_cost_total, - # --- Per-timestep series (energy/exports only; demand is a peak-based charge) --- "energy_cost_series_per_timestep" => energy_cost_series, "export_benefit_series_per_timestep" => export_benefit_series, "tou_peaks_by_ratchet_kw" => tou_previous_peak_demands, - "monthly_peaks_kw" => monthly_previous_peak_demands, + "monthly_peaks_kw" => monthly_previous_peak_demands, # Optimal peaks (whereas REopt's ElectricLoad.monthly_peaks_kw is BAU peaks) ), ) ) From cd8d01750c5517d22d5673da6a47220f267cba75 Mon Sep 17 00:00:00 2001 From: adfarth Date: Thu, 30 Jul 2026 18:18:39 -0600 Subject: [PATCH 37/45] add test, update errors --- julia_src/http.jl | 14 +- julia_src/mpc.jl | 61 +- reoptjl/src/run_jump_model.py | 20 +- reoptjl/test/posts/mpc.json | 8798 +++++++++++++++++++++++++++++ reoptjl/test/test_job_endpoint.py | 39 + reoptjl/views.py | 2 + 6 files changed, 8901 insertions(+), 33 deletions(-) create mode 100644 reoptjl/test/posts/mpc.json diff --git a/julia_src/http.jl b/julia_src/http.jl index 16a2c5644..06ec34dfd 100644 --- a/julia_src/http.jl +++ b/julia_src/http.jl @@ -84,7 +84,10 @@ function reopt(req::HTTP.Request) @info "Running MPC to obtain daily foresight optimized battery dispatch profile." mpc_results = get_mpc_results!(d; solver_name=solver_name) # TODO: Cache sizing run results and avoid a second call to REopt? Are those the same results? - if get(mpc_results, "skip_mpc", false) == true + if get(mpc_results, "status", "") == "error" + @error "MPC pre-solve failed input validation" messages=get(mpc_results, "Messages", Dict()) + return HTTP.Response(400, JSON.json(mpc_results)) + elseif get(mpc_results, "skip_mpc", false) == true @info "Cannot execute daily_foresight_optimized battery dispatch because optimal battery size is 0. Setting dispatch strategy to 'optimized'." d["ElectricStorage"]["dispatch_strategy"] = "optimized" else @@ -889,11 +892,14 @@ function mpc(req::HTTP.Request) error_response["reopt_version"] = string(pkgversion(reoptjl)) end GC.gc() - if isempty(error_response) + if !isempty(error_response) + return HTTP.Response(500, JSON.json(error_response)) + elseif get(results, "status", "") == "error" + @error "MPC failed input validation" messages=get(results, "Messages", Dict()) + return HTTP.Response(400, JSON.json(results)) + else @info "MPC ran successfully." return HTTP.Response(200, JSON.json(results)) - else - return HTTP.Response(500, JSON.json(error_response)) end end diff --git a/julia_src/mpc.jl b/julia_src/mpc.jl index e6084c0a1..654d44041 100644 --- a/julia_src/mpc.jl +++ b/julia_src/mpc.jl @@ -126,7 +126,6 @@ function get_technology_sizes!(d::Dict, model_inputs::reoptjl.REoptInputs, solve m = get_solver_model(get_solver_model_type(solver_settings["solver_name"]), solver_settings["solver_attributes"]) - # model_inputs = reoptjl.REoptInputs(sizing_post) sizing_results = reoptjl.run_reopt(m, model_inputs) if get(sizing_results, "status", "") != "optimal" @@ -191,15 +190,21 @@ function get_mpc_results!(d::Dict; solver_name::String="HiGHS")::Dict mpc_allowed_keys = Set(["PV", "ElectricStorage", "ElectricLoad", "ElectricTariff", "ElectricUtility", "Site", "Settings", "Financial"]) unsupported_keys = setdiff(keys(d), mpc_allowed_keys) if !isempty(unsupported_keys) - error("When using MPC (daily_foresight_optimized dispatch), only PV and ElectricStorage are supported technologies. " * - "Unsupported inputs found: $(join(unsupported_keys, ", ")).") + return build_mpc_response( + "error", + messages = Dict("errors" => ["When using MPC (daily_foresight_optimized dispatch), only PV and ElectricStorage are supported technologies. " * + "Unsupported inputs found: $(join(unsupported_keys, ", "))."]) + ) end # MPC only supports a single PV, so error on multiple PVs and normalize a one-element array down to a Dict so downstream code can treat d["PV"] as a Dict. # TODO: Handle multiple PVs if haskey(d, "PV") && isa(d["PV"], AbstractArray) if length(d["PV"]) > 1 - error("MPC: Multiple PV systems are not supported in MPC runs at this time.") + return build_mpc_response( + "error", + messages = Dict("errors" => ["MPC: Multiple PV systems are not supported in MPC runs at this time."]) + ) elseif length(d["PV"]) == 1 d["PV"] = d["PV"][1] else @@ -207,34 +212,45 @@ function get_mpc_results!(d::Dict; solver_name::String="HiGHS")::Dict end end - # Error if unsupported CO2/renewable-fraction constraints are set - _site_input = get(d, "Site", Dict()) - if !isnothing(get(_site_input, "CO2_emissions_reduction_min_fraction", nothing)) - error("MPC: Site.CO2_emissions_reduction_min_fraction is not supported in MPC runs.") - end - if get(_site_input, "include_grid_renewable_fraction_in_RE_constraints", false) == true - error("MPC: Site.include_grid_renewable_fraction_in_RE_constraints is not supported in MPC runs.") - end - if get(_site_input, "include_exported_elec_emissions_in_total", true) == false - error("MPC: Site.include_exported_elec_emissions_in_total = false is not supported in MPC runs.") - end - if get(_site_input, "include_exported_renewable_electricity_in_total", true) == false - error("MPC: Site.include_exported_renewable_electricity_in_total = false is not supported in MPC runs.") - end + # The checks below are removed because when MPC is called through REopt, the last REopt run will error if these goals are not met. + # If MPC is called directly, specifying these inputs IS an issue (because they're not consdired in MPC dispatch). If the MPC endpoint becomes public, we should re-enable these checks. + # # Error if unsupported CO2/renewable-fraction constraints are set + # _site_input = get(d, "Site", Dict()) + # if !isnothing(get(_site_input, "CO2_emissions_reduction_min_fraction", nothing)) + # error("MPC: Site.CO2_emissions_reduction_min_fraction is not supported in MPC runs.") + # end + # if get(_site_input, "include_grid_renewable_fraction_in_RE_constraints", false) == true + # error("MPC: Site.include_grid_renewable_fraction_in_RE_constraints is not supported in MPC runs.") + # end + # if get(_site_input, "include_exported_elec_emissions_in_total", true) == false + # error("MPC: Site.include_exported_elec_emissions_in_total = false is not supported in MPC runs.") + # end + # if get(_site_input, "include_exported_renewable_electricity_in_total", true) == false + # error("MPC: Site.include_exported_renewable_electricity_in_total = false is not supported in MPC runs.") + # end # Error for off-grid runs if haskey(d, "Settings") && get(d["Settings"], "off_grid_flag", false) == true - error("MPC: Off-grid runs are not currently supported in MPC.") + return build_mpc_response( + "error", + messages = Dict("errors" => ["MPC: Off-grid runs are not currently supported in MPC."]) + ) end # Error if rate tariff contains lookbacks or coincident peak charges. (These are not currently supported in MPC.) if (haskey(d["ElectricTariff"], "demand_lookback_months") && length(d["ElectricTariff"]["demand_lookback_months"]) > 0 ) || (haskey(d["ElectricTariff"], "demand_lookback_percent") && d["ElectricTariff"]["demand_lookback_percent"] > 0) || (haskey(d["ElectricTariff"], "demand_lookback_range") && d["ElectricTariff"]["demand_lookback_range"] > 0) - error("MPC: ElectricTariff with demand lookbacks is not currently supported in MPC runs.") + return build_mpc_response( + "error", + messages = Dict("errors" => ["MPC: ElectricTariff with demand lookbacks is not currently supported in MPC runs."]) + ) end if haskey(d["ElectricTariff"], "coincident_peak_load_active_time_steps") && d["ElectricTariff"]["coincident_peak_load_active_time_steps"] != [Int64[]] - error("MPC: ElectricTariff with coincident peak charges is not currently supported in MPC runs.") + return build_mpc_response( + "error", + messages = Dict("errors" => ["MPC: ElectricTariff with coincident peak charges is not currently supported in MPC runs."]) + ) end # TODO: show this warning only if tiered rates are detected in the tariff. @@ -343,9 +359,6 @@ function get_mpc_results!(d::Dict; solver_name::String="HiGHS")::Dict tou_previous_peak_demands = zeros(Float64, n_tou_ratchets) # Tracks past TOU peak demand per ratchet monthly_previous_peak_demands = zeros(Float64, 12) # Tracks past monthly peak demand - println("monthly_demand_rates: ", monthly_demand_rates) - - # Extract storage efficiency and SOC defaults from processed inputs rect_eff = Float64(s.storage.attr["ElectricStorage"].rectifier_efficiency_fraction) inv_eff = Float64(s.storage.attr["ElectricStorage"].inverter_efficiency_fraction) diff --git a/reoptjl/src/run_jump_model.py b/reoptjl/src/run_jump_model.py index 7a4733160..9644c7bb6 100644 --- a/reoptjl/src/run_jump_model.py +++ b/reoptjl/src/run_jump_model.py @@ -35,7 +35,11 @@ def on_failure(self, exc, task_id, args, kwargs, einfo): exc.save_to_db() msg = exc.message meta = APIMeta.objects.get(run_uuid=exc.run_uuid) - meta.status = "An error occurred. See messages for more." + # Server-side failures map to 500 at /results; other failures (e.g. infeasible, timeout) map to 400. + if isinstance(exc, (UnexpectedError, REoptFailedToStartError)): + meta.status = "Internal Server Error. See messages for more." + else: + meta.status = "error" meta.save(update_fields=["status"]) Message.create(meta=meta, message_type="error", message=msg).save() @@ -68,10 +72,16 @@ def run_jump_model(run_uuid): response_json = response.json() if response.status_code == 500: raise REoptFailedToStartError(task=name, message=response_json["error"], run_uuid=run_uuid, user_uuid=user_uuid) - results = response_json["results"] - reopt_version = response_json["reopt_version"] - if results["status"].strip().lower() != "error": - inputs_with_defaults_set_in_julia = response_json["inputs_with_defaults_set_in_julia"] + if response.status_code == 400: + # REopt.jl returned an input-validation error; store it as an error result so /results returns 400 with the messages. + results = {"status": "error", + "Messages": response_json.get("Messages", {"errors": ["Invalid inputs. Please check your inputs and try again."]})} + reopt_version = response_json.get("reopt_version", "") + else: + results = response_json["results"] + reopt_version = response_json["reopt_version"] + if results["status"].strip().lower() != "error": + inputs_with_defaults_set_in_julia = response_json["inputs_with_defaults_set_in_julia"] time_dict["pyjulia_run_reopt_seconds"] = time.time() - t_start results.update(time_dict) diff --git a/reoptjl/test/posts/mpc.json b/reoptjl/test/posts/mpc.json new file mode 100644 index 000000000..ac2a4ca07 --- /dev/null +++ b/reoptjl/test/posts/mpc.json @@ -0,0 +1,8798 @@ +{ + "Settings": { + "solver_name": "HiGHS", + "time_steps_per_hour": 1, + "timeout_seconds": 420, + "optimality_tolerance": 0.02, + "run_bau": false + }, + "Site": { + "latitude": 37.7749, + "longitude": -122.4194 + }, + "PV": { + "min_kw": 100.0, + "max_kw": 100.0, + "can_net_meter": true, + "production_factor_series": [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.207695, + 0.422279, + 0.560222, + 0.636338, + 0.651241, + 0.59589, + 0.485124, + 0.31164800000000004, + 0.06971, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.110515, + 0.24489, + 0.434685, + 0.478031, + 0.29884, + 0.22947700000000001, + 0.37041199999999996, + 0.294755, + 0.067825, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.022489000000000002, + 0.057006999999999995, + 0.034283, + 0.08069, + 0.069618, + 0.000787, + 0.017152, + 0.008091, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.010897, + 0.030341999999999997, + 0.024048, + 0.103774, + 0.002987, + 0.098589, + 0.055890999999999996, + 0.031303, + 0.0039689999999999994, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.22544, + 0.436521, + 0.567823, + 0.640915, + 0.651996, + 0.608505, + 0.508782, + 0.34101, + 0.10376200000000001, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.226905, + 0.431828, + 0.496223, + 0.63124, + 0.5209969999999999, + 0.539629, + 0.401755, + 0.248243, + 0.068388, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.062381, + 0.152695, + 0.033644, + 0.189455, + 0.11001000000000001, + 0.335787, + 0.07615300000000001, + 0.043837, + 0.007545, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.005589, + 0.019161, + 0.011602, + 0.043304, + 0.044791, + 0.030343, + 0.053302999999999996, + 0.029886, + 0.004236, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.046078, + 0.122215, + 0.30306900000000003, + 0.364937, + 0.36310899999999996, + 0.100177, + 0.11197, + 0.063728, + 0.012695999999999999, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.073214, + 0.014662000000000001, + 0.062383, + 0.0043040000000000005, + 0.002898, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.206088, + 0.413534, + 0.5486040000000001, + 0.535755, + 0.35776100000000005, + 0.40656200000000003, + 0.255694, + 0.163653, + 0.052244, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.031121, + 0.079763, + 0.216532, + 0.200732, + 0.644237, + 0.280151, + 0.006021, + 0.002331, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.23405600000000001, + 0.443721, + 0.574236, + 0.64771, + 0.6605599999999999, + 0.614382, + 0.513732, + 0.360351, + 0.135912, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.223237, + 0.427037, + 0.556472, + 0.621769, + 0.634969, + 0.596948, + 0.5063960000000001, + 0.352406, + 0.13306, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.218728, + 0.42540100000000003, + 0.568219, + 0.637793, + 0.653227, + 0.6152799999999999, + 0.523317, + 0.36587400000000003, + 0.141215, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.23777199999999998, + 0.44528199999999996, + 0.5741689999999999, + 0.487531, + 0.661188, + 0.616721, + 0.523869, + 0.36788600000000005, + 0.145238, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.15826400000000002, + 0.313322, + 0.442387, + 0.5035609999999999, + 0.646653, + 0.497986, + 0.32740600000000003, + 0.217306, + 0.077209, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 7.7e-05, + 0.005954, + 0.017921, + 0.020105, + 0.03295, + 0.0022029999999999997, + 0.0008219999999999999, + 0.00228, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.146098, + 0.266418, + 0.5167820000000001, + 0.610033, + 0.44228300000000004, + 0.049975, + 0.514537, + 0.360977, + 0.140149, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.120022, + 0.25899900000000003, + 0.5457609999999999, + 0.34677600000000003, + 0.052723, + 0.438317, + 0.08987099999999999, + 0.002341, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.156128, + 0.372561, + 0.401088, + 0.352929, + 0.285026, + 0.20338499999999998, + 0.11293, + 0.058656, + 0.016661000000000002, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.19702699999999998, + 0.406659, + 0.109709, + 0.07210899999999999, + 0.522181, + 0.16973, + 0.018692, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.00015900000000000002, + 0.07371, + 0.46699599999999997, + 0.497896, + 0.50987, + 0.472797, + 0.30132299999999995, + 0.11578000000000001, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.102631, + 0.221361, + 0.447398, + 0.5571900000000001, + 0.673879, + 0.634937, + 0.5453709999999999, + 0.396301, + 0.17660800000000001, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0025099999999999996, + 0.22553700000000002, + 0.423811, + 0.368827, + 0.46375900000000003, + 0.650763, + 0.620319, + 0.53724, + 0.39084500000000005, + 0.174275, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.002576, + 0.23149799999999998, + 0.435505, + 0.577054, + 0.6498339999999999, + 0.671845, + 0.637499, + 0.549935, + 0.400792, + 0.182867, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.00758, + 0.25863099999999994, + 0.46878699999999995, + 0.603314, + 0.679697, + 0.6936380000000001, + 0.644942, + 0.549987, + 0.401648, + 0.183955, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.003556, + 0.225736, + 0.425526, + 0.471517, + 0.679453, + 0.695043, + 0.651433, + 0.559501, + 0.408837, + 0.191579, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0030659999999999997, + 0.17253800000000002, + 0.319397, + 0.447594, + 0.5588869999999999, + 0.536493, + 0.558367, + 0.434576, + 0.273629, + 0.123788, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.006468, + 0.26311700000000005, + 0.40993999999999997, + 0.30082299999999995, + 0.684885, + 0.7004199999999999, + 0.658906, + 0.440605, + 0.416477, + 0.200399, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.009722, + 0.25576499999999996, + 0.46326799999999996, + 0.544865, + 0.575312, + 0.5256609999999999, + 0.33669600000000005, + 0.34551600000000005, + 0.248773, + 0.079561, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.006511, + 0.247534, + 0.445187, + 0.554033, + 0.628343, + 0.654242, + 0.6316269999999999, + 0.5464990000000001, + 0.405115, + 0.196443, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.01492, + 0.25507, + 0.133643, + 0.26287900000000003, + 0.37820299999999996, + 0.674183, + 0.639516, + 0.558255, + 0.413467, + 0.19937, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.015502, + 0.086509, + 0.139454, + 0.5923930000000001, + 0.6651549999999999, + 0.679215, + 0.642657, + 0.559689, + 0.41865800000000003, + 0.208602, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.020964, + 0.28124099999999996, + 0.49231400000000003, + 0.620063, + 0.691965, + 0.703221, + 0.661597, + 0.572276, + 0.43388499999999997, + 0.21993100000000002, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.021228, + 0.14013, + 0.234809, + 0.46230099999999996, + 0.250766, + 0.36852300000000004, + 0.649845, + 0.566859, + 0.42470600000000003, + 0.21536000000000002, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.021122, + 0.025151, + 0.057479999999999996, + 0.08925799999999999, + 0.103733, + 0.263445, + 0.242077, + 0.23626, + 0.165649, + 0.064395, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.066496, + 0.10229, + 0.118071, + 0.25259200000000004, + 0.18767599999999998, + 0.174407, + 0.067248, + 0.090411, + 0.030815000000000002, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.028866, + 0.284723, + 0.356714, + 0.523703, + 0.6942820000000001, + 0.713585, + 0.67173, + 0.5817859999999999, + 0.440543, + 0.22945400000000002, + 0.0079, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.032, + 0.29314100000000004, + 0.498997, + 0.629564, + 0.7036760000000001, + 0.7216950000000001, + 0.685867, + 0.594664, + 0.44993700000000003, + 0.234653, + 0.009337999999999999, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.033338, + 0.287651, + 0.48927800000000005, + 0.616632, + 0.680474, + 0.693491, + 0.654555, + 0.564772, + 0.433353, + 0.213252, + 0.003781, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.020056, + 0.08881399999999999, + 0.338297, + 0.543219, + 0.596919, + 0.69924, + 0.542607, + 0.413375, + 0.330295, + 0.22752, + 0.004777999999999999, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.040970999999999994, + 0.283454, + 0.475534, + 0.5974550000000001, + 0.669778, + 0.552811, + 0.446658, + 0.527128, + 0.377336, + 0.12930699999999998, + 0.00239, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.013573, + 0.025055, + 0.067513, + 0.11536400000000001, + 0.174123, + 0.104833, + 0.09042700000000001, + 0.054126, + 0.020904, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.04468, + 0.25911900000000004, + 0.439435, + 0.563762, + 0.632254, + 0.6521290000000001, + 0.328483, + 0.540633, + 0.25031200000000003, + 0.083521, + 0.010247, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.051542000000000004, + 0.026562000000000002, + 0.30834500000000004, + 0.6004160000000001, + 0.6815910000000001, + 0.39878199999999997, + 0.16822800000000002, + 0.386711, + 0.321494, + 0.23660499999999998, + 0.016772, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.060192, + 0.291545, + 0.361254, + 0.331589, + 0.385625, + 0.335976, + 0.233911, + 0.135821, + 0.014922000000000001, + 0.031966999999999995, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.044932, + 0.285211, + 0.493955, + 0.6397229999999999, + 0.718935, + 0.527409, + 0.707648, + 0.616122, + 0.351425, + 0.249001, + 0.020041, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.00994, + 0.07306399999999999, + 0.041134, + 0.317872, + 0.347862, + 0.418657, + 0.35969799999999996, + 0.339814, + 0.17905600000000002, + 0.040002, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.034241, + 0.15840100000000001, + 0.296391, + 0.08306699999999999, + 0.090551, + 0.047091, + 0.042392000000000006, + 0.12198, + 0.080349, + 0.002233, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.026936, + 0.220476, + 0.490147, + 0.32875099999999996, + 0.357146, + 0.42798200000000003, + 0.664018, + 0.572755, + 0.435245, + 0.236703, + 0.021262, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.010258, + 0.096562, + 0.203518, + 0.600028, + 0.591035, + 0.581933, + 0.546376, + 0.5808329999999999, + 0.447123, + 0.252392, + 0.027173, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.021838999999999997, + 0.11009300000000001, + 0.418191, + 0.555395, + 0.591322, + 0.570702, + 0.557001, + 0.49666699999999997, + 0.324747, + 0.090819, + 0.002615, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.091813, + 0.325463, + 0.15421, + 0.6510520000000001, + 0.26193900000000003, + 0.427861, + 0.7114360000000001, + 0.252874, + 0.288834, + 0.260892, + 0.035828000000000006, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.09758599999999999, + 0.33346699999999996, + 0.519919, + 0.6407160000000001, + 0.713893, + 0.734447, + 0.700998, + 0.616637, + 0.479126, + 0.275596, + 0.043648000000000006, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.102257, + 0.342515, + 0.531277, + 0.649798, + 0.7194769999999999, + 0.735367, + 0.697784, + 0.6113540000000001, + 0.47650099999999995, + 0.276463, + 0.04868, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.104298, + 0.341447, + 0.530255, + 0.648726, + 0.714005, + 0.615319, + 0.570394, + 0.435757, + 0.319392, + 0.169755, + 0.02958, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.017295, + 0.323784, + 0.467444, + 0.54375, + 0.716537, + 0.735182, + 0.5598289999999999, + 0.5356559999999999, + 0.388509, + 0.141916, + 0.016282, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.116723, + 0.353379, + 0.532111, + 0.6548120000000001, + 0.72369, + 0.7399990000000001, + 0.7082809999999999, + 0.621674, + 0.48418900000000004, + 0.29177, + 0.057391, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.111654, + 0.339012, + 0.34675900000000004, + 0.460963, + 0.582859, + 0.42449400000000004, + 0.373393, + 0.24651900000000002, + 0.05025, + 0.033569, + 0.011414, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.029684000000000002, + 0.015553, + 0.150826, + 0.096607, + 0.790417, + 0.746839, + 0.650021, + 0.49600099999999997, + 0.287842, + 0.059677999999999995, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.097219, + 0.029974, + 0.245173, + 0.509524, + 0.362653, + 0.421228, + 0.267154, + 0.25759899999999997, + 0.049686999999999995, + 0.035323, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.026494, + 0.025445, + 0.101385, + 0.063761, + 0.158956, + 0.164179, + 0.115258, + 0.055415, + 0.036472000000000004, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.039520000000000007, + 0.32023599999999997, + 0.21712700000000001, + 0.483665, + 0.427042, + 0.398914, + 0.40226, + 0.417226, + 0.25019, + 0.074965, + 0.023945, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.00437, + 0.100305, + 0.274823, + 0.403126, + 0.604246, + 0.588712, + 0.551582, + 0.605985, + 0.463137, + 0.26813, + 0.060101, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.024516, + 0.345265, + 0.516995, + 0.639512, + 0.473585, + 0.725527, + 0.482527, + 0.609947, + 0.471918, + 0.276389, + 0.063228, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.008816000000000001, + 0.06780800000000001, + 0.198193, + 0.446741, + 0.400309, + 0.541388, + 0.715101, + 0.626445, + 0.48399000000000003, + 0.282674, + 0.06483499999999999, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.042539, + 0.164435, + 0.553807, + 0.5011530000000001, + 0.758212, + 0.77442, + 0.737505, + 0.639857, + 0.49224599999999996, + 0.29106, + 0.070381, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.15869, + 0.40103500000000003, + 0.577339, + 0.700155, + 0.767641, + 0.784826, + 0.745556, + 0.652605, + 0.508755, + 0.305269, + 0.076014, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.054398, + 0.081043, + 0.42425599999999997, + 0.503707, + 0.737906, + 0.754303, + 0.717615, + 0.6364099999999999, + 0.495182, + 0.301902, + 0.07805, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.15837, + 0.39331099999999997, + 0.567373, + 0.681314, + 0.736961, + 0.737013, + 0.687886, + 0.60803, + 0.48419999999999996, + 0.29970600000000003, + 0.079087, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.052518999999999996, + 0.159327, + 0.328732, + 0.669928, + 0.731684, + 0.746134, + 0.717596, + 0.5362720000000001, + 0.493911, + 0.296259, + 0.076678, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.051247999999999995, + 0.187869, + 0.5580499999999999, + 0.6821889999999999, + 0.7531570000000001, + 0.769657, + 0.73427, + 0.650882, + 0.507289, + 0.307917, + 0.082402, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.09169799999999999, + 0.279419, + 0.29978699999999997, + 0.47239299999999995, + 0.40810700000000005, + 0.20960900000000002, + 0.16908099999999998, + 0.428692, + 0.498267, + 0.297099, + 0.078316, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0017800000000000001, + 0.168075, + 0.397638, + 0.565641, + 0.682569, + 0.7479389999999999, + 0.767616, + 0.735413, + 0.647673, + 0.507462, + 0.237204, + 0.007864, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.003414, + 0.10562600000000001, + 0.376731, + 0.5580940000000001, + 0.666061, + 0.725865, + 0.740677, + 0.702678, + 0.6132430000000001, + 0.473989, + 0.283315, + 0.077291, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0036720000000000004, + 0.052357999999999995, + 0.139844, + 0.553953, + 0.665559, + 0.732144, + 0.7444339999999999, + 0.7156509999999999, + 0.631231, + 0.49967399999999995, + 0.310479, + 0.089037, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.004647, + 0.18046, + 0.39882100000000004, + 0.414856, + 0.6189199999999999, + 0.6545829999999999, + 0.6138060000000001, + 0.673336, + 0.5296879999999999, + 0.426967, + 0.269061, + 0.065591, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.004355, + 0.150454, + 0.33557400000000004, + 0.528914, + 0.637541, + 0.578269, + 0.716262, + 0.680254, + 0.6026710000000001, + 0.476746, + 0.294038, + 0.056965, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.002179, + 0.00437, + 0.053927, + 0.109019, + 0.237154, + 0.227977, + 0.27866399999999997, + 0.42091199999999995, + 0.317135, + 0.24538300000000002, + 0.16309, + 0.064928, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.007954, + 0.036506, + 0.079628, + 0.21349700000000002, + 0.322758, + 0.446036, + 0.758214, + 0.7218450000000001, + 0.6352300000000001, + 0.499241, + 0.305756, + 0.08911799999999999, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0029620000000000002, + 0.093987, + 0.13712, + 0.194631, + 0.378106, + 0.45871, + 0.5164880000000001, + 0.460509, + 0.37861700000000004, + 0.251616, + 0.087476, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.011045, + 0.040895, + 0.412297, + 0.579653, + 0.196054, + 0.7642089999999999, + 0.40402499999999997, + 0.41291100000000003, + 0.380364, + 0.357733, + 0.214743, + 0.037257, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.014909, + 0.20342, + 0.425916, + 0.5928709999999999, + 0.711846, + 0.777106, + 0.790258, + 0.756649, + 0.668144, + 0.20056000000000002, + 0.103436, + 0.030566, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.019303999999999998, + 0.220282, + 0.45027100000000003, + 0.6142430000000001, + 0.72369, + 0.7888189999999999, + 0.7988139999999999, + 0.75714, + 0.595485, + 0.517654, + 0.324861, + 0.09987399999999999, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.021793, + 0.212108, + 0.428913, + 0.5901409999999999, + 0.703235, + 0.7663730000000001, + 0.7731849999999999, + 0.735835, + 0.521258, + 0.429628, + 0.24329499999999998, + 0.056109, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.022963, + 0.043286, + 0.124235, + 0.232799, + 0.320848, + 0.7670119999999999, + 0.785678, + 0.751068, + 0.657134, + 0.5162559999999999, + 0.322629, + 0.101645, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.026743, + 0.224262, + 0.444289, + 0.606557, + 0.711096, + 0.773561, + 0.789644, + 0.749735, + 0.659048, + 0.523871, + 0.332743, + 0.106444, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.028473, + 0.22575499999999998, + 0.442178, + 0.597698, + 0.704451, + 0.76274, + 0.773423, + 0.734248, + 0.6476649999999999, + 0.514004, + 0.325755, + 0.104397, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.030614000000000002, + 0.220653, + 0.432615, + 0.5874729999999999, + 0.696327, + 0.753764, + 0.76858, + 0.734579, + 0.6509579999999999, + 0.517126, + 0.246369, + 0.09568600000000001, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.004672, + 0.162977, + 0.443077, + 0.507745, + 0.706308, + 0.74123, + 0.789443, + 0.601442, + 0.557023, + 0.44275400000000004, + 0.283506, + 0.106104, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.027623, + 0.17741800000000002, + 0.357074, + 0.585418, + 0.685648, + 0.736874, + 0.7510979999999999, + 0.722754, + 0.550971, + 0.509244, + 0.308328, + 0.101213, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.015663, + 0.16794499999999998, + 0.43129, + 0.579802, + 0.6936599999999999, + 0.759934, + 0.774892, + 0.7403529999999999, + 0.657941, + 0.52416, + 0.331498, + 0.109362, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.029842, + 0.13053800000000002, + 0.309222, + 0.51897, + 0.636846, + 0.646696, + 0.676881, + 0.631426, + 0.524899, + 0.339399, + 0.202929, + 0.08028199999999999, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.125947, + 0.31171899999999997, + 0.482402, + 0.5644199999999999, + 0.597461, + 0.6387630000000001, + 0.530771, + 0.51403, + 0.482815, + 0.293091, + 0.099892, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.042969, + 0.056249, + 0.107878, + 0.18890700000000002, + 0.305965, + 0.315038, + 0.7746770000000001, + 0.5291119999999999, + 0.445243, + 0.397129, + 0.244786, + 0.086456, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.044275, + 0.027727, + 0.09003499999999999, + 0.155842, + 0.37283, + 0.551149, + 0.38921300000000003, + 0.6464930000000001, + 0.557837, + 0.465028, + 0.28536, + 0.086013, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.048019, + 0.196745, + 0.44526, + 0.601424, + 0.717346, + 0.7732559999999999, + 0.728509, + 0.6810170000000001, + 0.6363479999999999, + 0.5214439999999999, + 0.332935, + 0.113818, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.052713, + 0.252454, + 0.46536500000000003, + 0.625539, + 0.7366119999999999, + 0.797933, + 0.805922, + 0.775174, + 0.686875, + 0.540394, + 0.342638, + 0.116468, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.025056000000000002, + 0.254183, + 0.463, + 0.616322, + 0.721293, + 0.775361, + 0.790967, + 0.7548429999999999, + 0.666332, + 0.446007, + 0.305602, + 0.06133400000000001, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.12155200000000001, + 0.46793, + 0.470894, + 0.755336, + 0.820584, + 0.724289, + 0.673685, + 0.536342, + 0.467707, + 0.338795, + 0.11994400000000001, + 0.001472, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.061122, + 0.278408, + 0.49714600000000003, + 0.6572709999999999, + 0.767188, + 0.825215, + 0.832831, + 0.79164, + 0.693194, + 0.546177, + 0.350979, + 0.123056, + 0.001472, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.062035, + 0.264189, + 0.45987700000000004, + 0.598544, + 0.6976330000000001, + 0.757966, + 0.7777430000000001, + 0.750242, + 0.636547, + 0.487867, + 0.32704300000000003, + 0.11739100000000001, + 0.002422, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.020648, + 0.01117, + 0.113569, + 0.11582899999999999, + 0.246007, + 0.618874, + 0.5315019999999999, + 0.595178, + 0.492678, + 0.5332169999999999, + 0.339469, + 0.121077, + 0.002424, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.067418, + 0.279493, + 0.49157799999999996, + 0.615208, + 0.7614740000000001, + 0.822515, + 0.826419, + 0.780931, + 0.6849729999999999, + 0.539667, + 0.344538, + 0.12405100000000001, + 0.001916, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.070637, + 0.283006, + 0.49051100000000003, + 0.639332, + 0.741068, + 0.795591, + 0.797122, + 0.750928, + 0.6599919999999999, + 0.5190560000000001, + 0.33437700000000004, + 0.12055800000000001, + 0.001459, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.074152, + 0.28825599999999996, + 0.500058, + 0.659172, + 0.76954, + 0.833333, + 0.833333, + 0.797959, + 0.704894, + 0.5533790000000001, + 0.355991, + 0.129968, + 0.003236, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.074889, + 0.293091, + 0.5042719999999999, + 0.658667, + 0.766779, + 0.822448, + 0.8234600000000001, + 0.785347, + 0.694955, + 0.551687, + 0.356843, + 0.130835, + 0.003095, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.037212, + 0.14308600000000002, + 0.311227, + 0.30757, + 0.490679, + 0.345737, + 0.12741, + 0.377938, + 0.123617, + 0.29588600000000004, + 0.17526599999999998, + 0.07737000000000001, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.067246, + 0.282511, + 0.483958, + 0.6292279999999999, + 0.719526, + 0.7577229999999999, + 0.314807, + 0.7376269999999999, + 0.661076, + 0.525931, + 0.341142, + 0.127801, + 0.003975, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.028002, + 0.10915699999999999, + 0.289136, + 0.425682, + 0.44997000000000004, + 0.398326, + 0.33848, + 0.14156200000000002, + 0.314291, + 0.133382, + 0.060633, + 0.025675999999999997, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.081814, + 0.29616699999999996, + 0.498654, + 0.647913, + 0.6306090000000001, + 0.808295, + 0.816507, + 0.775126, + 0.680986, + 0.54342, + 0.35283499999999995, + 0.132697, + 0.0041849999999999995, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.085343, + 0.294456, + 0.485252, + 0.6235109999999999, + 0.714194, + 0.7650739999999999, + 0.781359, + 0.7536219999999999, + 0.6716340000000001, + 0.538512, + 0.35338200000000003, + 0.133828, + 0.004897, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.087977, + 0.267079, + 0.421356, + 0.630357, + 0.7295969999999999, + 0.783338, + 0.792134, + 0.7579589999999999, + 0.672363, + 0.541036, + 0.356166, + 0.135745, + 0.004637, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.07389100000000001, + 0.23507, + 0.34896699999999997, + 0.524833, + 0.664827, + 0.6858970000000001, + 0.725471, + 0.565309, + 0.37285, + 0.480601, + 0.341559, + 0.13161, + 0.007202, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.088703, + 0.291028, + 0.482028, + 0.627556, + 0.727248, + 0.77962, + 0.783077, + 0.74173, + 0.657898, + 0.521532, + 0.34216399999999997, + 0.116736, + 0.007184, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.08848099999999999, + 0.285978, + 0.468597, + 0.598646, + 0.673618, + 0.708876, + 0.713664, + 0.668434, + 0.6019690000000001, + 0.483587, + 0.28160199999999996, + 0.12948300000000001, + 0.008009, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.6e-05, + 0.080895, + 0.24996000000000002, + 0.41862099999999997, + 0.55862, + 0.688866, + 0.710542, + 0.749359, + 0.6859829999999999, + 0.633802, + 0.5047740000000001, + 0.294478, + 0.13263999999999998, + 0.010395, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.040119999999999996, + 0.084547, + 0.251187, + 0.49125, + 0.7337229999999999, + 0.696178, + 0.753887, + 0.506325, + 0.429825, + 0.332226, + 0.196345, + 0.107807, + 0.012041, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.001974, + 0.09655, + 0.298657, + 0.487712, + 0.629165, + 0.7306, + 0.7835449999999999, + 0.789498, + 0.754728, + 0.664607, + 0.505505, + 0.351837, + 0.139472, + 0.010841, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.001431, + 0.097452, + 0.311599, + 0.502648, + 0.647389, + 0.743358, + 0.7921269999999999, + 0.795764, + 0.741722, + 0.653215, + 0.520768, + 0.346831, + 0.137167, + 0.010234, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.004936, + 0.095904, + 0.300991, + 0.487262, + 0.629145, + 0.723993, + 0.7717849999999999, + 0.77195, + 0.7256739999999999, + 0.638793, + 0.512115, + 0.342082, + 0.136677, + 0.011221, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0020440000000000002, + 0.095974, + 0.297072, + 0.474027, + 0.605012, + 0.695874, + 0.742736, + 0.7494310000000001, + 0.7158909999999999, + 0.639689, + 0.516091, + 0.346294, + 0.139463, + 0.010402, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0025710000000000004, + 0.099634, + 0.125545, + 0.214989, + 0.27689800000000003, + 0.384141, + 0.447624, + 0.5753039999999999, + 0.588515, + 0.502235, + 0.348949, + 0.22737000000000002, + 0.12404699999999999, + 0.0154, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.004016, + 0.086669, + 0.311898, + 0.503537, + 0.651307, + 0.7556240000000001, + 0.8129149999999999, + 0.82048, + 0.779639, + 0.691472, + 0.547764, + 0.359165, + 0.14536000000000002, + 0.01998, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.005642, + 0.110009, + 0.219572, + 0.498907, + 0.47551, + 0.7317140000000001, + 0.782514, + 0.7900900000000001, + 0.749857, + 0.661058, + 0.522559, + 0.34376, + 0.144143, + 0.019156, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.004595, + 0.108375, + 0.310535, + 0.49135700000000004, + 0.626531, + 0.717198, + 0.7497119999999999, + 0.740721, + 0.704237, + 0.6344160000000001, + 0.513544, + 0.34401, + 0.142683, + 0.017547999999999998, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.005557, + 0.109097, + 0.303772, + 0.476466, + 0.6048020000000001, + 0.6937519999999999, + 0.749798, + 0.7649900000000001, + 0.734274, + 0.6551359999999999, + 0.522389, + 0.347792, + 0.147037, + 0.017007, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.005613000000000001, + 0.102176, + 0.274083, + 0.48810000000000003, + 0.6244959999999999, + 0.714416, + 0.759349, + 0.764383, + 0.730348, + 0.6545639999999999, + 0.5248379999999999, + 0.353545, + 0.14971500000000001, + 0.018359, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.007352, + 0.10667700000000001, + 0.222787, + 0.323993, + 0.440432, + 0.5293239999999999, + 0.707286, + 0.803968, + 0.7639729999999999, + 0.670115, + 0.451214, + 0.32716, + 0.149251, + 0.019969, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.007338, + 0.112447, + 0.19577799999999998, + 0.49967399999999995, + 0.638171, + 0.7393869999999999, + 0.791788, + 0.79999, + 0.7529239999999999, + 0.562156, + 0.23397300000000001, + 0.104749, + 0.077098, + 0.018154, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.008945, + 0.117691, + 0.33212400000000003, + 0.527736, + 0.6748379999999999, + 0.779161, + 0.8330960000000001, + 0.833333, + 0.793226, + 0.698527, + 0.56063, + 0.377036, + 0.158525, + 0.019178999999999998, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.009997, + 0.119729, + 0.336048, + 0.524143, + 0.662519, + 0.759269, + 0.81637, + 0.824706, + 0.783884, + 0.692042, + 0.556822, + 0.37542899999999996, + 0.158562, + 0.019191, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.010369999999999999, + 0.118947, + 0.327337, + 0.511619, + 0.646485, + 0.736004, + 0.7887609999999999, + 0.7963060000000001, + 0.757433, + 0.676044, + 0.5433830000000001, + 0.366131, + 0.157183, + 0.020559, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.012357, + 0.12007999999999999, + 0.323774, + 0.5068980000000001, + 0.647683, + 0.7438980000000001, + 0.798995, + 0.810144, + 0.771925, + 0.686506, + 0.5524260000000001, + 0.375444, + 0.160727, + 0.020858, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.011989000000000001, + 0.11749599999999999, + 0.25419200000000003, + 0.398294, + 0.400877, + 0.5124869999999999, + 0.557611, + 0.579722, + 0.5902050000000001, + 0.414634, + 0.382172, + 0.20396, + 0.068842, + 0.015655, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.013439, + 0.122954, + 0.32947000000000004, + 0.511314, + 0.64667, + 0.744016, + 0.796692, + 0.8050470000000001, + 0.7647419999999999, + 0.679663, + 0.550348, + 0.376439, + 0.161832, + 0.020120000000000002, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.011496000000000001, + 0.12006499999999999, + 0.326423, + 0.5072099999999999, + 0.6429389999999999, + 0.738278, + 0.787753, + 0.7917580000000001, + 0.748549, + 0.665971, + 0.539534, + 0.368103, + 0.160248, + 0.019939, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.012114000000000002, + 0.119616, + 0.323852, + 0.499141, + 0.6247820000000001, + 0.706117, + 0.746535, + 0.744782, + 0.715635, + 0.645179, + 0.5263920000000001, + 0.364307, + 0.159252, + 0.019081, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.012166, + 0.12052, + 0.320012, + 0.365527, + 0.603876, + 0.682699, + 0.7220019999999999, + 0.7347229999999999, + 0.7115199999999999, + 0.639481, + 0.521957, + 0.35888600000000004, + 0.157971, + 0.01912, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.012134, + 0.122408, + 0.32899, + 0.356559, + 0.617382, + 0.679177, + 0.726013, + 0.7407, + 0.45909500000000003, + 0.452621, + 0.365512, + 0.365162, + 0.126024, + 0.027617, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.013826, + 0.12295099999999999, + 0.13161099999999998, + 0.213845, + 0.629129, + 0.423654, + 0.553487, + 0.522511, + 0.431276, + 0.429702, + 0.343071, + 0.183126, + 0.09931100000000001, + 0.024231000000000003, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.015090999999999999, + 0.122309, + 0.086023, + 0.12706900000000002, + 0.30466000000000004, + 0.714503, + 0.601279, + 0.5160119999999999, + 0.37555099999999997, + 0.406671, + 0.291643, + 0.17912, + 0.097994, + 0.025934000000000002, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.002282, + 0.18245699999999998, + 0.295768, + 0.31481, + 0.49377499999999996, + 0.595351, + 0.60853, + 0.453839, + 0.39474200000000004, + 0.283304, + 0.21739, + 0.092012, + 0.026334, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.021034, + 0.079773, + 0.13078399999999998, + 0.141793, + 0.14632, + 0.585864, + 0.26032299999999997, + 0.630306, + 0.755147, + 0.666841, + 0.538431, + 0.367719, + 0.168125, + 0.031530999999999997, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.019996, + 0.026963, + 0.201859, + 0.238998, + 0.27355900000000005, + 0.34639600000000004, + 0.31611, + 0.372289, + 0.33307299999999995, + 0.510328, + 0.389312, + 0.249021, + 0.16745, + 0.027084, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.015973, + 0.050921999999999995, + 0.106054, + 0.236125, + 0.28130099999999997, + 0.435716, + 0.773492, + 0.7800779999999999, + 0.743549, + 0.6586219999999999, + 0.531336, + 0.365966, + 0.167825, + 0.028523, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.017864, + 0.027129, + 0.08003199999999999, + 0.13152, + 0.182107, + 0.264315, + 0.356851, + 0.620477, + 0.745107, + 0.65998, + 0.532121, + 0.366289, + 0.16822900000000002, + 0.029331, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.017897, + 0.020992, + 0.078553, + 0.25082, + 0.390909, + 0.46969299999999997, + 0.6200370000000001, + 0.49851, + 0.66011, + 0.6680320000000001, + 0.541029, + 0.373334, + 0.17175, + 0.028512, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.018651, + 0.112599, + 0.282601, + 0.42089, + 0.49280599999999997, + 0.625745, + 0.586268, + 0.48599000000000003, + 0.342324, + 0.419175, + 0.469794, + 0.29239, + 0.151174, + 0.032146, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.017297, + 0.121114, + 0.314191, + 0.48754899999999995, + 0.619668, + 0.7095309999999999, + 0.7584529999999999, + 0.76481, + 0.510628, + 0.457718, + 0.39039100000000004, + 0.375546, + 0.17272300000000002, + 0.027833, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.014946, + 0.124693, + 0.329579, + 0.500985, + 0.6280979999999999, + 0.7166710000000001, + 0.765813, + 0.770787, + 0.733469, + 0.62961, + 0.42783699999999997, + 0.323835, + 0.128507, + 0.029062, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.015742000000000003, + 0.124466, + 0.325493, + 0.496758, + 0.626302, + 0.721232, + 0.7711420000000001, + 0.781747, + 0.744568, + 0.663201, + 0.540038, + 0.374375, + 0.174834, + 0.03215, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.019091, + 0.125897, + 0.318746, + 0.49278, + 0.625355, + 0.718834, + 0.770458, + 0.7843730000000001, + 0.748819, + 0.666632, + 0.539734, + 0.37614299999999995, + 0.173984, + 0.028513999999999998, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.017242999999999998, + 0.124158, + 0.06264600000000001, + 0.178791, + 0.276097, + 0.428327, + 0.7704160000000001, + 0.777531, + 0.738263, + 0.478214, + 0.358983, + 0.367617, + 0.17359200000000002, + 0.031254000000000004, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.01849, + 0.125577, + 0.07566800000000001, + 0.125324, + 0.230327, + 0.7232029999999999, + 0.7762859999999999, + 0.785982, + 0.74378, + 0.661566, + 0.538037, + 0.373517, + 0.09324299999999999, + 0.030423, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.017256, + 0.12506799999999998, + 0.110572, + 0.24290199999999998, + 0.6252559999999999, + 0.720554, + 0.7714610000000001, + 0.77665, + 0.731527, + 0.651872, + 0.52824, + 0.367402, + 0.08143500000000001, + 0.021567, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.017864, + 0.12331, + 0.138566, + 0.476777, + 0.60011, + 0.687781, + 0.744903, + 0.757595, + 0.719746, + 0.6449, + 0.52823, + 0.370871, + 0.175671, + 0.029462, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.017667000000000002, + 0.12262300000000001, + 0.314322, + 0.478148, + 0.601547, + 0.692001, + 0.745992, + 0.759512, + 0.727566, + 0.649312, + 0.5300320000000001, + 0.372968, + 0.176089, + 0.029172, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.017689, + 0.119718, + 0.082723, + 0.188954, + 0.6159260000000001, + 0.70438, + 0.75805, + 0.543986, + 0.356286, + 0.307839, + 0.163659, + 0.101762, + 0.176803, + 0.034805, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.001501, + 0.033394, + 0.208316, + 0.275793, + 0.534165, + 0.6711670000000001, + 0.759541, + 0.7219779999999999, + 0.6432190000000001, + 0.522374, + 0.365564, + 0.177534, + 0.036857999999999995, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.019123, + 0.123994, + 0.17273500000000003, + 0.281583, + 0.420527, + 0.7169030000000001, + 0.768156, + 0.778011, + 0.739894, + 0.589782, + 0.417119, + 0.276129, + 0.17735499999999998, + 0.029594000000000002, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.019771, + 0.1276, + 0.329236, + 0.510213, + 0.64652, + 0.745016, + 0.801312, + 0.8141889999999999, + 0.780685, + 0.6998690000000001, + 0.569515, + 0.402697, + 0.193085, + 0.033735999999999995, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.018617, + 0.12625, + 0.328121, + 0.5059359999999999, + 0.638141, + 0.73481, + 0.788671, + 0.8012469999999999, + 0.764558, + 0.683237, + 0.5587880000000001, + 0.39440800000000004, + 0.188965, + 0.032936, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.016128, + 0.123399, + 0.32430200000000003, + 0.497112, + 0.6244700000000001, + 0.712294, + 0.760725, + 0.774938, + 0.744657, + 0.6708529999999999, + 0.5507989999999999, + 0.391155, + 0.188228, + 0.031150999999999998, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.017238, + 0.037786, + 0.082593, + 0.155993, + 0.266724, + 0.31041599999999997, + 0.52718, + 0.794987, + 0.48786900000000005, + 0.417819, + 0.334315, + 0.230732, + 0.113611, + 0.038749000000000006, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.015051, + 0.121002, + 0.324491, + 0.502126, + 0.6363869999999999, + 0.733967, + 0.789828, + 0.796649, + 0.755122, + 0.675611, + 0.552755, + 0.392126, + 0.18762, + 0.030208, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.014263999999999999, + 0.12002299999999999, + 0.32279199999999997, + 0.50312, + 0.640899, + 0.7380249999999999, + 0.7907240000000001, + 0.800277, + 0.76375, + 0.683246, + 0.5559769999999999, + 0.39469600000000005, + 0.191445, + 0.029419, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.013573, + 0.12124, + 0.32852, + 0.51175, + 0.652975, + 0.756659, + 0.8125800000000001, + 0.82533, + 0.779586, + 0.699496, + 0.5706910000000001, + 0.405403, + 0.195549, + 0.029963, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.016452, + 0.120715, + 0.325237, + 0.501415, + 0.636452, + 0.731885, + 0.786635, + 0.798526, + 0.7681570000000001, + 0.6892780000000001, + 0.562674, + 0.39933300000000005, + 0.193166, + 0.032457, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.016456, + 0.118185, + 0.316253, + 0.489392, + 0.621872, + 0.726076, + 0.7841480000000001, + 0.799498, + 0.767494, + 0.687311, + 0.563801, + 0.401065, + 0.195015, + 0.030074, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.013586, + 0.117809, + 0.319064, + 0.49144299999999996, + 0.623199, + 0.721373, + 0.778143, + 0.7918440000000001, + 0.762365, + 0.6810320000000001, + 0.561431, + 0.40121100000000004, + 0.195976, + 0.032311, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.013515000000000001, + 0.116745, + 0.31525, + 0.485568, + 0.610792, + 0.7035739999999999, + 0.761147, + 0.776, + 0.741736, + 0.6657540000000001, + 0.54587, + 0.388789, + 0.188826, + 0.029390999999999997, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.013968999999999999, + 0.034817, + 0.022286999999999998, + 0.26031299999999996, + 0.19918100000000002, + 0.15325899999999998, + 0.137296, + 0.134786, + 0.218141, + 0.399507, + 0.196552, + 0.14671299999999998, + 0.038700000000000005, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.032327, + 0.06714700000000001, + 0.052448, + 0.114373, + 0.142308, + 0.162687, + 0.14993399999999998, + 0.12407599999999999, + 0.127264, + 0.082131, + 0.13867, + 0.00783, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.012013999999999999, + 0.004995, + 0.01875, + 0.064679, + 0.087142, + 0.08417, + 0.097608, + 0.211388, + 0.246262, + 0.451718, + 0.276364, + 0.37488900000000003, + 0.07338299999999999, + 0.028211, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.010019, + 0.10659199999999999, + 0.302065, + 0.47190899999999997, + 0.603576, + 0.696914, + 0.748737, + 0.75799, + 0.725409, + 0.651159, + 0.53738, + 0.385779, + 0.187872, + 0.026566, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.01062, + 0.10738299999999999, + 0.306761, + 0.47857299999999997, + 0.607958, + 0.7013189999999999, + 0.753356, + 0.763183, + 0.7293740000000001, + 0.6544059999999999, + 0.538975, + 0.387293, + 0.189708, + 0.028763, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.011284, + 0.109024, + 0.303321, + 0.470437, + 0.593844, + 0.686339, + 0.7411599999999999, + 0.757658, + 0.72926, + 0.656399, + 0.5423490000000001, + 0.38921100000000003, + 0.19109700000000002, + 0.029571, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.011295999999999999, + 0.110669, + 0.309966, + 0.48045499999999997, + 0.6034660000000001, + 0.6939270000000001, + 0.748915, + 0.764276, + 0.733005, + 0.663678, + 0.546762, + 0.39276799999999995, + 0.195298, + 0.031758, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.013355, + 0.108283, + 0.305229, + 0.47979, + 0.608902, + 0.701546, + 0.754189, + 0.754992, + 0.7229909999999999, + 0.6466369999999999, + 0.529201, + 0.39785899999999996, + 0.197648, + 0.033838, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.015186999999999999, + 0.004396, + 0.212429, + 0.170851, + 0.393761, + 0.706807, + 0.761787, + 0.771726, + 0.734264, + 0.6604690000000001, + 0.541841, + 0.387553, + 0.19239599999999998, + 0.035893999999999995, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.012277, + 0.105961, + 0.136349, + 0.163071, + 0.333057, + 0.707661, + 0.762103, + 0.7740119999999999, + 0.741294, + 0.668716, + 0.549509, + 0.396413, + 0.19567099999999998, + 0.030087, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.010157, + 0.104338, + 0.300464, + 0.477176, + 0.612568, + 0.707696, + 0.761226, + 0.7703479999999999, + 0.738544, + 0.66174, + 0.545092, + 0.390384, + 0.192938, + 0.032158, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.22877799999999998, + 0.24671, + 0.61099, + 0.518475, + 0.757718, + 0.7679450000000001, + 0.736183, + 0.6628, + 0.546933, + 0.396155, + 0.194349, + 0.033595, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.012846, + 0.104833, + 0.1574, + 0.349649, + 0.5929909999999999, + 0.6817920000000001, + 0.737738, + 0.751026, + 0.734365, + 0.658663, + 0.543479, + 0.391357, + 0.193249, + 0.03269, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.012921, + 0.10499599999999999, + 0.236078, + 0.450474, + 0.573652, + 0.654376, + 0.710185, + 0.72766, + 0.697688, + 0.627068, + 0.5133690000000001, + 0.367672, + 0.186548, + 0.040939, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.011651, + 0.10309399999999999, + 0.274181, + 0.431048, + 0.550468, + 0.602677, + 0.658768, + 0.6761, + 0.6570969999999999, + 0.589818, + 0.479751, + 0.33807600000000004, + 0.174224, + 0.042985999999999996, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.011941, + 0.10133400000000001, + 0.26412599999999997, + 0.41965199999999997, + 0.532482, + 0.591616, + 0.652441, + 0.676687, + 0.647587, + 0.580236, + 0.470041, + 0.324135, + 0.16828700000000002, + 0.043366999999999996, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.012697, + 0.102635, + 0.255157, + 0.411253, + 0.535554, + 0.6492169999999999, + 0.704309, + 0.715807, + 0.657593, + 0.588662, + 0.47988, + 0.339948, + 0.176194, + 0.048122, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.012064, + 0.104624, + 0.26977300000000004, + 0.43310899999999997, + 0.568138, + 0.634916, + 0.708646, + 0.743589, + 0.715839, + 0.646525, + 0.5362089999999999, + 0.391578, + 0.198019, + 0.044446, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.011448999999999999, + 0.103948, + 0.26410500000000003, + 0.42638, + 0.5592820000000001, + 0.615514, + 0.6728200000000001, + 0.6855460000000001, + 0.625622, + 0.550622, + 0.44047699999999995, + 0.301221, + 0.152387, + 0.033542999999999996, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.003758, + 0.09410500000000001, + 0.13639099999999998, + 0.23612200000000003, + 0.328591, + 0.580373, + 0.648182, + 0.674729, + 0.6532190000000001, + 0.590871, + 0.486405, + 0.346995, + 0.179572, + 0.044267, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.010534, + 0.097538, + 0.154745, + 0.32769, + 0.56738, + 0.660772, + 0.719405, + 0.734521, + 0.700231, + 0.626323, + 0.5134249999999999, + 0.364247, + 0.180813, + 0.040645, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.009798999999999999, + 0.09900400000000001, + 0.09458, + 0.182376, + 0.26516, + 0.36677699999999996, + 0.5940979999999999, + 0.460793, + 0.72274, + 0.429085, + 0.527577, + 0.37349400000000005, + 0.184832, + 0.039966999999999996, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.009944000000000001, + 0.097467, + 0.029405, + 0.06197999999999999, + 0.125157, + 0.371761, + 0.5609160000000001, + 0.754015, + 0.72562, + 0.654449, + 0.537044, + 0.38439, + 0.19250399999999998, + 0.037823, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.008166, + 0.099285, + 0.063757, + 0.146037, + 0.249859, + 0.5238250000000001, + 0.724213, + 0.739248, + 0.7250979999999999, + 0.650181, + 0.5356069999999999, + 0.384454, + 0.192366, + 0.034639, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.007744, + 0.09795999999999999, + 0.099763, + 0.23904, + 0.350635, + 0.733731, + 0.7915639999999999, + 0.803003, + 0.773721, + 0.693151, + 0.571206, + 0.410582, + 0.202003, + 0.032241, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.006982, + 0.030633, + 0.089609, + 0.18224500000000002, + 0.29823700000000003, + 0.539766, + 0.767008, + 0.782852, + 0.7498579999999999, + 0.678584, + 0.561551, + 0.403002, + 0.198469, + 0.032985, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.006292, + 0.097045, + 0.094936, + 0.160084, + 0.252238, + 0.330622, + 0.646216, + 0.6573669999999999, + 0.549135, + 0.6783669999999999, + 0.5543640000000001, + 0.392495, + 0.194032, + 0.039469000000000004, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.005793, + 0.017884, + 0.054570999999999995, + 0.074378, + 0.111643, + 0.19058, + 0.424976, + 0.377923, + 0.590525, + 0.44951100000000005, + 0.25779, + 0.252684, + 0.080567, + 0.0013939999999999998, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.017122, + 0.292223, + 0.131564, + 0.129653, + 0.11605, + 0.429508, + 0.45538799999999996, + 0.593252, + 0.696373, + 0.42951999999999996, + 0.313035, + 0.138058, + 0.032211, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.004949, + 0.015502, + 0.066892, + 0.464961, + 0.32999, + 0.670271, + 0.724501, + 0.740326, + 0.695189, + 0.618593, + 0.498122, + 0.346719, + 0.171014, + 0.036234999999999996, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.003669, + 0.092007, + 0.28005, + 0.458039, + 0.595346, + 0.685022, + 0.7494679999999999, + 0.770077, + 0.753625, + 0.678376, + 0.562316, + 0.40467899999999996, + 0.19651400000000002, + 0.030562000000000002, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.004136999999999999, + 0.09312300000000001, + 0.292676, + 0.478173, + 0.6147079999999999, + 0.710612, + 0.7686609999999999, + 0.7813410000000001, + 0.7506649999999999, + 0.678271, + 0.556412, + 0.385995, + 0.188857, + 0.035771000000000004, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.003749, + 0.091194, + 0.282068, + 0.46097899999999997, + 0.595208, + 0.6924239999999999, + 0.751557, + 0.76842, + 0.7278060000000001, + 0.656251, + 0.542549, + 0.38782, + 0.187976, + 0.028846, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0025670000000000003, + 0.08610599999999999, + 0.282717, + 0.463063, + 0.598043, + 0.701151, + 0.763845, + 0.783995, + 0.751491, + 0.679756, + 0.559903, + 0.403275, + 0.19164099999999998, + 0.02395, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.008025000000000001, + 0.085283, + 0.09686499999999999, + 0.14163499999999998, + 0.17796199999999998, + 0.24358000000000002, + 0.311884, + 0.5878150000000001, + 0.575168, + 0.322108, + 0.439795, + 0.20380500000000001, + 0.131167, + 0.026132000000000002, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0022400000000000002, + 0.083544, + 0.09713899999999999, + 0.109293, + 0.16634200000000002, + 0.25605399999999995, + 0.33430200000000004, + 0.468298, + 0.5515249999999999, + 0.49306099999999997, + 0.431448, + 0.391867, + 0.184454, + 0.026316, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.003029, + 0.081888, + 0.091944, + 0.33751, + 0.278038, + 0.7260209999999999, + 0.788763, + 0.804582, + 0.770023, + 0.690458, + 0.562119, + 0.40074, + 0.188723, + 0.023535, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.00139, + 0.078624, + 0.144262, + 0.45333999999999997, + 0.591727, + 0.6882569999999999, + 0.7530030000000001, + 0.772559, + 0.746761, + 0.669008, + 0.5479189999999999, + 0.393201, + 0.18550999999999998, + 0.021201, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0014650000000000002, + 0.08122499999999999, + 0.143595, + 0.47109500000000004, + 0.43645100000000003, + 0.717369, + 0.780265, + 0.7940349999999999, + 0.759884, + 0.684477, + 0.563179, + 0.368029, + 0.17403, + 0.028694, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.001768, + 0.034997, + 0.228962, + 0.413642, + 0.540157, + 0.496544, + 0.632058, + 0.358714, + 0.511038, + 0.5498350000000001, + 0.484404, + 0.352137, + 0.16747700000000001, + 0.028271, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.002297, + 0.081055, + 0.26716, + 0.446297, + 0.588515, + 0.6805979999999999, + 0.7361409999999999, + 0.74602, + 0.704822, + 0.6265729999999999, + 0.5055470000000001, + 0.348312, + 0.164793, + 0.030579000000000002, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.001829, + 0.078529, + 0.153773, + 0.182204, + 0.58836, + 0.684542, + 0.743713, + 0.7519410000000001, + 0.720915, + 0.32249, + 0.288676, + 0.21, + 0.111453, + 0.024106000000000002, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.001389, + 0.076974, + 0.10411799999999999, + 0.231026, + 0.27592, + 0.312024, + 0.442178, + 0.388232, + 0.34825, + 0.653569, + 0.326533, + 0.373606, + 0.17034, + 0.021162, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.001417, + 0.076737, + 0.066887, + 0.130271, + 0.206044, + 0.275166, + 0.598327, + 0.787374, + 0.756013, + 0.437475, + 0.382978, + 0.388443, + 0.176458, + 0.018925, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.000924, + 0.074822, + 0.046741, + 0.09890900000000001, + 0.146153, + 0.21000899999999997, + 0.282202, + 0.297567, + 0.317298, + 0.678545, + 0.553433, + 0.278227, + 0.173966, + 0.016632, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.000482, + 0.07431499999999999, + 0.038613999999999996, + 0.139427, + 0.6136, + 0.351498, + 0.324897, + 0.30159600000000003, + 0.307107, + 0.273677, + 0.171186, + 0.108861, + 0.173599, + 0.017242999999999998, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.00045900000000000004, + 0.074509, + 0.038, + 0.10371, + 0.10421599999999999, + 0.14637999999999998, + 0.438581, + 0.473175, + 0.47200400000000003, + 0.439111, + 0.5521029999999999, + 0.190199, + 0.10216, + 0.017463, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.075034, + 0.082718, + 0.24115199999999998, + 0.606649, + 0.71049, + 0.771027, + 0.786333, + 0.753093, + 0.6750470000000001, + 0.550732, + 0.379487, + 0.1684, + 0.020065000000000003, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.07245, + 0.179595, + 0.143514, + 0.318639, + 0.7108239999999999, + 0.770436, + 0.782548, + 0.748754, + 0.6686259999999999, + 0.545319, + 0.373399, + 0.164659, + 0.018155, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.072958, + 0.27398500000000003, + 0.45807600000000004, + 0.5881130000000001, + 0.684447, + 0.746874, + 0.767076, + 0.738247, + 0.6622100000000001, + 0.5410560000000001, + 0.377513, + 0.16531800000000002, + 0.01358, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.07315600000000001, + 0.168526, + 0.37621699999999997, + 0.6189880000000001, + 0.718658, + 0.7771290000000001, + 0.796036, + 0.763882, + 0.6840700000000001, + 0.556948, + 0.381424, + 0.165737, + 0.016182, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.07219400000000001, + 0.28031, + 0.47402, + 0.613753, + 0.7113780000000001, + 0.7690650000000001, + 0.783811, + 0.754809, + 0.673239, + 0.548581, + 0.380083, + 0.163602, + 0.013112, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.071427, + 0.282519, + 0.476737, + 0.6179600000000001, + 0.721441, + 0.7812910000000001, + 0.802622, + 0.768201, + 0.687123, + 0.558901, + 0.384158, + 0.164227, + 0.012882999999999999, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.07142, + 0.28150200000000003, + 0.47492399999999996, + 0.613799, + 0.710958, + 0.769211, + 0.7819109999999999, + 0.751104, + 0.67379, + 0.5487110000000001, + 0.377618, + 0.161153, + 0.012528000000000001, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.070471, + 0.279532, + 0.471774, + 0.609475, + 0.706569, + 0.7678339999999999, + 0.785687, + 0.751826, + 0.674085, + 0.546129, + 0.37551, + 0.156636, + 0.011427, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.07111100000000001, + 0.275492, + 0.47280700000000003, + 0.618214, + 0.7202799999999999, + 0.781066, + 0.797456, + 0.7635, + 0.68243, + 0.551745, + 0.373727, + 0.15471000000000001, + 0.010248, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.06931699999999999, + 0.27876999999999996, + 0.48077, + 0.628019, + 0.7322329999999999, + 0.792431, + 0.8069700000000001, + 0.7713880000000001, + 0.686742, + 0.554183, + 0.370765, + 0.151665, + 0.009032, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.066729, + 0.060238999999999994, + 0.12030400000000001, + 0.20492, + 0.727187, + 0.789087, + 0.803326, + 0.7692559999999999, + 0.683883, + 0.55015, + 0.367124, + 0.14898599999999998, + 0.007723, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.066943, + 0.26824000000000003, + 0.46156, + 0.59554, + 0.6898289999999999, + 0.749663, + 0.767619, + 0.7413569999999999, + 0.661937, + 0.53327, + 0.35802999999999996, + 0.14319300000000001, + 0.005699, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.064701, + 0.26375299999999996, + 0.449702, + 0.5847469999999999, + 0.686847, + 0.750565, + 0.766986, + 0.7343010000000001, + 0.653555, + 0.526183, + 0.223696, + 0.095664, + 0.00471, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.063134, + 0.056537, + 0.146715, + 0.260139, + 0.708307, + 0.766364, + 0.7750049999999999, + 0.735241, + 0.651503, + 0.523926, + 0.349391, + 0.13699799999999998, + 0.004189, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.06303199999999999, + 0.087004, + 0.16555, + 0.13255, + 0.26376499999999997, + 0.44682, + 0.78854, + 0.75075, + 0.6649149999999999, + 0.531584, + 0.354223, + 0.136024, + 0.00387, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.062834, + 0.061764, + 0.21978899999999998, + 0.604011, + 0.706488, + 0.768179, + 0.780649, + 0.744112, + 0.661617, + 0.531258, + 0.352725, + 0.13499, + 0.0034, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.061985, + 0.110968, + 0.262685, + 0.593878, + 0.696, + 0.755641, + 0.7724289999999999, + 0.740506, + 0.655255, + 0.524693, + 0.339793, + 0.12795600000000001, + 0.0028399999999999996, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.060903, + 0.044276, + 0.091652, + 0.16186, + 0.32560500000000003, + 0.437329, + 0.36658999999999997, + 0.331049, + 0.37931099999999995, + 0.37037200000000003, + 0.116191, + 0.030126999999999998, + 0.004861, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.018888000000000002, + 0.0043040000000000005, + 0.133963, + 0.592598, + 0.45269299999999996, + 0.42350099999999996, + 0.759057, + 0.721438, + 0.497493, + 0.507491, + 0.242527, + 0.12127500000000001, + 0.004615, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.049387, + 0.12957400000000002, + 0.447529, + 0.594086, + 0.696349, + 0.753147, + 0.761147, + 0.722887, + 0.6397619999999999, + 0.343109, + 0.16083699999999998, + 0.118828, + 0.002298, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.046494, + 0.16522900000000001, + 0.222255, + 0.097719, + 0.099006, + 0.181837, + 0.12926300000000002, + 0.147102, + 0.132619, + 0.10119700000000001, + 0.042618, + 0.046629, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0027519999999999997, + 0.115165, + 0.093289, + 0.046822, + 0.176256, + 0.380156, + 0.207031, + 0.2296, + 0.295696, + 0.178033, + 0.26714, + 0.05001, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.046945999999999995, + 0.103502, + 0.22362, + 0.564345, + 0.349068, + 0.7256509999999999, + 0.287511, + 0.271318, + 0.189382, + 0.069565, + 0.105419, + 0.051781, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.005525, + 0.019878, + 0.052762, + 0.016998000000000003, + 0.044707000000000004, + 0.059487000000000005, + 0.094413, + 0.252527, + 0.178067, + 0.18603999999999998, + 0.10785800000000001, + 0.027280000000000002, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.029485, + 0.068054, + 0.2364, + 0.27556400000000003, + 0.503134, + 0.743007, + 0.75637, + 0.724163, + 0.639276, + 0.5033960000000001, + 0.302086, + 0.099577, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.053216, + 0.251101, + 0.454838, + 0.6062580000000001, + 0.719015, + 0.775149, + 0.78623, + 0.74907, + 0.660022, + 0.5203390000000001, + 0.31559699999999996, + 0.101404, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.053634, + 0.252173, + 0.45603899999999997, + 0.608504, + 0.717505, + 0.774586, + 0.7811480000000001, + 0.739577, + 0.6502490000000001, + 0.511695, + 0.309996, + 0.098569, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.052307, + 0.25113800000000003, + 0.45194999999999996, + 0.603209, + 0.70387, + 0.761327, + 0.769518, + 0.733683, + 0.64275, + 0.506339, + 0.312166, + 0.096439, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.051603, + 0.258728, + 0.46372399999999997, + 0.607355, + 0.702324, + 0.748641, + 0.744611, + 0.697677, + 0.612783, + 0.48474599999999995, + 0.304379, + 0.093498, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.051321, + 0.257668, + 0.45863, + 0.594221, + 0.676433, + 0.7114389999999999, + 0.7138089999999999, + 0.6969160000000001, + 0.6223289999999999, + 0.49459699999999995, + 0.308034, + 0.091195, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.050485999999999996, + 0.164012, + 0.458529, + 0.604928, + 0.710269, + 0.764668, + 0.7716839999999999, + 0.730172, + 0.637846, + 0.497673, + 0.304908, + 0.08705800000000001, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.048874, + 0.103821, + 0.21676499999999999, + 0.319427, + 0.683195, + 0.741734, + 0.755362, + 0.7183010000000001, + 0.626524, + 0.490141, + 0.29953399999999997, + 0.084652, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.048909, + 0.252048, + 0.452807, + 0.597665, + 0.6968730000000001, + 0.7520990000000001, + 0.7560520000000001, + 0.710031, + 0.619445, + 0.483213, + 0.29206299999999996, + 0.080476, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.008218999999999999, + 0.062746, + 0.343072, + 0.587448, + 0.679215, + 0.73119, + 0.7393909999999999, + 0.703063, + 0.615703, + 0.481752, + 0.131167, + 0.078533, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.046773, + 0.25224, + 0.450604, + 0.581271, + 0.66724, + 0.621202, + 0.559766, + 0.290469, + 0.295916, + 0.063854, + 0.039456000000000005, + 0.035485, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.007078, + 0.11536, + 0.154068, + 0.239208, + 0.432833, + 0.452584, + 0.500207, + 0.542018, + 0.46946499999999997, + 0.340873, + 0.096689, + 0.025527, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.042451, + 0.021914000000000003, + 0.434939, + 0.585476, + 0.37264600000000003, + 0.535252, + 0.464078, + 0.7079880000000001, + 0.617049, + 0.475537, + 0.272673, + 0.066726, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.041644, + 0.103419, + 0.186767, + 0.5937319999999999, + 0.38583, + 0.347459, + 0.758167, + 0.715911, + 0.619743, + 0.476314, + 0.269769, + 0.06359000000000001, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.041915, + 0.052778, + 0.11571, + 0.221702, + 0.6660119999999999, + 0.707726, + 0.7145750000000001, + 0.6720349999999999, + 0.5862329999999999, + 0.301093, + 0.26199900000000004, + 0.05977300000000001, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.040936, + 0.245555, + 0.451152, + 0.594269, + 0.685691, + 0.725351, + 0.7218239999999999, + 0.674923, + 0.586419, + 0.453869, + 0.26692, + 0.059666, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.03964, + 0.239674, + 0.441878, + 0.579503, + 0.665412, + 0.70053, + 0.683704, + 0.6424249999999999, + 0.572988, + 0.446689, + 0.259878, + 0.055873, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.037937, + 0.23413499999999998, + 0.42833499999999997, + 0.559914, + 0.650413, + 0.70513, + 0.715101, + 0.676275, + 0.586256, + 0.447036, + 0.253321, + 0.051641, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.036616, + 0.23306100000000002, + 0.42856700000000003, + 0.566746, + 0.658908, + 0.712979, + 0.273412, + 0.20024, + 0.210772, + 0.12337300000000001, + 0.048593000000000004, + 0.049364, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.036052, + 0.23161400000000001, + 0.43627699999999997, + 0.430145, + 0.692299, + 0.43016899999999997, + 0.744698, + 0.694641, + 0.600911, + 0.45447000000000004, + 0.251268, + 0.047200000000000006, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.016725999999999998, + 0.11996, + 0.190281, + 0.18260300000000002, + 0.251654, + 0.416777, + 0.311896, + 0.342411, + 0.601939, + 0.45538799999999996, + 0.25166499999999997, + 0.04499, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.034146, + 0.234789, + 0.434764, + 0.5783780000000001, + 0.677605, + 0.729558, + 0.736546, + 0.689751, + 0.595518, + 0.451852, + 0.248224, + 0.042103, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.032858, + 0.234234, + 0.435102, + 0.570606, + 0.662284, + 0.7171230000000001, + 0.725706, + 0.6809850000000001, + 0.589055, + 0.44195999999999996, + 0.236514, + 0.037571, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.030258, + 0.228497, + 0.42511200000000005, + 0.561797, + 0.659902, + 0.718371, + 0.7262569999999999, + 0.682596, + 0.587812, + 0.440435, + 0.237548, + 0.035122999999999995, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.028769, + 0.040116, + 0.149019, + 0.5778260000000001, + 0.676714, + 0.725862, + 0.727406, + 0.677336, + 0.580351, + 0.435901, + 0.232681, + 0.03165, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.005873000000000001, + 0.05745, + 0.070354, + 0.145361, + 0.276031, + 0.318728, + 0.714883, + 0.6678200000000001, + 0.439892, + 0.24615199999999998, + 0.055658, + 0.026646, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.010898, + 0.053048000000000005, + 0.073686, + 0.08358700000000001, + 0.125563, + 0.163946, + 0.154197, + 0.16926, + 0.255404, + 0.222258, + 0.022745, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.021639, + 0.219482, + 0.42365600000000003, + 0.573014, + 0.6631710000000001, + 0.714418, + 0.713721, + 0.662055, + 0.565356, + 0.417231, + 0.21596, + 0.018524000000000002, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.020017, + 0.221425, + 0.426117, + 0.5691710000000001, + 0.6593680000000001, + 0.701554, + 0.69929, + 0.646841, + 0.552672, + 0.410928, + 0.203491, + 0.013211, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.015887000000000002, + 0.210938, + 0.408814, + 0.545889, + 0.628721, + 0.654883, + 0.660805, + 0.626325, + 0.542539, + 0.400688, + 0.15998500000000002, + 0.012243, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.015208000000000001, + 0.21765, + 0.41752999999999996, + 0.549406, + 0.632591, + 0.673997, + 0.677658, + 0.642331, + 0.548235, + 0.401411, + 0.145277, + 0.011348, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.013535, + 0.210733, + 0.406833, + 0.547661, + 0.6461399999999999, + 0.698605, + 0.70241, + 0.6026079999999999, + 0.455407, + 0.20909899999999998, + 0.196403, + 0.008032, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.047063, + 0.07460599999999999, + 0.29309500000000005, + 0.50876, + 0.727662, + 0.728486, + 0.527083, + 0.57286, + 0.41376100000000005, + 0.201266, + 0.008241, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.013196, + 0.218811, + 0.431058, + 0.581635, + 0.355814, + 0.393898, + 0.727796, + 0.672336, + 0.444598, + 0.15283000000000002, + 0.089271, + 0.007621, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.012566, + 0.21428999999999998, + 0.422602, + 0.566615, + 0.664284, + 0.713619, + 0.718136, + 0.6693600000000001, + 0.5692480000000001, + 0.419314, + 0.201549, + 0.006019, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.012557, + 0.22023900000000002, + 0.434192, + 0.582461, + 0.678133, + 0.7249629999999999, + 0.7252569999999999, + 0.670073, + 0.56751, + 0.413868, + 0.195304, + 0.004944, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.010303000000000001, + 0.208334, + 0.420602, + 0.572177, + 0.674265, + 0.723615, + 0.723899, + 0.665089, + 0.558895, + 0.40475099999999997, + 0.18867699999999998, + 0.003299, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.009943, + 0.210407, + 0.416373, + 0.461478, + 0.6478200000000001, + 0.699751, + 0.6996290000000001, + 0.636987, + 0.533709, + 0.380959, + 0.101137, + 0.001592, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.007014, + 0.17302099999999998, + 0.100578, + 0.5501900000000001, + 0.648019, + 0.6374690000000001, + 0.6909120000000001, + 0.639799, + 0.5364, + 0.381908, + 0.16755199999999998, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.004782, + 0.006548, + 0.034409999999999996, + 0.032368, + 0.149793, + 0.101551, + 0.017712, + 0.032756999999999994, + 0.01075, + 0.005068, + 0.016055, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.003782, + 0.011712, + 0.03513, + 0.054396, + 0.191519, + 0.254391, + 0.174083, + 0.181369, + 0.114663, + 0.067932, + 0.027174, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.00456, + 0.19268000000000002, + 0.39708699999999997, + 0.544356, + 0.6422920000000001, + 0.689196, + 0.6861670000000001, + 0.633378, + 0.532332, + 0.379832, + 0.161846, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.003271, + 0.197648, + 0.072952, + 0.359967, + 0.6032029999999999, + 0.674269, + 0.672571, + 0.6217039999999999, + 0.526077, + 0.372739, + 0.117737, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.037519, + 0.118131, + 0.271037, + 0.447394, + 0.565115, + 0.6666470000000001, + 0.6211749999999999, + 0.5185, + 0.368333, + 0.154619, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.002837, + 0.18903399999999998, + 0.395341, + 0.539532, + 0.635814, + 0.684397, + 0.682763, + 0.6285069999999999, + 0.526528, + 0.367362, + 0.152617, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.20630400000000002, + 0.422473, + 0.573425, + 0.6698419999999999, + 0.712172, + 0.70272, + 0.63859, + 0.529393, + 0.373365, + 0.154067, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.193789, + 0.408322, + 0.5538289999999999, + 0.6358830000000001, + 0.672077, + 0.67788, + 0.632753, + 0.528596, + 0.363955, + 0.144868, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.17715799999999998, + 0.037519, + 0.085596, + 0.22354, + 0.150351, + 0.14341399999999999, + 0.190971, + 0.342519, + 0.13118100000000002, + 0.140293, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.019774, + 0.072339, + 0.11859099999999999, + 0.231626, + 0.537931, + 0.693981, + 0.639796, + 0.535243, + 0.368725, + 0.14376499999999998, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.054695999999999995, + 0.150714, + 0.36448, + 0.217615, + 0.546058, + 0.452536, + 0.5567219999999999, + 0.46962, + 0.359802, + 0.134535, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.014599, + 0.397248, + 0.560635, + 0.658057, + 0.720816, + 0.715422, + 0.653673, + 0.325007, + 0.36343200000000003, + 0.133243, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.075804, + 0.23005199999999998, + 0.279673, + 0.61168, + 0.6977490000000001, + 0.491079, + 0.63648, + 0.5232910000000001, + 0.351323, + 0.12747, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.169025, + 0.39040199999999997, + 0.25982, + 0.49509800000000004, + 0.694016, + 0.689037, + 0.6303920000000001, + 0.519901, + 0.348014, + 0.015561, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.163648, + 0.377446, + 0.5316839999999999, + 0.147767, + 0.347079, + 0.667241, + 0.613993, + 0.506486, + 0.34086900000000003, + 0.120567, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.170267, + 0.38594799999999996, + 0.5339740000000001, + 0.626856, + 0.6693669999999999, + 0.6614690000000001, + 0.601403, + 0.49387400000000004, + 0.329827, + 0.114333, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.15903, + 0.365031, + 0.510793, + 0.606676, + 0.6488740000000001, + 0.640845, + 0.586047, + 0.482803, + 0.326324, + 0.112199, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.157155, + 0.367833, + 0.5149320000000001, + 0.6066889999999999, + 0.645025, + 0.635437, + 0.580117, + 0.48292599999999997, + 0.32219200000000003, + 0.108298, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.167697, + 0.045020000000000004, + 0.108587, + 0.19986, + 0.380894, + 0.6550739999999999, + 0.600916, + 0.5009239999999999, + 0.33419600000000005, + 0.110689, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.160291, + 0.054792, + 0.148983, + 0.19856000000000001, + 0.554485, + 0.654157, + 0.595567, + 0.491637, + 0.16375, + 0.106486, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.036153, + 0.13190700000000002, + 0.144793, + 0.155528, + 0.19497, + 0.10528499999999999, + 0.117372, + 0.106992, + 0.067453, + 0.086387, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.145009, + 0.36654899999999996, + 0.525515, + 0.628753, + 0.672117, + 0.66337, + 0.609757, + 0.502155, + 0.33248, + 0.10377800000000001, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.099326, + 0.237247, + 0.217076, + 0.417549, + 0.374998, + 0.400719, + 0.403937, + 0.17356100000000002, + 0.259808, + 0.04269, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.15512399999999998, + 0.38606999999999997, + 0.5509109999999999, + 0.647608, + 0.6922200000000001, + 0.680134, + 0.618429, + 0.503468, + 0.325989, + 0.097506, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.14688800000000002, + 0.367476, + 0.5165230000000001, + 0.6042240000000001, + 0.649552, + 0.652689, + 0.601259, + 0.49030399999999996, + 0.315618, + 0.089229, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.00282, + 0.065374, + 0.06556999999999999, + 0.09196, + 0.081842, + 0.055698, + 0.101548, + 0.08664, + 0.153448, + 0.012676, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.072771, + 0.155337, + 0.30438299999999996, + 0.30615, + 0.301221, + 0.404387, + 0.25063599999999997, + 0.153184, + 0.13897900000000002, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.12151999999999999, + 0.15256299999999998, + 0.204154, + 0.294755, + 0.088546, + 0.125015, + 0.111239, + 0.16584200000000002, + 0.277208, + 0.067205, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.021837, + 0.172847, + 0.333735, + 0.37463, + 0.118137, + 0.166679, + 0.045265, + 0.121636, + 0.0043029999999999995, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.102994, + 0.31802199999999997, + 0.48637, + 0.249355, + 0.21543500000000002, + 0.639988, + 0.5702910000000001, + 0.455561, + 0.279702, + 0.064611, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.11159999999999999, + 0.330081, + 0.492464, + 0.5938479999999999, + 0.639313, + 0.636021, + 0.57711, + 0.465206, + 0.293691, + 0.069892, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.124408, + 0.345734, + 0.510216, + 0.606967, + 0.649924, + 0.639601, + 0.577689, + 0.467798, + 0.292263, + 0.06800199999999999, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.114897, + 0.339818, + 0.502745, + 0.604611, + 0.642788, + 0.628312, + 0.564039, + 0.44971, + 0.282666, + 0.060213, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.100816, + 0.149375, + 0.080733, + 0.143083, + 0.029922, + 0.077057, + 0.327374, + 0.250195, + 0.089389, + 0.046901000000000005, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.105865, + 0.140363, + 0.065292, + 0.12192700000000001, + 0.021096, + 0.03713, + 0.08520799999999999, + 0.095861, + 0.153179, + 0.002419, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.006724, + 0.002274, + 0.218692, + 0.46231900000000004, + 0.63751, + 0.386472, + 0.351781, + 0.248465, + 0.253622, + 0.037558999999999995, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.09554399999999999, + 0.092549, + 0.470888, + 0.568247, + 0.618829, + 0.418186, + 0.292743, + 0.126427, + 0.054466, + 0.005438, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.089524, + 0.31415699999999996, + 0.48466000000000004, + 0.5851369999999999, + 0.632293, + 0.621621, + 0.560523, + 0.44731299999999996, + 0.27578800000000003, + 0.035813000000000005, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.09669199999999999, + 0.329865, + 0.49500299999999997, + 0.589234, + 0.6313, + 0.618016, + 0.555369, + 0.447514, + 0.277829, + 0.042247, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.040038, + 0.100956, + 0.29577499999999995, + 0.440085, + 0.475562, + 0.60354, + 0.549668, + 0.442303, + 0.266445, + 0.031738, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.037166, + 0.359454, + 0.32966, + 0.532563, + 0.629447, + 0.579843, + 0.468908, + 0.19475800000000001, + 0.037732999999999996, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.085157, + 0.33581099999999997, + 0.513514, + 0.6185729999999999, + 0.66907, + 0.663337, + 0.601445, + 0.478952, + 0.28866, + 0.039542, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.08052200000000001, + 0.33885899999999997, + 0.514571, + 0.625602, + 0.676106, + 0.668609, + 0.60321, + 0.483903, + 0.294549, + 0.041943, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.072522, + 0.33386, + 0.5135510000000001, + 0.6163529999999999, + 0.6650320000000001, + 0.657848, + 0.5895370000000001, + 0.467971, + 0.2831, + 0.029879000000000003, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.030361, + 0.16774, + 0.27061799999999997, + 0.430272, + 0.585603, + 0.459374, + 0.45499700000000004, + 0.276931, + 0.197446, + 0.032334, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.051043, + 0.30494499999999997, + 0.48424700000000004, + 0.5955750000000001, + 0.650799, + 0.641634, + 0.580016, + 0.459, + 0.27347000000000005, + 0.030786, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.047353, + 0.301939, + 0.478007, + 0.586032, + 0.634158, + 0.6282709999999999, + 0.568155, + 0.45525299999999996, + 0.27690800000000004, + 0.035792000000000004, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.020583999999999998, + 0.14386500000000002, + 0.269198, + 0.500752, + 0.36566899999999997, + 0.457079, + 0.395386, + 0.161571, + 0.192339, + 0.006709, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0036379999999999997, + 0.040121000000000004, + 0.057902999999999996, + 0.057275, + 0.123192, + 0.085161, + 0.112676, + 0.22146000000000002, + 0.031337, + 0.0059050000000000005, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.042859, + 0.038104, + 0.129663, + 0.079961, + 0.20484200000000002, + 0.062014, + 0.11486, + 0.081828, + 0.000804, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.004687, + 0.127452, + 0.036534, + 0.13252699999999998, + 0.05534, + 0.048898000000000004, + 0.010955999999999999, + 0.030615, + 0.0, + 0.031921, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.027622, + 0.280421, + 0.45673, + 0.571893, + 0.620841, + 0.6171760000000001, + 0.558654, + 0.450077, + 0.273764, + 0.035171, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.029021, + 0.29875599999999997, + 0.488084, + 0.5953890000000001, + 0.649371, + 0.642302, + 0.579766, + 0.459941, + 0.278207, + 0.034389, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.024632, + 0.28309300000000004, + 0.469962, + 0.5844429999999999, + 0.635895, + 0.629531, + 0.568164, + 0.450895, + 0.27214, + 0.03344, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.023339, + 0.280745, + 0.465478, + 0.5748070000000001, + 0.6232190000000001, + 0.620653, + 0.562562, + 0.451157, + 0.275655, + 0.034586, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.022476, + 0.285697, + 0.471498, + 0.5787340000000001, + 0.632184, + 0.627077, + 0.5680599999999999, + 0.456219, + 0.278186, + 0.035561, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.019162, + 0.27452699999999997, + 0.461979, + 0.576836, + 0.625655, + 0.6215900000000001, + 0.563875, + 0.449472, + 0.272434, + 0.033602, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.014070000000000001, + 0.27456200000000003, + 0.465611, + 0.582663, + 0.6310520000000001, + 0.624709, + 0.568688, + 0.453159, + 0.27370999999999995, + 0.033262, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.000549, + 0.06019, + 0.019434999999999997, + 0.27879899999999996, + 0.366548, + 0.119098, + 0.037265, + 0.012471999999999999, + 0.040909999999999995, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.005386999999999999, + 0.09322799999999999, + 0.411584, + 0.3983, + 0.290843, + 0.10846800000000001, + 0.117991, + 0.042327, + 0.024497, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.035429, + 0.020318000000000003, + 0.275372, + 0.449861, + 0.129708, + 0.40101600000000004, + 0.10377, + 0.18141, + 0.005507, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.010394, + 0.017341000000000002, + 0.367632, + 0.134552, + 0.07384, + 0.257359, + 0.176508, + 0.045712, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.03066, + 0.021280999999999998, + 0.102838, + 0.092095, + 0.088723, + 0.030473, + 0.108304, + 0.020708999999999998, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 2.4e-05, + 0.014702999999999999, + 0.07937999999999999, + 0.086605, + 0.072087, + 0.08878100000000001, + 0.014531, + 0.014476000000000001, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.047119999999999995, + 0.10545600000000001, + 0.08333499999999999, + 0.134996, + 0.193302, + 0.11107500000000001, + 0.177164, + 0.016797999999999997, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.051546999999999996, + 0.41633499999999996, + 0.535533, + 0.565662, + 0.447768, + 0.32101, + 0.256375, + 0.104744, + 0.003762, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.126812, + 0.367576, + 0.459642, + 0.36076400000000003, + 0.168426, + 0.203407, + 0.088589, + 0.00328, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0013109999999999999, + 0.11211700000000001, + 0.411627, + 0.5515869999999999, + 0.617072, + 0.617684, + 0.57199, + 0.463167, + 0.28522699999999995, + 0.039644, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.270694, + 0.472675, + 0.59624, + 0.657344, + 0.659728, + 0.602637, + 0.49268900000000004, + 0.30993200000000004, + 0.047591, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.25683999999999996, + 0.45623, + 0.573627, + 0.6295259999999999, + 0.628937, + 0.576683, + 0.463113, + 0.284615, + 0.037795999999999996, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.19900299999999999, + 0.26733100000000004, + 0.53418, + 0.434674, + 0.32583100000000004, + 0.55662, + 0.444478, + 0.184032, + 0.016429, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.016259, + 0.0030099999999999997, + 0.017943999999999998, + 0.043079, + 0.050006, + 0.01554, + 0.01411, + 0.053131, + 0.001287, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.116388, + 0.44166000000000005, + 0.568314, + 0.628334, + 0.630308, + 0.581491, + 0.469296, + 0.29585500000000003, + 0.047240000000000004, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.050122, + 0.399708, + 0.3785, + 0.363068, + 0.5497569999999999, + 0.527504, + 0.39016199999999995, + 0.136928, + 0.037068, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.15160900000000002, + 0.263996, + 0.27651299999999995, + 0.26359899999999997, + 0.283422, + 0.142758, + 0.028759, + 0.219005, + 0.008166, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.10223399999999999, + 0.17571199999999998, + 0.320901, + 0.264941, + 0.318867, + 0.336154, + 0.243585, + 0.026018999999999997, + 0.001746, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.010048, + 0.20984, + 0.23355199999999998, + 0.093792, + 0.06717400000000001, + 0.06515800000000001, + 0.010235, + 0.024574000000000002, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.188452, + 0.39397699999999997, + 0.53483, + 0.61465, + 0.633432, + 0.583827, + 0.47733499999999995, + 0.30777699999999997, + 0.055341999999999995, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.247196, + 0.45849, + 0.589357, + 0.6594869999999999, + 0.664524, + 0.610888, + 0.497631, + 0.322501, + 0.06749200000000001, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.115419, + 0.283392, + 0.497602, + 0.487976, + 0.513755, + 0.482264, + 0.334339, + 0.160522, + 0.069884, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.059505, + 0.41926100000000005, + 0.536193, + 0.598533, + 0.601776, + 0.39882999999999996, + 0.346023, + 0.283566, + 0.021283999999999997, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.083165, + 0.23922, + 0.061994, + 0.13736500000000001, + 0.094553, + 0.134045, + 0.055619, + 0.030178, + 0.069961, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ] + }, + "ElectricStorage": { + "min_kw": 20.0, + "max_kw": 20.0, + "min_kwh": 40.0, + "max_kwh": 40.0, + "soc_init_fraction": 0.5, + "soc_min_fraction": 0.2, + "dispatch_strategy": "daily_foresight_optimized" + }, + "ElectricLoad": { + "year": 2025 + }, + "ElectricTariff": { + "monthly_demand_rates": [200.0, 20.0, 10.0, 10.0, 10.0, 10.0, 10.0, 10.0, 10.0, 10.0, 10.0, 10.0] + }, + "ElectricUtility": { + "net_metering_limit_kw": 0.0 + } +} \ No newline at end of file diff --git a/reoptjl/test/test_job_endpoint.py b/reoptjl/test/test_job_endpoint.py index 501fb1e55..82e3f671e 100644 --- a/reoptjl/test/test_job_endpoint.py +++ b/reoptjl/test/test_job_endpoint.py @@ -113,6 +113,45 @@ def test_process_reopt_error(self): assert(r['messages']['has_stacktrace']==True) assert(resp.status_code==400) + def test_mpc(self): + """ + Purpose of this test is to ensure that the MPC endpoint works as expected + """ + + post_file = os.path.join('reoptjl', 'test', 'posts', 'mpc.json') + post = json.load(open(post_file, 'r')) + + # Checking that MPC correctly identifies peak load of Jan and Feb and fully dispatches BESS for both peaks. + loads_kw = [10] * 8760 + loads_kw[743] = 50 # 1/31/25 23:00 + loads_kw[745] = 100 # 2/1/25 01:00 + post["ElectricLoad"]["loads_kw"] = loads_kw + post["ElectricTariff"]["monthly_energy_rates"] = [0.10] * 12 + + # expected_jan_demand_cost = post["ElectricTariff"]["monthly_demand_rates"][0] * (50 - post["ElectricStorage"]["min_kw"]) + # expected_feb_demand_cost = post["ElectricTariff"]["monthly_demand_rates"][1] * (100 - post["ElectricStorage"]["min_kw"]) + + # resp = self.api_client.post('/v3/job/', format='json', data=post) + # self.assertHttpCreated(resp) + # r = json.loads(resp.content) + # run_uuid = r.get('run_uuid') + + # resp = self.api_client.get(f'/v3/job/{run_uuid}/results') + # r = json.loads(resp.content) + # results = r["outputs"] + # assert(resp.status_code==200) + # self.assertAlmostEqual(results["ElectricTariff"]["monthly_demand_cost_series_before_tax"][0], expected_jan_demand_cost, delta=0.01) + # self.assertAlmostEqual(results["ElectricTariff"]["monthly_demand_cost_series_before_tax"][1], expected_feb_demand_cost, delta=0.01) + + # Should error if off-grid + post["Settings"]["off_grid_flag"] = True + resp = self.api_client.post('/v3/job/', format='json', data=post) + self.assertHttpCreated(resp) + r = json.loads(resp.content) + run_uuid = r.get('run_uuid') + resp = self.api_client.get(f'/v3/job/{run_uuid}/results') + assert(resp.status_code==400) + def test_thermal_in_results(self): """ Purpose of this test is to check that the expected thermal loads, techs, and storage are included in the results diff --git a/reoptjl/views.py b/reoptjl/views.py index 08b1e21e8..5ebf9e64b 100644 --- a/reoptjl/views.py +++ b/reoptjl/views.py @@ -381,6 +381,8 @@ def results(request, run_uuid): if meta.status == "error": return JsonResponse(r, status=400) + if meta.status == "Internal Server Error. See messages for more.": + return JsonResponse(r, status=500) return JsonResponse(r) From db41b1c463a4e15c63515cc4f090abd6c95b25e0 Mon Sep 17 00:00:00 2001 From: adfarth Date: Thu, 30 Jul 2026 18:54:14 -0600 Subject: [PATCH 38/45] Update run_jump_model.py --- reoptjl/src/run_jump_model.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/reoptjl/src/run_jump_model.py b/reoptjl/src/run_jump_model.py index 9644c7bb6..7e0f3980b 100644 --- a/reoptjl/src/run_jump_model.py +++ b/reoptjl/src/run_jump_model.py @@ -74,8 +74,13 @@ def run_jump_model(run_uuid): raise REoptFailedToStartError(task=name, message=response_json["error"], run_uuid=run_uuid, user_uuid=user_uuid) if response.status_code == 400: # REopt.jl returned an input-validation error; store it as an error result so /results returns 400 with the messages. - results = {"status": "error", - "Messages": response_json.get("Messages", {"errors": ["Invalid inputs. Please check your inputs and try again."]})} + if "results" in response_json: + # Regular reopt error: Messages (incl. has_stacktrace) are nested under "results". + results = response_json["results"] + else: + # MPC-style error envelope has Messages at the top level. + results = {"status": "error", + "Messages": response_json.get("Messages", {"errors": ["Invalid inputs. Please check your inputs and try again."]})} reopt_version = response_json.get("reopt_version", "") else: results = response_json["results"] From 7e536d985b1bd1776bdd76a6cc07570b1ce326b6 Mon Sep 17 00:00:00 2001 From: adfarth Date: Fri, 31 Jul 2026 08:36:44 -0600 Subject: [PATCH 39/45] rmv outages check --- julia_src/mpc.jl | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/julia_src/mpc.jl b/julia_src/mpc.jl index 654d44041..855eb9b6f 100644 --- a/julia_src/mpc.jl +++ b/julia_src/mpc.jl @@ -256,14 +256,6 @@ function get_mpc_results!(d::Dict; solver_name::String="HiGHS")::Dict # TODO: show this warning only if tiered rates are detected in the tariff. @warn "Using MPC to determine dispatch. MPC does not model: tiered electricity rates; rates will be flattened to the first tier." - # TODO: Test with outage inputs before enabling this warning. - # # Warning for outage inputs (MPC does not model outages) - # _utility_input = get(d, "ElectricUtility", Dict()) - # if any(k -> haskey(_utility_input, k), ("outage_start_time_step", "outage_start_time_steps", "outage_durations")) - # @warn "MPC: Outage inputs detected (outage_start_time_step, outage_start_time_steps, outage_durations). " * - # "MPC does not model outages; these inputs will be ignored." - # end - ## Set up MPC inputs ## # TODO: MPC horizons and timeout are currently hard coded @@ -439,6 +431,7 @@ function get_mpc_results!(d::Dict; solver_name::String="HiGHS")::Dict soc_init_frac = soc_0 # Build MPC post + # Update this fn if mpc capabilities are updated (e.g., to support outages, multiple PVs, or more tariff inputs) function build_mpc_post(current_horizon_pv, current_horizon_load, current_horizon_energy_rates, current_horizon_emissions, current_horizon_tou_ts, current_horizon_monthly_ts, tou_previous_peak_demands, monthly_previous_peak_demands, soc_init_frac, From deeddf2ddfdad3772d6198bf2a5172b16e35b5b0 Mon Sep 17 00:00:00 2001 From: wbecker Date: Fri, 31 Jul 2026 11:50:57 -0600 Subject: [PATCH 40/45] Update REopt.jl to avoid Generator key error if max_kw=0 --- julia_src/Manifest.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/julia_src/Manifest.toml b/julia_src/Manifest.toml index 1f618bbcd..ab77dd47c 100644 --- a/julia_src/Manifest.toml +++ b/julia_src/Manifest.toml @@ -948,7 +948,7 @@ version = "1.11.0" [[deps.REopt]] deps = ["ArchGDAL", "CSV", "CoolProp", "DataFrames", "Dates", "DelimitedFiles", "HTTP", "JLD", "JSON", "JuMP", "LinDistFlow", "LinearAlgebra", "Logging", "MathOptInterface", "Requires", "Roots", "Statistics", "TestEnv"] -git-tree-sha1 = "d7d3e02b4e692858787aba765e67001bd080521b" +git-tree-sha1 = "f4124784956d7a7dbd65068b0b52ee886276e4b8" repo-rev = "dispatch-options" repo-url = "https://github.com/NatLabRockies/REopt.jl.git" uuid = "d36ad4e8-d74a-4f7a-ace1-eaea049febf6" From 2445ed565ad1144c0416f86d9d704fb3804bd247 Mon Sep 17 00:00:00 2001 From: wbecker Date: Fri, 31 Jul 2026 12:59:41 -0600 Subject: [PATCH 41/45] Allow tech like Generator to work with MPC if max_kw=0. --- julia_src/mpc.jl | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/julia_src/mpc.jl b/julia_src/mpc.jl index 855eb9b6f..2c2aa98f8 100644 --- a/julia_src/mpc.jl +++ b/julia_src/mpc.jl @@ -189,6 +189,13 @@ function get_mpc_results!(d::Dict; solver_name::String="HiGHS")::Dict # Error if any techs other than PV and ElectricStorage are provided mpc_allowed_keys = Set(["PV", "ElectricStorage", "ElectricLoad", "ElectricTariff", "ElectricUtility", "Site", "Settings", "Financial"]) unsupported_keys = setdiff(keys(d), mpc_allowed_keys) + # Ignore disallowed technologies that are explicitly disabled with max_kw = 0. + for key in copy(unsupported_keys) + val = get(d, key, nothing) + if val isa AbstractDict && haskey(val, "max_kw") && val["max_kw"] == 0 + setdiff!(unsupported_keys, [key]) + end + end if !isempty(unsupported_keys) return build_mpc_response( "error", From 6a2e60faf77a520bb97d3a786b6b054a7e343c90 Mon Sep 17 00:00:00 2001 From: lixiangk1 <72464565+lixiangk1@users.noreply.github.com> Date: Mon, 3 Aug 2026 00:11:55 -0600 Subject: [PATCH 42/45] Pre-solve for sizes for SAM dispatch strategies, restructure functions --- julia_src/heuristic_dispatch_sizing.jl | 197 ++++++++++++++++++++++++ julia_src/http.jl | 42 +++++- julia_src/mpc.jl | 200 +++---------------------- 3 files changed, 253 insertions(+), 186 deletions(-) create mode 100644 julia_src/heuristic_dispatch_sizing.jl diff --git a/julia_src/heuristic_dispatch_sizing.jl b/julia_src/heuristic_dispatch_sizing.jl new file mode 100644 index 000000000..a4f0ba8c1 --- /dev/null +++ b/julia_src/heuristic_dispatch_sizing.jl @@ -0,0 +1,197 @@ +# REopt®, Copyright (c) Alliance for Sustainable Energy, LLC. See also https://github.com/NatLabRockies/REopt_API/blob/master/LICENSE. +# ============================================================================ +# Heuristic battery dispatch sizing pre-solve +# ---------------------------------------------------------------------------- +# Validates acceptable technologies and calls REopt to size PV and ElectricStorage +# when the user does not provide fixed sizes for heuristic battery dispatch strategies: +# * daily_foresight_optimized (MPC) +# * peak_shaving_look_ahead, peak_shaving_look_behind, self_consumption (SAM dispatches) +# These strategies require fixed, non-zero technology sizes. +# ============================================================================ + +""" + build_dispatch_response(status; skip_heuristic_dispatch=false, messages=Dict(), result_dict=Dict()) + +Build a consistent heuristic-dispatch response envelope with status, version info, and messages. +Ensures all response paths (success, error, skip) have uniform structure. +""" +function build_dispatch_response(status::String; skip_heuristic_dispatch=false, messages=Dict(), result_dict=Dict()) + response = Dict( + "status" => status, + "reopt_version" => string(pkgversion(reoptjl)), + "Messages" => messages, + "skip_heuristic_dispatch" => skip_heuristic_dispatch, + ) + # Merge result data if provided (for success case) + return merge(response, result_dict) +end + +""" + get_technology_sizes!(d::Dict, model_inputs::reoptjl.REoptInputs, solver_settings::Dict) + +Determine PV and ElectricStorage sizes. If min_kw != max_kw and/or min_kwh != max_kwh, +call REopt to size technologies. User input battery sizes must also be greater than zero. + d is mutated in-place to update PV and ElectricStorage sizes. + model_inputs has already been processed to remove inputs not used in REopt.jl +""" +function get_technology_sizes!(d::Dict, model_inputs::reoptjl.REoptInputs, solver_settings::Dict) + # pv and batt are updated in-place from the dictionary `d` and used in the final REopt run + pv = get!(d, "PV", Dict()) + batt = get!(d, "ElectricStorage", Dict()) + + function is_fixed(dct, lo_key, hi_key) + lo = get(dct, lo_key, nothing) + hi = get(dct, hi_key, nothing) + return lo !== nothing && hi !== nothing && Float64(lo) == Float64(hi) + end + + # Check if both PV and BESS sizes fixed + pv_fixed = is_fixed(pv, "min_kw", "max_kw") + batt_fixed = is_fixed(batt, "min_kw", "max_kw") && + is_fixed(batt, "min_kwh", "max_kwh") + + if pv_fixed && batt_fixed + pv_kw = Float64(pv["min_kw"]) + batt_kw = Float64(batt["min_kw"]) + batt_kwh = Float64(batt["min_kwh"]) + return (pv_kw = pv_kw, batt_kw = batt_kw, batt_kwh = batt_kwh, skip_heuristic_dispatch = false, pv_production_factor_series = nothing) + end + + @info "PV and/or ElectricStorage sizes are not specified — running REopt sizing first." + # TODO: Should we "remove tiers" here for sizing or allow for optimizing with tiers? + + m = get_solver_model(get_solver_model_type(solver_settings["solver_name"]), solver_settings["solver_attributes"]) + + sizing_results = reoptjl.run_reopt(m, model_inputs) + + if get(sizing_results, "status", "") != "optimal" + status = get(sizing_results, "status", "unknown") + msgs = get(sizing_results, "Messages", Dict()) + errs = get(msgs, "errors", []) + warns = get(msgs, "warnings", []) + error("REopt sizing pre-step did not solve (status = $(status)). " * + "REopt errors: $(errs). REopt warnings: $(warns).") + end + + pv_kw = Float64(get(get(sizing_results, "PV", Dict()), "size_kw", 0.0)) + batt_kw = Float64(get(get(sizing_results, "ElectricStorage", Dict()), "size_kw", 0.0)) + batt_kwh = Float64(get(get(sizing_results, "ElectricStorage", Dict()), "size_kwh", 0.0)) + pv_production_factor_series = get(get(sizing_results, "PV", Dict()), "production_factor_series", nothing) + + # Skip the MPC loop if no battery is sized + if batt_kw <= 0.0 || batt_kwh <= 0.0 + return (pv_kw = pv_kw, batt_kw = 0.0, batt_kwh = 0.0, skip_heuristic_dispatch = true, pv_production_factor_series = pv_production_factor_series) + end + + # Fix inputs for final REopt run in http.jl + pv["min_kw"] = pv_kw + pv["max_kw"] = pv_kw + batt["min_kw"] = batt_kw + batt["max_kw"] = batt_kw + batt["min_kwh"] = batt_kwh + batt["max_kwh"] = batt_kwh + + @info "REopt sizing solved with PV = $(pv_kw) kW and battery = $(batt_kw) kW / $(batt_kwh) kWh." + return (; pv_kw, batt_kw, batt_kwh, skip_heuristic_dispatch = false, pv_production_factor_series) +end + +""" + validate_and_size_pv_storage!(d::Dict; solver_name, strategy_label) + +Validates accepted technologies for heuristic battery dispatch strategies (daily_foresight_optimized, +peak_shaving_look_ahead, peak_shaving_look_behind, self_consumption). Then calls get_technology_sizes +to size PV and ElectricStorage if fixed sizes are not defined by the user. +""" +function validate_and_size_pv_storage!(d::Dict; solver_name::String="HiGHS", strategy_label::String="a heuristic battery dispatch strategy") + fail(error_dict) = (; error = error_dict, technology_sizes = nothing, model_inputs = nothing, + solver_settings = Dict(), time_steps_per_hour = 1) + + ## Validation on allowable inputs for predetermined heuristic battery dispatch strategies + # Error if any techs other than PV and ElectricStorage are provided + allowed_keys = Set(["PV", "ElectricStorage", "ElectricLoad", "ElectricTariff", "ElectricUtility", "Site", "Settings", "Financial"]) + unsupported_keys = setdiff(keys(d), allowed_keys) + # Ignore disallowed technologies that are explicitly disabled with max_kw = 0 + for key in copy(unsupported_keys) + val = get(d, key, nothing) + if val isa AbstractDict && haskey(val, "max_kw") && val["max_kw"] == 0 + setdiff!(unsupported_keys, [key]) + end + end + if !isempty(unsupported_keys) + return fail(build_dispatch_response( + "error", + messages = Dict("errors" => ["When using $(strategy_label), only PV and ElectricStorage are supported technologies. " * + "Unsupported inputs found: $(join(unsupported_keys, ", "))."]) + )) + end + + # Heuristic dispatch through the API only supports a single PV, so error on multiple PVs and normalize a one-element array down to a Dict so downstream code can treat d["PV"] as a Dict. + # TODO: Handle multiple PVs + if haskey(d, "PV") && isa(d["PV"], AbstractArray) + if length(d["PV"]) > 1 + return fail(build_dispatch_response( + "error", + messages = Dict("errors" => ["$(strategy_label): Multiple PV systems are not supported at this time."]) + )) + elseif length(d["PV"]) == 1 + d["PV"] = d["PV"][1] + else + delete!(d, "PV") # empty PV array -> treat as no PV + end + end + + # Check max storage sizing is greater than zero + batt = get(d, "ElectricStorage", Dict()) + if Float64(get(batt, "max_kw", 1.0)) <= 0.0 || Float64(get(batt, "max_kwh", 1.0)) <= 0.0 + return fail(build_dispatch_response( + "error", + messages = Dict("errors" => ["When using $(strategy_label), ElectricStorage max_kw and max_kwh must both be greater than zero."]) + )) + end + + settings = get!(d, "Settings", Dict()) + time_steps_per_hour = Int(get(settings, "time_steps_per_hour", 1)) + + # Update sizing_post to remove solver settings and dispatch inputs, to be able to validate inputs using REoptInputs + sizing_post = deepcopy(d) + sizing_settings = get(sizing_post, "Settings", Dict()) + solver_settings = Dict() + delete!(sizing_settings, "run_bau") # Remove run_bau from sizing run + solver_settings["timeout_seconds"] = pop!(sizing_settings, "timeout_seconds", 600) # Only gets used in sizing run + solver_settings["optimality_tolerance"] = pop!(sizing_settings, "optimality_tolerance", 0.001) # Update to a higher value if solve time becomes an issue + solver_settings["solver_attributes"] = SolverAttributes(solver_settings["timeout_seconds"], solver_settings["optimality_tolerance"]) + solver_settings["solver_name"] = solver_name + # Delete inputs specific to the heuristic battery dispatch run + if haskey(sizing_post, "ElectricStorage") + delete!(sizing_post["ElectricStorage"], "dispatch_strategy") + delete!(sizing_post["ElectricStorage"], "fixed_soc_series_fraction") + end + + # Process and validate inputs using REoptInputs + model_inputs = nothing + try + model_inputs = reoptjl.REoptInputs(sizing_post) + + # REoptInputs returns an error Dict (rather than throwing) when input validation fails. + # Surface those messages instead of falling through to get_technology_sizes!, which expects a REoptInputs and would otherwise raise a confusing MethodError. + if isa(model_inputs, Dict) + @error "REopt input validation failed during sizing pre-solve." messages=get(model_inputs, "Messages", Dict()) + return fail(build_dispatch_response( + "error", + messages = get(model_inputs, "Messages", Dict("errors" => ["REopt input validation failed."])) + )) + end + @info "Successfully processed REopt inputs." + catch e + @error "Something went wrong during REopt inputs processing!" exception=(e, catch_backtrace()) + return fail(build_dispatch_response( + "error", + messages = Dict("errors" => [sprint(showerror, e)]) + )) + end + + # If fixed PV and battery sizes are not provided, call REopt first in a sizing run + technology_sizes = get_technology_sizes!(d, model_inputs, solver_settings) + + return (; error = nothing, technology_sizes, model_inputs, solver_settings, time_steps_per_hour) +end diff --git a/julia_src/http.jl b/julia_src/http.jl index 06ec34dfd..6f0dc4eae 100644 --- a/julia_src/http.jl +++ b/julia_src/http.jl @@ -8,6 +8,7 @@ DotEnv.load!() const test_nrel_developer_api_key = ENV["NREL_DEVELOPER_API_KEY"] ENV["NREL_DEVELOPER_EMAIL"] = "reopt@nlr.gov" +include("heuristic_dispatch_sizing.jl") include("mpc.jl") include("os_solvers.jl") @@ -74,20 +75,23 @@ function reopt(req::HTTP.Request) Specify Settings.solver_name = 'HiGHS' or 'Cbc' or 'SCIP'" end - # ---- API-only battery heuristic dispatch strategy: "daily_foresight_optimized" ---- - # When ElectricStorage.dispatch_strategy == "daily_foresight_optimized", if needed, first run REopt to get optimal sizing of PV and battery. - # Then run the MPC rolling-horizon loop to get a SOC profile (skip MPC dispatch if optimal battery size is 0). - # Then set ElectricStorage.fixed_soc_series_fraction = MPC SOC and fix PV and BESS sizing before running the main REopt optimization. + # When ElectricStorage.dispatch_strategy == "daily_foresight_optimized", "peak_shaving_look_ahead", "peak_shaving_look_behind", or + # "self_consumption", if needed, first run REopt to get optimal sizing of PV and battery. Fix PV and battery sizing before running + # the main REopt optimization (skipping heuristic dispatch if optimal battery size is 0). + # For ElectricStorage.dispatch_strategy == daily_foresight_optimized and non-zero battery sizing, run the MPC rolling-horizon loop + # first to get an SOC profile and set ElectricStorage.fixed_soc_series_fraction = MPC SOC. electric_storage = get(d, "ElectricStorage", Dict()) - if get(electric_storage, "dispatch_strategy", nothing) == "daily_foresight_optimized" + dispatch_strategy = get(electric_storage, "dispatch_strategy", nothing) + sam_dispatch_strategies = ("peak_shaving_look_ahead", "peak_shaving_look_behind", "self_consumption") + + if dispatch_strategy == "daily_foresight_optimized" try @info "Running MPC to obtain daily foresight optimized battery dispatch profile." mpc_results = get_mpc_results!(d; solver_name=solver_name) - # TODO: Cache sizing run results and avoid a second call to REopt? Are those the same results? if get(mpc_results, "status", "") == "error" @error "MPC pre-solve failed input validation" messages=get(mpc_results, "Messages", Dict()) return HTTP.Response(400, JSON.json(mpc_results)) - elseif get(mpc_results, "skip_mpc", false) == true + elseif get(mpc_results, "skip_heuristic_dispatch", false) == true @info "Cannot execute daily_foresight_optimized battery dispatch because optimal battery size is 0. Setting dispatch strategy to 'optimized'." d["ElectricStorage"]["dispatch_strategy"] = "optimized" else @@ -102,6 +106,30 @@ function reopt(req::HTTP.Request) "reopt_version" => string(pkgversion(reoptjl)), ))) end + elseif dispatch_strategy in sam_dispatch_strategies + try + @info "Running REopt sizing to fix PV/ElectricStorage sizes for the '$(dispatch_strategy)' dispatch strategy." + sized = validate_and_size_pv_storage!(d; solver_name=solver_name, + strategy_label="the '$(dispatch_strategy)' dispatch strategy") + if sized.error !== nothing + @error "Sizing pre-solve failed input validation" messages=get(sized.error, "Messages", Dict()) + return HTTP.Response(400, JSON.json(sized.error)) + elseif sized.technology_sizes.skip_heuristic_dispatch + @info "Optimal battery size is 0 for the '$(dispatch_strategy)' dispatch strategy. Setting dispatch strategy to 'optimized'." + d["ElectricStorage"]["dispatch_strategy"] = "optimized" + elseif dispatch_strategy == "self_consumption" && + Float64(get(get(d, "PV", Dict()), "existing_kw", 0.0)) + sized.technology_sizes.pv_kw <= 0.0 + @info "The self_consumption dispatch strategy requires PV, but total PV size is 0. Setting dispatch strategy to 'optimized'." + d["ElectricStorage"]["dispatch_strategy"] = "optimized" + end + # else: PV/ElectricStorage sizes are fixed in d; keep dispatch_strategy for the main run + catch e + @error "Sizing pre-solve failed" exception=(e, catch_backtrace()) + return HTTP.Response(500, JSON.json(Dict( + "error" => "Sizing pre-solve failed: " * sprint(showerror, e), + "reopt_version" => string(pkgversion(reoptjl)), + ))) + end end timeout_seconds = pop!(settings, "timeout_seconds") diff --git a/julia_src/mpc.jl b/julia_src/mpc.jl index 2c2aa98f8..506ef4866 100644 --- a/julia_src/mpc.jl +++ b/julia_src/mpc.jl @@ -67,98 +67,6 @@ function generate_pv_production_factors(d::Dict, time_steps_per_hour::Int) return Vector{Float64}(pv_production_factor_series) end -""" - build_mpc_response(status; skip_mpc=false, messages=Dict(), result_dict=Dict()) - -Build a consistent MPC response envelope with status, version info, and messages. -Ensures all response paths (success, error, skip) have uniform structure. -""" -function build_mpc_response(status::String; skip_mpc=false, messages=Dict(), result_dict=Dict()) - response = Dict( - "status" => status, - "reopt_version" => string(pkgversion(reoptjl)), - "Messages" => messages, - "skip_mpc" => skip_mpc, - ) - # Merge result data if provided (for success case) - return merge(response, result_dict) -end - -""" - get_technology_sizes!(d::Dict, model_inputs::reoptjl.REoptInputs, solver_settings::Dict) - -Determine PV and ElectricStorage sizes for the MPC loop. If min_kw != max_kw and/or min_kwh != max_kwh, -call REopt to size technologies. User input battery sizes must also be greater than zero. - d is mutated in-place to update PV and ElectricStorage sizes. - model_inputs has already been processed to remove inputs not used in REopt.jl -""" -function get_technology_sizes!(d::Dict, model_inputs::reoptjl.REoptInputs, solver_settings::Dict) - # pv and batt are updated in-place from the dictionary `d` and used in the final REopt run - pv = get!(d, "PV", Dict()) - batt = get!(d, "ElectricStorage", Dict()) - - function is_fixed(dct, lo_key, hi_key) - lo = get(dct, lo_key, nothing) - hi = get(dct, hi_key, nothing) - return lo !== nothing && hi !== nothing && Float64(lo) == Float64(hi) - end - - # Check storage sizes are greater than zero - if Float64(get(batt, "max_kw", 1.0)) <= 0.0 || Float64(get(batt, "max_kwh", 1.0)) <= 0.0 - error("ElectricStorage max_kw and max_kwh must both be greater than zero " * - "to run the daily_foresight_optimized dispatch option.") - end - - # Check if both PV and BESS sizes fixed - pv_fixed = is_fixed(pv, "min_kw", "max_kw") - batt_fixed = is_fixed(batt, "min_kw", "max_kw") && - is_fixed(batt, "min_kwh", "max_kwh") - - if pv_fixed && batt_fixed - pv_kw = Float64(pv["min_kw"]) - batt_kw = Float64(batt["min_kw"]) - batt_kwh = Float64(batt["min_kwh"]) - return (pv_kw = pv_kw, batt_kw = batt_kw, batt_kwh = batt_kwh, skip_mpc = false, pv_production_factor_series = nothing) - end - - @info "MPC: PV and/or ElectricStorage sizes are not specified — running REopt sizing first." - # TODO: Should we "remove tiers" here for sizing or allow for optimizing with tiers? - - m = get_solver_model(get_solver_model_type(solver_settings["solver_name"]), solver_settings["solver_attributes"]) - - sizing_results = reoptjl.run_reopt(m, model_inputs) - - if get(sizing_results, "status", "") != "optimal" - status = get(sizing_results, "status", "unknown") - msgs = get(sizing_results, "Messages", Dict()) - errs = get(msgs, "errors", []) - warns = get(msgs, "warnings", []) - error("MPC sizing pre-step did not solve (status = $(status)). " * - "REopt errors: $(errs). REopt warnings: $(warns).") - end - - pv_kw = Float64(get(get(sizing_results, "PV", Dict()), "size_kw", 0.0)) - batt_kw = Float64(get(get(sizing_results, "ElectricStorage", Dict()), "size_kw", 0.0)) - batt_kwh = Float64(get(get(sizing_results, "ElectricStorage", Dict()), "size_kwh", 0.0)) - pv_production_factor_series = get(get(sizing_results, "PV", Dict()), "production_factor_series", nothing) - - # Skip the MPC loop if no battery is sized - if batt_kw <= 0.0 || batt_kwh <= 0.0 - return (pv_kw = pv_kw, batt_kw = 0.0, batt_kwh = 0.0, skip_mpc = true, pv_production_factor_series = pv_production_factor_series) - end - - # Fix inputs for final REopt run in http.jl - pv["min_kw"] = pv_kw - pv["max_kw"] = pv_kw - batt["min_kw"] = batt_kw - batt["max_kw"] = batt_kw - batt["min_kwh"] = batt_kwh - batt["max_kwh"] = batt_kwh - - @info "MPC: REopt sizing solved with PV = $(pv_kw) kW and battery = $(batt_kw) kW / $(batt_kwh) kWh." - return (; pv_kw, batt_kw, batt_kwh, skip_mpc = false, pv_production_factor_series) -end - function get_mpc_results!(d::Dict; solver_name::String="HiGHS")::Dict """ Run a full-year rolling-horizon MPC dispatch for PV + ElectricStorage by @@ -171,7 +79,7 @@ function get_mpc_results!(d::Dict; solver_name::String="HiGHS")::Dict - status: "optimal", "error", or "skipped" - reopt_version: Version of REopt.jl used - Messages: Dict with optional errors, warnings, or info - - skip_mpc: Boolean indicating if MPC was skipped + - skip_heuristic_dispatch: Boolean indicating if MPC was skipped For "optimal" status, also includes: - MPC: Metadata (time_steps_per_hour, horizon_time_steps) @@ -185,40 +93,6 @@ function get_mpc_results!(d::Dict; solver_name::String="HiGHS")::Dict """ - ## Validation on allowable inputs for MPC ## - # Error if any techs other than PV and ElectricStorage are provided - mpc_allowed_keys = Set(["PV", "ElectricStorage", "ElectricLoad", "ElectricTariff", "ElectricUtility", "Site", "Settings", "Financial"]) - unsupported_keys = setdiff(keys(d), mpc_allowed_keys) - # Ignore disallowed technologies that are explicitly disabled with max_kw = 0. - for key in copy(unsupported_keys) - val = get(d, key, nothing) - if val isa AbstractDict && haskey(val, "max_kw") && val["max_kw"] == 0 - setdiff!(unsupported_keys, [key]) - end - end - if !isempty(unsupported_keys) - return build_mpc_response( - "error", - messages = Dict("errors" => ["When using MPC (daily_foresight_optimized dispatch), only PV and ElectricStorage are supported technologies. " * - "Unsupported inputs found: $(join(unsupported_keys, ", "))."]) - ) - end - - # MPC only supports a single PV, so error on multiple PVs and normalize a one-element array down to a Dict so downstream code can treat d["PV"] as a Dict. - # TODO: Handle multiple PVs - if haskey(d, "PV") && isa(d["PV"], AbstractArray) - if length(d["PV"]) > 1 - return build_mpc_response( - "error", - messages = Dict("errors" => ["MPC: Multiple PV systems are not supported in MPC runs at this time."]) - ) - elseif length(d["PV"]) == 1 - d["PV"] = d["PV"][1] - else - delete!(d, "PV") # empty PV array -> treat as no PV - end - end - # The checks below are removed because when MPC is called through REopt, the last REopt run will error if these goals are not met. # If MPC is called directly, specifying these inputs IS an issue (because they're not consdired in MPC dispatch). If the MPC endpoint becomes public, we should re-enable these checks. # # Error if unsupported CO2/renewable-fraction constraints are set @@ -238,7 +112,7 @@ function get_mpc_results!(d::Dict; solver_name::String="HiGHS")::Dict # Error for off-grid runs if haskey(d, "Settings") && get(d["Settings"], "off_grid_flag", false) == true - return build_mpc_response( + return build_dispatch_response( "error", messages = Dict("errors" => ["MPC: Off-grid runs are not currently supported in MPC."]) ) @@ -248,13 +122,13 @@ function get_mpc_results!(d::Dict; solver_name::String="HiGHS")::Dict if (haskey(d["ElectricTariff"], "demand_lookback_months") && length(d["ElectricTariff"]["demand_lookback_months"]) > 0 ) || (haskey(d["ElectricTariff"], "demand_lookback_percent") && d["ElectricTariff"]["demand_lookback_percent"] > 0) || (haskey(d["ElectricTariff"], "demand_lookback_range") && d["ElectricTariff"]["demand_lookback_range"] > 0) - return build_mpc_response( + return build_dispatch_response( "error", messages = Dict("errors" => ["MPC: ElectricTariff with demand lookbacks is not currently supported in MPC runs."]) ) end if haskey(d["ElectricTariff"], "coincident_peak_load_active_time_steps") && d["ElectricTariff"]["coincident_peak_load_active_time_steps"] != [Int64[]] - return build_mpc_response( + return build_dispatch_response( "error", messages = Dict("errors" => ["MPC: ElectricTariff with coincident peak charges is not currently supported in MPC runs."]) ) @@ -263,63 +137,31 @@ function get_mpc_results!(d::Dict; solver_name::String="HiGHS")::Dict # TODO: show this warning only if tiered rates are detected in the tariff. @warn "Using MPC to determine dispatch. MPC does not model: tiered electricity rates; rates will be flattened to the first tier." - ## Set up MPC inputs ## - + ## Set up MPC inputs # TODO: MPC horizons and timeout are currently hard coded - settings = get!(d, "Settings", Dict()) - time_steps_per_hour = Int(get(settings, "time_steps_per_hour", 1)) + per_iter_timeout_s = 30.0 length_of_data = 8760 * time_steps_per_hour horizon = 24 * time_steps_per_hour - per_iter_timeout_s = 30.0 - - # Update sizing_post to remove solver settings and dispatch inputs, to be able to validate inputs using REoptInputs - sizing_post = deepcopy(d) - settings = get(sizing_post, "Settings", Dict()) - solver_settings=Dict() - delete!(settings, "run_bau") # Remove run_bau from sizing run - solver_settings["timeout_seconds"] = pop!(settings, "timeout_seconds", 600) # only gets used in sizing run. - solver_settings["optimality_tolerance"] = pop!(settings, "optimality_tolerance", 0.001) # Update to a higher value if solve time becomes an issue - solver_settings["solver_attributes"] = SolverAttributes(solver_settings["timeout_seconds"], solver_settings["optimality_tolerance"]) - solver_settings["solver_name"] = solver_name - # Delete inputs specific to the heuristic battery dispatch run - if haskey(sizing_post, "ElectricStorage") - delete!(sizing_post["ElectricStorage"], "dispatch_strategy") - delete!(sizing_post["ElectricStorage"], "fixed_soc_series_fraction") - end - # Process and validate inputs using REoptInputs - model_inputs = nothing - try - model_inputs = reoptjl.REoptInputs(sizing_post) - - # REoptInputs returns an error Dict (rather than throwing) when input validation fails. - # Surface those messages instead of falling through to get_technology_sizes!, which expects a REoptInputs and would otherwise raise a confusing MethodError. - if isa(model_inputs, Dict) - @error "REopt input validation failed during MPC pre-solve." messages=get(model_inputs, "Messages", Dict()) - return build_mpc_response( - "error", - messages = get(model_inputs, "Messages", Dict("errors" => ["REopt input validation failed."])) - ) - else - @info "Successfully processed REopt inputs." - end - catch e - @error "Something went wrong during REopt inputs processing!" exception=(e, catch_backtrace()) - return build_mpc_response( - "error", - messages = Dict("errors" => [sprint(showerror, e)]) - ) + # Restrict inputs to PV + ElectricStorage and run a REopt "optimized" sizing pass if sizes are + # not user-fixed. This mutates `d` to fix the resulting PV/ElectricStorage sizes. + sized = validate_and_size_pv_storage!(d; solver_name=solver_name, + strategy_label="MPC (daily_foresight_optimized dispatch)") + if sized.error !== nothing + return sized.error end + technology_sizes = sized.technology_sizes + model_inputs = sized.model_inputs + solver_settings = sized.solver_settings + time_steps_per_hour = sized.time_steps_per_hour - # MPC requires fixed PV and battery sizes. If not provided, call REopt first in a sizing run. - technology_sizes = get_technology_sizes!(d, model_inputs, solver_settings) s = model_inputs.s # Access the processed Scenario struct # Skip MPC if no battery is optimally sized - if technology_sizes.skip_mpc - return build_mpc_response( + if technology_sizes.skip_heuristic_dispatch + return build_dispatch_response( "skipped", - skip_mpc = true, + skip_heuristic_dispatch = true, messages = Dict("info" => ["No battery was optimally sized in REopt pre-step; MPC dispatch not needed."]) ) end @@ -432,7 +274,7 @@ function get_mpc_results!(d::Dict; solver_name::String="HiGHS")::Dict ), ) energy_cost_series = Float64[] # grid purchase (energy) charges per timestep - export_benefit_series = Float64[] # NEM/WHL export credits per timestep (positive = revenue) + export_benefit_series = Float64[] # NEM/WHL export credits per timestep (positive = revenue) total_energy_cost = 0.0 total_export_benefit = 0.0 soc_init_frac = soc_0 @@ -593,7 +435,7 @@ function get_mpc_results!(d::Dict; solver_name::String="HiGHS")::Dict tou_demand_cost_total = n_tou_ratchets > 0 ? sum(tou_previous_peak_demands .* tou_demand_rates) : 0.0 - return build_mpc_response( + return build_dispatch_response( "optimal", result_dict = Dict( "MPC" => Dict( From 4b48ba2c94b83abd16dc45f43b22d26c2e9bcf08 Mon Sep 17 00:00:00 2001 From: wbecker Date: Mon, 3 Aug 2026 08:22:39 -0600 Subject: [PATCH 43/45] Fix NREL -> NLR for exception (error) handling messages --- reo/exceptions.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/reo/exceptions.py b/reo/exceptions.py index 1fca1087a..be8ad5751 100644 --- a/reo/exceptions.py +++ b/reo/exceptions.py @@ -1,4 +1,4 @@ -# REopt®, Copyright (c) Alliance for Sustainable Energy, LLC. See also https://github.com/NREL/REopt_API/blob/master/LICENSE. +# REopt®, Copyright (c) Alliance for Sustainable Energy, LLC. See also https://github.com/NatLabRockies/REopt_API/blob/master/LICENSE. from reo.models import ErrorModel import logging log = logging.getLogger(__name__) @@ -22,7 +22,7 @@ def __init__(self, task='', name='', run_uuid='', message='', traceback='', user :param traceback: sys.exc_info()[2] """ if message == "Wind Dataset Timed Out": - msg_with_email = (" Please try again later or email reopt@nrel.gov for support or un-check the wind option " + msg_with_email = (" Please try again later or email reopt@nlr.gov for support or un-check the wind option " "to take wind out of the analysis") elif message.startswith("PV Watts could not locate a dataset station"): msg_with_email = (" Please increase your PV search radius parameter, or choose an alternate " @@ -30,14 +30,14 @@ def __init__(self, task='', name='', run_uuid='', message='', traceback='', user "You can also use a search radius of 0 to return PV Watts results regardless of distance " "to the nearest station.") elif run_uuid: - msg_with_email = (" Please visit https://github.com/NREL/REopt-API-Analysis/discussions for support with " - "common API usage issues. You can also email reopt@nrel.gov with your run_uuid ({}) for support.".format(run_uuid)) + msg_with_email = (" Please visit https://github.com/NatLabRockies/REopt-Analysis-Scripts/discussions for support with " + "common API usage issues. You can also email reopt@nlr.gov with your run_uuid ({}) for support.".format(run_uuid)) elif user_uuid: - msg_with_email = (" Please visit https://github.com/NREL/REopt-API-Analysis/discussions for support with " - "common API usage issues. You can also email reopt@nrel.gov with your user_uuid ({}) for support.".format(user_uuid)) + msg_with_email = (" Please visit https://github.com/NatLabRockies/REopt-Analysis-Scripts/discussions for support with " + "common API usage issues. You can also email reopt@nlr.gov with your user_uuid ({}) for support.".format(user_uuid)) else: - msg_with_email = (" Please visit https://github.com/NREL/REopt-API-Analysis/discussions for support with " - "common API usage issues. You can also email reopt@nrel.gov for support.") + msg_with_email = (" Please visit https://github.com/NatLabRockies/REopt-Analysis-Scripts/discussions for support with " + "common API usage issues. You can also email reopt@nlr.gov for support.") if 'infeasible' not in traceback: self.message = message + msg_with_email # msg_with_email included in messages: error response, but not in error table From 2060164553b155dc7b7fd8e2adc2f6582990f987 Mon Sep 17 00:00:00 2001 From: lixiangk1 <72464565+lixiangk1@users.noreply.github.com> Date: Mon, 3 Aug 2026 09:59:50 -0600 Subject: [PATCH 44/45] Move MPC var def after function call --- julia_src/mpc.jl | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/julia_src/mpc.jl b/julia_src/mpc.jl index 506ef4866..d718e76ad 100644 --- a/julia_src/mpc.jl +++ b/julia_src/mpc.jl @@ -137,12 +137,6 @@ function get_mpc_results!(d::Dict; solver_name::String="HiGHS")::Dict # TODO: show this warning only if tiered rates are detected in the tariff. @warn "Using MPC to determine dispatch. MPC does not model: tiered electricity rates; rates will be flattened to the first tier." - ## Set up MPC inputs - # TODO: MPC horizons and timeout are currently hard coded - per_iter_timeout_s = 30.0 - length_of_data = 8760 * time_steps_per_hour - horizon = 24 * time_steps_per_hour - # Restrict inputs to PV + ElectricStorage and run a REopt "optimized" sizing pass if sizes are # not user-fixed. This mutates `d` to fix the resulting PV/ElectricStorage sizes. sized = validate_and_size_pv_storage!(d; solver_name=solver_name, @@ -155,6 +149,12 @@ function get_mpc_results!(d::Dict; solver_name::String="HiGHS")::Dict solver_settings = sized.solver_settings time_steps_per_hour = sized.time_steps_per_hour + ## Set up MPC inputs + # TODO: MPC horizons and timeout are currently hard coded + per_iter_timeout_s = 30.0 + length_of_data = 8760 * time_steps_per_hour + horizon = 24 * time_steps_per_hour + s = model_inputs.s # Access the processed Scenario struct # Skip MPC if no battery is optimally sized From 424201569fc550addb3bb2f3934b764ec8319aa1 Mon Sep 17 00:00:00 2001 From: wbecker Date: Mon, 3 Aug 2026 12:43:15 -0600 Subject: [PATCH 45/45] Update REopt#dispatch-options to silence result log and update export --- julia_src/Manifest.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/julia_src/Manifest.toml b/julia_src/Manifest.toml index ab77dd47c..1b7baf230 100644 --- a/julia_src/Manifest.toml +++ b/julia_src/Manifest.toml @@ -948,7 +948,7 @@ version = "1.11.0" [[deps.REopt]] deps = ["ArchGDAL", "CSV", "CoolProp", "DataFrames", "Dates", "DelimitedFiles", "HTTP", "JLD", "JSON", "JuMP", "LinDistFlow", "LinearAlgebra", "Logging", "MathOptInterface", "Requires", "Roots", "Statistics", "TestEnv"] -git-tree-sha1 = "f4124784956d7a7dbd65068b0b52ee886276e4b8" +git-tree-sha1 = "516529f9d2d2ea650b02ff8b2089bd42723e7bb1" repo-rev = "dispatch-options" repo-url = "https://github.com/NatLabRockies/REopt.jl.git" uuid = "d36ad4e8-d74a-4f7a-ace1-eaea049febf6"