Irreversible actuator damage in a resource-dependent LPV control model
EXP-01 is the baseline simulation used to verify the post-failure mathematics before introducing a supervisory reconfiguration layer. The result is produced by model execution first, written to CSV, and visualized only afterwards.
Dmytro Humennyi · Дмитро Гуменний
[email protected]
01 // TASK
Execute the baseline resource-dependent control model under an irreversible actuator-damage event and observe how structural survivability, parametric integrity and functional integrity evolve without supervisory reconfiguration.
It establishes a numerical reference trajectory. EXP-02 will reuse the same plant, damage event and integrity metrics, but will add the supervisory mechanism. Any claimed improvement must be measured against this baseline.
02 // MATHEMATICAL MODEL
The experiment is formulated in discrete time, \(t_k=k\Delta t\), with \(\Delta t=0.01\,\mathrm{s}\). The plant state is denoted by \(x_k\), the actuator-resource vector by \(\boldsymbol{\rho}_k\), and the control vector by \(\mathbf{u}_k\).
Resource-dependent plant model
For EXP-01 the scalar reference coefficients are \(A_d=0.9\), \(B=1\), and \(E_d=1\). Structural degradation enters the model through the effective input matrix:
The scalar feedback command is distributed equally between both actuator channels:
Therefore, the state equation actually executed in EXP-01 is
Resource degradation
EXP-01 represents irreversible structural damage. Therefore,
The regime-dependent coefficients used by the simulation are
Structural survivability
Parametric integrity
The reference state is \(x^\star=0\), hence \(e_k=x_k-x^\star=x_k\). Parametric integrity is
Functional integrity
Thus \(F_k\) decreases because of loss of physical resources \((S_k\downarrow)\), deterioration of the controlled state \((P_k\downarrow)\), or both.
Operational regimes
Baseline control law
The broader framework contains \(K_N=0.8\), \(K_K=0.5\), and \(K_{SK}=0.2\). However, EXP-01 is intentionally a non-reconfigurable baseline. The nominal gain is retained for the whole experiment:
\(K_K\) and \(K_{SK}\) belong to the general framework but are not applied by the EXP-01 baseline controller.
External disturbance
Notation
- \(x_k\)
- plant state at sample \(k\);
- \(\mathbf{u}_k\)
- two-channel actuator command vector;
- \(\boldsymbol{\rho}_k\)
- remaining operability of the actuator resources;
- \(D_{i,k}\)
- external structural-damage impulse;
- \(S_k\)
- structural survivability;
- \(P_k\)
- parametric integrity;
- \(F_k\)
- functional integrity;
- \(s_k\)
- operational regime \(N\), \(K\), or \(SK\);
- \(\xi_k\)
- deterministic disturbance.
03 // FAILURE MODEL
Let \(k_f\) correspond to \(t_f=5.00\,\mathrm{s}\). Immediately before failure, nominal degradation has reduced the second resource to \(\rho_{2,k_f}^{-}=0.95\).
Damage impulse
The actuator loss is introduced through the structural-damage term in the resource equation:
The damage impulse is selected so that the actuator becomes completely unavailable:
Consequently,
Effect on control authority
For every sample after the failure, Eq. (2) becomes
Hence the second actuator has no remaining control authority:
Irreversibility
Since \(u^{\mathrm{rep}}_{2,k}=0\) and \(\rho_{2,k}\in[0,1]\), the damaged resource cannot recover:
04 // EXECUTION PIPELINE
The plotter is not part of the simulation state generation.
0.010 s
16.0 s
1,601
Observed regime transitions
05 // DATA
Every row below is read from the generated CSV, not reconstructed in HTML. Open full CSV · Open experiment manifest
| \(t_k\,[\mathrm{s}]\) | \(x_k\) | \(\rho_{1,k}\) | \(\rho_{2,k}\) | \(S_k\) | \(P_k\) | \(F_k\) | \(s_k\) |
|---|---|---|---|---|---|---|---|
| 0.00 | 0.20000 | 1.0000 | 1.0000 | 1.00000 | 0.90484 | 0.90484 | N |
| 4.99 | 0.10414 | 0.9501 | 0.9501 | 0.95010 | 0.94926 | 0.90189 | N |
| 5.00 | 0.10415 | 0.9500 | 0.0000 | 0.47500 | 0.94926 | 0.45090 | K |
| 5.03 | 0.92310 | 0.9485 | 0.0000 | 0.47425 | 0.63031 | 0.29892 | SK |
| 6.00 | 1.47582 | 0.8515 | 0.0000 | 0.42575 | 0.47811 | 0.20356 | SK |
| 10.00 | 2.09051 | 0.4515 | 0.0000 | 0.22575 | 0.35160 | 0.07937 | SK |
| 14.52 | 3.92803 | 0.0000 | 0.0000 | 0.00000 | 0.14029 | 0.00000 | SK |
| 16.00 | 4.00000 | 0.0000 | 0.0000 | 0.00000 | 0.13534 | 0.00000 | SK |
06 // VISUALIZATION
07 // RESULT
Measured baseline
What the result means
The experiment numerically demonstrates that a structurally degraded system can cross the integrity-regime boundaries after irreversible resource loss even though the controller continues to produce commands.
What the result does not mean
EXP-01 does not yet demonstrate superiority of a supervisory strategy. It is the required baseline for the next matched experiment.
08 // SOURCE CODE
The complete validated source is shipped with this landing page. The equations and the plotting layer are separated.
from __future__ import annotations
from dataclasses import dataclass
import numpy as np
@dataclass(frozen=True)
class PlantParameters:
"""
Reference scalar plant from the mathematical framework:
x[k+1] = A*x[k] + B_d(rho[k])*u[k] + E*xi[k]
A=0.9 and B=1.0 are the values used in the reference scalar model.
For EXP-01 we specialize the general LPV input matrix to two parallel
actuator channels:
B_d(rho) = (B/2) * [rho_1, rho_2]
This is an explicit experiment specialization of the general
B_d(rho) term, not a new state equation.
"""
A: float = 0.9
B: float = 1.0
E: float = 1.0
class LPVPlant:
def __init__(self, params: PlantParameters, x0: float):
self.p = params
self.x = float(x0)
def input_matrix(self, rho: np.ndarray) -> np.ndarray:
rho = np.asarray(rho, dtype=float)
if rho.shape != (2,):
raise ValueError("EXP-01 expects exactly two actuator resources.")
return (self.p.B / 2.0) * rho
def step(self, u: np.ndarray, rho: np.ndarray, xi: float) -> float:
"""
Implements:
x[k+1] = A_d(rho_k)x[k] + B_d(rho_k)u[k] + E_d(rho_k)xi[k]
EXP-01 uses constant A and E, while B_d depends on resource health.
"""
u = np.asarray(u, dtype=float)
Bd = self.input_matrix(rho)
x_next = self.p.A * self.x + float(Bd @ u) + self.p.E * float(xi)
self.x = float(x_next)
return self.x
from __future__ import annotations
from dataclasses import dataclass
import numpy as np
@dataclass(frozen=True)
class DegradationParameters:
"""
Reference regime-dependent degradation rates and damage coefficients.
lambda_N = 0.01
lambda_K = 0.05
lambda_SK = 0.10
EXP-01 is irreversible:
u_rep = 0
"""
lambda_N: float = 0.01
lambda_K: float = 0.05
lambda_SK: float = 0.10
eta_N: float = 1.0
eta_K: float = 1.5
eta_SK: float = 2.0
def degradation_rate(self, regime: str) -> float:
return {
"N": self.lambda_N,
"K": self.lambda_K,
"SK": self.lambda_SK,
}[regime]
def damage_sensitivity(self, regime: str) -> float:
return {
"N": self.eta_N,
"K": self.eta_K,
"SK": self.eta_SK,
}[regime]
class ResourceModel:
def __init__(self, params: DegradationParameters, rho0: np.ndarray):
self.p = params
self.rho = np.clip(np.asarray(rho0, dtype=float), 0.0, 1.0)
def apply_damage_event(self, resource_id: int, D: float, regime: str) -> None:
"""
Damage term from:
rho[i,k+1] =
rho[i,k]
- lambda_i(s_k)*dt
- eta_i*D[i,k]
+ mu_i*u_rep[i,k]
For an irreversible experiment u_rep = 0.
This method applies only the instantaneous -eta*D term.
"""
eta = self.p.damage_sensitivity(regime)
self.rho[resource_id] = np.clip(
self.rho[resource_id] - eta * float(D),
0.0,
1.0,
)
def destroy_irreversibly(self, resource_id: int, regime: str) -> float:
"""
Compute the damage impulse D needed to reduce the selected resource
exactly to rho=0 under the theory's -eta*D damage term.
Returns D for logging.
"""
eta = self.p.damage_sensitivity(regime)
D = float(self.rho[resource_id] / eta)
self.apply_damage_event(resource_id, D, regime)
return D
def step_degradation(self, regime: str, dt: float) -> np.ndarray:
"""
Continuous resource consumption:
rho[k+1] = rho[k] - lambda(s_k)*dt
No repair/compensation term is present in EXP-01.
"""
lam = self.p.degradation_rate(regime)
self.rho = np.clip(self.rho - lam * float(dt), 0.0, 1.0)
return self.rho.copy()
from __future__ import annotations
from dataclasses import dataclass
import math
import numpy as np
@dataclass(frozen=True)
class IntegrityParameters:
beta: float = 0.5
F_N: float = 0.8
F_K: float = 0.3
def structural_survivability(rho: np.ndarray) -> float:
"""
Eq. (5):
S_k = (1/r) * sum_i rho_i,k
"""
rho = np.asarray(rho, dtype=float)
return float(np.mean(rho))
def parametric_integrity(error: float, beta: float = 0.5) -> float:
"""
Eq. (6):
P_k = exp(-beta * |e_k|)
"""
return float(math.exp(-float(beta) * abs(float(error))))
def functional_integrity(S: float, P: float) -> float:
"""
Eq. (7):
F_k = S_k * P_k
"""
return float(S * P)
def classify_regime(F: float, params: IntegrityParameters) -> str:
"""
Eqs. (8)-(10):
N if F >= 0.8
K if 0.3 <= F < 0.8
SK if F < 0.3
"""
if F >= params.F_N:
return "N"
if F >= params.F_K:
return "K"
return "SK"
from __future__ import annotations
from dataclasses import dataclass
import numpy as np
@dataclass(frozen=True)
class ControllerGains:
"""
Reference regime-dependent gains:
K_N = 0.8
K_K = 0.5
K_SK = 0.2
"""
K_N: float = 0.8
K_K: float = 0.5
K_SK: float = 0.2
def for_regime(self, regime: str) -> float:
return {
"N": self.K_N,
"K": self.K_K,
"SK": self.K_SK,
}[regime]
class TwoActuatorController:
def __init__(self, gains: ControllerGains):
self.gains = gains
def command(self, x_hat: float, regime: str, adaptive: bool) -> tuple[float, np.ndarray]:
"""
Eq. (11):
u_k = -K_s * x_hat_k
For the baseline experiment adaptive=False:
K_N remains fixed even after the failure.
The scalar command is split equally between two physical actuator
channels. The plant's B_d(rho) determines how much of each command
reaches the plant.
"""
K = self.gains.for_regime(regime) if adaptive else self.gains.K_N
u_cmd = -K * float(x_hat)
u_channels = np.array([u_cmd / 2.0, u_cmd / 2.0], dtype=float)
return float(u_cmd), u_channels
from __future__ import annotations
from dataclasses import dataclass, asdict
from pathlib import Path
import csv
import json
import numpy as np
from model import LPVPlant, PlantParameters
from resources import ResourceModel, DegradationParameters
from integrity import (
IntegrityParameters,
structural_survivability,
parametric_integrity,
functional_integrity,
classify_regime,
)
from controller import ControllerGains, TwoActuatorController
@dataclass(frozen=True)
class ExperimentConfig:
dt: float = 0.01
t_end: float = 16.0
fault_time: float = 5.0
# reference scalar plant initial condition
x0: float = 0.20
rho0_1: float = 1.0
rho0_2: float = 1.0
# deterministic disturbance injection for EXP-01
xi_nominal: float = 0.05
xi_post_fault: float = 0.40
# baseline = fixed nominal gain; supervisor experiment will set True
adaptive_control: bool = False
# deterministic by construction; kept in manifest for reproducibility
seed: int = 26082026
def run(config: ExperimentConfig) -> list[dict]:
np.random.seed(config.seed)
plant = LPVPlant(PlantParameters(), x0=config.x0)
resources = ResourceModel(
DegradationParameters(),
rho0=np.array([config.rho0_1, config.rho0_2], dtype=float),
)
integrity_cfg = IntegrityParameters()
controller = TwoActuatorController(ControllerGains())
rows: list[dict] = []
fault_applied = False
times = np.arange(0.0, config.t_end + config.dt / 2.0, config.dt)
for k, t in enumerate(times):
# 1. Evaluate current integrity before the external event.
error_pre = plant.x
S_pre = structural_survivability(resources.rho)
P_pre = parametric_integrity(error_pre, integrity_cfg.beta)
F_pre = functional_integrity(S_pre, P_pre)
regime_pre = classify_regime(F_pre, integrity_cfg)
# 2. External, irreversible structural damage.
D2 = 0.0
if (not fault_applied) and t >= config.fault_time:
# Uses the theory's -eta*D damage term to make rho_2 = 0.
D2 = resources.destroy_irreversibly(resource_id=1, regime=regime_pre)
fault_applied = True
# 3. Recompute integrity after the damage event.
error = plant.x
S = structural_survivability(resources.rho)
P = parametric_integrity(error, integrity_cfg.beta)
F = functional_integrity(S, P)
regime = classify_regime(F, integrity_cfg)
# 4. Baseline control: Eq. (11) with K_N fixed.
# The regime is still computed and logged; it also drives
# the degradation law, as in Eq. (4).
u_cmd, u_channels = controller.command(
x_hat=plant.x,
regime=regime,
adaptive=config.adaptive_control,
)
# 5. Disturbance injection.
xi = config.xi_nominal if t < config.fault_time else config.xi_post_fault
# 6. LPV plant transition Eq. (1), specialized to scalar Eq. (3).
x_k = plant.x
rho_k = resources.rho.copy()
Bd = plant.input_matrix(rho_k)
x_next = plant.step(u_channels, rho_k, xi)
# 7. Resource degradation Eq. (4), with u_rep = 0.
rho_next = resources.step_degradation(regime=regime, dt=config.dt)
rows.append({
"k": k,
"time_s": float(t),
"x_k": float(x_k),
"x_k1": float(x_next),
"rho1_k": float(rho_k[0]),
"rho2_k": float(rho_k[1]),
"rho1_k1": float(rho_next[0]),
"rho2_k1": float(rho_next[1]),
"B1_eff": float(Bd[0]),
"B2_eff": float(Bd[1]),
"damage_D2": float(D2),
"S_k": float(S),
"P_k": float(P),
"F_k": float(F),
"regime": regime,
"u_cmd": float(u_cmd),
"u1_cmd": float(u_channels[0]),
"u2_cmd": float(u_channels[1]),
"xi_k": float(xi),
})
return rows
def save_csv(rows: list[dict], path: Path) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
with path.open("w", newline="", encoding="utf-8") as f:
writer = csv.DictWriter(f, fieldnames=list(rows[0].keys()))
writer.writeheader()
writer.writerows(rows)
def main() -> None:
project_root = Path(__file__).resolve().parents[1]
cfg = ExperimentConfig()
rows = run(cfg)
data_path = project_root / "data" / "exp01_baseline.csv"
save_csv(rows, data_path)
manifest = {
"experiment": "EXP-01",
"purpose": "baseline with irreversible actuator #2 destruction",
"config": asdict(cfg),
"theory_mapping": {
"plant": "Eq. (1), scalar reference Eq. (3)",
"resource_degradation": "Eq. (4), u_rep = 0",
"S": "Eq. (5)",
"P": "Eq. (6)",
"F": "Eq. (7)",
"regime": "Eqs. (8)-(10)",
"control": "Eq. (11), baseline fixes K=K_N",
},
"experiment_specialization": {
"B_d_rho": "(B/2) * [rho1, rho2]",
"fault": "damage impulse D2 computed so -eta*D2 makes rho2 exactly zero",
"disturbance": {
"before_fault": cfg.xi_nominal,
"after_fault": cfg.xi_post_fault,
},
},
}
(project_root / "data" / "manifest.json").write_text(
json.dumps(manifest, indent=2),
encoding="utf-8",
)
# Key events
fault_row = next(r for r in rows if r["damage_D2"] > 0.0)
first_K = next((r for r in rows if r["regime"] == "K"), None)
first_SK = next((r for r in rows if r["regime"] == "SK"), None)
first_F0 = next((r for r in rows if r["F_k"] <= 0.0), None)
print("EXP-01 / mathematical baseline")
print(f"fault: t={fault_row['time_s']:.2f}s, rho2={fault_row['rho2_k']:.3f}")
if first_K:
print(f"K: t={first_K['time_s']:.2f}s, F={first_K['F_k']:.6f}")
if first_SK:
print(f"SK: t={first_SK['time_s']:.2f}s, F={first_SK['F_k']:.6f}")
if first_F0:
print(f"F=0: t={first_F0['time_s']:.2f}s")
else:
print("F=0: not reached in simulation horizon")
print(f"CSV: {data_path}")
if __name__ == "__main__":
main()
09 // NEXT VALIDATION SCENARIOS
| Scenario | Degradation | Mechanism to test |
|---|---|---|
| Scenario A · Thermal | actuator heating / cooling degradation | thermal margin, torque limiting, load redistribution |
| Scenario B · Sensor | encoder degradation + IMU drift | adaptive covariance and sensor fusion |
| Scenario C · Communication | CAN loss / latency growth | communication failover and degraded operation |
| Scenario D · Cascade | power transient across subsystems | sequenced recovery + Lyapunov stability verification |
10 // PUBLICATION
Preprint submitted.
A publication covering the mathematical framework and the post-failure supervisory-control approach has been submitted as a preprint and is currently under review.
The bibliographic reference and public link will be published on this page after the preprint becomes publicly available.
Status: awaiting public release.
Author
Dmytro Humennyi · Дмитро Гуменний
[email protected]
Relation to this experiment
EXP-01 is a reproducible numerical baseline accompanying the broader research direction. It exposes the model assumptions, executable implementation, raw data and integrity metrics used to evaluate system behavior after irreversible structural damage.