@@ -30,6 +30,12 @@ class BaseSolver(object):
30
30
specified by 'root_method' (e.g. "lm", "hybr", ...)
31
31
root_tol : float, optional
32
32
The tolerance for the initial-condition solver (default is 1e-6).
33
+ solve_sensitivity_equations : bool, optional
34
+ Whether to explicitly formulate the sensitivity equations for sensitivity
35
+ to input parameters. The formulation is as per "Park, S., Kato, D., Gima, Z.,
36
+ Klein, R., & Moura, S. (2018). Optimal experimental design for parameterization
37
+ of an electrochemical lithium-ion battery model. Journal of The Electrochemical
38
+ Society, 165(7), A1309.". See #1100 for details
33
39
"""
34
40
35
41
def __init__ (
@@ -40,6 +46,7 @@ def __init__(
40
46
root_method = None ,
41
47
root_tol = 1e-6 ,
42
48
max_steps = "deprecated" ,
49
+ solve_sensitivity_equations = False ,
43
50
):
44
51
self ._method = method
45
52
self ._rtol = rtol
@@ -57,6 +64,7 @@ def __init__(
57
64
self .name = "Base solver"
58
65
self .ode_solver = False
59
66
self .algebraic_solver = False
67
+ self .solve_sensitivity_equations = solve_sensitivity_equations
60
68
61
69
@property
62
70
def method (self ):
@@ -191,17 +199,28 @@ def set_up(self, model, inputs=None):
191
199
)
192
200
model .convert_to_format = "casadi"
193
201
202
+ # Only allow solving sensitivity equations with the casadi format for now
203
+ if (
204
+ self .solve_sensitivity_equations is True
205
+ and model .convert_to_format != "casadi"
206
+ ):
207
+ raise NotImplementedError (
208
+ "model should be converted to casadi format in order to solve "
209
+ "sensitivity equations"
210
+ )
211
+
194
212
if model .convert_to_format != "casadi" :
195
213
simp = pybamm .Simplification ()
196
214
# Create Jacobian from concatenated rhs and algebraic
197
- y = pybamm .StateVector (slice (0 , model .concatenated_initial_conditions . size ))
215
+ y = pybamm .StateVector (slice (0 , model .len_rhs_and_alg ))
198
216
# set up Jacobian object, for re-use of dict
199
217
jacobian = pybamm .Jacobian ()
200
218
else :
201
219
# Convert model attributes to casadi
202
220
t_casadi = casadi .MX .sym ("t" )
203
- y_diff = casadi .MX .sym ("y_diff" , model .concatenated_rhs .size )
204
- y_alg = casadi .MX .sym ("y_alg" , model .concatenated_algebraic .size )
221
+ # Create the symbolic state vectors
222
+ y_diff = casadi .MX .sym ("y_diff" , model .len_rhs )
223
+ y_alg = casadi .MX .sym ("y_alg" , model .len_alg )
205
224
y_casadi = casadi .vertcat (y_diff , y_alg )
206
225
p_casadi = {}
207
226
for name , value in inputs .items ():
@@ -210,6 +229,13 @@ def set_up(self, model, inputs=None):
210
229
else :
211
230
p_casadi [name ] = casadi .MX .sym (name , value .shape [0 ])
212
231
p_casadi_stacked = casadi .vertcat (* [p for p in p_casadi .values ()])
232
+ # sensitivity vectors
233
+ if self .solve_sensitivity_equations is True :
234
+ S_x = casadi .MX .sym ("S_x" , model .len_rhs * p_casadi_stacked .shape [0 ])
235
+ S_z = casadi .MX .sym ("S_z" , model .len_alg * p_casadi_stacked .shape [0 ])
236
+ y_and_S = casadi .vertcat (y_diff , S_x , y_alg , S_z )
237
+ else :
238
+ y_and_S = y_casadi
213
239
214
240
def process (func , name , use_jacobian = None ):
215
241
def report (string ):
@@ -258,16 +284,40 @@ def report(string):
258
284
# Process with CasADi
259
285
report (f"Converting { name } to CasADi" )
260
286
func = func .to_casadi (t_casadi , y_casadi , inputs = p_casadi )
287
+ # Add sensitivity vectors to the rhs and algebraic equations
288
+ if self .solve_sensitivity_equations is True :
289
+ if name == "rhs" :
290
+ report (f"Creating sensitivity equations for rhs using CasADi" )
291
+ df_dx = casadi .jacobian (func , y_diff )
292
+ df_dp = casadi .jacobian (func , p_casadi_stacked )
293
+ if model .len_alg == 0 :
294
+ S_rhs = df_dx @ S_x + df_dp
295
+ else :
296
+ df_dz = casadi .jacobian (func , y_alg )
297
+ S_rhs = df_dx @ S_x + df_dz @ S_z + df_dp
298
+ func = casadi .vertcat (func , S_rhs )
299
+ elif name == "initial_conditions" :
300
+ if model .len_rhs == 0 or model .len_alg == 0 :
301
+ S_0 = casadi .jacobian (func , p_casadi_stacked ).reshape (
302
+ (- 1 , 1 )
303
+ )
304
+ func = casadi .vertcat (func , S_0 )
305
+ else :
306
+ x0 = func [: model .len_rhs ]
307
+ z0 = func [model .len_rhs :]
308
+ Sx_0 = casadi .jacobian (x0 , p_casadi_stacked )
309
+ Sz_0 = casadi .jacobian (z0 , p_casadi_stacked )
310
+ func = casadi .vertcat (x0 , Sx_0 , z0 , Sz_0 )
261
311
if use_jacobian :
262
312
report (f"Calculating jacobian for { name } using CasADi" )
263
- jac_casadi = casadi .jacobian (func , y_casadi )
313
+ jac_casadi = casadi .jacobian (func , y_and_S )
264
314
jac = casadi .Function (
265
- name , [t_casadi , y_casadi , p_casadi_stacked ], [jac_casadi ]
315
+ name , [t_casadi , y_and_S , p_casadi_stacked ], [jac_casadi ]
266
316
)
267
317
else :
268
318
jac = None
269
319
func = casadi .Function (
270
- name , [t_casadi , y_casadi , p_casadi_stacked ], [func ]
320
+ name , [t_casadi , y_and_S , p_casadi_stacked ], [func ]
271
321
)
272
322
if name == "residuals" :
273
323
func_call = Residuals (func , name , model )
@@ -277,6 +327,7 @@ def report(string):
277
327
jac_call = SolverCallable (jac , name + "_jac" , model )
278
328
else :
279
329
jac_call = None
330
+
280
331
return func , func_call , jac_call
281
332
282
333
# Check for heaviside functions in rhs and algebraic and add discontinuity
@@ -324,8 +375,18 @@ def report(string):
324
375
)[0 ]
325
376
init_eval = InitialConditions (initial_conditions , model )
326
377
378
+ if self .solve_sensitivity_equations is True :
379
+ init_eval .y_dummy = np .zeros (
380
+ (
381
+ model .len_rhs_and_alg * (np .vstack (list (inputs .values ())).size + 1 ),
382
+ 1 ,
383
+ )
384
+ )
385
+ else :
386
+ init_eval .y_dummy = np .zeros ((model .len_rhs_and_alg , 1 ))
387
+
327
388
# Process rhs, algebraic and event expressions
328
- rhs , rhs_eval , jac_rhs = process (model .concatenated_rhs , "RHS " )
389
+ rhs , rhs_eval , jac_rhs = process (model .concatenated_rhs , "rhs " )
329
390
algebraic , algebraic_eval , jac_algebraic = process (
330
391
model .concatenated_algebraic , "algebraic"
331
392
)
@@ -423,7 +484,7 @@ def _set_initial_conditions(self, model, inputs, update_rhs):
423
484
y0_from_inputs = model .init_eval (inputs )
424
485
# Reuse old solution for algebraic equations
425
486
y0_from_model = model .y0
426
- len_rhs = model .concatenated_rhs . size
487
+ len_rhs = model .len_rhs
427
488
# update model.y0, which is used for initialising the algebraic solver
428
489
if len_rhs == 0 :
429
490
model .y0 = y0_from_model
@@ -861,7 +922,7 @@ def __init__(self, function, name, model):
861
922
862
923
def __call__ (self , t , y , inputs ):
863
924
y = y .reshape (- 1 , 1 )
864
- if self .name in ["RHS " , "algebraic" , "residuals" ]:
925
+ if self .name in ["rhs " , "algebraic" , "residuals" ]:
865
926
pybamm .logger .debug (
866
927
"Evaluating {} for {} at t={}" .format (
867
928
self .name , self .model .name , t * self .timescale
@@ -874,7 +935,7 @@ def __call__(self, t, y, inputs):
874
935
def function (self , t , y , inputs ):
875
936
if self .form == "casadi" :
876
937
states_eval = self ._function (t , y , inputs )
877
- if self .name in ["RHS " , "algebraic" , "residuals" , "event" ]:
938
+ if self .name in ["rhs " , "algebraic" , "residuals" , "event" ]:
878
939
return states_eval .full ()
879
940
else :
880
941
# keep jacobians sparse
@@ -901,7 +962,6 @@ class InitialConditions(SolverCallable):
901
962
902
963
def __init__ (self , function , model ):
903
964
super ().__init__ (function , "initial conditions" , model )
904
- self .y_dummy = np .zeros (model .concatenated_initial_conditions .shape )
905
965
906
966
def __call__ (self , inputs ):
907
967
if self .form == "casadi" :
0 commit comments