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

from __future__ import division, print_function, absolute_import 

import numpy as np 

 

 

def check_arguments(fun, y0, support_complex): 

"""Helper function for checking arguments common to all solvers.""" 

y0 = np.asarray(y0) 

if np.issubdtype(y0.dtype, np.complexfloating): 

if not support_complex: 

raise ValueError("`y0` is complex, but the chosen solver does " 

"not support integration in a complex domain.") 

dtype = complex 

else: 

dtype = float 

y0 = y0.astype(dtype, copy=False) 

 

if y0.ndim != 1: 

raise ValueError("`y0` must be 1-dimensional.") 

 

def fun_wrapped(t, y): 

return np.asarray(fun(t, y), dtype=dtype) 

 

return fun_wrapped, y0 

 

 

class OdeSolver(object): 

"""Base class for ODE solvers. 

 

In order to implement a new solver you need to follow the guidelines: 

 

1. A constructor must accept parameters presented in the base class 

(listed below) along with any other parameters specific to a solver. 

2. A constructor must accept arbitrary extraneous arguments 

``**extraneous``, but warn that these arguments are irrelevant 

using `common.warn_extraneous` function. Do not pass these 

arguments to the base class. 

3. A solver must implement a private method `_step_impl(self)` which 

propagates a solver one step further. It must return tuple 

``(success, message)``, where ``success`` is a boolean indicating 

whether a step was successful, and ``message`` is a string 

containing description of a failure if a step failed or None 

otherwise. 

4. A solver must implement a private method `_dense_output_impl(self)` 

which returns a `DenseOutput` object covering the last successful 

step. 

5. A solver must have attributes listed below in Attributes section. 

Note that `t_old` and `step_size` are updated automatically. 

6. Use `fun(self, t, y)` method for the system rhs evaluation, this 

way the number of function evaluations (`nfev`) will be tracked 

automatically. 

7. For convenience a base class provides `fun_single(self, t, y)` and 

`fun_vectorized(self, t, y)` for evaluating the rhs in 

non-vectorized and vectorized fashions respectively (regardless of 

how `fun` from the constructor is implemented). These calls don't 

increment `nfev`. 

8. If a solver uses a Jacobian matrix and LU decompositions, it should 

track the number of Jacobian evaluations (`njev`) and the number of 

LU decompositions (`nlu`). 

9. By convention the function evaluations used to compute a finite 

difference approximation of the Jacobian should not be counted in 

`nfev`, thus use `fun_single(self, t, y)` or 

`fun_vectorized(self, t, y)` when computing a finite difference 

approximation of the Jacobian. 

 

Parameters 

---------- 

fun : callable 

Right-hand side of the system. The calling signature is ``fun(t, y)``. 

Here ``t`` is a scalar and there are two options for ndarray ``y``. 

It can either have shape (n,), then ``fun`` must return array_like with 

shape (n,). Or alternatively it can have shape (n, n_points), then 

``fun`` must return array_like with shape (n, n_points) (each column 

corresponds to a single column in ``y``). The choice between the two 

options is determined by `vectorized` argument (see below). 

t0 : float 

Initial time. 

y0 : array_like, shape (n,) 

Initial state. 

t_bound : float 

Boundary time --- the integration won't continue beyond it. It also 

determines the direction of the integration. 

vectorized : bool 

Whether `fun` is implemented in a vectorized fashion. 

support_complex : bool, optional 

Whether integration in a complex domain should be supported. 

Generally determined by a derived solver class capabilities. 

Default is False. 

 

Attributes 

---------- 

n : int 

Number of equations. 

status : string 

Current status of the solver: 'running', 'finished' or 'failed'. 

t_bound : float 

Boundary time. 

direction : float 

Integration direction: +1 or -1. 

t : float 

Current time. 

y : ndarray 

Current state. 

t_old : float 

Previous time. None if no steps were made yet. 

step_size : float 

Size of the last successful step. None if no steps were made yet. 

nfev : int 

Number of the system's rhs evaluations. 

njev : int 

Number of the Jacobian evaluations. 

nlu : int 

Number of LU decompositions. 

""" 

TOO_SMALL_STEP = "Required step size is less than spacing between numbers." 

 

