estimation_playground.py 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208
  1. import numpy as np
  2. import matplotlib.pyplot as plt
  3. # -------------------------------
  4. # Define the battery model functions
  5. # -------------------------------
  6. def ocv_model(soc, temp):
  7. """
  8. Open-circuit voltage model as a function of SOC and temperature.
  9. For demonstration, we use a simple linear relationship.
  10. In a real application, this should be replaced with an empirically derived model.
  11. Parameters:
  12. soc: state-of-charge (0 to 1)
  13. temp: temperature (°C) [not used in this simple model]
  14. Returns:
  15. OCV (V)
  16. """
  17. # Example: voltage ranges from 3.0V to 4.2V
  18. return 3.0 + 1.2 * soc
  19. def battery_model(x, current, dt):
  20. """
  21. State propagation model.
  22. State vector x = [SOC, capacity]
  23. - SOC: state-of-charge (0 to 1)
  24. - capacity: battery capacity in Ah
  25. The SOC decreases with discharge current.
  26. Parameters:
  27. x: state vector [soc, capacity]
  28. current: battery current (A) (positive for discharge)
  29. dt: time interval (seconds)
  30. Returns:
  31. Updated state vector.
  32. """
  33. soc, capacity = x
  34. # Note: capacity is in Ah and dt is in seconds, so convert capacity to Coulombs (Ah * 3600)
  35. soc_new = soc - (current * dt) / (capacity * 3600)
  36. # For simplicity, assume capacity changes very slowly; we let it remain nearly constant.
  37. return np.array([soc_new, capacity])
  38. def jacobian_f(x, current, dt):
  39. """
  40. Computes the Jacobian of the battery_model function with respect to state x.
  41. """
  42. soc, capacity = x
  43. # f1 = soc - (current*dt)/(capacity*3600)
  44. df1_dsoc = 1.0
  45. df1_dcap = (current * dt) / (capacity**2 * 3600)
  46. # f2 = capacity (assumed constant), so its derivatives are:
  47. df2_dsoc = 0.0
  48. df2_dcap = 1.0
  49. return np.array([[df1_dsoc, df1_dcap],
  50. [df2_dsoc, df2_dcap]])
  51. def measurement_model(x, current, temp, R=0.05):
  52. """
  53. Measurement model: the terminal voltage is given by the open-circuit voltage
  54. minus the voltage drop across the internal resistance.
  55. Parameters:
  56. x: state vector [soc, capacity]
  57. current: battery current (A)
  58. temp: temperature (°C)
  59. R: internal resistance (Ohm)
  60. Returns:
  61. Terminal voltage (V)
  62. """
  63. soc, _ = x
  64. return ocv_model(soc, temp) - current * R
  65. def jacobian_h(x, current, temp, R=0.05):
  66. """
  67. Computes the Jacobian of the measurement_model with respect to the state x.
  68. Here we assume the OCV model derivative with respect to SOC is constant (1.2 V per unit SOC).
  69. """
  70. # dV/dsoc: derivative of ocv_model with respect to soc (in our simple model, it's constant)
  71. dV_dsoc = 1.2
  72. dV_dcap = 0.0 # Voltage is not directly affected by capacity in this simple model.
  73. return np.array([dV_dsoc, dV_dcap]).reshape(1, -1)
  74. # -------------------------------
  75. # Extended Kalman Filter functions
  76. # -------------------------------
  77. def ekf_predict(x, P, current, dt, Q):
  78. """
  79. EKF prediction step.
  80. Parameters:
  81. x: current state estimate
  82. P: current state covariance matrix
  83. current: current battery current (A)
  84. dt: time step (s)
  85. Q: process noise covariance matrix
  86. Returns:
  87. x_pred: predicted state
  88. P_pred: predicted covariance
  89. """
  90. x_pred = battery_model(x, current, dt)
  91. F = jacobian_f(x, current, dt)
  92. P_pred = F @ P @ F.T + Q
  93. return x_pred, P_pred
  94. def ekf_update(x_pred, P_pred, z, current, temp, R_internal=0.05, R_measure=0.01):
  95. """
  96. EKF update step.
  97. Parameters:
  98. x_pred: predicted state
  99. P_pred: predicted covariance matrix
  100. z: measured voltage (V)
  101. current: battery current (A)
  102. temp: temperature (°C)
  103. R_internal: internal resistance used in the measurement model
  104. R_measure: measurement noise variance
  105. Returns:
  106. x_updated: updated state estimate
  107. P_updated: updated covariance matrix
  108. """
  109. H = jacobian_h(x_pred, current, temp, R_internal)
  110. z_pred = measurement_model(x_pred, current, temp, R_internal)
  111. y = z - z_pred # innovation or measurement residual
  112. S = H @ P_pred @ H.T + R_measure # innovation covariance
  113. K = P_pred @ H.T @ np.linalg.inv(S) # Kalman gain
  114. x_updated = x_pred + (K * y).flatten() # update state (flatten since K*y is a vector)
  115. P_updated = (np.eye(len(x_pred)) - K @ H) @ P_pred
  116. return x_updated, P_updated
  117. # -------------------------------
  118. # Simulation / Filtering Example
  119. # -------------------------------
  120. # Simulation parameters
  121. dt = 0.25 # time step in seconds
  122. num_steps = 100 # number of simulation steps
  123. # Initialize state:
  124. # Assume starting at 100% SOC and a capacity of 3 Ah (e.g., 3000 mAh)
  125. x_est = np.array([1.0, 3.0])
  126. # Initial state covariance
  127. P_est = np.diag([0.01, 0.01])
  128. # Process noise covariance matrix
  129. Q = np.diag([1e-5, 1e-6])
  130. # Measurement noise variance (for voltage measurement)
  131. R_measure = 0.01
  132. # Internal resistance (Ohm)
  133. R_internal = 0.05
  134. # Generate dummy measurement data
  135. # For example, assume a constant discharge current of 0.5 A and constant temperature (25°C)
  136. current_array = np.full(num_steps, 0.5)
  137. temp_array = np.full(num_steps, 25)
  138. voltage_meas_array = np.zeros(num_steps)
  139. voltage_true_array = np.zeros(num_steps)
  140. # For simulation purposes, we generate a "true" state trajectory.
  141. x_true = np.array([1.0, 3.0]) # initial true state
  142. # Lists to store estimates for plotting
  143. soc_estimates = []
  144. cap_estimates = []
  145. for k in range(num_steps):
  146. # --- Simulation: generate true state and corresponding measurement ---
  147. x_true = battery_model(x_true, current_array[k], dt)
  148. voltage_true = measurement_model(x_true, current_array[k], temp_array[k], R_internal)
  149. # Simulate measurement noise
  150. noise = np.random.normal(0, np.sqrt(R_measure))
  151. voltage_meas = voltage_true + noise
  152. voltage_true_array[k] = voltage_true
  153. voltage_meas_array[k] = voltage_meas
  154. # --- EKF Predict Step ---
  155. x_pred, P_pred = ekf_predict(x_est, P_est, current_array[k], dt, Q)
  156. # --- EKF Update Step ---
  157. x_est, P_est = ekf_update(x_pred, P_pred, voltage_meas, current_array[k], temp_array[k],
  158. R_internal, R_measure)
  159. soc_estimates.append(x_est[0])
  160. cap_estimates.append(x_est[1])
  161. # Optional: print the step results
  162. print(f"Step {k:03d}: Measured Voltage = {voltage_meas:.3f} V, Estimated SOC = {x_est[0]:.4f}, Capacity = {x_est[1]:.4f} Ah")
  163. # -------------------------------
  164. # Plotting results for visualization
  165. # -------------------------------
  166. plt.figure(figsize=(12, 5))
  167. plt.subplot(1, 2, 1)
  168. plt.plot(soc_estimates, label='Estimated SOC')
  169. plt.xlabel('Time Step')
  170. plt.ylabel('State of Charge')
  171. plt.title('SOC Estimation')
  172. plt.legend()
  173. plt.subplot(1, 2, 2)
  174. plt.plot(cap_estimates, label='Estimated Capacity (Ah)')
  175. plt.xlabel('Time Step')
  176. plt.ylabel('Capacity (Ah)')
  177. plt.title('Capacity (SOH) Estimation')
  178. plt.legend()
  179. plt.tight_layout()
  180. plt.show()