hrs <- sprintf("h%02d", 0:23) # 24 hourly timeslices: h00 ... h23
YF <- 1 / 365 # this calendar is ONE DAY out of a year
cal <- newCalendar(
timetable = make_timetable(struct = list(ANNUAL = "ANNUAL", HOUR = hrs),
year_fraction = YF),
year_fraction = YF, name = "one_day")7 Electric vehicles: when to charge
In Section 6.3 a vehicle was a technology: fuel in, passenger-kilometres out, now. That is the right object for a fleet’s annual fuel use, and the wrong one for the question that actually makes electric vehicles interesting — when the electricity is drawn from the grid. A technology consumes in the same timeslice it produces. A car charges at night and drives in the morning.
Moving energy between timeslices is what a storage does, so here the vehicle is the storage:
@input |
grid electricity | the on-board charger, kW |
@storage |
the battery | kWh |
@output |
kilometres | the motor, km/h |
The whole chapter runs on one day, resolved hourly:
Saying year_fraction = 1/365 is what makes a timeslice a real hour: each slice gets share = 1/8760, so cap.up = 7 below means 7 kW and needs no further argument. It also sets the slice weight to 365, which is why the costs quoted are a day’s bill repeated across the year.
car <- newStorage(
"CAR",
input = list(comm = "CHG", cap.up = 7), # 7 kW on-board charger
storage = list(comm = "BAT", cap.up = 60), # 60 kWh battery
output = list(comm = "KM"), # kilometres
seff = data.frame(inpeff = 0.90, outeff = 6),
af = data.frame(cinp.up = 1, cout.up = 1),
capacity = list(cap.up = 120), # motor, km/h
duration = data.frame(duration.lo = 0, duration.up = 1e6),
vintage = data.frame(olife = 10L))Two things in there are worth stopping on.
seff is not a round trip here. inpeff = 0.90 is charging loss (grid kWh → battery kWh) but outeff = 6 is six kilometres per kWh — a conversion between two different commodities, not an efficiency between 0 and 1. Because @input, @storage and @output may each name a different commodity, seff became a pair of cross-commodity factors. Only when all three coincide is it the round-trip efficiency you are used to.
duration is switched off on purpose. duration is the ratio of storing capacity to output capacity, and it is normally a number of hours. Here the reservoir is in kWh and the motor in km/h — different dimensions — so the ratio is not an hour count and pinning it would be meaningless. Opening it and bounding the battery directly with storage = list(cap.up = 60) says what we mean.
7.1 5.1 — Passenger car: the schedule is given
Learning goal: a storage whose input, stored and output commodities are all different; charging as a timing decision under a price signal.
The driver commutes 30 km at 08:00 and 30 km back at 18:00 — that is fixed. The grid price is not: cheap before 06:00, expensive between 17:00 and 21:00. The only decision left is when to charge.
price <- ifelse(0:23 < 6, 0.05, ifelse(0:23 >= 17 & 0:23 <= 21, 0.40, 0.15))
m1 <- newModel(
name = "ev_commute", region = "R1", horizon = newHorizon(2020),
discount = 0, calendar = cal, # the one-day calendar above
repo = newRepository("r1",
newCommodity("ELC", timeframe = "HOUR"),
newCommodity("BAT", timeframe = "HOUR"),
newCommodity("KM", timeframe = "HOUR"),
newSupply("GRID", commodity = "ELC",
supply = data.frame(timeslice = hrs, cost = price)),
car,
newDemand("TRIPS", commodity = "KM",
demand = data.frame(timeslice = c("h08", "h18"), demand = 30))))Solved, this draws 11.11 kWh — 60 km ÷ 6 km/kWh ÷ 0.90 — and takes all of it in hours 04 and 05, the last of the cheap window, for an annual bill of 202.78 (0.5556 for the day, repeated 365 times). Charging as late as the cheap tariff allows is not an accident: with nothing else to separate the cheap hours, any of them would do, and the solver simply reports one optimal answer.
Exercises.
5.1a Reproduce the numbers. Check the energy chain by hand: vStorageInp * inpeff == vStorageOut / outeff.
5.1b Shrink the charger to cap.up = 3. How many hours does charging spread over, and does the bill change? Explain why or why not.
5.1c Add a second cheap window at midday (solar). Does the car use it? Add a storage = list(cap.up = 15) small battery and try again — what changed?
5.1d The answer above is degenerate: several charging schedules cost exactly the same. Add a tiny per-kWh varom and confirm the solution stops moving between runs. Why is degeneracy worth noticing in a real study?
7.2 5.2 — Freight van: the schedule is a decision too
Learning goal: what happens when the service requirement is a total rather than a profile, and how to express that with newConstraint().
A delivery van must cover 240 km in the day. When it drives is up to the operator. Instead of an hourly newDemand(), put the requirement on the daily total and let the optimiser choose both the driving hours and the charging hours:
route <- newConstraint(
name = "ROUTE", eq = ">=",
for.each = list(region = "R1", year = 2020L),
term1 = list(variable = "vStorageOut",
for.sum = list(stg = "VAN", comm = "KM", timeslice = hrs)),
defVal = 240)The van (22 kW charger, 100 kWh battery) covers its 240 km and charges entirely in hours 00–05, at an annual cost of 811.11. The driving itself spreads across all 24 hours, because nothing distinguishes them — a degenerate schedule again, and a useful prompt.
Exercises.
5.2a Build it and confirm the 240 km is met and charging sits in the cheap window.
5.2b The driving schedule is degenerate. Add a delivery window — no driving before 07:00 or after 19:00 — with af = data.frame(timeslice = <night hours>, cout.up = 0). Now when does it drive, and when does it charge?
5.2c Give the van a depot-only charger: cinp.up = 0 except between 22:00 and 06:00. Does the route survive? What is the smallest battery that still works?
5.2d Compare against modelling the same van as a newTechnology() with an annual fuel demand. What question can the storage answer that the technology cannot?
The tidy way to say “240 km a day, hours free” would be to declare KM at a DAY timeframe and let the hourly driving aggregate up. That works for a technology — eqTechOutTot has a term that sums finer flows onto a coarser commodity — but the storage equivalent does not yet exist, so the model comes out infeasible. Until it does, the constraint above is the way to express it.
7.4 Where this connects
- Section 6.3 modelled vehicles as technologies. Everything there is still true for annual fuel accounting; this chapter is about timing, which needs storage.
- Chapter 6 introduced multi-input/multi-output processes; a storage with three different commodities is the same idea applied across time.
storage_duration()in energyRt splits a storage level into how long the energy actually sits there — see Section 8.14, where it separates a battery that cycles daily from a hydrogen store that holds energy for months. On a car it would separate the overnight charge from the buffer that never empties.