33#
44
55import numpy as np
6+ import re
7+ import pybamm
68
79examples = """
810
9- Discharge at 1C for 0.5 hours,
10- Discharge at C/20 for 0.5 hours,
11- Charge at 0.5 C for 45 minutes,
12- Discharge at 1 A for 90 seconds,
13- Charge at 200mA for 45 minutes (1 minute period),
14- Discharge at 1 W for 0.5 hours,
15- Charge at 200 mW for 45 minutes,
16- Rest for 10 minutes (5 minute period),
17- Hold at 1 V for 20 seconds,
18- Charge at 1 C until 4.1V,
19- Hold at 4.1 V until 50 mA,
20- Hold at 3V until C/50,
21- Run US06 (A),
22- Run US06 (A) for 20 seconds,
23- Run US06 (V) for 45 minutes,
24- Run US06 (W) for 2 hours,
11+ "Discharge at 1C for 0.5 hours at 27oC",
12+ "Discharge at C/20 for 0.5 hours at 29oC",
13+ "Charge at 0.5 C for 45 minutes at -5oC",
14+ "Discharge at 1 A for 0.5 hours at -5.1oC",
15+ "Charge at 200 mA for 45 minutes at 10.2oC (1 minute period)",
16+ "Discharge at 1W for 0.5 hours at -10.4oC",
17+ "Charge at 200mW for 45 minutes",
18+ "Rest for 10 minutes (5 minute period)",
19+ "Hold at 1V for 20 seconds",
20+ "Charge at 1 C until 4.1V",
21+ "Hold at 4.1 V until 50mA",
22+ "Hold at 3V until C/50",
23+ "Discharge at C/3 for 2 hours or until 2.5 V at 26oC",
24+ "Run US06 (A) at -5oC",
25+ "Run US06 (V) for 5 minutes",
26+ "Run US06 (W) for 0.5 hours",
27+
2528 """
2629
2730
@@ -31,12 +34,18 @@ class Experiment:
3134 list of operating conditions should be passed in. Each operating condition should
3235 be of the form "Do this for this long" or "Do this until this happens". For example,
3336 "Charge at 1 C for 1 hour", or "Charge at 1 C until 4.2 V", or "Charge at 1 C for 1
34- hour or until 4.2 V". The instructions can be of the form "(Dis)charge at x A/C/W",
35- "Rest", or "Hold at x V". The running time should be a time in seconds, minutes or
37+ hour or until 4.2 V at 25oC". The instructions can be of the form
38+ "(Dis)charge at x A/C/W", "Rest", or "Hold at x V until y A at z oC". The running
39+ time should be a time in seconds, minutes or
3640 hours, e.g. "10 seconds", "3 minutes" or "1 hour". The stopping conditions should be
3741 a circuit state, e.g. "1 A", "C/50" or "3 V". The parameter drive_cycles is
3842 mandatory to run drive cycle. For example, "Run x", then x must be the key
39- of drive_cycles dictionary.
43+ of drive_cycles dictionary. The temperature should be provided after the stopping
44+ condition but before the period, e.g. "1 A at 25 oC (1 second period)". It is
45+ not essential to provide a temperature and a global temperature can be set either
46+ from within the paramter values of passing a temperature to this experiment class.
47+ If the temperature is not specified in a line, then the global temperature is used,
48+ even if another temperature has been set in an earlier line.
4049
4150 Parameters
4251 ----------
@@ -45,6 +54,10 @@ class Experiment:
4554 period : string, optional
4655 Period (1/frequency) at which to record outputs. Default is 1 minute. Can be
4756 overwritten by individual operating conditions.
57+ temperature: float, optional
58+ The ambient air temperature in degrees Celsius at which to run the experiment.
59+ Default is None whereby the ambient temperature is taken from the parameter set.
60+ This value is overwritten if the temperature is specified in a step.
4861 termination : list, optional
4962 List of conditions under which to terminate the experiment. Default is None.
5063 drive_cycles : dict
@@ -60,6 +73,7 @@ def __init__(
6073 self ,
6174 operating_conditions ,
6275 period = "1 minute" ,
76+ temperature = None ,
6377 termination = None ,
6478 drive_cycles = {},
6579 cccv_handling = "two-step" ,
@@ -71,12 +85,15 @@ def __init__(
7185 self .args = (
7286 operating_conditions ,
7387 period ,
88+ temperature ,
7489 termination ,
7590 drive_cycles ,
7691 cccv_handling ,
7792 )
7893
7994 self .period = self .convert_time_to_seconds (period .split ())
95+ self .temperature = temperature
96+
8097 operating_conditions_cycles = []
8198 for cycle in operating_conditions :
8299 # Check types and convert strings to 1-tuples
@@ -163,16 +180,26 @@ def read_string(self, cond, drive_cycles):
163180 cond_CC , cond_CV = cond .split (" then " )
164181 op_CC = self .read_string (cond_CC , drive_cycles )
165182 op_CV = self .read_string (cond_CV , drive_cycles )
183+
184+ if op_CC ["temperature" ] != op_CV ["temperature" ]:
185+ raise ValueError (
186+ "The temperature for the CC and CV steps must be the same."
187+ f"Got { op_CC ['temperature' ]} and { op_CV ['temperature' ]} "
188+ f"from { op_CC ['string' ]} and { op_CV ['string' ]} "
189+ )
190+
166191 tag_CC = op_CC ["tags" ] or []
167192 tag_CV = op_CV ["tags" ] or []
168193 tags = list (np .unique (tag_CC + tag_CV ))
169194 if len (tags ) == 0 :
170195 tags = None
196+
171197 outputs = {
172198 "type" : "CCCV" ,
173199 "Voltage input [V]" : op_CV ["Voltage input [V]" ],
174200 "time" : op_CV ["time" ],
175201 "period" : op_CV ["period" ],
202+ "temperature" : op_CC ["temperature" ],
176203 "dc_data" : None ,
177204 "string" : cond ,
178205 "events" : op_CV ["events" ],
@@ -198,6 +225,11 @@ def read_string(self, cond, drive_cycles):
198225 period = self .convert_time_to_seconds (time .split ())
199226 else :
200227 period = self .period
228+
229+ # Temperature part of the condition is removed here
230+ unprocessed_cond = cond
231+ temperature , cond = self ._read_and_drop_temperature (cond )
232+
201233 # Read instructions
202234 if "Run" in cond :
203235 cond_list = cond .split ()
@@ -234,13 +266,17 @@ def read_string(self, cond, drive_cycles):
234266 cond_list = cond .split ()
235267 idx_for = cond_list .index ("for" )
236268 idx_until = cond_list .index ("or" )
269+
237270 electric = self .convert_electric (cond_list [:idx_for ])
271+
238272 time = self .convert_time_to_seconds (cond_list [idx_for + 1 : idx_until ])
239273 events = self .convert_electric (cond_list [idx_until + 2 :])
274+
240275 elif "for" in cond :
241276 # e.g. for 3 hours
242277 cond_list = cond .split ()
243278 idx = cond_list .index ("for" )
279+
244280 electric = self .convert_electric (cond_list [:idx ])
245281 time = self .convert_time_to_seconds (cond_list [idx + 1 :])
246282 events = None
@@ -264,8 +300,9 @@ def read_string(self, cond, drive_cycles):
264300 ** electric ,
265301 "time" : time ,
266302 "period" : period ,
303+ "temperature" : temperature ,
267304 "dc_data" : dc_data ,
268- "string" : cond ,
305+ "string" : unprocessed_cond ,
269306 "events" : events ,
270307 "tags" : tags ,
271308 }
@@ -329,6 +366,9 @@ def convert_electric(self, electric):
329366 raise ValueError (
330367 "Instruction must be 'discharge', 'charge', 'rest', 'hold' or "
331368 f"'Run'. For example: { examples } "
369+ ""
370+ "The following instruction does not comply: "
371+ f"{ instruction } "
332372 )
333373 elif len (electric ) == 2 :
334374 # e.g. 3 A, 4.1 V
@@ -377,6 +417,44 @@ def convert_electric(self, electric):
377417 )
378418 )
379419
420+ def _detect_mistyped_temperatures (self , cond ):
421+ if "oC" in cond :
422+ raise ValueError (f"Temperature not written correctly on step: '{ cond } '" )
423+
424+ def _read_and_drop_temperature (self , cond ):
425+ matches = re .findall (r"at\s-*\d+\.*\d*\s*oC" , cond )
426+
427+ if len (matches ) == 0 :
428+ self ._detect_mistyped_temperatures (cond )
429+
430+ if self .temperature is None :
431+ pybamm .logger .warning (
432+ "Temperature not found on step: "
433+ f"'{ cond } ', using temperature "
434+ "from parameter values."
435+ )
436+
437+ else :
438+ pybamm .logger .warning (
439+ f"Temperature not found on step: '{ cond } ', "
440+ f"using global temperature "
441+ f"({ self .temperature } oC) instead"
442+ )
443+
444+ temperature = self .temperature
445+ reduced_cond = cond
446+
447+ elif len (matches ) == 1 :
448+ match = matches [0 ]
449+ numerical_part = re .findall (r"-*\d+\.*\d*" , match )[0 ]
450+ temperature = float (numerical_part )
451+ reduced_cond = cond .replace (match , "" )
452+
453+ else :
454+ raise ValueError (f"More than one temperature found on step: '{ cond } '" )
455+
456+ return temperature , reduced_cond
457+
380458 def convert_time_to_seconds (self , time_and_units ):
381459 """Convert a time in seconds, minutes or hours to a time in seconds"""
382460 time , units = time_and_units
@@ -442,7 +520,7 @@ def is_cccv(self, step, next_step):
442520 # e.g. step="Charge at 2.0 A until 4.2V"
443521 # next_step="Hold at 4.2V until C/50"
444522 if (
445- step .startswith ("Charge" )
523+ ( step .startswith ("Charge" ) or step . startswith ( "Discharge" ) )
446524 and "until" in step
447525 and "V" in step
448526 and "Hold at " in next_step
0 commit comments