2727_MODEL_ROLLING_WINDOW = 15
2828
2929_CONTINUOUS_OPERATION_MIN_TEMP_C = 175.0
30- _MAX_TRANSIENT_TEMP_CHANGE_C = 10.0
30+ _STATISTICAL_BUFFER_SIZE = 5
31+ _STATISTICAL_MIN_BUFFER = 3
32+ _STATISTICAL_Z_SCORE = 2.0
33+ _STATISTICAL_MIN_STD = 1.5
3134
3235
3336def extract_plot_data (
@@ -64,17 +67,12 @@ def pixel_to_data(px: float, py: float) -> tuple[float, float]:
6467def _calculate_variance_analysis (
6568 df_actual : pd .DataFrame , df_model : pd .DataFrame , steady_state_start_years : float = _STEADY_STATE_START_YEARS
6669) -> pd .DataFrame :
67- df_steady_state = df_actual [df_actual ['Time_Years' ] > steady_state_start_years ].copy ()
70+ post_ramp_mask = df_actual ['Time_Years' ] > steady_state_start_years
71+ n_before = len (df_actual [post_ramp_mask ])
6872
69- # TODO: Replace hardcoded minimum temperature threshold with a robust statistical baseline envelope
70- n_before = len ( df_steady_state )
73+ is_steady = _get_steady_state_mask ( df_actual , steady_state_start_years )
74+ df_steady_state = df_actual [ is_steady ]. copy ( )
7175
72- temp_diffs = df_steady_state ['Temperature_C' ].diff ().abs ().fillna (0.0 )
73- is_steady = (df_steady_state ['Temperature_C' ] >= _CONTINUOUS_OPERATION_MIN_TEMP_C ) & (
74- temp_diffs <= _MAX_TRANSIENT_TEMP_CHANGE_C
75- )
76-
77- df_steady_state = df_steady_state [is_steady ].copy ()
7876 _log .info (f'Continuous operation filter: dropped { n_before - len (df_steady_state )} transient/dip points.' )
7977
8078 model_interpolator = interp1d (
@@ -195,6 +193,40 @@ def _dedupe_centers(centers_px: list[tuple[int, int]], min_dist_px: float) -> li
195193 return accepted
196194
197195
196+ def _get_steady_state_mask (df_prod : pd .DataFrame , steady_state_start_years : float ) -> pd .Series :
197+ """
198+ Identifies steady-state production points iteratively. Maintains a rolling
199+ buffer of verified plateau points to mathematically isolate and reject
200+ the steep walls and floors of transient shut-ins.
201+ """
202+ is_steady = pd .Series (False , index = df_prod .index )
203+ post_ramp_idx = df_prod .index [df_prod ['Time_Years' ] > steady_state_start_years ]
204+
205+ valid_buffer : list [float ] = []
206+
207+ for idx in post_ramp_idx :
208+ temp = df_prod .at [idx , 'Temperature_C' ]
209+
210+ if temp < _CONTINUOUS_OPERATION_MIN_TEMP_C :
211+ continue
212+
213+ if len (valid_buffer ) >= _STATISTICAL_MIN_BUFFER :
214+ history = valid_buffer [- _STATISTICAL_BUFFER_SIZE :]
215+ mean_temp = float (np .mean (history ))
216+ std_temp = float (np .std (history , ddof = 1 ))
217+
218+ # Prevent excessively tight windows during flat extractions
219+ std_temp = max (std_temp , _STATISTICAL_MIN_STD )
220+
221+ if abs (temp - mean_temp ) > _STATISTICAL_Z_SCORE * std_temp :
222+ continue
223+
224+ is_steady .at [idx ] = True
225+ valid_buffer .append (temp )
226+
227+ return is_steady
228+
229+
198230def _regenerate_graph_from_csv (
199231 production_csv_path : Path ,
200232 model_csv_path : Path ,
@@ -205,16 +237,8 @@ def _regenerate_graph_from_csv(
205237 df_prod = pd .read_csv (production_csv_path )
206238 df_model = pd .read_csv (model_csv_path )
207239
208- # Apply the exclusion logic directly via boolean mask to avoid float-merge bugs across CSVs
209240 is_ramp_up = df_prod ['Time_Years' ] <= steady_state_start_years
210-
211- post_ramp_up_mask = ~ is_ramp_up
212- temp_diffs = df_prod .loc [post_ramp_up_mask , 'Temperature_C' ].diff ().abs ().fillna (0.0 )
213-
214- is_steady = pd .Series (False , index = df_prod .index )
215- is_steady .loc [post_ramp_up_mask ] = (
216- df_prod .loc [post_ramp_up_mask , 'Temperature_C' ] >= _CONTINUOUS_OPERATION_MIN_TEMP_C
217- ) & (temp_diffs <= _MAX_TRANSIENT_TEMP_CHANGE_C )
241+ is_steady = _get_steady_state_mask (df_prod , steady_state_start_years )
218242
219243 df_included = df_prod [is_ramp_up | is_steady ]
220244 df_excluded = df_prod [~ (is_ramp_up | is_steady )]
0 commit comments