flextools 1.13.0
-
Breaking:
charging_power_minis now always a power in kW, insmart_charging(),smart_v2g()andschedule_sessions(). Up to 1.12.0 a value below 1 was a share of the nominal power, socharging_power_min = 0.5meant 50% and now means 0.5 kW: callers passing a share must pass the kW value instead. The default ofschedule_sessions()goes from 0.5 (50%) to 0. A value that is not a single number of at least 0 is an error. - One minimum applies to charging and to discharging: with
charging_power_min = 4,method = "curtail"curtails down to 4 kW and no further, andsmart_v2g()charges or discharges at 4 kW or more, or not at all. A session may still charge less than the minimum in its last charging slot, where it only needs that much to finish (the tail of its charge).smart_v2g()used to push a discharge inside(-min, 0)to the SoC floor’s limit, so a session the floor allowed only -2 kW discharged at -2 kW against a 4 kW minimum; it now does not discharge in that slot. - A session whose nominal
Poweris belowcharging_power_min(e.g. 3.7 kW against 4 kW) is not flexible, in both functions and whatever themethod: it is never responsive, charges as uncontrolled (never curtailed, never discharged), and the setpoint carries its load as fixed instead of planning flexibility it cannot give. -
Departure deadlines in the setpoint.
smart_charging()andsmart_v2g()merge the sessions of a profile into one flexible load, and the setpoint could postpone energy owed to a car that leaves early into the slots of the cars that stay longer, forcing the scheduler to catch up before each departure. The setpoint now keeps, at every slot, at least theenergy_minshare of the energy of the sessions that have already departed. A new internal helper,get_flex_envelope(), computes this and the existing nominal-power ceiling (LFmax) once for both functions. -
smart_v2g()plans charge and discharge only while vehicles are connected: its setpoint is bounded by the nominal power of the connected responsive sessions (andload_capacity) in both directions, where it used to have no bound and could discharge with no vehicle plugged in. Its cumulative load never drops below the energy owed to the departed sessions minus what the connected ones can still discharge down to their SoC floor. When a grid capacity asks for more than the connected vehicles can give (an import cap of -20 kW with 15 kW plugged in, or a cap before the first arrival), the setpoint approaches it as far as their power allows, with a message, before falling back to relaxing the capacity to the original profile. -
smart_v2g()reads the arrival State-of-Charge of each session from a new optionalStateOfChargecolumn ofsessions(a fraction,0 <= StateOfCharge < 1, default 0.5 when the column is absent), and gainssoc_min(last, default 0.2,0 <= soc_min < 1), the SoC floor of every session. Both used to be fixed at 0.5 and 0.2 inside the scheduler. The battery capacity isEnergy / (1 - StateOfCharge): the session’s energy takes it from its arrival SoC to full (twice the energy at 0.5, as before). Withenergy_maxbelow 1 a session leaves below full, atStateOfCharge + energy_max * (1 - StateOfCharge). A session that arrives below its floor charges up to the floor first, as fast as it needs to, and can neither be curtailed nor discharge before it reaches it.
flextools 1.12.0
- New
get_tank_unmet_heat(): the unmet heat of any tank schedule (the heat that would have been required in each slot to keep the tank at least at its comfort floor), such as the static schedule’s.add_tank_optimization()computes itsunmet_heat_kwattribute with it, so both share one definition. -
add_tank_optimization()gainson_failure(last, default"flexible"): with"error", a window the solver fails on stops with an error naming its firstdatetimeand the solver status, instead of runningflexiblewith a message.
flextools 1.11.0
-
add_tank_optimization()gains a resistive backup heater and a soft comfort floor, so every window has a schedule. The new argumentPb(last, defaultNULL) is the backup heater’s rating in kW electric (= kW thermal, COP 1);NULLor 0 means none. The balance becomesS[t] = S[t-1] * (1 - loss/100)^dt + (cop * P + B - heat_demand) * dtwith0 <= B <= Pb(also wherecop = 0). The level is measured above the inlet water temperature (SOC = 0, where the tank gives no more heat). When the compressor and the backup heater cannot holdSOCmin, the tank cools below it, down to the inlet temperature at most, and the unmet heat is the heat that would have been required to keep it at least at its floor (the heat it gives below the floor, plus the demand it cannot serve at the inlet temperature). The backup heater’s load enters the objective like the compressor’s, plus2 * hper kW; going below the floor costs20 * h / dtper kWh and slot (the end-of-window rule becomes soft at the same price),hbeing the window’s highest value of a kW of load in the objective (the highest absolute price for"cost", the largest derivative of the squared net flow for"grid", both weighted for a numeric objective). So the solver uses the compressor first, then the backup heater, and only then lets the tank cool below its floor. The soft floor only enters a window that has no schedule without it. The result gains the attributesbackup_kwandunmet_heat_kw(one value per row, rounded to 2 decimals). Without a backup heater and in windows whose band holds, the results are unchanged. -
add_tank_optimization()makes the grid capacities soft, asadd_battery_optimization()does: a window with no schedule that holds the band within them is solved with the soft comfort floor and a penalised slack on each capacity (10 * hper kW, between the backup heater and the comfort floor), bounded by how far the site misses the capacity with the heat pump runningflexible(import) or off (export). This replaces the relaxation of the capacities to the static schedule’s net flow and the fallback toflexible: every window now has a schedule, and theinfeasible_windowsattribute is gone. A missed capacity is reported with a message. -
add_tank_optimization()also optimises the partial windows the run cuts at both ends: when the first slot is not a window start, the slots up to the firstwindow_start_hourform a leading window (fromSOCini, with the end-of-window rule at its last slot), and the slots after the last full window a trailing one, both limited to their firstflex_window_hourslike any window. Previously they ranflexible. Withflex_window_hours = 24 * window_daysevery slot is now optimised. - The slots that still run
flexible(pastflex_window_hours, or in a window the solver failed on) behave like the tank too: the compressor stops as a thermostat would atSOCmax(the returned power there isflexiblereduced to what keeps the level at the top), and below the floor the tank cools to the inlet temperature at most, with unmet heat. The level therefore never leaves[0, SOCmax], and the clamp into the band at each window start is gone:add_tank_optimization()no longer returnstank_clamp_kwh, andget_tank_level()loses itsclampargument. -
get_tank_level()gainsbackup(default 0) and stops the level at the inlet temperature (0). - New article “Thermal optimization” on the tank model, the backup heater and the soft comfort floor, with a worked example in litres and degrees.
flextools 1.10.0
-
smart_v2g()gainsenergy_max(default1, right afterenergy_min), the same capsmart_charging(energy_max = )already has: every session charges at most this share of the energy it requires, net of discharge. Energy discharged still has to be charged back grossed up bydischarge_eff, so with losses a session takes more thanenergy_maxfrom the grid. The V2G setpoint optimisation now carries betweenenergy_minandenergy_maxof the responsive sessions’ energy (previously exactly all of it): it targetsenergy_max, drops energy down toenergy_minbefore relaxing the grid capacity, and when even the minimum does not fit relaxes the capacity only as far as the minimum-energy profile needs. Both ratios are validated like insmart_charging(). With the defaults (energy_min = energy_max = 1) the results are unchanged.
flextools 1.9.0
- New
add_tank_optimization(): a heat pump charging one thermal tank, optimised on the tank’s heat. It mirrorsadd_battery_optimization()(sameopt_dataconventions, windows fromget_flex_windows(),"grid"and numeric weights on OSQP,"cost"on HiGHS) with the tank in place of the battery: the compressor’s electric powerPheats the tank at the COP, theheat_demandcolumn discharges it, and the level followsS[t] = S[t-1] * (1 - loss/100)^dt + (cop * P - heat_demand) * dtinside[SOCmin, SOCmax]ofTcap, with0 <= P <= Pc,P = 0wherecop = 0, and every window ending at least as full as it started. The level carries across windows.opt_datagainsheat_demand,copandflexible(the static schedule, run outside every window and in a window with no feasible solution). It returns the compressor’s power, with the attributesinfeasible_windows(the firstdatetimeof each window that fell back) andtank_clamp_kwh(the heat the clamp into the band at each window start took out of the tank, signed, per row).SOCini = NULLmeansSOCmax;"capacity"and inputs out of range stop with an error naming the argument. - New
get_tank_level(), the tank’s counterpart ofget_storage_level(): the level in kWh at the start of each slot, applying the clamps it is given.
flextools 1.8.0
-
smart_charging()now splits every session whose connection crosses an optimization window boundary into one part per window before scheduling, with the energy shared by connection time. Previously such a session belonged to the window it started in, was never marked responsive there because its nominal charging ended past the window, and charged unmanaged at full power — with its raw demand added back into the setpoint, so the setpoint itself exceeded the grid capacity. Its flexibility, all of it on the far side of the boundary, was never used. Now each part is scheduled in the window it is connected in; the sessions schedule gains aPartcolumn and a split session returns rows for each part under the sameSessionid (delivered energy is stillsum(Energy)bySession). A part that could not fill one time slot at nominal power is folded into its longest neighbour instead of being emitted —evsim::get_demand()renders sub-slot charging as a whole slot at nominal power, which would inflate the optimizer’s view of it — so a session arriving shortly before a boundary moves whole into the next window. This changes results for any fleet with sessions spanning a window boundary; fleets without such sessions are unaffected (the package’s own golden test was re-captured for the two scenarios where the California fixture crosses the 06:00 boundary; the rest is byte-identical to 1.6.0). - The responsiveness test “charging ends inside the window” compared against the window’s last slot instead of its end, so a session whose charging ended exactly on the next boundary was excluded. Fixed.
-
The 95% arrival-time band is gone. Sessions whose connection times fell outside the profile’s
mean +- 2 sdband in a window were “not considered”: never scheduled, charged unmanaged at full power, and not counted in the responsive share. The band existed to stop one late arrival from stretching the setpoint span and smearing the profile’s energy into slots where only that car was plugged in; the setpoint LP now bounds every slot by the nominal power of the sessions actually connected, which removes that failure mode at the source. Every session that charges inside a window is now considered.set_responsive()loses itsopt_objectiveargument. This changes results for fleets with unusual arrivals. - Responsiveness is now looked up per session by its own
TimecycleandProfile, instead of by the window’s most common time cycle. A Friday session carried over into Saturday’s window keeps Friday’s responsiveness; before, it was looked up under Saturday and, when that profile was not configured for Saturday, left unscheduled. - Fixed: with a single eligible session in a window, the responsive draw used
sample(idx, 1), which samples from1:idxrather than returningidx, so the wrong row could be marked responsive. -
A met capacity no longer comes back one cent over it. Three roundings to 2 decimals stacked up so that a setpoint of 3.00 kW met exactly was reported as 3.01 (33 of 39 over-cap slots on the reference fleet): the flexibility requirement was rounded before the decision to act, the window demand was rebuilt from the session rows through
evsim::get_demand(), which rounds each session’s slot power, and each profile column of the returned demand was rounded separately while callers sum the columns. Decisions are now taken on exact arithmetic against named tolerances (SCHEDULE_FLEX_TOL_KW = 0.001, the unchanged 0.1 kW / 0.025 kWh flexibility thresholds);schedule_sessions()returns the scheduled demand per slot (demand, summed from the exact per-slot powers) andsmart_charging_window()uses it instead of rebuilding it; the session rows and the demand profile carry four decimals. The demand of non-responsive sessions still comes fromevsim::get_demand(). - Fixed a latent energy loss for straddling sessions: the window they spilled into overwrote the profile’s demand with the demand of its own sessions only, so the tail of a straddler vanished whenever another session of the same profile was scheduled that day. No session spills any more.
flextools 1.7.1
- Fixed: with
energy_min = 0, two fallback paths of the 1.7.0 energy range built ac(0, 0)ratio for a follow-up LP, which then stopped with “the maximum energy ratio must be higher than 0” — once when the capacity slice LP removed EV energy and could re-add none of it (the caps were already full of static load), and once when a window was infeasible for a reason energy cannot fix (production above the export capacity, load capacity). The first now returns the profile with the energy simply dropped; the second falls back to the original-profile capacity relaxation, as 1.6.0 did, after the minimum-energy relaxation has been tried where it exists.smart_charging()withenergy_min = 0no longer errors on such windows.
flextools 1.7.0
- New
energy_maxargument insmart_charging(),smart_charging_window()andschedule_sessions(): the maximum share (between 0 and 1) of the energy each session requires that may be charged. Every session stops atenergy_max * Energy, and the setpoint optimization targets exactly that share for the responsive sessions, so the setpoint and the scheduled demand agree — also withmethod = "none", where the setpoint is the result. Default1. Must be at leastenergy_min. -
energy_minnow also bounds the setpoint optimization, not only the scheduler. The energy-conservation row of every demand LP (grid,cost,capacity, combined) becomes a range[energy_min, energy_max] * sum(LF)with a uniform reward on the optimized load that dominates the objective’s marginal gain from dropping energy, so the optimizer keeps as much energy as the grid capacity admits and drops the least that restores feasibility — never belowenergy_min. This changes results for callers passingenergy_min < 1: against a capacity the fleet cannot fit under, the setpoint now holds the capacity and sheds energy, where before it conserved the energy and relaxed the capacity; and where the capacity does not bind, the energy is still delivered in full but a linear objective with a flat price (thecostobjective is a tie among many equal-cost profiles) may land on a different profile than before. At the defaults (energy_min = 1,energy_max = 1) the setpoints are identical to 1.6.0 and the scheduled demand is unchanged up to the scheduler’s own 2-decimal rounding. - The infeasible-window fallback follows the same order: energy first, capacity second. When even
energy_min * sum(LF)does not fit, the grid capacity is relaxed only as far as the minimum-energy profile needs (previously: as far as the full original profile needs), with its own warning message. Theopt_objective = "none"branch ofget_setpoints()likewise inflates the available capacity only as far asenergy_minrequires (previously: always enough for 100% of the energy). -
smart_charging()andschedule_sessions()now validate the ratios and stop onenergy_min > energy_max,energy_max = 0or values outside[0, 1]. - Not touched:
smart_v2g()keeps its own setpoint path and has noenergy_max; V2G is still under development.
flextools 1.6.0
-
The battery
capacityobjective now minimizes battery usage subject to the grid capacity, instead of minimizing the net grid flow with a capacity-sized slice of the battery. It keeps every constraint of the net power formulation and changes only the objective, fromsum((L + B - G)^2)tosum(B^2). This changes results foropt_objective = "capacity": the battery now discharges onto the capacity line rather than below it, cycles only the overshoot volume, and recharges at the lowest power the window allows.The previous formulation reserved a slice of the battery sized to the overshoot and ran the net power objective on it, so the reserve was the only thing holding the optimizer back from flattening. Sizing that reserve against the usable SOC band in 1.5.0 - necessary, since a reserve too small could not clear the window at all - roughly doubled it, and the flattening it had been restraining became plainly visible: on a 4000 kW peak against a 3400 kW capacity the battery pulled the profile down to 2788 kW and cycled twice the energy the overshoot needed. Minimizing the battery directly removes the need for a reserve, so the nameplate no longer decides how hard the battery works - the capacity does, and two batteries that can both clear a window now return the same profile. Use
"grid"to minimize the peak itself. Fixed: the slack penalty was derived from the grid capacities rather than from the flows a solution can actually reach, so a capacity standing in for “unlimited” as a large finite number (rather than
Inf, which is filtered out) produced a penalty large enough to wreck the solver’s conditioning - OSQP returned no solution and the battery was disabled for the whole window. It is now derived frommax|L - G| + max(Bc, Bd), which bounds both objectives’ gradients and is tighter than the capacities in the common case.Fixed: a battery window that fell back to the heuristic or to a disabled battery reported an empty solver status, because the message read
result$info$statuswheresolve_osqp()storesresult$status_message.
flextools 1.5.0
- A grid capacity the battery cannot fully meet is now concentrated rather than spread: the capacity is met exactly for as many slots as the battery’s energy covers, and missed on the remainder. Both spend the same energy and leave the same volume unserved, but the previous behaviour - a consequence of the slack penalty being linear in the volume missed, so the quadratic term chose the flattest of the equally-priced answers - left every affected slot marginally over its capacity.
congestion_timecounts slots, not kWh, so it stayed at 100% of the window for every battery size and only dropped once a battery covered the window outright. This changes results for thegrid,capacityand combined objectives on windows where a capacity is out of reach. Runs the battery misses on power rather than on energy are left alone, since no arrangement of the energy meets the capacity there. Thecostobjective is unaffected: being linear, it already concentrated. - Fixed: the
capacityobjective reserved exactly the overshoot volume as the battery’s capacity, but the SOC band is a percentage of that reserve - so only a fraction of the volume was usable aroundSOCini, and the capacity went unmet however much battery the caller actually had. At the commonSOCini = 50a 4-hour forced export reached -63 kW against its -100 kW capacity, and doubling the battery changed nothing. The reserve is now sized against the usable SOC band.
flextools 1.4.0
- Grid capacities are now soft constraints for the battery: a capacity the battery cannot physically reach is approached at full power instead of being dropped, which previously made the optimizer fall back to plain net-power flattening. The result is still guaranteed never to be worse than the profile without a battery, and slots that can meet their capacity stay strictly capped. Windows with no capacity to miss are solved exactly as before.
- Fixed: a negative
import_capacityorexport_capacity— an obligation to flow the other way, e.g. a congestion contract requiring export — produced an inconsistent variable box (Col N has inconsistent bounds [0, -100]) on thecostand combined objectives. The solve failed and the battery was disabled for the whole window. Capacities are now imposed on the net flow. - Fixed: the
capacityobjective sized its curtailment from the one-sided imported/exported series rather than the net flow, which mis-sized the usable battery capacity whenever a capacity was negative.
flextools 1.3.0
- Changed optimization backend from OSQP to HiGHS solver https://highs.dev/
flextools 1.2.0
- Added V2G functions
- Introduced
timefullypackage - Function
plot_net_powerallows grid capacities indf
flextools 1.1.0
- Added parallel processing
- Multiple bug fix in
smart_charging()function and introduction of “flex types” - Optimization functions accepting
import_capacityandexport_capacityinstead ofgrid_capacity - Improvements in
plot_smart_charging()function (e.g.by = "FlexType") - Added
view_smart_charging_logs()function
