modelbase2 0.2.0__py3-none-any.whl → 0.4.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- modelbase2/__init__.py +12 -1
- modelbase2/distributions.py +33 -0
- modelbase2/experimental/__init__.py +2 -0
- modelbase2/experimental/_backup.py +1017 -0
- modelbase2/experimental/strikepy.py +562 -0
- modelbase2/experimental/symbolic.py +286 -0
- modelbase2/fit.py +6 -6
- modelbase2/model.py +0 -1
- modelbase2/nnarchitectures.py +128 -0
- modelbase2/npe.py +15 -82
- modelbase2/plot.py +4 -1
- modelbase2/simulator.py +7 -3
- modelbase2/surrogates/__init__.py +1 -2
- modelbase2/surrogates/_poly.py +32 -5
- modelbase2/surrogates/_torch.py +8 -64
- modelbase2/surrogates.py +7 -1
- {modelbase2-0.2.0.dist-info → modelbase2-0.4.0.dist-info}/METADATA +14 -1
- {modelbase2-0.2.0.dist-info → modelbase2-0.4.0.dist-info}/RECORD +20 -16
- {modelbase2-0.2.0.dist-info → modelbase2-0.4.0.dist-info}/WHEEL +0 -0
- {modelbase2-0.2.0.dist-info → modelbase2-0.4.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,562 @@
|
|
1
|
+
# ruff: noqa: D100, D101, D102, D103, D104, D105, D106, D107, D200, D203, D400, D401
|
2
|
+
|
3
|
+
"""Reimplementation of strikepy from.
|
4
|
+
|
5
|
+
StrikePy: https://github.com/afvillaverde/StrikePy
|
6
|
+
STRIKE-GOLDD: https://github.com/afvillaverde/strike-goldd
|
7
|
+
|
8
|
+
FIXME:
|
9
|
+
- no handling of derived variables
|
10
|
+
- performance issues of generic_rank
|
11
|
+
"""
|
12
|
+
|
13
|
+
import textwrap
|
14
|
+
from concurrent.futures import ProcessPoolExecutor
|
15
|
+
from dataclasses import dataclass, field
|
16
|
+
from functools import partial
|
17
|
+
from math import ceil, inf
|
18
|
+
from time import time
|
19
|
+
from typing import cast
|
20
|
+
|
21
|
+
import numpy as np
|
22
|
+
import numpy.typing as npt
|
23
|
+
import symbtools as st
|
24
|
+
import sympy
|
25
|
+
import sympy as sym
|
26
|
+
import tqdm
|
27
|
+
from sympy import Matrix
|
28
|
+
from sympy.matrices import zeros
|
29
|
+
|
30
|
+
__all__ = ["Model", "Options", "Result", "strike_goldd"]
|
31
|
+
|
32
|
+
|
33
|
+
@dataclass
|
34
|
+
class Options:
|
35
|
+
check_observability: bool = True
|
36
|
+
max_lie_time: float = inf
|
37
|
+
non_zero_known_input_derivatives: list[int] = field(default_factory=lambda: [100])
|
38
|
+
non_zero_unknown_input_derivatives: list[int] = field(default_factory=lambda: [100])
|
39
|
+
prev_ident_pars: set[sympy.Symbol] = field(default_factory=set)
|
40
|
+
|
41
|
+
|
42
|
+
@dataclass
|
43
|
+
class Model:
|
44
|
+
states: list[sym.Symbol]
|
45
|
+
pars: list[sym.Symbol]
|
46
|
+
eqs: list[sym.Expr]
|
47
|
+
outputs: list[sym.Symbol]
|
48
|
+
known_inputs: list[sym.Symbol] = field(default_factory=list)
|
49
|
+
unknown_inputs: list[sym.Symbol] = field(default_factory=list)
|
50
|
+
|
51
|
+
|
52
|
+
@dataclass
|
53
|
+
class Result:
|
54
|
+
rank: int
|
55
|
+
model: Model
|
56
|
+
is_fispo: bool
|
57
|
+
par_ident: list
|
58
|
+
par_unident: list
|
59
|
+
state_obs: list
|
60
|
+
state_unobs: list
|
61
|
+
input_obs: list
|
62
|
+
input_unobs: list
|
63
|
+
|
64
|
+
def all_inputs_observable(self) -> bool:
|
65
|
+
return bool(
|
66
|
+
len(self.par_ident) == len(self.model.pars)
|
67
|
+
and len(self.model.unknown_inputs) > 0
|
68
|
+
)
|
69
|
+
|
70
|
+
def summary(self) -> str:
|
71
|
+
return textwrap.dedent(f"""\
|
72
|
+
Summary
|
73
|
+
=======
|
74
|
+
The model {"is" if self.is_fispo else "is not"} FISPO.
|
75
|
+
Identifiable parameters: {self.par_ident}
|
76
|
+
Unidentifiable parameters: {self.par_unident}
|
77
|
+
Identifiable variables: {self.state_obs}
|
78
|
+
Unidentifiable variables: {self.state_unobs}
|
79
|
+
Identifiable inputs: {self.input_obs}
|
80
|
+
Unidentifiable inputs: {self.input_unobs}
|
81
|
+
""")
|
82
|
+
|
83
|
+
|
84
|
+
def _rationalize_all_numbers(expr: sym.Matrix) -> sym.Matrix:
|
85
|
+
"""Convert all numbers in expr to sympy.Rational-objects."""
|
86
|
+
numbers_atoms = list(expr.atoms(sym.Number))
|
87
|
+
rationalized_number_tpls = [(n, sym.Rational(n)) for n in numbers_atoms]
|
88
|
+
return expr.subs(rationalized_number_tpls)
|
89
|
+
|
90
|
+
|
91
|
+
def _calculate_num_rank(inp: tuple[int, list[int]], onx: Matrix) -> tuple[int, int]:
|
92
|
+
idx, indices = inp
|
93
|
+
return idx, st.generic_rank(onx.col(indices))
|
94
|
+
|
95
|
+
|
96
|
+
def _elim_and_recalc(
|
97
|
+
*,
|
98
|
+
model: Model,
|
99
|
+
res: Result,
|
100
|
+
options: Options,
|
101
|
+
unmeas_xred_indices: list[int],
|
102
|
+
onx: sym.Matrix,
|
103
|
+
unidflag: bool,
|
104
|
+
w1: list[sym.Symbol],
|
105
|
+
) -> None:
|
106
|
+
onx = _rationalize_all_numbers(onx)
|
107
|
+
par_ident = res.par_ident
|
108
|
+
state_obs = res.state_obs
|
109
|
+
input_obs = res.input_obs
|
110
|
+
|
111
|
+
r = sym.shape(onx)[1]
|
112
|
+
new_ident_pars = par_ident
|
113
|
+
new_nonid_pars = []
|
114
|
+
new_obs_states = state_obs
|
115
|
+
new_unobs_states = []
|
116
|
+
new_obs_in = input_obs
|
117
|
+
new_unobs_in = []
|
118
|
+
|
119
|
+
all_indices: list[tuple[int, list[int]]] = []
|
120
|
+
for idx in range(len(model.pars)):
|
121
|
+
if model.pars[idx] not in par_ident:
|
122
|
+
indices = list(range(r))
|
123
|
+
indices.pop(len(model.states) + idx)
|
124
|
+
all_indices.append((idx, indices))
|
125
|
+
with ProcessPoolExecutor() as ppe:
|
126
|
+
num_ranks = list(ppe.map(partial(_calculate_num_rank, onx=onx), all_indices))
|
127
|
+
for idx, num_rank in num_ranks:
|
128
|
+
if num_rank == res.rank:
|
129
|
+
if unidflag:
|
130
|
+
new_nonid_pars.append(model.pars[idx])
|
131
|
+
else:
|
132
|
+
new_ident_pars.append(model.pars[idx])
|
133
|
+
|
134
|
+
# At each iteration we try removing a different state from 'xred':
|
135
|
+
if options.check_observability:
|
136
|
+
all_indices = []
|
137
|
+
for idx in range(len(unmeas_xred_indices)):
|
138
|
+
orig_idx = unmeas_xred_indices[idx]
|
139
|
+
if model.states[orig_idx] not in state_obs:
|
140
|
+
indices = list(range(r))
|
141
|
+
indices.pop(orig_idx)
|
142
|
+
all_indices.append((orig_idx, indices))
|
143
|
+
with ProcessPoolExecutor() as ppe:
|
144
|
+
num_ranks = list(
|
145
|
+
ppe.map(partial(_calculate_num_rank, onx=onx), all_indices)
|
146
|
+
)
|
147
|
+
for orig_idx, num_rank in num_ranks:
|
148
|
+
if num_rank == res.rank:
|
149
|
+
if unidflag:
|
150
|
+
new_unobs_states.append(model.states[orig_idx])
|
151
|
+
else:
|
152
|
+
new_obs_states.append(model.states[orig_idx])
|
153
|
+
|
154
|
+
# At each iteration we try removing a different column from onx:
|
155
|
+
all_indices = []
|
156
|
+
for idx in range(len(w1)):
|
157
|
+
if w1[idx] not in input_obs:
|
158
|
+
indices = list(range(r))
|
159
|
+
indices.pop(len(model.states) + len(model.pars) + idx)
|
160
|
+
all_indices.append((idx, indices))
|
161
|
+
with ProcessPoolExecutor() as ppe:
|
162
|
+
num_ranks = list(ppe.map(partial(_calculate_num_rank, onx=onx), all_indices))
|
163
|
+
for idx, num_rank in num_ranks:
|
164
|
+
if num_rank == res.rank:
|
165
|
+
if unidflag:
|
166
|
+
new_unobs_in.append(w1[idx])
|
167
|
+
else:
|
168
|
+
new_obs_in.append(w1[idx])
|
169
|
+
|
170
|
+
res.par_ident = new_ident_pars
|
171
|
+
res.par_unident = new_nonid_pars
|
172
|
+
res.state_obs = new_obs_states
|
173
|
+
res.state_unobs = new_unobs_states
|
174
|
+
res.input_obs = new_obs_in
|
175
|
+
res.input_unobs = new_unobs_in
|
176
|
+
|
177
|
+
|
178
|
+
def _remove_identified_parameters(model: Model, options: Options) -> None:
|
179
|
+
if len(options.prev_ident_pars) != 0:
|
180
|
+
model.pars = [i for i in model.pars if i not in options.prev_ident_pars]
|
181
|
+
|
182
|
+
|
183
|
+
def _get_measured_states(model: Model) -> tuple[list[sym.Symbol], list[int]]:
|
184
|
+
# Check which states are directly measured, if any.
|
185
|
+
# Basically it is checked if any state is directly on the output,
|
186
|
+
# then that state is directly measurable.
|
187
|
+
is_measured: list[bool] = [False for i in range(len(model.states))]
|
188
|
+
for i, state in enumerate(model.states):
|
189
|
+
if state in model.outputs:
|
190
|
+
is_measured[i] = True
|
191
|
+
|
192
|
+
measured_state_idxs: list[int] = [i for i, j in enumerate(is_measured) if j]
|
193
|
+
unmeasured_state_idxs = [i for i, j in enumerate(is_measured) if not j]
|
194
|
+
measured_state_names = [model.states[i] for i in measured_state_idxs]
|
195
|
+
return measured_state_names, unmeasured_state_idxs
|
196
|
+
|
197
|
+
|
198
|
+
def _create_derivatives(
|
199
|
+
elements: list[sym.Symbol], n_min_lie_derivatives: int, n_derivatives: list[int]
|
200
|
+
) -> list[list[sym.Symbol]]:
|
201
|
+
derivatives: list[list[float | sym.Symbol]] = []
|
202
|
+
for ind_u, element in enumerate(elements):
|
203
|
+
auxiliar: list[float | sym.Symbol] = [sym.Symbol(f"{element}")]
|
204
|
+
for k in range(n_min_lie_derivatives):
|
205
|
+
auxiliar.append(sym.Symbol(f"{element}_d{k + 1}")) # noqa: PERF401
|
206
|
+
derivatives.append(auxiliar)
|
207
|
+
|
208
|
+
if len(derivatives[0]) >= n_derivatives[ind_u] + 1:
|
209
|
+
for i in range(len(derivatives[0][(n_derivatives[ind_u] + 1) :])):
|
210
|
+
derivatives[ind_u][(n_derivatives[ind_u] + 1) + i] = 0
|
211
|
+
return derivatives # type: ignore
|
212
|
+
|
213
|
+
|
214
|
+
def _create_w1_vector(
|
215
|
+
model: Model, w_der: list[list[sym.Symbol]]
|
216
|
+
) -> tuple[list[sym.Symbol], list[sym.Symbol]]:
|
217
|
+
w1vector = []
|
218
|
+
w1vector_dot = []
|
219
|
+
|
220
|
+
if len(model.unknown_inputs) == 0:
|
221
|
+
return w1vector, w1vector_dot
|
222
|
+
|
223
|
+
w1vector.extend(w_der[:-1])
|
224
|
+
w1vector_dot.extend(w_der[1:])
|
225
|
+
|
226
|
+
# -- Include as states only nonzero inputs / derivatives:
|
227
|
+
nzi = []
|
228
|
+
nzj = []
|
229
|
+
nz_w1vec = []
|
230
|
+
for fila in range(len(w1vector)):
|
231
|
+
if w1vector[fila][0] != 0:
|
232
|
+
nzi.append([fila])
|
233
|
+
nzj.append([1])
|
234
|
+
nz_w1vec.append(w1vector[fila])
|
235
|
+
|
236
|
+
w1vector = nz_w1vec
|
237
|
+
w1vector_dot = w1vector_dot[0 : len(nzi)]
|
238
|
+
return w1vector, w1vector_dot
|
239
|
+
|
240
|
+
|
241
|
+
def _create_xaug_faug(
|
242
|
+
model: Model,
|
243
|
+
w1vector: list[sym.Symbol],
|
244
|
+
w1vector_dot: list[sym.Symbol],
|
245
|
+
) -> tuple[npt.NDArray, npt.NDArray]:
|
246
|
+
xaug = np.array(model.states)
|
247
|
+
xaug = np.append(xaug, model.pars, axis=0) # type: ignore
|
248
|
+
if len(w1vector) != 0:
|
249
|
+
xaug = np.append(xaug, w1vector, axis=0) # type: ignore
|
250
|
+
|
251
|
+
faug = np.atleast_2d(np.array(model.eqs, dtype=object)).T
|
252
|
+
faug = np.append(faug, zeros(len(model.pars), 1), axis=0)
|
253
|
+
if len(w1vector) != 0:
|
254
|
+
faug = np.append(faug, w1vector_dot, axis=0) # type: ignore
|
255
|
+
return xaug, faug
|
256
|
+
|
257
|
+
|
258
|
+
def _compute_extra_term(
|
259
|
+
extra_term: npt.NDArray,
|
260
|
+
ind: int,
|
261
|
+
past_lie: sym.Matrix,
|
262
|
+
input_der: list[list[sym.Symbol]],
|
263
|
+
zero_input_der_dummy_name: sym.Symbol,
|
264
|
+
) -> npt.NDArray:
|
265
|
+
for i in range(ind):
|
266
|
+
column = len(input_der) - 1
|
267
|
+
if i < column:
|
268
|
+
lo_u_der = input_der[i]
|
269
|
+
if lo_u_der == 0:
|
270
|
+
lo_u_der = zero_input_der_dummy_name
|
271
|
+
lo_u_der = np.array([lo_u_der])
|
272
|
+
hi_u_der = input_der[i + 1]
|
273
|
+
hi_u_der = Matrix([hi_u_der])
|
274
|
+
intermedio = past_lie.jacobian(lo_u_der) * hi_u_der
|
275
|
+
extra_term = extra_term + intermedio if extra_term else intermedio
|
276
|
+
return extra_term
|
277
|
+
|
278
|
+
|
279
|
+
def _compute_n_min_lie_derivatives(model: Model) -> int:
|
280
|
+
n_outputs = len(model.outputs)
|
281
|
+
n_states = len(model.states)
|
282
|
+
n_unknown_pars = len(model.pars)
|
283
|
+
n_unknown_inp = len(model.unknown_inputs)
|
284
|
+
n_vars_to_observe = n_states + n_unknown_pars + n_unknown_inp
|
285
|
+
return ceil((n_vars_to_observe - n_outputs) / n_outputs)
|
286
|
+
|
287
|
+
|
288
|
+
def _test_fispo(
|
289
|
+
model: Model,
|
290
|
+
res: Result,
|
291
|
+
measured_state_names: list[sym.Symbol],
|
292
|
+
) -> bool:
|
293
|
+
if len(res.par_ident) == len(model.pars) and (
|
294
|
+
len(res.state_obs) + len(measured_state_names)
|
295
|
+
) == len(model.states):
|
296
|
+
res.is_fispo = True
|
297
|
+
res.state_obs = model.states
|
298
|
+
res.input_obs = model.unknown_inputs
|
299
|
+
res.par_ident = model.pars
|
300
|
+
|
301
|
+
return res.is_fispo
|
302
|
+
|
303
|
+
|
304
|
+
def _create_onx(
|
305
|
+
model: Model,
|
306
|
+
n_min_lie_derivatives: int,
|
307
|
+
options: Options,
|
308
|
+
w1vector: list[sym.Symbol],
|
309
|
+
xaug: npt.ArrayLike,
|
310
|
+
faug: npt.ArrayLike,
|
311
|
+
input_der: list[list[sym.Symbol]],
|
312
|
+
zero_input_der_dummy_name: sym.Symbol,
|
313
|
+
) -> tuple[npt.NDArray, sym.Matrix]:
|
314
|
+
onx = np.array(
|
315
|
+
zeros(
|
316
|
+
len(model.outputs) * (1 + n_min_lie_derivatives),
|
317
|
+
len(model.states) + len(model.pars) + len(w1vector),
|
318
|
+
)
|
319
|
+
)
|
320
|
+
jacobian = sym.Matrix(model.outputs).jacobian(xaug)
|
321
|
+
|
322
|
+
# first row(s) of onx (derivative of the output with respect to the vector states+unknown parameters).
|
323
|
+
onx[0 : len(model.outputs)] = np.array(jacobian)
|
324
|
+
|
325
|
+
past_Lie = sym.Matrix(model.outputs)
|
326
|
+
extra_term = np.array(0)
|
327
|
+
|
328
|
+
# loop as long as I don't complete the preset Lie derivatives or go over the maximum time
|
329
|
+
t_start = time()
|
330
|
+
|
331
|
+
onx[(len(model.outputs)) : 2 * len(model.outputs)] = past_Lie.jacobian(xaug)
|
332
|
+
for ind in range(1, n_min_lie_derivatives):
|
333
|
+
if (time() - t_start) > options.max_lie_time:
|
334
|
+
msg = "More Lie derivatives would be needed to analyse the model."
|
335
|
+
raise TimeoutError(msg)
|
336
|
+
|
337
|
+
lie_derivatives = Matrix(
|
338
|
+
(onx[(ind * len(model.outputs)) : (ind + 1) * len(model.outputs)][:]).dot(
|
339
|
+
faug
|
340
|
+
)
|
341
|
+
)
|
342
|
+
extra_term = _compute_extra_term(
|
343
|
+
extra_term,
|
344
|
+
ind=ind,
|
345
|
+
past_lie=past_Lie,
|
346
|
+
input_der=input_der,
|
347
|
+
zero_input_der_dummy_name=zero_input_der_dummy_name,
|
348
|
+
)
|
349
|
+
|
350
|
+
ext_Lie = lie_derivatives + extra_term if extra_term else lie_derivatives
|
351
|
+
past_Lie = ext_Lie
|
352
|
+
onx[((ind + 1) * len(model.outputs)) : (ind + 2) * len(model.outputs)] = (
|
353
|
+
sym.Matrix(ext_Lie).jacobian(xaug)
|
354
|
+
)
|
355
|
+
return onx, cast(sym.Matrix, past_Lie)
|
356
|
+
|
357
|
+
|
358
|
+
def strike_goldd(model: Model, options: Options | None = None) -> Result:
|
359
|
+
options = Options() if options is None else options
|
360
|
+
|
361
|
+
# Check if the size of nnzDerU and nnzDerW are appropriate
|
362
|
+
if len(model.known_inputs) > len(options.non_zero_known_input_derivatives):
|
363
|
+
msg = (
|
364
|
+
"The number of known inputs is higher than the size of nnzDerU "
|
365
|
+
"and must have the same size."
|
366
|
+
)
|
367
|
+
raise ValueError(msg)
|
368
|
+
if len(model.unknown_inputs) > len(options.non_zero_unknown_input_derivatives):
|
369
|
+
msg = (
|
370
|
+
"The number of unknown inputs is higher than the size of nnzDerW "
|
371
|
+
"and must have the same size."
|
372
|
+
)
|
373
|
+
raise ValueError(msg)
|
374
|
+
|
375
|
+
_remove_identified_parameters(model, options)
|
376
|
+
|
377
|
+
res = Result(
|
378
|
+
rank=0,
|
379
|
+
is_fispo=False,
|
380
|
+
model=model,
|
381
|
+
par_ident=[],
|
382
|
+
par_unident=[],
|
383
|
+
state_obs=[],
|
384
|
+
state_unobs=[],
|
385
|
+
input_obs=[],
|
386
|
+
input_unobs=[],
|
387
|
+
)
|
388
|
+
|
389
|
+
lastrank = None
|
390
|
+
unidflag = False
|
391
|
+
skip_elim: bool = False
|
392
|
+
|
393
|
+
n_min_lie_derivatives = _compute_n_min_lie_derivatives(model)
|
394
|
+
measured_state_names, unmeasured_state_idxs = _get_measured_states(model)
|
395
|
+
|
396
|
+
input_der = _create_derivatives(
|
397
|
+
model.known_inputs,
|
398
|
+
n_min_lie_derivatives=n_min_lie_derivatives,
|
399
|
+
n_derivatives=options.non_zero_known_input_derivatives,
|
400
|
+
)
|
401
|
+
zero_input_der_dummy_name = sym.Symbol("zero_input_der_dummy_name")
|
402
|
+
|
403
|
+
w_der: list[list[sym.Symbol]] = _create_derivatives(
|
404
|
+
model.unknown_inputs,
|
405
|
+
n_min_lie_derivatives=n_min_lie_derivatives,
|
406
|
+
n_derivatives=options.non_zero_unknown_input_derivatives,
|
407
|
+
)
|
408
|
+
|
409
|
+
w1vector, w1vector_dot = _create_w1_vector(
|
410
|
+
model,
|
411
|
+
w_der=w_der,
|
412
|
+
)
|
413
|
+
|
414
|
+
xaug, faug = _create_xaug_faug(
|
415
|
+
model,
|
416
|
+
w1vector=w1vector,
|
417
|
+
w1vector_dot=w1vector_dot,
|
418
|
+
)
|
419
|
+
|
420
|
+
onx, past_Lie = _create_onx(
|
421
|
+
model,
|
422
|
+
n_min_lie_derivatives=n_min_lie_derivatives,
|
423
|
+
options=options,
|
424
|
+
w1vector=w1vector,
|
425
|
+
xaug=xaug,
|
426
|
+
faug=faug,
|
427
|
+
input_der=input_der,
|
428
|
+
zero_input_der_dummy_name=zero_input_der_dummy_name,
|
429
|
+
)
|
430
|
+
|
431
|
+
t_start = time()
|
432
|
+
|
433
|
+
pbar = tqdm.tqdm(desc="Main loop")
|
434
|
+
while True:
|
435
|
+
pbar.update(1)
|
436
|
+
if time() - t_start > options.max_lie_time:
|
437
|
+
msg = "More Lie derivatives would be needed to see if the model is structurally unidentifiable as a whole."
|
438
|
+
raise TimeoutError(msg)
|
439
|
+
|
440
|
+
# FIXME: For some problems this starts to be really slow
|
441
|
+
# can't directly be fixed by using numpy.linalg.matrix_rank because
|
442
|
+
# that can't handle the symbolic stuff
|
443
|
+
res.rank = st.generic_rank(_rationalize_all_numbers(Matrix(onx)))
|
444
|
+
|
445
|
+
# If the onx matrix already has full rank... all is observable and identifiable
|
446
|
+
if res.rank == len(xaug):
|
447
|
+
res.state_obs = model.states
|
448
|
+
res.input_obs = model.unknown_inputs
|
449
|
+
res.par_ident = model.pars
|
450
|
+
break
|
451
|
+
|
452
|
+
# If there are unknown inputs, we may want to check id/obs of (x,p,w) and not of dw/dt:
|
453
|
+
if len(model.unknown_inputs) > 0:
|
454
|
+
_elim_and_recalc(
|
455
|
+
model=model,
|
456
|
+
res=res,
|
457
|
+
options=options,
|
458
|
+
unmeas_xred_indices=unmeasured_state_idxs,
|
459
|
+
onx=Matrix(onx),
|
460
|
+
unidflag=unidflag,
|
461
|
+
w1=w1vector,
|
462
|
+
)
|
463
|
+
|
464
|
+
if _test_fispo(
|
465
|
+
model=model, res=res, measured_state_names=measured_state_names
|
466
|
+
):
|
467
|
+
break
|
468
|
+
|
469
|
+
# If possible (& necessary), calculate one more Lie derivative and retry:
|
470
|
+
if n_min_lie_derivatives < len(xaug) and res.rank != lastrank:
|
471
|
+
ind = n_min_lie_derivatives
|
472
|
+
n_min_lie_derivatives = (
|
473
|
+
n_min_lie_derivatives + 1
|
474
|
+
) # One is added to the number of derivatives already made
|
475
|
+
extra_term = np.array(0) # reset for each new Lie derivative
|
476
|
+
# - Known input derivatives: ----------------------------------
|
477
|
+
# Extra terms of extended Lie derivatives
|
478
|
+
# may have to add extra input derivatives (note that 'nd' has grown):
|
479
|
+
if len(model.known_inputs) > 0:
|
480
|
+
input_der = _create_derivatives(
|
481
|
+
model.known_inputs,
|
482
|
+
n_min_lie_derivatives=n_min_lie_derivatives,
|
483
|
+
n_derivatives=options.non_zero_known_input_derivatives,
|
484
|
+
)
|
485
|
+
extra_term = _compute_extra_term(
|
486
|
+
extra_term,
|
487
|
+
ind=ind,
|
488
|
+
past_lie=past_Lie,
|
489
|
+
input_der=input_der,
|
490
|
+
zero_input_der_dummy_name=zero_input_der_dummy_name,
|
491
|
+
)
|
492
|
+
|
493
|
+
# add new derivatives, if they are not zero
|
494
|
+
if len(model.unknown_inputs) > 0:
|
495
|
+
prev_size = len(w1vector)
|
496
|
+
w_der = _create_derivatives(
|
497
|
+
model.unknown_inputs,
|
498
|
+
n_min_lie_derivatives=n_min_lie_derivatives + 1,
|
499
|
+
n_derivatives=options.non_zero_unknown_input_derivatives,
|
500
|
+
)
|
501
|
+
w1vector, w1vector_dot = _create_w1_vector(
|
502
|
+
model,
|
503
|
+
w_der=w_der,
|
504
|
+
)
|
505
|
+
xaug, faug = _create_xaug_faug(
|
506
|
+
model, w1vector=w1vector, w1vector_dot=w1vector_dot
|
507
|
+
)
|
508
|
+
|
509
|
+
# Augment size of the Obs-Id matrix if needed
|
510
|
+
new_size = len(w1vector)
|
511
|
+
onx = np.append(
|
512
|
+
onx,
|
513
|
+
zeros((ind + 1) * len(model.outputs), new_size - prev_size),
|
514
|
+
axis=1,
|
515
|
+
)
|
516
|
+
|
517
|
+
newLie = Matrix(
|
518
|
+
(
|
519
|
+
onx[(ind * len(model.outputs)) : (ind + 1) * len(model.outputs)][:] # type: ignore
|
520
|
+
).dot(faug) # type: ignore
|
521
|
+
)
|
522
|
+
past_Lie = newLie + extra_term if extra_term else newLie
|
523
|
+
newOnx = sym.Matrix(past_Lie).jacobian(xaug)
|
524
|
+
onx = np.append(onx, newOnx, axis=0)
|
525
|
+
lastrank = res.rank
|
526
|
+
|
527
|
+
# If that is not possible, there are several possible causes:
|
528
|
+
# This is the case when you have onx with all possible derivatives done
|
529
|
+
# and it is not full rank, the maximum time for the next derivative has passed
|
530
|
+
# or the matrix no longer increases in rank as derivatives are increased.
|
531
|
+
else:
|
532
|
+
# The maximum number of Lie derivatives has been reached
|
533
|
+
if n_min_lie_derivatives >= len(xaug):
|
534
|
+
unidflag = True
|
535
|
+
elif res.rank == lastrank:
|
536
|
+
onx = onx[0 : (-1 - (len(model.outputs) - 1))] # type: ignore
|
537
|
+
# It is indicated that the number of derivatives needed was
|
538
|
+
# one less than the number of derivatives made
|
539
|
+
n_min_lie_derivatives = n_min_lie_derivatives - 1
|
540
|
+
unidflag = True
|
541
|
+
|
542
|
+
if not skip_elim and not res.is_fispo:
|
543
|
+
# Eliminate columns one by one to check identifiability
|
544
|
+
# of the associated parameters
|
545
|
+
_elim_and_recalc(
|
546
|
+
model=model,
|
547
|
+
res=res,
|
548
|
+
options=options,
|
549
|
+
unmeas_xred_indices=unmeasured_state_idxs,
|
550
|
+
onx=Matrix(onx),
|
551
|
+
unidflag=unidflag,
|
552
|
+
w1=w1vector,
|
553
|
+
)
|
554
|
+
|
555
|
+
if _test_fispo(
|
556
|
+
model=model, res=res, measured_state_names=measured_state_names
|
557
|
+
):
|
558
|
+
break
|
559
|
+
|
560
|
+
break
|
561
|
+
pbar.close()
|
562
|
+
return res
|