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,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
         |