Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Issue 2382 simulation only #2395

Merged
merged 9 commits into from
Oct 28, 2022
Merged
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,15 @@

## Optimizations

- Reformatted how simulations with experiments are built ([#2395](https://github.com/pybamm-team/PyBaMM/pull/2395))
- Added small perturbation to initial conditions for casadi solver. This seems to help the solver converge better in some cases ([#2356](https://github.com/pybamm-team/PyBaMM/pull/2356))
- Added `ExplicitTimeIntegral` functionality to move variables which do not appear anywhere on the rhs to a new location, and to integrate those variables explicitly when `get` is called by the solution object. ([#2348](https://github.com/pybamm-team/PyBaMM/pull/2348))
- Added more rules for simplifying expressions ([#2211](https://github.com/pybamm-team/PyBaMM/pull/2211))
- Sped up calculations of Electrode SOH variables for summary variables ([#2210](https://github.com/pybamm-team/PyBaMM/pull/2210))

## Breaking change

- Removed `pybamm.SymbolReplacer` as it is no longer needed to set up simulations with experiments, which is the only place where it was being used ([#2395](https://github.com/pybamm-team/PyBaMM/pull/2395))
- Removed `get_infinite_nested_dict`, `BaseModel.check_default_variables_dictionaries`, and `Discretisation.create_jacobian` methods, which were not used by any other functionality in the repository ([#2384](https://github.com/pybamm-team/PyBaMM/pull/2384))
- Dropped support for Python 3.7 after the release of Numpy v1.22.0 ([#2379](https://github.com/pybamm-team/PyBaMM/pull/2379))
- Removed parameter cli tools (add/edit/remove parameters). Parameter sets can now more easily be added via python scripts. ([#2342](https://github.com/pybamm-team/PyBaMM/pull/2342))
Expand Down
2 changes: 1 addition & 1 deletion examples/scripts/experimental_protocols/cccv.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import pybamm
import matplotlib.pyplot as plt

pybamm.set_logging_level("NOTICE")
pybamm.set_logging_level("INFO")
experiment = pybamm.Experiment(
[
(
Expand Down
1 change: 0 additions & 1 deletion pybamm/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,6 @@
from .expression_tree.operations.jacobian import Jacobian
from .expression_tree.operations.convert_to_casadi import CasadiConverter
from .expression_tree.operations.unpack_symbols import SymbolUnpacker
from .expression_tree.operations.replace_symbols import SymbolReplacer
from .expression_tree.operations.evaluate_julia import (
get_julia_function,
get_julia_mtk_model,
Expand Down
238 changes: 105 additions & 133 deletions pybamm/experiments/experiment.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,14 @@ def __init__(
if cccv_handling not in ["two-step", "ode"]:
raise ValueError("cccv_handling should be either 'two-step' or 'ode'")
self.cccv_handling = cccv_handling
# Save arguments for copying
self.args = (
operating_conditions,
period,
termination,
drive_cycles,
cccv_handling,
)

self.period = self.convert_time_to_seconds(period.split())
operating_conditions_cycles = []
Expand All @@ -89,7 +97,9 @@ def __init__(
next_step = None
finished = True
if self.is_cccv(step, next_step):
processed_cycle.append(step + " then " + next_step)
processed_cycle.append(
f"{step} then {next_step[0].lower()}{next_step[1:]}"
)
idx += 2
else:
processed_cycle.append(step)
Expand All @@ -108,58 +118,31 @@ def __init__(
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
).replace(
"\n ", " "
)
"Operating conditions should be strings or tuples of strings, not "
f"{type(badly_typed_conditions[0])}. For example: {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_cycles = operating_conditions_cycles
self.operating_conditions_strings = operating_conditions
self.operating_conditions, self.events = self.read_operating_conditions(
operating_conditions, drive_cycles
)
self.operating_conditions = [
self.read_string(cond, drive_cycles) for cond in operating_conditions
]

self.termination_string = termination
self.termination = self.read_termination(termination)

def __str__(self):
return str(self.operating_conditions_strings)
return str(self.operating_conditions_cycles)

def copy(self):
return Experiment(*self.args)

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
Expand All @@ -178,20 +161,26 @@ def read_string(self, cond, drive_cycles):
# If the string contains " then ", then this is a two-step CCCV experiment
# and we need to split it into two strings
cond_CC, cond_CV = cond.split(" then ")
op_CC, _ = self.read_string(cond_CC, drive_cycles)
op_CV, event_CV = self.read_string(cond_CV, drive_cycles)
return (
{
"electric": op_CC["electric"] + op_CV["electric"],
"time": op_CV["time"],
"period": op_CV["period"],
"dc_data": None,
},
event_CV,
)
op_CC = self.read_string(cond_CC, drive_cycles)
op_CV = self.read_string(cond_CV, drive_cycles)
outputs = {
"type": "CCCV",
"Voltage input [V]": op_CV["Voltage input [V]"],
"time": op_CV["time"],
"period": op_CV["period"],
"dc_data": None,
"string": cond,
"events": op_CV["events"],
}
if "Current input [A]" in op_CC:
outputs["Current input [A]"] = op_CC["Current input [A]"]
else:
outputs["C-rate input [-]"] = op_CC["C-rate input [-]"]
return outputs

# Read period
if " period)" in cond:
cond, time_period = cond.split("(")
cond, time_period = cond.split(" (")
time, _ = time_period.split(" period)")
period = self.convert_time_to_seconds(time.split())
else:
Expand All @@ -207,36 +196,24 @@ def read_string(self, cond, drive_cycles):
"Type of drive cycle must be specified using '(A)', '(V)' or '(W)'."
f" For example: {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_name = cond_list[1] + "_ext_{}".format(end_time)
dc_data = ext_drive_cycle
# Find the type of drive cycle ("A", "V", or "W")
typ = cond_list[2][1]
electric = (dc_name, 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_name = cond_list[1]
dc_data = drive_cycles[cond_list[1]]
# Find the type of drive cycle ("A", "V", or "W")
typ = cond_list[2][1]
electric = (dc_name, 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
# Check for Events
if "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(dc_data, end_time)
# Drive cycle as numpy array
dc_data = ext_drive_cycle
# Set time and period to 1 second for first step and
# then calculate the difference in consecutive time steps
time = dc_data[:, 0][-1]
period = np.min(np.diff(dc_data[:, 0]))
# Find the unit of drive cycle ("A", "V", or "W")
unit = cond_list[2][1]
electric = {"dc_data": dc_data, "unit": unit}
events = None
else:
dc_data = None
if "for" in cond and "or until" in cond:
Expand All @@ -263,19 +240,34 @@ def read_string(self, cond, drive_cycles):
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
).replace(
"\n ", " "
)
"Operating conditions must contain keyword 'for' or 'until' or "
f"'Run'. For example: {examples}"
)

return (
{"electric": electric, "time": time, "period": period, "dc_data": dc_data},
events,
)
self.unit_to_type(electric)
self.unit_to_type(events)

return {
**electric,
"time": time,
"period": period,
"dc_data": dc_data,
"string": cond,
"events": events,
}

def unit_to_type(self, electric):
if electric is not None:
unit = electric.pop("unit")
if unit == "C":
electric["type"] = "C-rate"
elif unit == "A":
electric["type"] = "current"
elif unit == "V":
electric["type"] = "voltage"
elif unit == "W":
electric["type"] = "power"
return electric

def extend_drive_cycle(self, drive_cycle, end_time):
"Extends the drive cycle to enable for event"
Expand Down Expand Up @@ -304,63 +296,34 @@ def convert_electric(self, electric):
"""Convert electrical instructions to consistent output"""
# Rest == zero current
if electric[0].lower() == "rest":
return (0, "A")
return {"Current input [A]": 0, "unit": "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
value_unit = value + unit
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
).replace(
"\n ", " "
)
"Instruction must be 'discharge', 'charge', 'rest', 'hold' or "
f"'Run'. For example: {examples}"
)
elif len(electric) == 2:
# e.g. 3 A, 4.1 V
value, unit = electric
value_unit = value + unit
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(
Expand All @@ -369,19 +332,28 @@ def convert_electric(self, electric):
" ".join(electric), examples
)
)
# e.g. C/2, 1A
if value_unit[0] == "C":
# e.g. C/2
unit = value_unit[0]
value = 1 / float(value_unit[2:])
else:
unit = value_unit[-1:]
if "m" in value_unit:
# e.g. 1mA
value = float(value_unit[:-2]) / 1000
else:
# e.g. 1A
value = float(value_unit[:-1])
# Read value and units
if unit == "C":
return (sign * float(value), "C")
return {"C-rate input [-]": sign * float(value), "unit": unit}
elif unit == "A":
return (sign * float(value), "A")
elif unit == "mA":
return (sign * float(value) / 1000, "A")
return {"Current input [A]": sign * float(value), "unit": unit}
elif unit == "V":
return (float(value), "V")
return {"Voltage input [V]": float(value), "unit": unit}
elif unit == "W":
return (sign * float(value), "W")
elif unit == "mW":
return (sign * float(value) / 1000, "W")
return {"Power input [W]": sign * float(value), "unit": unit}
else:
raise ValueError(
"""units must be 'C', 'A', 'mA', 'V', 'W' or 'mW', not '{}'.
Expand Down Expand Up @@ -462,9 +434,9 @@ def is_cccv(self, step, next_step):
and "Hold at " in next_step
and "V until" in next_step
):
_, events = self.read_string(step, None)
next_op, _ = self.read_string(next_step, None)
op = self.read_string(step, None)
next_op = self.read_string(next_step, None)
# Check that the event conditions are the same as the hold conditions
if events == next_op["electric"]:
if op["events"] == {k: v for k, v in next_op.items() if k in op["events"]}:
return True
return False
Loading