-
-
Notifications
You must be signed in to change notification settings - Fork 606
/
Copy pathexperiment.py
406 lines (383 loc) · 16.1 KB
/
experiment.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
#
# Experiment class
#
import numpy as np
examples = """
Discharge at 1C for 0.5 hours,
Discharge at C/20 for 0.5 hours,
Charge at 0.5 C for 45 minutes,
Discharge at 1 A for 90 seconds,
Charge at 200mA for 45 minutes (1 minute period),
Discharge at 1 W for 0.5 hours,
Charge at 200 mW for 45 minutes,
Rest for 10 minutes (5 minute period),
Hold at 1 V for 20 seconds,
Charge at 1 C until 4.1V,
Hold at 4.1 V until 50 mA,
Hold at 3V until C/50,
Run US06 (A),
Run US06 (A) for 20 seconds,
Run US06 (V) for 45 minutes,
Run US06 (W) for 2 hours,
"""
class Experiment:
"""
Base class for experimental conditions under which to run the model. In general, a
list of operating conditions should be passed in. Each operating condition should
be of the form "Do this for this long" or "Do this until this happens". For example,
"Charge at 1 C for 1 hour", or "Charge at 1 C until 4.2 V", or "Charge at 1 C for 1
hour or until 4.2 V". The instructions can be of the form "(Dis)charge at x A/C/W",
"Rest", or "Hold at x V". The running time should be a time in seconds, minutes or
hours, e.g. "10 seconds", "3 minutes" or "1 hour". The stopping conditions should be
a circuit state, e.g. "1 A", "C/50" or "3 V". The parameter drive_cycles is
mandatory to run drive cycle. For example, "Run x", then x must be the key
of drive_cycles dictionary.
Parameters
----------
operating_conditions : list
List of operating conditions
parameters : dict
Dictionary of parameters to use for this experiment, replacing default
parameters as appropriate
period : string, optional
Period (1/frequency) at which to record outputs. Default is 1 minute. Can be
overwritten by individual operating conditions.
termination : list, optional
List of conditions under which to terminate the experiment. Default is None.
use_simulation_setup_type : str
Whether to use the "new" (default) or "old" simulation set-up type. "new" is
faster at simulating individual steps but has higher set-up overhead
drive_cycles : dict
Dictionary of drive cycles to use for this experiment.
"""
def __init__(
self,
operating_conditions,
parameters=None,
period="1 minute",
termination=None,
use_simulation_setup_type="new",
drive_cycles={},
):
self.period = self.convert_time_to_seconds(period.split())
operating_conditions_cycles = []
for cycle in operating_conditions:
# Check types and convert strings to 1-tuples
if (isinstance(cycle, tuple) or isinstance(cycle, str)) and all(
[isinstance(cond, str) for cond in cycle]
):
operating_conditions_cycles.append(
cycle if isinstance(cycle, tuple) else (cycle,)
)
else:
try:
# Condition is not a string
badly_typed_conditions = [
cond for cond in cycle if not isinstance(cond, str)
]
except TypeError:
# Cycle is not a tuple or string
badly_typed_conditions = []
badly_typed_conditions = badly_typed_conditions or [cycle]
raise TypeError(
"""Operating conditions should be strings or tuples of strings, not {}. For example: {}
""".format(
type(badly_typed_conditions[0]), examples
)
)
self.cycle_lengths = [len(cycle) for cycle in operating_conditions_cycles]
operating_conditions = [
cond for cycle in operating_conditions_cycles for cond in cycle
]
self.operating_conditions_strings = operating_conditions
self.operating_conditions, self.events = self.read_operating_conditions(
operating_conditions, drive_cycles
)
parameters = parameters or {}
if isinstance(parameters, dict):
self.parameters = parameters
else:
raise TypeError("experimental parameters should be a dictionary")
self.termination = self.read_termination(termination)
self.use_simulation_setup_type = use_simulation_setup_type
def __str__(self):
return str(self.operating_conditions_strings)
def __repr__(self):
return "pybamm.Experiment({!s})".format(self)
def read_operating_conditions(self, operating_conditions, drive_cycles):
"""
Convert operating conditions to the appropriate format
Parameters
----------
operating_conditions : list
List of operating conditions
drive_cycles : dictionary
Dictionary of Drive Cycles
Returns
-------
operating_conditions : list
Operating conditions in the tuple format
"""
converted_operating_conditions = []
events = []
for cond in operating_conditions:
next_op, next_event = self.read_string(cond, drive_cycles)
converted_operating_conditions.append(next_op)
events.append(next_event)
return converted_operating_conditions, events
def read_string(self, cond, drive_cycles):
"""
Convert a string to a tuple of the right format
Parameters
----------
cond : str
String of appropriate form for example "Charge at x C for y hours". x and y
must be numbers, 'C' denotes the unit of the external circuit (can be A for
current, C for C-rate, V for voltage or W for power), and 'hours' denotes
the unit of time (can be second(s), minute(s) or hour(s))
"""
# Read period
if " period)" in cond:
cond, time_period = cond.split("(")
time, _ = time_period.split(" period)")
period = self.convert_time_to_seconds(time.split())
else:
period = self.period
# Read instructions
if "Run" in cond:
cond_list = cond.split()
if "at" in cond:
raise ValueError(
"""Instruction must be
For example: {}""".format(
examples
)
)
dc_types = ["(A)", "(V)", "(W)"]
if all(x not in cond for x in dc_types):
raise ValueError(
"""Type of drive cycle must be
specified using '(A)', '(V)' or '(W)'.
For example: {}""".format(
examples
)
)
# Check for Events
elif "for" in cond:
# e.g. for 3 hours
idx = cond_list.index("for")
end_time = self.convert_time_to_seconds(cond_list[idx + 1 :])
ext_drive_cycle = self.extend_drive_cycle(
drive_cycles[cond_list[1]], end_time
)
# Drive cycle as numpy array
dc_data = ext_drive_cycle
# Find the type of drive cycle ("A", "V", or "W")
typ = cond_list[2][1]
electric = (dc_data, typ)
time = ext_drive_cycle[:, 0][-1]
period = np.min(np.diff(ext_drive_cycle[:, 0]))
events = None
else:
# e.g. Run US06
# Drive cycle as numpy array
dc_data = drive_cycles[cond_list[1]]
# Find the type of drive cycle ("A", "V", or "W")
typ = cond_list[2][1]
electric = (dc_data, typ)
# Set time and period to 1 second for first step and
# then calculate the difference in consecutive time steps
time = drive_cycles[cond_list[1]][:, 0][-1]
period = np.min(np.diff(drive_cycles[cond_list[1]][:, 0]))
events = None
elif "Run" not in cond:
if "for" in cond and "or until" in cond:
# e.g. for 3 hours or until 4.2 V
cond_list = cond.split()
idx_for = cond_list.index("for")
idx_until = cond_list.index("or")
electric = self.convert_electric(cond_list[:idx_for])
time = self.convert_time_to_seconds(cond_list[idx_for + 1 : idx_until])
events = self.convert_electric(cond_list[idx_until + 2 :])
elif "for" in cond:
# e.g. for 3 hours
cond_list = cond.split()
idx = cond_list.index("for")
electric = self.convert_electric(cond_list[:idx])
time = self.convert_time_to_seconds(cond_list[idx + 1 :])
events = None
elif "until" in cond:
# e.g. until 4.2 V
cond_list = cond.split()
idx = cond_list.index("until")
electric = self.convert_electric(cond_list[:idx])
time = None
events = self.convert_electric(cond_list[idx + 1 :])
else:
raise ValueError(
"""Operating conditions must contain keyword 'for' or 'until' or 'Run'.
For example: {}""".format(
examples
)
)
return electric + (time,) + (period,), events
def extend_drive_cycle(self, drive_cycle, end_time):
"Extends the drive cycle to enable for event"
temp_time = []
temp_time.append(drive_cycle[:, 0])
loop_end_time = temp_time[0][-1]
i = 1
while loop_end_time <= end_time:
# Extend the drive cycle until the drive cycle time
# becomes greater than specified end time
temp_time.append(
np.append(temp_time[i - 1], temp_time[0] + temp_time[i - 1][-1] + 1)
)
loop_end_time = temp_time[i][-1]
i += 1
time = temp_time[-1]
drive_data = np.tile(drive_cycle[:, 1], i)
# Combine the drive cycle time and data
ext_drive_cycle = np.column_stack((time, drive_data))
# Limit the drive cycle to the specified end_time
ext_drive_cycle = ext_drive_cycle[ext_drive_cycle[:, 0] <= end_time]
del temp_time
return ext_drive_cycle
def convert_electric(self, electric):
"""Convert electrical instructions to consistent output"""
# Rest == zero current
if electric[0].lower() == "rest":
return (0, "A")
else:
if len(electric) in [3, 4]:
if len(electric) == 4:
# e.g. Charge at 4 A, Hold at 3 V
instruction, _, value, unit = electric
elif len(electric) == 3:
# e.g. Discharge at C/2, Charge at 1A
instruction, _, value_unit = electric
if value_unit[0] == "C":
# e.g. C/2
unit = value_unit[0]
value = 1 / float(value_unit[2:])
else:
# e.g. 1A
if "m" in value_unit:
# e.g. 1mA
unit = value_unit[-2:]
value = float(value_unit[:-2])
else:
# e.g. 1A
unit = value_unit[-1]
value = float(value_unit[:-1])
# Read instruction
if instruction.lower() in ["discharge", "hold"]:
sign = 1
elif instruction.lower() == "charge":
sign = -1
else:
raise ValueError(
"""Instruction must be 'discharge', 'charge', 'rest', 'hold' or 'Run'.
For example: {}""".format(
examples
)
)
elif len(electric) == 2:
# e.g. 3 A, 4.1 V
value, unit = electric
sign = 1
elif len(electric) == 1:
# e.g. C/2, 1A
value_unit = electric[0]
if value_unit[0] == "C":
# e.g. C/2
unit = value_unit[0]
value = 1 / float(value_unit[2:])
else:
if "m" in value_unit:
# e.g. 1mA
unit = value_unit[-2:]
value = float(value_unit[:-2])
else:
# e.g. 1A
unit = value_unit[-1]
value = float(value_unit[:-1])
sign = 1
else:
raise ValueError(
"""Instruction '{}' not recognized. Some acceptable examples are: {}
""".format(
" ".join(electric), examples
)
)
# Read value and units
if unit == "C":
return (sign * float(value), "C")
elif unit == "A":
return (sign * float(value), "A")
elif unit == "mA":
return (sign * float(value) / 1000, "A")
elif unit == "V":
return (float(value), "V")
elif unit == "W":
return (sign * float(value), "W")
elif unit == "mW":
return (sign * float(value) / 1000, "W")
else:
raise ValueError(
"""units must be 'C', 'A', 'mA', 'V', 'W' or 'mW', not '{}'.
For example: {}
""".format(
unit, examples
)
)
def convert_time_to_seconds(self, time_and_units):
"""Convert a time in seconds, minutes or hours to a time in seconds"""
time, units = time_and_units
if units in ["second", "seconds", "s", "sec"]:
time_in_seconds = float(time)
elif units in ["minute", "minutes", "m", "min"]:
time_in_seconds = float(time) * 60
elif units in ["hour", "hours", "h", "hr"]:
time_in_seconds = float(time) * 3600
else:
raise ValueError(
"""time units must be 'seconds', 'minutes' or 'hours'. For example: {}
""".format(
examples
)
)
return time_in_seconds
def read_termination(self, termination):
"""
Read the termination reason. If this condition is hit, the experiment will stop.
"""
if termination is None:
return {}
elif isinstance(termination, str):
termination = [termination]
termination_dict = {}
for term in termination:
term_list = term.split()
if term_list[-1] == "capacity":
end_discharge = "".join(term_list[:-1])
if end_discharge.endswith("%"):
end_discharge_percent = end_discharge.split("%")[0]
termination_dict["capacity"] = (float(end_discharge_percent), "%")
elif end_discharge.endswith("Ah"):
end_discharge_Ah = end_discharge.split("Ah")[0]
termination_dict["capacity"] = (float(end_discharge_Ah), "Ah")
elif end_discharge.endswith("A.h"):
end_discharge_Ah = end_discharge.split("A.h")[0]
termination_dict["capacity"] = (float(end_discharge_Ah), "Ah")
else:
raise ValueError(
"Capacity termination must be given in the form "
"'80%', '4Ah', or '4A.h'"
)
else:
raise ValueError(
"Only capacity can be provided as a termination reason, "
"e.g. '80% capacity' or '4 Ah capacity'"
)
return termination_dict