import numpy as np
from dataclasses import dataclass
from pybamm import Experiment
from pybamm.experiment.step.steps import Current
import json
from .connection import connect
from .simulation import Simulation
from dandeliion.client.apps.simulation.core.models.export import BPX
from dandeliion.client.tools.misc import unflatten_dict, update_dict
models = {
'DFN': 'Battery_Pouch_1D',
}
discretizations = {
}
initial_condition_fields = {
'Initial temperature [K]': 'params.cell.T0',
'Initial concentration in electrolyte [mol.m-3]': 'params.cell.c0',
'Initial state of charge': 'params.cell.Z0',
}
sim_params = {
'x_n': 'params.anode.N',
'x_s': 'params.separator.N',
'x_p': 'params.cathode.N',
'r_n': 'params.anode.M',
'r_p': 'params.cathode.M',
}
[docs]
@dataclass
class Simulator:
"""Data class containing informations about simulation server.
Attributes:
credential (tuple[str,str]): tuple consisting of the username and password
"""
# server: str
credential: tuple[str, str]
@dataclass
class DandeliionExperiment:
"""Class contains information extracted from :class:`Experiment`
to be used in Dandellion simulations
Attributes:
current(dict): dictionary for times and currents
t_output (list): list of times where outputs should be generated
t_max (float): final time for simulation run (if not stopped by other criteria)
V_min (float): minimum voltage allowed in simulation run (stop criterion)
"""
current: dict
t_output: list
t_max: float
V_min: float
def convertExperiment(
experiment: Experiment, # pybamm Experiment
dt_eval: float, # sets minimum step size for discretisation
) -> DandeliionExperiment:
""" Processes :class:`Experiment` and returns :class:`DandeliionExperiment`
Args:
experiment (Experiment): A pybamm Experiment
dt_eval (float): sets minimum step size for discretisation
Returns:
DandeliionExperiment: instance of Dandeliion Experiment class containing the
processed information of the provided pybamm Experiment instance
"""
# check for termination condition (max time, min voltage opt., others fail)
V_min = experiment.termination.get('voltage', None)
t_max = None
t_output = None
if 'time' in experiment.termination:
t_max = experiment.termination['time'] # seconds
t_output = np.arange(0., t_max, experiment.period).tolist()
# check for unsupported termination conditions
if set(experiment.termination.keys()) - {'time', 'voltage'}:
raise NotImplementedError("Only supported termination conditions are 'time' and 'voltage'")
# build current input based on Current steps
current = {'x': [], 'y': []} # x -> t[s], y -> I[A]
for step in experiment.steps:
if step.start_time:
raise NotImplementedError('Dandeliion does not support experiment steps with start times yet.')
if not isinstance(step, Current):
raise NotImplementedError('Dandeliion only supports Current steps for experiments so far.')
if not step.duration:
raise NotImplementedError('Dandeliion only supports steps with explicity defined durations.')
if current['x']:
last_final = current['x'][-1]
current['x'].append(last_final + dt_eval)
current['x'].append(last_final + step.duration)
current['y'].append(-1. * step.value)
current['y'].append(-1. * step.value)
else:
current['x'].append(0)
current['x'].append(step.duration - dt_eval)
current['y'].append(-1. * step.value)
current['y'].append(-1. * step.value)
# TODO should we /do we need to extrapolate current to t_max?
return DandeliionExperiment(
current=current,
t_output=t_output,
V_min=V_min,
t_max=t_max,
)
[docs]
class Solution:
"""Dictionary-style class for the solutions of a simulation run
returned by :meth:`solve`. Currently contains:
* 'Time [s]'
* 'Voltage [V]'
* 'Current [A]'
"""
[docs]
def __init__(self, sim: Simulation):
"""
Args:
sim (Simulation): Dandeliion simulation run (has to have finished successfully)
"""
self._results = sim.results
def __getitem__(self, key: str):
"""Returns the results requested by the key.
Args:
key (str): key for results to be returned.
Returns:
object: data as requested by provided key
"""
if key == "Time [s]":
return self._results.total_voltage['t(s)']
elif key == "Voltage [V]":
return self._results.total_voltage['total_voltage(V)']
elif key == "Current [A]":
return self._results.total_current['total_current(A)']
else:
raise KeyError(f'The following key is not (yet) found in the provided results: {key}')
[docs]
def solve(
simulator: Simulator,
params: str,
experiment: Experiment,
var_pts: dict,
model: str,
initial_condition: dict = None,
t_output: list = None,
dt_eval: float = 0.1,
) -> Solution:
"""Method for submitting/running a Dandeliion simulation.
Args:
simulator (Simulator): instance of simulator class providing information
to connect to simulation server
params (str): path to BPX parameter file
experiment (Experiment): instance of pybamm Experiment;
currently only those supported with
* only :class:`pybamm.experiment.step.steps.Current` steps (or their
equivalent in str representation) as steps
* time and/or voltage termination criteria
var_pts (dict): simulation mesh specified by the following parameters in dictionary:
* x_n: Number of nodes in the electrolyte (negative electrode)
* x_s: Number of nodes in the electrolyte (separator)
* x_p: Number of nodes in the electrolyte (positive electrode)
* r_n: Number of nodes in particles (negative electrode)
* r_p: Number of nodes in particles (positive electrode)
model (str): name of model to be simulated
initial_condition (dict, optional): dictionary of additional initial conditions
(overwrites parameters provided in parameter file if they exist).
Currently supported initial conditions are:
* 'Initial temperature [K]'
* 'Initial concentration in electrolyte [mol.m-3]'
* 'Initial state of charge'
t_output (list, optional): list of times to create outputs for. If not provided, then output times derived from experiment will be used (requires time stop criterion to be provided then)
dt_eval (float, optional): time step used for resolving discontinuities in experiment. Default is 0.1 seconds.
Returns:
:class:`Solution`: solution for this simulation run
"""
connect(
username=simulator.credential[0],
password=simulator.credential[1],
# endpoint=f'{simulator.server}/accounts/', # TODO
)
with open(params) as f:
data = BPX.import_(data=json.load(f), model=models[model])
# add/overwrite initial conditions
if initial_condition:
update_dict(data, unflatten_dict(
{initial_condition_fields[field]: value
for field, value in initial_condition.items()}
))
# add/overwrite simulation params
update_dict(data, unflatten_dict(
{sim_params[field]: value
for field, value in var_pts.items()}
))
# fix discretisation to FECV (default)
data['params']['discretisation'] = 'FECV'
# convert Experiment into something Dandeliion can use
experiment = convertExperiment(experiment, dt_eval)
# set V_min if provided in Experiment
if experiment.V_min is not None:
data['params']['cell']['V_min'] = experiment.V_min
# set charge/discharge current
data['params']['cell']['current'] = experiment.current
# set output times and t_max
if t_output is None and experiment.t_output is None:
raise ValueError('Either Experiment has to provide time termination condition'
+ ' or output list t_output has to be exlicitly provided to this function')
if t_output is None:
t_output = experiment.t_output
# set output times
data['params']['cell']['t_output'] = t_output
# set maximum discharge time
if experiment.t_max is not None:
data['params']['cell']['t_max'] = experiment.t_max
else:
data['params']['cell']['t_max'] = t_output[-1]
data['agree'] = True
# run simulation
sim = Simulation(
data=data,
# endpoint_results=f'{simulator.server}/results/', # TODO
)
sim.compute()
return Solution(sim)