modelbase2 0.3.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/distributions.py +5 -2
- 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/npe.py +8 -3
- modelbase2/simulator.py +7 -3
- modelbase2/surrogates/_poly.py +3 -1
- modelbase2/surrogates/_torch.py +4 -2
- modelbase2/surrogates.py +7 -1
- {modelbase2-0.3.0.dist-info → modelbase2-0.4.0.dist-info}/METADATA +2 -1
- {modelbase2-0.3.0.dist-info → modelbase2-0.4.0.dist-info}/RECORD +16 -13
- {modelbase2-0.3.0.dist-info → modelbase2-0.4.0.dist-info}/WHEEL +0 -0
- {modelbase2-0.3.0.dist-info → modelbase2-0.4.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,1017 @@
|
|
1
|
+
# ruff: noqa: D100, D101, D102, D103, D104, D105, D106, D107, D200, D203, D400, D401, T201
|
2
|
+
|
3
|
+
__all__ = [
|
4
|
+
"Model",
|
5
|
+
"Options",
|
6
|
+
"ScanResult",
|
7
|
+
"elim_and_recalc",
|
8
|
+
"rationalize_all_numbers",
|
9
|
+
"strike_goldd",
|
10
|
+
]
|
11
|
+
|
12
|
+
|
13
|
+
from dataclasses import dataclass, field
|
14
|
+
from datetime import datetime
|
15
|
+
from math import ceil, inf
|
16
|
+
from pathlib import Path
|
17
|
+
from time import time
|
18
|
+
from typing import cast
|
19
|
+
|
20
|
+
import numpy as np
|
21
|
+
import symbtools as st
|
22
|
+
import sympy as sp
|
23
|
+
from sympy.matrices import zeros
|
24
|
+
|
25
|
+
|
26
|
+
@dataclass
|
27
|
+
class Model:
|
28
|
+
x: list[list[sp.Symbol]] # known variables
|
29
|
+
p: list[list[sp.Symbol]] # unknown parameters
|
30
|
+
w: list # unknown symbols
|
31
|
+
u: list # known symbols
|
32
|
+
f: list # dynamic equations
|
33
|
+
h: list # outputs
|
34
|
+
|
35
|
+
|
36
|
+
@dataclass
|
37
|
+
class Options:
|
38
|
+
name: str
|
39
|
+
check_obser = 1
|
40
|
+
max_lie_time = inf
|
41
|
+
nnz_der_u: list[float] = field(default_factory=lambda: [inf])
|
42
|
+
nnz_der_w: list[float] = field(default_factory=lambda: [inf])
|
43
|
+
prev_ident_pars: list = field(default_factory=list)
|
44
|
+
|
45
|
+
|
46
|
+
@dataclass
|
47
|
+
class ScanResult: ...
|
48
|
+
|
49
|
+
|
50
|
+
def rationalize_all_numbers(expr: sp.Matrix) -> sp.Matrix:
|
51
|
+
numbers_atoms = list(expr.atoms(sp.Number))
|
52
|
+
rationalized_number_tpls = [(n, sp.Rational(n)) for n in numbers_atoms]
|
53
|
+
return cast(sp.Matrix, expr.subs(rationalized_number_tpls))
|
54
|
+
|
55
|
+
|
56
|
+
def elim_and_recalc(
|
57
|
+
unmeas_xred_indices,
|
58
|
+
rangoinicial,
|
59
|
+
numonx,
|
60
|
+
p,
|
61
|
+
x,
|
62
|
+
unidflag,
|
63
|
+
w1vector,
|
64
|
+
*args,
|
65
|
+
):
|
66
|
+
numonx = rationalize_all_numbers(sp.Matrix(numonx))
|
67
|
+
# Depending on the number of arguments you pass to the function, there are two cases:
|
68
|
+
|
69
|
+
# called when there is no 'w'
|
70
|
+
if len(args) == 0:
|
71
|
+
pred = p
|
72
|
+
xred = x
|
73
|
+
wred = w1vector
|
74
|
+
identifiables = []
|
75
|
+
obs_states = []
|
76
|
+
obs_inputs = []
|
77
|
+
q = len(pred)
|
78
|
+
n = len(xred)
|
79
|
+
nw = len(wred)
|
80
|
+
|
81
|
+
# called when there are 'w'
|
82
|
+
if len(args) == 3:
|
83
|
+
pred = p
|
84
|
+
xred = x
|
85
|
+
wred = w1vector
|
86
|
+
identifiables = args[0]
|
87
|
+
obs_states = args[1]
|
88
|
+
obs_inputs = args[2]
|
89
|
+
q = len(pred)
|
90
|
+
n = len(xred)
|
91
|
+
nw = len(wred)
|
92
|
+
|
93
|
+
# before: q+n+nw; but with unknown inputs there may also be derivatives
|
94
|
+
r = sp.shape(sp.Matrix(numonx))[1]
|
95
|
+
new_ident_pars = identifiables
|
96
|
+
new_nonid_pars = []
|
97
|
+
new_obs_states = obs_states
|
98
|
+
new_unobs_states = []
|
99
|
+
new_obs_in = obs_inputs
|
100
|
+
new_unobs_in = []
|
101
|
+
|
102
|
+
# ========================================================================
|
103
|
+
# ELIMINATE A PARAMETER:
|
104
|
+
# ========================================================================
|
105
|
+
# At each iteration we remove a different column (= parameter) from onx:
|
106
|
+
for ind in range(q): # for each parameter of p...
|
107
|
+
if q <= 1: # check if the parameter has already been marked as identifiable
|
108
|
+
isidentifiable = pred[ind] in identifiables
|
109
|
+
else:
|
110
|
+
isidentifiable = any(pred[ind] in arr for arr in identifiables)
|
111
|
+
if isidentifiable:
|
112
|
+
print(
|
113
|
+
f"\n Parameter {pred[ind]} has already been classified as identifiable."
|
114
|
+
)
|
115
|
+
else:
|
116
|
+
indices = []
|
117
|
+
for i in range(r):
|
118
|
+
indices.append(i)
|
119
|
+
indices.pop(n + ind)
|
120
|
+
column_del_numonx = sp.Matrix(numonx).col(indices) # one column is removed
|
121
|
+
num_rank = st.generic_rank(
|
122
|
+
sp.Matrix(column_del_numonx)
|
123
|
+
) # the range is calculated without that column
|
124
|
+
if num_rank == rangoinicial:
|
125
|
+
if unidflag == 1:
|
126
|
+
print(
|
127
|
+
f"\n => Parameter {pred[ind]} is structurally unidentifiable"
|
128
|
+
)
|
129
|
+
new_nonid_pars.append(pred[ind])
|
130
|
+
else:
|
131
|
+
print(
|
132
|
+
f"\n => We cannot decide about parameter {pred[ind]} at the moment"
|
133
|
+
)
|
134
|
+
else:
|
135
|
+
print(f"\n => Parameter {pred[ind]} is structurally identifiable")
|
136
|
+
new_ident_pars.append(pred[ind])
|
137
|
+
|
138
|
+
# ========================================================================
|
139
|
+
# ELIMINATE A STATE:
|
140
|
+
# ========================================================================
|
141
|
+
# At each iteration we try removing a different state from 'xred':
|
142
|
+
if options.checkObser == 1:
|
143
|
+
for ind in range(len(unmeas_xred_indices)): # for each unmeasured state
|
144
|
+
original_index = unmeas_xred_indices[ind]
|
145
|
+
if len(obs_states) <= 1:
|
146
|
+
isobservable = xred[original_index] in obs_states
|
147
|
+
else:
|
148
|
+
isobservable = any(xred[original_index] in arr for arr in obs_states)
|
149
|
+
if isobservable:
|
150
|
+
print("\n State %s has already been classified as observable.".format())
|
151
|
+
else:
|
152
|
+
indices = []
|
153
|
+
for i in range(r):
|
154
|
+
indices.append(i)
|
155
|
+
indices.pop(original_index) # remove the column that we want to check
|
156
|
+
column_del_numonx = sp.Matrix(numonx).col(indices)
|
157
|
+
num_rank = st.generic_rank(sp.Matrix(column_del_numonx))
|
158
|
+
if num_rank == rangoinicial:
|
159
|
+
if unidflag == 1:
|
160
|
+
print(f"\n => State {xred[original_index]} is unobservable")
|
161
|
+
new_unobs_states.append(xred[original_index])
|
162
|
+
else: # if this function was called because the necessary number of derivatives was not calculated...
|
163
|
+
print(
|
164
|
+
f"\n => We cannot decide about state {xred[original_index]} at the moment"
|
165
|
+
)
|
166
|
+
else:
|
167
|
+
print(f"\n => State {xred[original_index]} is observable")
|
168
|
+
new_obs_states.append(xred[original_index])
|
169
|
+
|
170
|
+
# ========================================================================
|
171
|
+
# ELIMINATE AN UNKNOWN INPUT:
|
172
|
+
# ========================================================================
|
173
|
+
# At each iteration we try removing a different column from onx:
|
174
|
+
for ind in range(nw): # for each unknown input...
|
175
|
+
if (
|
176
|
+
len(obs_inputs) <= 1
|
177
|
+
): # check if the unknown input has already been marked as observable
|
178
|
+
isobservable = wred[ind] in obs_inputs
|
179
|
+
else:
|
180
|
+
isobservable = any(wred[ind] in arr for arr in obs_inputs)
|
181
|
+
if isobservable:
|
182
|
+
print("\n Input %s has already been classified as observable.".format())
|
183
|
+
else:
|
184
|
+
indices = []
|
185
|
+
for i in range(r):
|
186
|
+
indices.append(i)
|
187
|
+
indices.pop(n + q + ind) # remove the column that we want to check
|
188
|
+
column_del_numonx = sp.Matrix(numonx).col(indices)
|
189
|
+
num_rank = st.generic_rank(sp.Matrix(column_del_numonx))
|
190
|
+
if num_rank == rangoinicial:
|
191
|
+
if unidflag == 1:
|
192
|
+
print(f"\n => Input {wred[ind]} is unobservable")
|
193
|
+
new_unobs_in.append(wred[ind])
|
194
|
+
else:
|
195
|
+
print(
|
196
|
+
f"\n => We cannot decide about input {wred[ind]} at the moment"
|
197
|
+
)
|
198
|
+
else:
|
199
|
+
print(f"\n => Input {wred[ind]} is observable")
|
200
|
+
new_obs_in.append(wred[ind])
|
201
|
+
return (
|
202
|
+
new_ident_pars,
|
203
|
+
new_nonid_pars,
|
204
|
+
new_obs_states,
|
205
|
+
new_unobs_states,
|
206
|
+
new_obs_in,
|
207
|
+
new_unobs_in,
|
208
|
+
)
|
209
|
+
|
210
|
+
|
211
|
+
def strike_goldd(model: Model, options: Options) -> ScanResult:
|
212
|
+
results_dir = Path("results")
|
213
|
+
results_dir.mkdir(parents=True, exist_ok=True)
|
214
|
+
|
215
|
+
# Initialize variables:
|
216
|
+
identifiables = [] # identifiable parameters.
|
217
|
+
nonidentif = [] # unidentifiable parameters.
|
218
|
+
obs_states = [] # observable states.
|
219
|
+
unobs_states = [] # unobservable states.
|
220
|
+
obs_inputs = [] # observable inputs.
|
221
|
+
unobs_inputs = [] # unobservable inputs.
|
222
|
+
lastrank = None
|
223
|
+
unidflag = 0
|
224
|
+
skip_elim = 0
|
225
|
+
is_fispo = 0
|
226
|
+
|
227
|
+
# Dimensions of the problem:
|
228
|
+
m = len(model.h) # number of outputs
|
229
|
+
n = len(model.x) # number of states
|
230
|
+
q = len(model.p) # number of unknown parameters
|
231
|
+
nw = len(model.w)
|
232
|
+
r = n + q + nw # number of unknown variables to observe / identify
|
233
|
+
nd = ceil((r - m) / m) # minimum number of Lie derivatives for Oi to have full rank
|
234
|
+
|
235
|
+
# Check which states are directly measured, if any.
|
236
|
+
# Basically it is checked if any state is directly on the output,
|
237
|
+
# then that state is directly measurable.
|
238
|
+
saidas = model.h if m == 1 else [model.h[i] for i in range(m)]
|
239
|
+
estados = model.x if n == 1 else [model.x[i][0] for i in range(n)]
|
240
|
+
ismeasured = [0 for i in range(n)]
|
241
|
+
|
242
|
+
if len(saidas) == 1:
|
243
|
+
for i in range(n):
|
244
|
+
if estados[i] in saidas:
|
245
|
+
ismeasured[i] = 1
|
246
|
+
else:
|
247
|
+
for i in range(n):
|
248
|
+
if any(estados[i] in arr for arr in saidas):
|
249
|
+
ismeasured[i] = 1
|
250
|
+
|
251
|
+
measured_states_idx = [i for i in range(n) if ismeasured[i] == 1]
|
252
|
+
unmeasured_states_idx = [i for i in range(n) if ismeasured[i] == 0]
|
253
|
+
|
254
|
+
# names of the measured states
|
255
|
+
meas_x = []
|
256
|
+
if len(measured_states_idx) == 1 and n == 1:
|
257
|
+
meas_x = estados
|
258
|
+
if len(measured_states_idx) == 1 and n != 1:
|
259
|
+
meas_x.append(estados[measured_states_idx[0]])
|
260
|
+
if len(measured_states_idx) > 1:
|
261
|
+
for i in range(len(measured_states_idx)):
|
262
|
+
meas_x.append([estados[measured_states_idx[i]]])
|
263
|
+
|
264
|
+
print(
|
265
|
+
f"Building the observability-identifiability matrix requires at least {nd} Lie derivatives"
|
266
|
+
)
|
267
|
+
print("Calculating derivatives: ")
|
268
|
+
|
269
|
+
########################################################################
|
270
|
+
# Check if the size of nnzDerU and nnzDerW are appropriate
|
271
|
+
if len(model.u) > len(options.nnz_der_u):
|
272
|
+
msg = """ The number of known inputs is higher than the size of nnzDerU and must have the same size.
|
273
|
+
Go to the options file and modify it.
|
274
|
+
For more information about the error see point 7 of the StrikePy instruction manual."""
|
275
|
+
raise ValueError(msg)
|
276
|
+
if len(model.w) > len(options.nnz_der_w):
|
277
|
+
msg = """ The number of unknown inputs is higher than the size of nnzDerW and must have the same size.
|
278
|
+
Go to the options file and modify it.
|
279
|
+
For more information about the error see point 7 of the StrikePy instruction manual. """
|
280
|
+
raise ValueError(msg)
|
281
|
+
|
282
|
+
########################################################################
|
283
|
+
# Input derivates:
|
284
|
+
|
285
|
+
# Create array of known inputs and set certain derivatives to zero:
|
286
|
+
input_der = []
|
287
|
+
if len(model.u) > 0:
|
288
|
+
for ind_u in range(len(model.u)): # create array of derivatives of the inputs
|
289
|
+
if len(model.u) == 1:
|
290
|
+
locals()[f"{model.u[ind_u]}"] = sp.Symbol(
|
291
|
+
f"{model.u[ind_u]}"
|
292
|
+
) # the first element is the underived input
|
293
|
+
auxiliar = [locals()[f"{model.u[ind_u]}"]]
|
294
|
+
else:
|
295
|
+
locals()[f"{model.u[ind_u][0]}"] = sp.Symbol(
|
296
|
+
f"{model.u[ind_u][0]}"
|
297
|
+
) # the first element is the underived input
|
298
|
+
auxiliar = [locals()[f"{model.u[ind_u][0]}"]]
|
299
|
+
for k in range(nd):
|
300
|
+
if len(model.u) == 1:
|
301
|
+
locals()[f"{model.u[ind_u]}_d{k + 1}"] = sp.Symbol(
|
302
|
+
f"{model.u[ind_u]}_d{k + 1}"
|
303
|
+
)
|
304
|
+
auxiliar.append(locals()[f"{model.u[ind_u]}_d{k + 1}"])
|
305
|
+
else:
|
306
|
+
locals()[f"{model.u[ind_u][0]}_d{k + 1}"] = sp.Symbol(
|
307
|
+
f"{model.u[ind_u][0]}_d{k + 1}"
|
308
|
+
)
|
309
|
+
auxiliar.append(locals()[f"{model.u[ind_u][0]}_d{k + 1}"])
|
310
|
+
if len(model.u) == 1:
|
311
|
+
input_der = auxiliar
|
312
|
+
if len(input_der) >= options.nnz_der_u[0] + 1:
|
313
|
+
for i in range(len(input_der[(options.nnz_der_u[0] + 1) :])):
|
314
|
+
input_der[(options.nnz_der_u[0] + 1) + i] = 0
|
315
|
+
else:
|
316
|
+
input_der.append(auxiliar)
|
317
|
+
if len(input_der[0]) >= options.nnz_der_u[ind_u] + 1:
|
318
|
+
for i in range(len(input_der[0][(options.nnz_der_u[ind_u] + 1) :])):
|
319
|
+
input_der[ind_u][(options.nnz_der_u[ind_u] + 1) + i] = 0
|
320
|
+
zero_input_der_dummy_name = sp.Symbol("zero_input_der_dummy_name")
|
321
|
+
|
322
|
+
# Create array of unknown inputs and set certain derivatives to zero:
|
323
|
+
w_der = []
|
324
|
+
if len(model.w) > 0:
|
325
|
+
for ind_w in range(len(model.w)): # create array of derivatives of the inputs
|
326
|
+
if len(model.w) == 1:
|
327
|
+
locals()[f"{model.w[ind_w]}"] = sp.Symbol(
|
328
|
+
f"{model.w[ind_w]}"
|
329
|
+
) # the first element is the underived input
|
330
|
+
auxiliar = [locals()[f"{model.w[ind_w]}"]]
|
331
|
+
else:
|
332
|
+
locals()[f"{model.w[ind_w][0]}"] = sp.Symbol(
|
333
|
+
f"{model.w[ind_w][0]}"
|
334
|
+
) # the first element is the underived input
|
335
|
+
auxiliar = [locals()[f"{model.w[ind_w][0]}"]]
|
336
|
+
for k in range(nd + 1):
|
337
|
+
if len(model.w) == 1:
|
338
|
+
locals()[f"{model.w[ind_w]}_d{k + 1}"] = sp.Symbol(
|
339
|
+
f"{model.w[ind_w]}_d{k + 1}"
|
340
|
+
)
|
341
|
+
auxiliar.append(locals()[f"{model.w[ind_w]}_d{k + 1}"])
|
342
|
+
else:
|
343
|
+
locals()[f"{model.w[ind_w][0]}_d{k + 1}"] = sp.Symbol(
|
344
|
+
f"{model.w[ind_w][0]}_d{k + 1}"
|
345
|
+
)
|
346
|
+
auxiliar.append(locals()[f"{model.w[ind_w][0]}_d{k + 1}"])
|
347
|
+
if len(model.w) == 1:
|
348
|
+
w_der = auxiliar
|
349
|
+
if len(w_der) >= options.nnz_der_w[0] + 1:
|
350
|
+
for i in range(len(w_der[(options.nnz_der_w[0] + 1) :])):
|
351
|
+
w_der[(options.nnz_der_w[0] + 1) + i] = 0
|
352
|
+
else:
|
353
|
+
w_der.append(auxiliar)
|
354
|
+
if len(w_der[0]) >= options.nnz_der_w[ind_w] + 1:
|
355
|
+
for i in range(len(w_der[0][(options.nnz_der_w[ind_w] + 1) :])):
|
356
|
+
w_der[ind_w][(options.nnzDerW[ind_w] + 1) + i] = 0
|
357
|
+
|
358
|
+
if sp.shape(sp.Matrix(w_der).T)[0] == 1:
|
359
|
+
w1vector = [[w_der[i]] for i in range(len(w_der) - 1)]
|
360
|
+
w1vector_dot = [[w_der[i]] for i in range(1, len(w_der))]
|
361
|
+
|
362
|
+
else:
|
363
|
+
w1vector = []
|
364
|
+
for k in range(sp.shape(sp.Matrix(w_der))[1] - 1):
|
365
|
+
for i in w_der:
|
366
|
+
w1vector.append([i[k]])
|
367
|
+
w1vector_dot = []
|
368
|
+
for k in range(sp.shape(sp.Matrix(w_der))[1]):
|
369
|
+
for i in w_der:
|
370
|
+
if k != 0:
|
371
|
+
w1vector_dot.append([i[k]])
|
372
|
+
|
373
|
+
# -- Include as states only nonzero inputs / derivatives:
|
374
|
+
nzi = [[fila] for fila in range(len(w1vector)) if w1vector[fila][0] != 0]
|
375
|
+
nzj = [[1] for fila in range(len(w1vector)) if w1vector[fila][0] != 0]
|
376
|
+
nz_w1vec = [
|
377
|
+
w1vector[fila] for fila in range(len(w1vector)) if w1vector[fila][0] != 0
|
378
|
+
]
|
379
|
+
w1vector = nz_w1vec
|
380
|
+
w1vector_dot = w1vector_dot[0 : len(nzi)]
|
381
|
+
|
382
|
+
else:
|
383
|
+
w1vector = []
|
384
|
+
w1vector_dot = []
|
385
|
+
|
386
|
+
########################################################################
|
387
|
+
# Augment state vector, dynamics:
|
388
|
+
if len(model.x) == 1:
|
389
|
+
xaug = []
|
390
|
+
xaug.append(model.x)
|
391
|
+
xaug = np.append(xaug, model.p, axis=0)
|
392
|
+
if len(w1vector) != 0:
|
393
|
+
xaug = np.append(xaug, w1vector, axis=0)
|
394
|
+
|
395
|
+
faug = []
|
396
|
+
faug.append(model.f)
|
397
|
+
faug = np.append(faug, zeros(len(model.p), 1), axis=0)
|
398
|
+
if len(w1vector) != 0:
|
399
|
+
faug = np.append(faug, w1vector_dot, axis=0)
|
400
|
+
|
401
|
+
else:
|
402
|
+
xaug = model.x
|
403
|
+
xaug = np.append(xaug, model.p, axis=0)
|
404
|
+
if len(w1vector) != 0:
|
405
|
+
xaug = np.append(xaug, w1vector, axis=0)
|
406
|
+
|
407
|
+
faug = model.f
|
408
|
+
faug = np.append(faug, zeros(len(model.p), 1), axis=0)
|
409
|
+
if len(w1vector) != 0:
|
410
|
+
faug = np.append(faug, w1vector_dot, axis=0)
|
411
|
+
########################################################################
|
412
|
+
# Build Oi:
|
413
|
+
onx = np.array(zeros(m * (1 + nd), n + q + len(w1vector)))
|
414
|
+
jacobiano = sp.Matrix(model.h).jacobian(xaug)
|
415
|
+
onx[0 : len(model.h)] = np.array(
|
416
|
+
jacobiano
|
417
|
+
) # first row(s) of onx (derivative of the output with respect to the vector states+unknown parameters).
|
418
|
+
ind = 0 # Lie derivative index (sometimes called 'k')
|
419
|
+
|
420
|
+
########################################################################
|
421
|
+
past_Lie = model.h
|
422
|
+
extra_term = np.array(0)
|
423
|
+
|
424
|
+
# loop as long as I don't complete the preset Lie derivatives or go over the maximum time set for each derivative
|
425
|
+
while ind < nd:
|
426
|
+
Lieh = sp.Matrix((onx[(ind * m) : (ind + 1) * m][:]).dot(faug))
|
427
|
+
if ind > 0 and len(model.u) > 0:
|
428
|
+
for i in range(ind):
|
429
|
+
if len(model.u) == 1:
|
430
|
+
column = len(input_der) - 1
|
431
|
+
if i < column:
|
432
|
+
lo_u_der = input_der[i]
|
433
|
+
if lo_u_der == 0:
|
434
|
+
lo_u_der = zero_input_der_dummy_name
|
435
|
+
lo_u_der = np.array([lo_u_der])
|
436
|
+
hi_u_der = input_der[i + 1]
|
437
|
+
hi_u_der = sp.Matrix([hi_u_der])
|
438
|
+
|
439
|
+
intermedio = sp.Matrix([past_Lie]).jacobian(lo_u_der) * hi_u_der
|
440
|
+
if extra_term:
|
441
|
+
extra_term = extra_term + intermedio
|
442
|
+
else:
|
443
|
+
extra_term = intermedio
|
444
|
+
else:
|
445
|
+
column = len(input_der[0]) - 1
|
446
|
+
if i < column:
|
447
|
+
lo_u_der = []
|
448
|
+
hi_u_der = []
|
449
|
+
for fila in input_der:
|
450
|
+
lo_u_der.append(fila[i])
|
451
|
+
hi_u_der.append(fila[i + 1])
|
452
|
+
for i in range(len(lo_u_der)):
|
453
|
+
if lo_u_der[i] == 0:
|
454
|
+
lo_u_der[i] = zero_input_der_dummy_name
|
455
|
+
lo_u_der = np.array(lo_u_der)
|
456
|
+
hi_u_der = sp.Matrix(hi_u_der)
|
457
|
+
intermedio = sp.Matrix([past_Lie]).jacobian(lo_u_der) * hi_u_der
|
458
|
+
if extra_term:
|
459
|
+
extra_term = extra_term + intermedio
|
460
|
+
else:
|
461
|
+
extra_term = intermedio
|
462
|
+
ext_Lie = Lieh + extra_term if extra_term else Lieh
|
463
|
+
past_Lie = ext_Lie
|
464
|
+
onx[((ind + 1) * m) : (ind + 2) * m] = sp.Matrix(ext_Lie).jacobian(xaug)
|
465
|
+
|
466
|
+
ind = ind + 1
|
467
|
+
print(end=f" {ind}")
|
468
|
+
|
469
|
+
if (
|
470
|
+
ind == nd
|
471
|
+
): # If I have done all the minimum derivatives to build onx (I have not exceeded the time limit)....
|
472
|
+
increaseLie = 1
|
473
|
+
while (
|
474
|
+
increaseLie == 1
|
475
|
+
): # while increaseLie is 1 I will increase the size of onx
|
476
|
+
print(
|
477
|
+
f"\n >>> Observability-Identifiability matrix built with {nd} Lie derivatives"
|
478
|
+
)
|
479
|
+
# =============================================================================================
|
480
|
+
# The observability/identifiability matrix is saved in a .txt file
|
481
|
+
|
482
|
+
with (
|
483
|
+
results_dir / f"obs_ident_matrix_{options.name}_{nd}_Lie_deriv.txt"
|
484
|
+
).open("w") as file:
|
485
|
+
file.write(f"onx = {onx.tolist()!s}")
|
486
|
+
|
487
|
+
# =============================================================================================
|
488
|
+
# Check identifiability by calculating rank:
|
489
|
+
print(
|
490
|
+
f" >>> Calculating rank of matrix with size {sp.shape(sp.Matrix(onx))[0]}x{sp.shape(sp.Matrix(onx))[1]}..."
|
491
|
+
)
|
492
|
+
rational_onx = rationalize_all_numbers(sp.Matrix(onx))
|
493
|
+
rango = st.generic_rank(sp.Matrix(rational_onx))
|
494
|
+
print(f" Rank = {rango} (calculated in {toc} seconds)")
|
495
|
+
if (
|
496
|
+
rango == len(xaug)
|
497
|
+
): # If the onx matrix already has full rank... all is observable and identifiable
|
498
|
+
obs_states = model.x
|
499
|
+
obs_inputs = model.w
|
500
|
+
identifiables = model.p
|
501
|
+
increaseLie = (
|
502
|
+
0 # stop increasing the number of onx rows with derivatives
|
503
|
+
)
|
504
|
+
|
505
|
+
else: # With that number of Lie derivatives the array is not full rank.
|
506
|
+
# ----------------------------------------------------------
|
507
|
+
# If there are unknown inputs, we may want to check id/obs of (x,p,w) and not of dw/dt:
|
508
|
+
if len(model.w) > 0:
|
509
|
+
[
|
510
|
+
identifiables,
|
511
|
+
nonidentif,
|
512
|
+
obs_states,
|
513
|
+
unobs_states,
|
514
|
+
obs_inputs,
|
515
|
+
unobs_inputs,
|
516
|
+
] = elim_and_recalc(
|
517
|
+
unmeasured_states_idx,
|
518
|
+
rango,
|
519
|
+
onx,
|
520
|
+
model.p,
|
521
|
+
model.x,
|
522
|
+
unidflag,
|
523
|
+
w1vector,
|
524
|
+
identifiables,
|
525
|
+
obs_states,
|
526
|
+
obs_inputs,
|
527
|
+
)
|
528
|
+
|
529
|
+
# Check which unknown inputs are observable:
|
530
|
+
obs_in_no_der = []
|
531
|
+
if len(model.w) == 1 and len(obs_inputs) > 0:
|
532
|
+
if model.w == obs_inputs:
|
533
|
+
obs_in_no_der = model.w
|
534
|
+
if len(model.w) > 1 and len(obs_inputs) > 0:
|
535
|
+
for elemento in model.w:
|
536
|
+
if len(obs_inputs) == 1:
|
537
|
+
if elemento == obs_inputs:
|
538
|
+
obs_in_no_der = elemento
|
539
|
+
else:
|
540
|
+
for input_ in obs_inputs:
|
541
|
+
if elemento == input_:
|
542
|
+
obs_in_no_der.append(elemento[0])
|
543
|
+
if (
|
544
|
+
len(identifiables) == len(model.p)
|
545
|
+
and len(obs_states) + len(meas_x) == len(model.x)
|
546
|
+
and len(obs_in_no_der) == len(model.w)
|
547
|
+
):
|
548
|
+
obs_states = model.x
|
549
|
+
obs_inputs = obs_in_no_der
|
550
|
+
identifiables = model.p
|
551
|
+
increaseLie = 0 # -> with this we skip the next 'if' block and jump to the end of the algorithm
|
552
|
+
is_fispo = 1
|
553
|
+
# ----------------------------------------------------------
|
554
|
+
# If possible (& necessary), calculate one more Lie derivative and retry:
|
555
|
+
if (
|
556
|
+
nd < len(xaug)
|
557
|
+
and lasttime < options.max_lie_time
|
558
|
+
and rango != lastrank
|
559
|
+
and increaseLie == 1
|
560
|
+
):
|
561
|
+
ind = nd
|
562
|
+
nd = (
|
563
|
+
nd + 1
|
564
|
+
) # One is added to the number of derivatives already made
|
565
|
+
extra_term = np.array(0) # reset for each new Lie derivative
|
566
|
+
# - Known input derivatives: ----------------------------------
|
567
|
+
if len(model.u) > 0: # Extra terms of extended Lie derivatives
|
568
|
+
# may have to add extra input derivatives (note that 'nd' has grown):
|
569
|
+
input_der = []
|
570
|
+
for ind_u in range(
|
571
|
+
len(model.u)
|
572
|
+
): # create array of derivatives of the inputs
|
573
|
+
if len(model.u) == 1:
|
574
|
+
locals()[f"{model.u[ind_u]}"] = sp.Symbol(
|
575
|
+
f"{model.u[ind_u]}"
|
576
|
+
) # the first element is the underived input
|
577
|
+
auxiliar = [locals()[f"{model.u[ind_u]}"]]
|
578
|
+
else:
|
579
|
+
locals()[f"{model.u[ind_u][0]}"] = sp.Symbol(
|
580
|
+
f"{model.u[ind_u][0]}"
|
581
|
+
) # the first element is the underived input
|
582
|
+
auxiliar = [locals()[f"{model.u[ind_u][0]}"]]
|
583
|
+
for k in range(nd):
|
584
|
+
if len(model.u) == 1:
|
585
|
+
locals()[f"{model.u[ind_u]}_d{k + 1}"] = sp.Symbol(
|
586
|
+
f"{model.u[ind_u]}_d{k + 1}"
|
587
|
+
)
|
588
|
+
auxiliar.append(
|
589
|
+
locals()[f"{model.u[ind_u]}_d{k + 1}"]
|
590
|
+
)
|
591
|
+
else:
|
592
|
+
locals()[f"{model.u[ind_u][0]}_d{k + 1}"] = (
|
593
|
+
sp.Symbol(f"{model.u[ind_u][0]}_d{k + 1}")
|
594
|
+
)
|
595
|
+
auxiliar.append(
|
596
|
+
locals()[f"{model.u[ind_u][0]}_d{k + 1}"]
|
597
|
+
)
|
598
|
+
if len(model.u) == 1:
|
599
|
+
input_der = auxiliar
|
600
|
+
if len(input_der) >= options.nnz_der_u[0] + 1:
|
601
|
+
for i in range(
|
602
|
+
len(input_der[(options.nnz_der_u[0] + 1) :])
|
603
|
+
):
|
604
|
+
input_der[(options.nnz_der_u[0] + 1) + i] = 0
|
605
|
+
else:
|
606
|
+
input_der.append(auxiliar)
|
607
|
+
if len(input_der[0]) >= options.nnz_der_u[ind_u] + 1:
|
608
|
+
for i in range(
|
609
|
+
len(
|
610
|
+
input_der[0][
|
611
|
+
(options.nnz_der_u[ind_u] + 1) :
|
612
|
+
]
|
613
|
+
)
|
614
|
+
):
|
615
|
+
input_der[ind_u][
|
616
|
+
(options.nnzDerU[ind_u] + 1) + i
|
617
|
+
] = 0
|
618
|
+
|
619
|
+
for i in range(ind):
|
620
|
+
if len(model.u) == 1:
|
621
|
+
column = len(input_der) - 1
|
622
|
+
if i < column:
|
623
|
+
lo_u_der = input_der[i]
|
624
|
+
if lo_u_der == 0:
|
625
|
+
lo_u_der = zero_input_der_dummy_name
|
626
|
+
lo_u_der = np.array([lo_u_der])
|
627
|
+
hi_u_der = input_der[i + 1]
|
628
|
+
hi_u_der = sp.Matrix([hi_u_der])
|
629
|
+
|
630
|
+
intermedio = (
|
631
|
+
sp.Matrix([past_Lie]).jacobian(lo_u_der)
|
632
|
+
* hi_u_der
|
633
|
+
)
|
634
|
+
if extra_term:
|
635
|
+
extra_term = extra_term + intermedio
|
636
|
+
else:
|
637
|
+
extra_term = intermedio
|
638
|
+
else:
|
639
|
+
column = len(input_der[0]) - 1
|
640
|
+
if i < column:
|
641
|
+
lo_u_der = []
|
642
|
+
hi_u_der = []
|
643
|
+
for fila in input_der:
|
644
|
+
lo_u_der.append(fila[i])
|
645
|
+
hi_u_der.append(fila[i + 1])
|
646
|
+
for i in range(len(lo_u_der)):
|
647
|
+
if lo_u_der[i] == 0:
|
648
|
+
lo_u_der[i] = zero_input_der_dummy_name
|
649
|
+
lo_u_der = np.array(lo_u_der)
|
650
|
+
hi_u_der = sp.Matrix(hi_u_der)
|
651
|
+
intermedio = (
|
652
|
+
sp.Matrix([past_Lie]).jacobian(lo_u_der)
|
653
|
+
* hi_u_der
|
654
|
+
)
|
655
|
+
if extra_term:
|
656
|
+
extra_term = extra_term + intermedio
|
657
|
+
else:
|
658
|
+
extra_term = intermedio
|
659
|
+
|
660
|
+
# - Unknown input derivatives:----------------
|
661
|
+
# add new derivatives, if they are not zero:
|
662
|
+
if len(model.w) > 0:
|
663
|
+
prev_size = len(w1vector)
|
664
|
+
w_der = []
|
665
|
+
for ind_w in range(
|
666
|
+
len(model.w)
|
667
|
+
): # create array of derivatives of the inputs
|
668
|
+
if len(model.w) == 1:
|
669
|
+
locals()[f"{model.w[ind_w]}"] = sp.Symbol(
|
670
|
+
f"{model.w[ind_w]}"
|
671
|
+
) # the first element is the underived input
|
672
|
+
auxiliar = [locals()[f"{model.w[ind_w]}"]]
|
673
|
+
else:
|
674
|
+
locals()[f"{model.w[ind_w][0]}"] = sp.Symbol(
|
675
|
+
f"{model.w[ind_w][0]}"
|
676
|
+
) # the first element is the underived input
|
677
|
+
auxiliar = [locals()[f"{model.w[ind_w][0]}"]]
|
678
|
+
for k in range(nd + 1):
|
679
|
+
if len(model.w) == 1:
|
680
|
+
locals()[f"{model.w[ind_w]}_d{k + 1}"] = sp.Symbol(
|
681
|
+
f"{model.w[ind_w]}_d{k + 1}"
|
682
|
+
)
|
683
|
+
auxiliar.append(
|
684
|
+
locals()[f"{model.w[ind_w]}_d{k + 1}"]
|
685
|
+
)
|
686
|
+
else:
|
687
|
+
locals()[f"{model.w[ind_w][0]}_d{k + 1}"] = (
|
688
|
+
sp.Symbol(f"{model.w[ind_w][0]}_d{k + 1}")
|
689
|
+
)
|
690
|
+
auxiliar.append(
|
691
|
+
locals()[f"{model.w[ind_w][0]}_d{k + 1}"]
|
692
|
+
)
|
693
|
+
if len(model.w) == 1:
|
694
|
+
w_der = auxiliar
|
695
|
+
if len(w_der) >= options.nnz_der_w[0] + 1:
|
696
|
+
for i in range(
|
697
|
+
len(w_der[(options.nnz_der_w[0] + 1) :])
|
698
|
+
):
|
699
|
+
w_der[(options.nnz_der_w[0] + 1) + i] = 0
|
700
|
+
else:
|
701
|
+
w_der.append(auxiliar)
|
702
|
+
if len(w_der[0]) >= options.nnz_der_w[ind_w] + 1:
|
703
|
+
for i in range(
|
704
|
+
len(w_der[0][(options.nnz_der_w[ind_w] + 1) :])
|
705
|
+
):
|
706
|
+
w_der[ind_w][
|
707
|
+
(options.nnzDerW[ind_w] + 1) + i
|
708
|
+
] = 0
|
709
|
+
|
710
|
+
if sp.shape(sp.Matrix(w_der).T)[0] == 1:
|
711
|
+
w1vector = []
|
712
|
+
for i in range(len(w_der) - 1):
|
713
|
+
w1vector.append([w_der[i]])
|
714
|
+
w1vector_dot = []
|
715
|
+
for i in range(len(w_der)):
|
716
|
+
if i != 0:
|
717
|
+
w1vector_dot.append([w_der[i]])
|
718
|
+
|
719
|
+
else:
|
720
|
+
w1vector = []
|
721
|
+
for k in range(sp.shape(sp.Matrix(w_der))[1] - 1):
|
722
|
+
for i in w_der:
|
723
|
+
w1vector.append([i[k]])
|
724
|
+
w1vector_dot = []
|
725
|
+
for k in range(sp.shape(sp.Matrix(w_der))[1]):
|
726
|
+
for i in w_der:
|
727
|
+
if k != 0:
|
728
|
+
w1vector_dot.append([i[k]])
|
729
|
+
|
730
|
+
# -- Include as states only nonzero inputs / derivatives:
|
731
|
+
nzi = []
|
732
|
+
for fila in range(len(w1vector)):
|
733
|
+
if w1vector[fila][0] != 0:
|
734
|
+
nzi.append([fila])
|
735
|
+
nzj = []
|
736
|
+
for fila in range(len(w1vector)):
|
737
|
+
if w1vector[fila][0] != 0:
|
738
|
+
nzj.append([1])
|
739
|
+
nz_w1vec = []
|
740
|
+
for fila in range(len(w1vector)):
|
741
|
+
if w1vector[fila][0] != 0:
|
742
|
+
nz_w1vec.append(w1vector[fila])
|
743
|
+
w1vector = nz_w1vec
|
744
|
+
w1vector_dot = w1vector_dot[0 : len(nzi)]
|
745
|
+
|
746
|
+
########################################################################
|
747
|
+
# Augment state vector, dynamics:
|
748
|
+
if len(model.x) == 1:
|
749
|
+
xaug = []
|
750
|
+
xaug.append(model.x)
|
751
|
+
xaug = np.append(xaug, model.p, axis=0)
|
752
|
+
if len(w1vector) != 0:
|
753
|
+
xaug = np.append(xaug, w1vector, axis=0)
|
754
|
+
|
755
|
+
faug = []
|
756
|
+
faug.append(model.f)
|
757
|
+
faug = np.append(faug, zeros(len(model.p), 1), axis=0)
|
758
|
+
if len(w1vector) != 0:
|
759
|
+
faug = np.append(faug, w1vector_dot, axis=0)
|
760
|
+
|
761
|
+
else:
|
762
|
+
xaug = model.x
|
763
|
+
xaug = np.append(xaug, model.p, axis=0)
|
764
|
+
if len(w1vector) != 0:
|
765
|
+
xaug = np.append(xaug, w1vector, axis=0)
|
766
|
+
|
767
|
+
faug = model.f
|
768
|
+
faug = np.append(faug, zeros(len(model.p), 1), axis=0)
|
769
|
+
if len(w1vector) != 0:
|
770
|
+
faug = np.append(faug, w1vector_dot, axis=0)
|
771
|
+
########################################################################
|
772
|
+
# -- Augment size of the Obs-Id matrix if needed:
|
773
|
+
new_size = len(w1vector)
|
774
|
+
onx = np.append(
|
775
|
+
onx, zeros((ind + 1) * m, new_size - prev_size), axis=1
|
776
|
+
)
|
777
|
+
########################################################################
|
778
|
+
newLie = sp.Matrix((onx[(ind * m) : (ind + 1) * m][:]).dot(faug))
|
779
|
+
past_Lie = newLie + extra_term if extra_term else newLie
|
780
|
+
newOnx = sp.Matrix(past_Lie).jacobian(xaug)
|
781
|
+
onx = np.append(onx, newOnx, axis=0)
|
782
|
+
|
783
|
+
lastrank = rango
|
784
|
+
|
785
|
+
# If that is not possible, there are several possible causes:
|
786
|
+
# This is the case when you have onx with all possible derivatives done and it is not full rank, the maximum time for the next derivative has passed
|
787
|
+
# or the matrix no longer increases in rank as derivatives are increased.
|
788
|
+
else:
|
789
|
+
if nd >= len(
|
790
|
+
xaug
|
791
|
+
): # The maximum number of Lie derivatives has been reached
|
792
|
+
unidflag = 1
|
793
|
+
print(
|
794
|
+
"\n >>> The model is structurally unidentifiable as a whole"
|
795
|
+
)
|
796
|
+
elif rango == lastrank:
|
797
|
+
onx = onx[0 : (-1 - (m - 1))]
|
798
|
+
nd = (
|
799
|
+
nd - 1
|
800
|
+
) # It is indicated that the number of derivatives needed was one less than the number of derivatives made
|
801
|
+
unidflag = 1
|
802
|
+
elif lasttime >= options.max_lie_time:
|
803
|
+
print(
|
804
|
+
"\n => More Lie derivatives would be needed to see if the model is structurally unidentifiable as a whole."
|
805
|
+
)
|
806
|
+
print(
|
807
|
+
" However, the maximum computation time allowed for calculating each of them has been reached."
|
808
|
+
)
|
809
|
+
print(
|
810
|
+
f" You can increase it by changing <<maxLietime>> in options (currently maxLietime = {options.max_lie_time})"
|
811
|
+
)
|
812
|
+
unidflag = 0
|
813
|
+
if skip_elim == 0 and is_fispo == 0:
|
814
|
+
# Eliminate columns one by one to check identifiability of the associated parameters:
|
815
|
+
[
|
816
|
+
identifiables,
|
817
|
+
nonidentif,
|
818
|
+
obs_states,
|
819
|
+
unobs_states,
|
820
|
+
obs_inputs,
|
821
|
+
unobs_inputs,
|
822
|
+
] = elim_and_recalc(
|
823
|
+
unmeasured_states_idx,
|
824
|
+
rango,
|
825
|
+
onx,
|
826
|
+
model.p,
|
827
|
+
model.x,
|
828
|
+
unidflag,
|
829
|
+
w1vector,
|
830
|
+
identifiables,
|
831
|
+
obs_states,
|
832
|
+
obs_inputs,
|
833
|
+
)
|
834
|
+
|
835
|
+
# Check which unknown inputs are observable:
|
836
|
+
obs_in_no_der = []
|
837
|
+
if (
|
838
|
+
len(model.w) == 1
|
839
|
+
and len(obs_inputs) > 0
|
840
|
+
and model.w == obs_inputs
|
841
|
+
):
|
842
|
+
obs_in_no_der = model.w
|
843
|
+
if len(model.w) > 1 and len(obs_inputs) > 0:
|
844
|
+
for elemento in model.w: # for each unknown input
|
845
|
+
if len(obs_inputs) == 1:
|
846
|
+
if elemento == obs_inputs:
|
847
|
+
obs_in_no_der = elemento
|
848
|
+
else:
|
849
|
+
for input in obs_inputs:
|
850
|
+
if elemento == input:
|
851
|
+
obs_in_no_der.append(elemento[0])
|
852
|
+
|
853
|
+
if (
|
854
|
+
len(identifiables) == len(model.p)
|
855
|
+
and (len(obs_states) + len(meas_x)) == len(model.x)
|
856
|
+
and len(obs_in_no_der) == len(model.w)
|
857
|
+
):
|
858
|
+
obs_states = model.x
|
859
|
+
obs_inputs = obs_in_no_der
|
860
|
+
identifiables = model.p
|
861
|
+
increaseLie = 0 # -> with this we skip the next 'if' block and jump to the end of the algorithm
|
862
|
+
is_fispo = 1
|
863
|
+
increaseLie = 0
|
864
|
+
|
865
|
+
else: # If the maxLietime has been reached, but the minimum of Lie derivatives has not been calculated:
|
866
|
+
print("\n => More Lie derivatives would be needed to analyse the model.")
|
867
|
+
print(
|
868
|
+
" However, the maximum computation time allowed for calculating each of them has been reached."
|
869
|
+
)
|
870
|
+
print(
|
871
|
+
f" You can increase it by changing <<maxLietime>> in options (currently maxLietime = {options.max_lie_time})"
|
872
|
+
)
|
873
|
+
print(
|
874
|
+
f"\n >>> Calculating rank of matrix with size {sp.shape(sp.Matrix(onx))[0]}x{sp.shape(sp.Matrix(onx))[1]}..."
|
875
|
+
)
|
876
|
+
# =============================================================================================
|
877
|
+
# The observability/identifiability matrix is saved in a .txt file
|
878
|
+
file_path = results_dir / f"obs_ident_matrix_{options.name}_{nd}_Lie_deriv.txt"
|
879
|
+
with file_path.open("w") as file:
|
880
|
+
file.write(f"onx = {onx.tolist()!s}")
|
881
|
+
|
882
|
+
# =============================================================================================
|
883
|
+
rational_onx = rationalize_all_numbers(sp.Matrix(onx))
|
884
|
+
rango = st.generic_rank(sp.Matrix(rational_onx))
|
885
|
+
|
886
|
+
print(f"\n Rank = {rango}")
|
887
|
+
(
|
888
|
+
identifiables,
|
889
|
+
nonidentif,
|
890
|
+
obs_states,
|
891
|
+
unobs_states,
|
892
|
+
obs_inputs,
|
893
|
+
unobs_inputs,
|
894
|
+
) = elim_and_recalc(
|
895
|
+
unmeasured_states_idx, rango, onx, identifiables, obs_states, obs_inputs
|
896
|
+
)
|
897
|
+
# ======================================================================================
|
898
|
+
# Build the vectors of identifiable / unidentifiable parameters, and of observable / unobservable states and inputs:
|
899
|
+
if len(identifiables) != 0:
|
900
|
+
p_id = sp.Matrix(identifiables).T
|
901
|
+
p_id = np.array(p_id).tolist()[0]
|
902
|
+
else:
|
903
|
+
p_id = []
|
904
|
+
|
905
|
+
if len(nonidentif) != 0:
|
906
|
+
p_un = sp.Matrix(nonidentif).T
|
907
|
+
p_un = np.array(p_un).tolist()[0]
|
908
|
+
else:
|
909
|
+
p_un = []
|
910
|
+
|
911
|
+
if len(obs_states) != 0:
|
912
|
+
obs_states = sp.Matrix(obs_states).T
|
913
|
+
obs_states = np.array(obs_states).tolist()[0]
|
914
|
+
|
915
|
+
if len(unobs_states) != 0:
|
916
|
+
unobs_states = sp.Matrix(unobs_states).T
|
917
|
+
unobs_states = np.array(unobs_states).tolist()[0]
|
918
|
+
|
919
|
+
if len(obs_inputs) != 0:
|
920
|
+
obs_inputs = sp.Matrix(obs_inputs).T
|
921
|
+
obs_inputs = np.array(obs_inputs).tolist()[0]
|
922
|
+
|
923
|
+
if len(unobs_inputs) != 0:
|
924
|
+
unobs_inputs = sp.Matrix(unobs_inputs).T
|
925
|
+
unobs_inputs = np.array(unobs_inputs).tolist()[0]
|
926
|
+
# ========================================================================================
|
927
|
+
# The observability/identifiability matrix is saved in a .txt file
|
928
|
+
|
929
|
+
file_path = results_dir / f"obs_ident_matrix_{options.name}_{nd}_Lie_deriv.txt"
|
930
|
+
with file_path.open("w") as file:
|
931
|
+
file.write(f"onx = {onx.tolist()!s}")
|
932
|
+
|
933
|
+
# The summary of the results is saved in a .txt file
|
934
|
+
file_path = (
|
935
|
+
results_dir
|
936
|
+
/ f"id_results_{options.name}_{datetime.today().strftime('%d-%m-%Y')}.txt"
|
937
|
+
)
|
938
|
+
with file_path.open("w") as file:
|
939
|
+
file.write("\n RESULTS SUMMARY:")
|
940
|
+
|
941
|
+
# Report results:
|
942
|
+
# result
|
943
|
+
# fispo: bool
|
944
|
+
|
945
|
+
print("\n ------------------------ ")
|
946
|
+
print(" RESULTS SUMMARY:")
|
947
|
+
print(" ------------------------ ")
|
948
|
+
if (
|
949
|
+
len(p_id) == len(model.p)
|
950
|
+
and len(obs_states) == len(model.x)
|
951
|
+
and len(obs_inputs) == len(model.w)
|
952
|
+
):
|
953
|
+
print("\n >>> The model is Fully Input-State-Parameter Observable (FISPO):")
|
954
|
+
if len(model.w) > 0:
|
955
|
+
print("\n All its unknown inputs are observable.")
|
956
|
+
file.write("\n All its unknown inputs are observable.")
|
957
|
+
print("\n All its states are observable.")
|
958
|
+
print("\n All its parameters are locally structurally identifiable.")
|
959
|
+
else:
|
960
|
+
if len(p_id) == len(model.p):
|
961
|
+
print("\n >>> The model is structurally identifiable:")
|
962
|
+
print("\n All its parameters are structurally identifiable.")
|
963
|
+
file.write(
|
964
|
+
"\n >>> The model is structurally identifiable:\n All its parameters are structurally identifiable."
|
965
|
+
)
|
966
|
+
elif unidflag:
|
967
|
+
print("\n >>> The model is structurally unidentifiable.")
|
968
|
+
print(f"\n >>> These parameters are identifiable:\n {p_id} ")
|
969
|
+
print(f"\n >>> These parameters are unidentifiable:\n {p_un}")
|
970
|
+
file.write(
|
971
|
+
f"\n >>> The model is structurally unidentifiable.\n >>> These parameters are identifiable:\n {p_id}\n >>> These parameters are unidentifiable:\n {p_un}"
|
972
|
+
)
|
973
|
+
else:
|
974
|
+
print(f"\n >>> These parameters are identifiable:\n {p_id}")
|
975
|
+
file.write(f"\n >>> These parameters are identifiable:\n {p_id}")
|
976
|
+
|
977
|
+
if len(obs_states) > 0:
|
978
|
+
print(
|
979
|
+
f"\n >>> These states are observable (and their initial conditions, if unknown, are identifiable):\n {obs_states}"
|
980
|
+
)
|
981
|
+
file.write(
|
982
|
+
f"\n >>> These states are observable (and their initial conditions, if unknown, are identifiable):\n {obs_states}"
|
983
|
+
)
|
984
|
+
if len(unobs_states) > 0:
|
985
|
+
print(
|
986
|
+
f"\n >>> These states are unobservable (and their initial conditions, if unknown, are unidentifiable):\n {unobs_states}"
|
987
|
+
)
|
988
|
+
file.write(
|
989
|
+
f"\n >>> These states are unobservable (and their initial conditions, if unknown, are unidentifiable):\n {unobs_states}"
|
990
|
+
)
|
991
|
+
|
992
|
+
if len(meas_x) != 0: # para mostrarlo en una fila, como el resto
|
993
|
+
meas_x = sp.Matrix(meas_x).T
|
994
|
+
meas_x = np.array(meas_x).tolist()[0]
|
995
|
+
else:
|
996
|
+
meas_x = []
|
997
|
+
|
998
|
+
if len(meas_x) > 0:
|
999
|
+
print(f"\n >>> These states are directly measured:\n {meas_x}")
|
1000
|
+
file.write(f"\n >>> These states are directly measured:\n {meas_x}")
|
1001
|
+
if len(obs_inputs) > 0:
|
1002
|
+
print(f"\n >>> These unmeasured inputs are observable:\n {obs_inputs}")
|
1003
|
+
file.write(
|
1004
|
+
f"\n >>> These unmeasured inputs are observable:\n {obs_inputs}"
|
1005
|
+
)
|
1006
|
+
if len(unobs_inputs) > 0:
|
1007
|
+
print(
|
1008
|
+
f"\n >>> These unmeasured inputs are unobservable:\n {unobs_inputs}"
|
1009
|
+
)
|
1010
|
+
file.write(
|
1011
|
+
f"\n >>> These unmeasured inputs are unobservable:\n {unobs_inputs}"
|
1012
|
+
)
|
1013
|
+
if len(model.u) > 0:
|
1014
|
+
print(f"\n >>> These inputs are known:\n {model.u}")
|
1015
|
+
file.write(f"\n >>> These inputs are known:\n {model.u}")
|
1016
|
+
|
1017
|
+
return ScanResult()
|