def __init__(self, fun, t0, y0, t_bound, vectorized, 

support_complex=False): 

self.t_old = None 

self.t = t0 

self._fun, self.y = check_arguments(fun, y0, support_complex) 

self.t_bound = t_bound 

self.vectorized = vectorized 

 

if vectorized: 

def fun_single(t, y): 

return self._fun(t, y[:, None]).ravel() 

fun_vectorized = self._fun 

else: 

fun_single = self._fun 

 

def fun_vectorized(t, y): 

f = np.empty_like(y) 

for i, yi in enumerate(y.T): 

f[:, i] = self._fun(t, yi) 

return f 

 

def fun(t, y): 

self.nfev += 1 

return self.fun_single(t, y) 

 

self.fun = fun 

self.fun_single = fun_single 

self.fun_vectorized = fun_vectorized 

 

self.direction = np.sign(t_bound - t0) if t_bound != t0 else 1 

self.n = self.y.size 

self.status = 'running' 

 

self.nfev = 0 

self.njev = 0 

self.nlu = 0 

 

@property 

def step_size(self): 

if self.t_old is None: 

return None 

else: 

return np.abs(self.t - self.t_old) 

 

def step(self): 

"""Perform one integration step. 

 

Returns 

------- 

message : string or None 

Report from the solver. Typically a reason for a failure if 

`self.status` is 'failed' after the step was taken or None 

otherwise. 

""" 

if self.status != 'running': 

raise RuntimeError("Attempt to step on a failed or finished " 

"solver.") 

 

if self.n == 0 or self.t == self.t_bound: 

# Handle corner cases of empty solver or no integration. 

self.t_old = self.t 

self.t = self.t_bound 

message = None 

self.status = 'finished' 

else: 

t = self.t 

success, message = self._step_impl() 

 

if not success: 

self.status = 'failed' 

else: 

self.t_old = t 

if self.direction * (self.t - self.t_bound) >= 0: 

self.status = 'finished' 

 

return message 

 

def dense_output(self): 

"""Compute a local interpolant over the last successful step. 

 

Returns 

------- 

sol : `DenseOutput` 

Local interpolant over the last successful step. 

""" 

if self.t_old is None: 

raise RuntimeError("Dense output is available after a successful " 

"step was made.") 

 

if self.n == 0 or self.t == self.t_old: 

# Handle corner cases of empty solver and no integration. 

return ConstantDenseOutput(self.t_old, self.t, self.y) 

else: 

return self._dense_output_impl() 

 

def _step_impl(self): 

raise NotImplementedError 

 

def _dense_output_impl(self): 

raise NotImplementedError 

 

 

class DenseOutput(object): 

"""Base class for local interpolant over step made by an ODE solver. 

 

It interpolates between `t_min` and `t_max` (see Attributes below). 

Evaluation outside this interval is not forbidden, but the accuracy is not 

guaranteed. 

 

Attributes 

---------- 

t_min, t_max : float 

Time range of the interpolation. 

""" 

def __init__(self, t_old, t): 

self.t_old = t_old 

self.t = t 

self.t_min = min(t, t_old) 

self.t_max = max(t, t_old) 

 

def __call__(self, t): 

"""Evaluate the interpolant. 

 

Parameters 

---------- 

t : float or array_like with shape (n_points,) 

Points to evaluate the solution at. 

 

Returns 

------- 

y : ndarray, shape (n,) or (n, n_points) 

Computed values. Shape depends on whether `t` was a scalar or a 

1-d array. 

""" 

t = np.asarray(t) 

if t.ndim > 1: 

raise ValueError("`t` must be float or 1-d array.") 

return self._call_impl(t) 

 

def _call_impl(self, t): 

raise NotImplementedError 

 

 

class ConstantDenseOutput(DenseOutput): 

"""Constant value interpolator. 

 

This class used for degenerate integration cases: equal integration limits 

or a system with 0 equations. 

""" 

def __init__(self, t_old, t, value): 

super(ConstantDenseOutput, self).__init__(t_old, t) 

self.value = value 

 

def _call_impl(self, t): 

if t.ndim == 0: 

return self.value 

else: 

ret = np.empty((self.value.shape[0], t.shape[0])) 

ret[:] = self.value[:, None] 

return ret