Prediction cost: 100, which is the cost of predicting 1 when the true state is 1.
Cantilever beam
15 Apr, 2026
What I test in this case study:
Note:
Modal properties:
Features:
\[ C_\text{total} = C_\text{prediction}(H, d) + C_\text{sensors}(e) \]
\[ C_\text{sensors}(e) = 5 + 1 \cdot \text{number of sensors} \]
\[ C_\text{prediction} = \begin{bmatrix} 0 & 50 \\ 10000 & 10 \end{bmatrix} = \begin{bmatrix} \text{True Negative} & \text{False Positive} \\ \text{False Negative} & \text{True Positive} \end{bmatrix} \]
where:
Prediction cost: 100, which is the cost of predicting 1 when the true state is 1.
No Damage and Damage: \[ P(H_0) = P(\theta_0) = 0.90 \]
\[
P(H_1) = 1 - P(H_0)
\]
Assume that all damage states with damage are equally likely
\[ P(\theta_i \mid H_1) = \frac{1}{N} \quad \text{for } i = 1, 2, ..., K \]
This gives. (Remember that \(P(\theta_i) = P(\theta_i, H_1)\) since \(\theta_i\) can only occur if \(H_1\) is true) \[
P(\theta_i) = P(\theta_i, H_1) = P(H_1) P(\theta_i \mid H_1) = P(H_1) \frac{1}{N} \quad \text{for } i = 1, 2, ..., K
\]
where \(N\) is the number of damage states considered.
\[ N = \sum \frac{n!}{k!(n-k)!} = 2^n = 2^4 = 16 \]
\(P(\theta_i) = P(\theta_i, H_1)\) for \(i = 1, 2, ..., K\) because \(\theta_i\) can only occur if \(H_1\) is true.
Note: this error function is only for the estimation of the natural frequencies.
\[ \epsilon_{\omega}(e) = \epsilon_0(e) + ... + \epsilon_n(e) \]
Each error term explains a different source of error in the estimation of the natural frequencies. I assume that the function above can be constructed in a way where all the error terms are independent, and that the total error can be calculated as the sum of the individual error terms. (This is not True, example of signal to noise ratio and the effect of temperature to the stiffness of the structure.)
Assumptions
This assumptions leads to.
\[ \epsilon_{vibration} \sim \mathcal{N}(0, \frac{1}{\# e_{vibration}^2}\sum_{i=1}^{\# e_{vibration}} \sigma(e_{vibration, i})^2) \]
Where:
Important
We can have an increasing error with more sensors, if the error from the sensor noise is high.
We have \(n\) independent normal distributions where we make 1 draw from each distribution.
\(X_1, X_2, \dots, X_n \text{ independent, } X_i \sim \mathcal{N}(\mu, \sigma_i^2)\)
we compute the sample mean.
\(\bar{X} = \frac{1}{n}\sum_{i=1}^n X_i\)
now do we find the distribution of \(\bar{X}\), it is know that the sample mean of a normal distribution is also normally distributed.
\[ \mathbb{E}[\bar{X}] = \mathbb{E}\left[\frac{1}{n}\sum_{i=1}^n X_i\right] = \frac{1}{n}\sum_{i=1}^n \mathbb{E}[X_i] = \frac{1}{n}(n\mu) = \mu \]
the draws are independent so the variance of the sample mean is:
\[ \mathrm{Var}(\bar{X}) = \mathrm{Var}\left(\frac{1}{n}\sum_{i=1}^n X_i\right) = \frac{1}{n^2} \sum_{i=1}^n \sigma_i^2 \]
note: \(1/n^2\) is from the scaling rule of the variance \(Var(aX + b) = a^2 Var(X)\), it can easily be seen by moving \(1/n\) inside the sum.
final result:
\[ \bar{X} \sim \mathcal{N}\left(\mu, \frac{1}{n^2}\sum_{i=1}^n \sigma_i^2\right) \]
Minimum require
\[ \sigma(e_{vibration, i}) = \omega_i \cdot 5\% \cdot f(\phi_i(e_{vibration, i})) \]
where:
Modal variables:
Model setup variables:
Ambient excitation variables:
Sensor variables:
A more complex model need to both include almost all of the Variables. Time and testing is need to formulate this model
def sigma_vibration(omega, phi, node_number):
# assumed sensor coefficient of variation (CV)
CV = 0.05
# calculate vibration standard deviation
sigma = omega * CV * (10 - 9 * abs(phi[node_number]))
return sigma
print(f"omega_1 = {omega_1:.2f} s^-1, mode shape phi_1 = {phi_1}")
print(f"omega_2 = {omega_2:.2f} s^-1, mode shape phi_2 = {phi_2}")
node_number = 4
print(f"node {node_number}: sigma_vibration for mode 1 = {sigma_vibration(omega_1, phi_1, node_number):.4f}")
print(f"node {node_number}: sigma_vibration for mode 2 = {sigma_vibration(omega_2, phi_2, node_number):.4f}")omega_1 = 1.04 s^-1, mode shape phi_1 = [0. 0.09252781 0.32807894 0.64695444 1. ]
omega_2 = 6.72 s^-1, mode shape phi_2 = [ 0. 0.50514687 1. 0.54397217 -0.72646916]
node 4: sigma_vibration for mode 1 = 0.0522
node 4: sigma_vibration for mode 2 = 1.1635
Damage scenarios:
[(0, 0, 0, 0), (0, 0, 0, 1), (0, 0, 1, 0), (0, 0, 1, 1), (0, 1, 0, 0), (0, 1, 0, 1), (0, 1, 1, 0), (0, 1, 1, 1), (1, 0, 0, 0), (1, 0, 0, 1), (1, 0, 1, 0), (1, 0, 1, 1), (1, 1, 0, 0), (1, 1, 0, 1), (1, 1, 1, 0), (1, 1, 1, 1)]
size of theta: 16
Plot of case: (1, 1, 1, 1):
# nodes
e_sensor = np.arange(len(X))
# list of fixed nodes
fixed_nodes = [bounds[i][0] for i in range(len(bounds))]
fixed_nodes
# remove fixed nodes
e_sensor = e_sensor[~np.isin(e_sensor, fixed_nodes)] # ~ is the logical NOT operator, which inverts the boolean array returned by np.isin
print(f"Sensor nodes: {e_sensor}")
# make an list whit all possible combinations of sensor placements
import itertools # generate the solution space of all possible combinations.
# e_configuration = list(itertools.combinations(e_sensor.tolist(), 3))
e_configuration = list(itertools.product([0, 1], repeat=len(e_sensor))) # all combinations of 0 and 1 for each sensor node
e_configuration = e_configuration* e_sensor
print(f"Number of sensor configurations: {len(e_configuration)}")
for i in range(len(e_configuration)):
print(f"Configuration {i} given in nodes with sensors: {e_configuration[i]}")Sensor nodes: [1 2 3 4]
Number of sensor configurations: 16
Configuration 0 given in nodes with sensors: [0 0 0 0]
Configuration 1 given in nodes with sensors: [0 0 0 4]
Configuration 2 given in nodes with sensors: [0 0 3 0]
Configuration 3 given in nodes with sensors: [0 0 3 4]
Configuration 4 given in nodes with sensors: [0 2 0 0]
Configuration 5 given in nodes with sensors: [0 2 0 4]
Configuration 6 given in nodes with sensors: [0 2 3 0]
Configuration 7 given in nodes with sensors: [0 2 3 4]
Configuration 8 given in nodes with sensors: [1 0 0 0]
Configuration 9 given in nodes with sensors: [1 0 0 4]
Configuration 10 given in nodes with sensors: [1 0 3 0]
Configuration 11 given in nodes with sensors: [1 0 3 4]
Configuration 12 given in nodes with sensors: [1 2 0 0]
Configuration 13 given in nodes with sensors: [1 2 0 4]
Configuration 14 given in nodes with sensors: [1 2 3 0]
Configuration 15 given in nodes with sensors: [1 2 3 4]
\[ f(y|\theta) \sim \mathcal{N}(\mu, \Sigma^2), \text{ where } \Sigma^2 = \frac{1}{n^2}\sum_{i=1}^n \sigma_i^2 \]
Undamaged case:
Natural frequencies (Hz = s^-1): [1.04366091 6.72223044]
Damaged cases:
Natural frequencies (Hz = s^-1):
[[1.0126304 4.63016511]
[0.80548911 3.69172521]
[0.78872241 3.37286305]
[0.56004051 4.87740021]
[0.55489562 4.11077885]
[0.5120402 3.3640654 ]
[0.50788611 3.08229423]
[0.39753106 4.75750033]
[0.39595636 3.78110917]
[0.38094819 2.57381063]
[0.37948544 2.37804179]
[0.34212249 2.92025964]
[0.34106067 2.71671264]
[0.33103083 2.23314855]
[0.33003456 2.12575592]]
Sudocode for the MCS:
… Othere code
… Othere code
| Symbol | Values | Meaning |
|---|---|---|
| \[N\] | \(\sim 10^{3-7}\) | Number of outer Monte Carlo samples (Bayes risk estimation) |
| \[H\] | \(2\) | Number of structural hypotheses \[H_i\] |
| \[\Theta\] | \(2^{(\text{number of elements in the structure})}\) | Number of \[\theta\]-samples used to approximate \[f(y\mid H)\] |
| \[G\] | small | Cost of evaluating the forward model \[g(\theta,e)\] * (\(g(x(\theta,e))\) is expensive) |
| \[Y\] | 2-100 | Dimension of feature vector \[y\] |
\[ O(N \cdot [ G + H \cdot \Theta \cdot Y^{(*)}]) \]
where:
How to reduce the cost of the MCS:
\[ O(N \cdot [ G + H \cdot \Theta_{\text{surrogate}} \cdot Y^{1}] + \text{Cost of surrogate model}) \]
Level 1 SHM system
flowchart TB
subgraph SensorDesign["Sensor configurations: e ."]
direction TB
style SensorDesign fill:#e8f5e9,stroke:#2e7d32,stroke-width:3px,color:#1b5e20
subgraph DamageNoDamage["Structural categories: H ."]
direction TB
style DamageNoDamage fill:#fff9c4,stroke:#f57f17,stroke-width:3px,color:#e65100
subgraph Measurement["Feature space: y ."]
direction TB
style Measurement fill:#fff3e0,stroke:#e65100,stroke-width:3px,color:#bf360c
A["Features<br/>y = g(θ, e) + ε(e)"]
A --> B
subgraph StructuralState["Structural states: θ ."]
direction TB
style StructuralState fill:#e3f2fd,stroke:#1565c0,stroke-width:3px,color:#0d47a1
B["Likelihood<br/>f(y | H) = ∑<sub>θ</sub> f(y | θ) P(θ | H)"]
end
C["Posterior<br/>P(H | y)"]
D["Decision Rule<br/>d = max P(H | y)"]
E["Frequentist Risk<br/> R(d) = ∫ f(y | H) L(H, d) dy"]
B --> C
C --> D
D --> E
end
F["Bayes Risk<br/> Ψ(e) = ∑<sub>H</sub> P(H) R(d)"]
E --> F
end
H["Optimization<br/>e* = argmin Ψ(e)"]
F --> H
end
Monte Carlo Simulation (MCS)
flowchart TB
subgraph SensorDesign["Sensor configurations: e ."]
direction TB
style SensorDesign fill:#e8f5e9,stroke:#2e7d32,stroke-width:3px,color:#1b5e20
subgraph Measurement["Monte Carlo Simulations: N ......"]
direction TB
style Measurement fill:#fff3e0,stroke:#e65100,stroke-width:3px,color:#bf360c
D2 --> B
A2{"Structural strategy (MCS)<br/> θ* ~ P(θ)"}
B2{"Generate error (MCS)<br/> ε(e) ~ f(ε | e)"}
C2["Features<br/>y = g(θ*, e) + ε(e)"]
A2 --> B2
B2 --> C2
C2 --> D2
subgraph StructuralState["Structural states: θ ."]
direction TB
style StructuralState fill:#e3f2fd,stroke:#1565c0,stroke-width:3px,color:#0d47a1
D2{"Likelihood of observation (MCS)<br/>f(y | θ)"}
B["Likelihood<br/>f(y | H) = ∑<sub>θ</sub> f(y | θ) P(θ | H)"]
end
subgraph DamageNoDamage["Structural categories: H ."]
direction TB
style DamageNoDamage fill:#fff9c4,stroke:#f57f17,stroke-width:3px,color:#e65100
C["Posterior<br/>P(H | y)"]
D["Decision Rule<br/>d = max P(H | y)"]
end
E["Loss function<br/> L(H, d) "]
B --> C
C --> D
D --> E
F["Bayes Risk<br/> Ψ(e) =1/N ∑<sub>N</sub> L(H, d)"]
end
E --> F
H["Optimization<br/>e* = argmin Ψ(e)"]
F --> H
end
Question / Note
\(f(y | \theta)\) is not an real probability density, it is just the pdf at the given observation \(y\) for a given state \(\theta\). the probability of an continuous random variable taking a specific value is 0.
"""Utilize vectorization and reduce the calls to np.random.choice and np.random.multivariate_normal."""
import numpy as np
from scipy.stats import multivariate_normal
np.random.seed(42)
# Number of Monte Carlo samples
N = 10**4
# True state features, damage states, initialization of bayes risk array
y_start = load["natural_frequencies"][0:, :2]
theta = OMA.theta
bayes_risk_sensors = np.zeros(len(e_configuration))
# Loop over sensor configurations
for ei in range(len(e_configuration)):
# sensor configuration + covariance matrix
e = e_configuration[ei]
cov = np.diag(sigma[:, ei]**2)
# Generate all states and H labels at once: theta ~ P(theta)
states = np.random.choice(range(len(prior)), size=N, p=prior)
H_vec = (states > 0).astype(int) # 0 if state == 0, else 1
# Generate all errors and observations (y) at once: y = g(theta) + epsilon, epsilon ~ P(epsilon|e, theta)
epsilon_vec = np.random.multivariate_normal(mean=np.zeros(2), cov=cov, size=N)
y_vec = np.maximum(y_start[states] + epsilon_vec, 0)
# Vectorized Likelihood calculation: pdf = f(y|theta_i)
pdf_epsilon_all = np.zeros((N, len(theta)))
for i in range(len(theta)):
pdf_epsilon_all[:, i] = multivariate_normal.pdf(y_vec - y_start[i], mean=np.zeros(2), cov=cov)
# Likelihood: f(y|H) = sum_i f(y|theta_i) * P(theta_i|H)
Likelihood_H0 = pdf_epsilon_all[:, 0] * prior[0] / P_H0 # P(theta_i|H) = prior[0] / P_H0 for the undamaged state
# Sum across all damage states (i=1 to end)
Likelihood_H1 = np.sum(pdf_epsilon_all[:, 1:] * (prior[1:] / P_H1), axis=1)
# Posterior probabilities: P(H|y) ∝ f(y|H) * P(H)
Post0 = Likelihood_H0 * P_H0
Post1 = Likelihood_H1 * P_H1
# Decisions rule: maximimum a posteriori (MAP)
d_vec = (Post1 > Post0).astype(int)
# Loss and Bayes Risk
total_costs = prediction_cost[H_vec, d_vec] + sensor_cost(len(e_configuration[ei]))
# Estimation of the bayes risk using Monte Carlo.
Bayes_Risk = np.mean(total_costs)
print(f"Bayes Risk: {Bayes_Risk}")
bayes_risk_sensors[ei] = Bayes_RiskBayes Risk: 66.115
Bayes Risk: 298.9
Bayes Risk: 110.58
Bayes Risk: 18.955
Bayes Risk: 61.105
Bayes Risk: 129.975
Bayes Risk: 82.1
Bayes Risk: 520.36
Bayes Risk: 195.61
Bayes Risk: 249.87
Bayes Risk: 128.57
Bayes Risk: 152.575
Bayes Risk: 79.01
Bayes Risk: 108.23
Bayes Risk: 65.59
Assuming that.
\[ R(d_0) = C_\text{prediction}(H_0, d_0) \cdot P(H_0) + C_\text{prediction}(H_1, d_0) \cdot P(H_1) \]
Minimum Bayes Risk: 18.955
Best sensor configuration: (np.int64(2),) (index: 3)
The results here are highly dependent on:
Use the mean value approximation to evaluate the Bayes risk.
Idea:
Simplify all stochastic variables to their mean value, and then evaluate the Bayes risk for this simplified system.
Down sides:
"""Mean value approximation"""
import numpy as np
from scipy.stats import multivariate_normal
# True state features, damage states, initialization of bayes risk array
y_start = load["natural_frequencies"][0:, :2]
theta = OMA.theta
bayes_risk_sensors = np.zeros(len(e_configuration))
# Loop over sensor configurations
for ei in range(len(e_configuration)):
# sensor configuration + covariance matrix
e = e_configuration[ei]
cov = np.diag(sigma[:, ei]**2)
# Loop over states (damage cases): theta
for theta_i in range(len(theta)):
# print(f"theta_i: {theta_i}, damage case: {theta[theta_i]}")
H_vec = int(theta_i > 0) # 0 if state == 0, else 1
# makes y whit 0 error
y_vec = np.maximum(y_start[theta_i], 0)
# Vectorized Likelihood calculation: pdf = f(y|theta_i)
pdf_epsilon_all = np.zeros( len(theta))
for i in range(len(theta)):
pdf_epsilon_all[i] = multivariate_normal.pdf(y_vec - y_start[i], mean=np.zeros(2), cov=cov)
# Likelihood: f(y|H) = sum_i f(y|theta_i) * P(theta_i|H)
Likelihood_H0 = pdf_epsilon_all[0] * prior[0] / P_H0 # P(theta_i|H) = prior[0] / P_H0 for the undamaged state
# Sum across all damage states (i=1 to end)
Likelihood_H1 = np.sum(pdf_epsilon_all[1:] * (prior[1:] / P_H1), axis=0)
# Posterior probabilities: P(H|y) ∝ f(y|H) * P(H)
Post0 = Likelihood_H0 * P_H0
Post1 = Likelihood_H1 * P_H1
# Decisions rule: maximimum a posteriori (MAP)
d_vec = (Post1 > Post0).astype(int)
# Loss and Bayes Risk
total_costs = prediction_cost[H_vec, d_vec] + sensor_cost(len(e_configuration[ei]))
# Estimation of the bayes risk using Monte Carlo.
Bayes_Risk = np.mean(total_costs)
print(f"Bayes Risk: {Bayes_Risk}")
bayes_risk_sensors[ei] = Bayes_RiskBayes Risk: 106.0
Bayes Risk: 106.0
Bayes Risk: 107.0
Bayes Risk: 106.0
Bayes Risk: 107.0
Bayes Risk: 107.0
Bayes Risk: 108.0
Bayes Risk: 106.0
Bayes Risk: 107.0
Bayes Risk: 107.0
Bayes Risk: 108.0
Bayes Risk: 107.0
Bayes Risk: 108.0
Bayes Risk: 108.0
Bayes Risk: 109.0
Minimum Bayes Risk: 106.0
Best sensor configuration: (np.int64(4),) (index: 0)
Method is proposed in the paper:
“A univariate dimension-reduction method for multi-dimensional integration in stochastic mechanics” (Rahman and Xu, 2004, p. 393) (pdf)
The method is used in the paper:
“An optimal sensor placement design framework for structural health monitoring using Bayes risk” (Yang et al., 2022, p. 1) (pdf)
Evaluate the 3 methods for evaluating the Bayes risk:
Minimum Bayes Risk: 951.0
Best sensor configuration: (np.int64(3), np.int64(4)) (index: 2)
Minimum Bayes Risk: 882.13
Best sensor configuration: (np.int64(4),) (index: 0)
Minimum Bayes Risk: 601.64
Best sensor configuration: (np.int64(4),) (index: 0)
Minimum Bayes Risk: 439.83
Best sensor configuration: (np.int64(4),) (index: 0)
Minimum Bayes Risk: 301.73
Best sensor configuration: (np.int64(2),) (index: 3)
Minimum Bayes Risk: 224.005
Best sensor configuration: (np.int64(2),) (index: 3)
Minimum Bayes Risk: 110.02
Best sensor configuration: (np.int64(2),) (index: 3)
Minimum Bayes Risk: 65.17
Best sensor configuration: (np.int64(2),) (index: 3)
Minimum Bayes Risk: 20.01
Best sensor configuration: (np.int64(2),) (index: 3)
Cost function (prediction_cost):
[[ 0 50]
[10000 100]]
Minimum Bayes Risk: 18.955
Best sensor configuration: (np.int64(2),) (index: 3)
Minimum Bayes Risk: 7.18
Best sensor configuration: (np.int64(2),) (index: 3)
Minimum Bayes Risk: 6.1
Best sensor configuration: (np.int64(2),) (index: 3)
Minimum Bayes Risk: 6.0
Best sensor configuration: (np.int64(3),) (index: 1)
Minimum Bayes Risk: 6.0
Best sensor configuration: (np.int64(3),) (index: 1)
Minimum Bayes Risk: 6.0
Best sensor configuration: (np.int64(4),) (index: 0)
Minimum Bayes Risk: 6.0
Best sensor configuration: (np.int64(2),) (index: 3)
Minimum Bayes Risk: 66.0
Best sensor configuration: (np.int64(2),) (index: 3)
Minimum Bayes Risk: 144.1
Best sensor configuration: (np.int64(2),) (index: 3)
I have chosen to split the work for next time into 4 main class:
Can be don leter:
Asumption / Simplification:
Other:
Do it now:
Can be done later:
Can be done later:
Improved evaluation of Bayes risk: (1-3 weeks)
Inclusion of additional features: ( ???? )
Optimization algorithms for sensor placement: (2-7 days)
Feature selection methods (e.g., PCA): (1 week)
How to best link the error/stochasticity of the sensor configuration to features???
Approache 1:
Approache 2:
Approache 3: