machinegnostics 0.0.1__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.
Files changed (93) hide show
  1. __init__.py +0 -0
  2. machinegnostics/__init__.py +24 -0
  3. machinegnostics/magcal/__init__.py +37 -0
  4. machinegnostics/magcal/characteristics.py +460 -0
  5. machinegnostics/magcal/criteria_eval.py +268 -0
  6. machinegnostics/magcal/criterion.py +140 -0
  7. machinegnostics/magcal/data_conversion.py +381 -0
  8. machinegnostics/magcal/gcor.py +64 -0
  9. machinegnostics/magcal/gdf/__init__.py +2 -0
  10. machinegnostics/magcal/gdf/base_df.py +39 -0
  11. machinegnostics/magcal/gdf/base_distfunc.py +1202 -0
  12. machinegnostics/magcal/gdf/base_egdf.py +823 -0
  13. machinegnostics/magcal/gdf/base_eldf.py +830 -0
  14. machinegnostics/magcal/gdf/base_qgdf.py +1234 -0
  15. machinegnostics/magcal/gdf/base_qldf.py +1019 -0
  16. machinegnostics/magcal/gdf/cluster_analysis.py +456 -0
  17. machinegnostics/magcal/gdf/data_cluster.py +975 -0
  18. machinegnostics/magcal/gdf/data_intervals.py +853 -0
  19. machinegnostics/magcal/gdf/data_membership.py +536 -0
  20. machinegnostics/magcal/gdf/der_egdf.py +243 -0
  21. machinegnostics/magcal/gdf/distfunc_engine.py +841 -0
  22. machinegnostics/magcal/gdf/egdf.py +324 -0
  23. machinegnostics/magcal/gdf/eldf.py +297 -0
  24. machinegnostics/magcal/gdf/eldf_intv.py +609 -0
  25. machinegnostics/magcal/gdf/eldf_ma.py +627 -0
  26. machinegnostics/magcal/gdf/homogeneity.py +1218 -0
  27. machinegnostics/magcal/gdf/intv_engine.py +1523 -0
  28. machinegnostics/magcal/gdf/marginal_intv_analysis.py +558 -0
  29. machinegnostics/magcal/gdf/qgdf.py +289 -0
  30. machinegnostics/magcal/gdf/qldf.py +296 -0
  31. machinegnostics/magcal/gdf/scedasticity.py +197 -0
  32. machinegnostics/magcal/gdf/wedf.py +181 -0
  33. machinegnostics/magcal/gdf/z0_estimator.py +1047 -0
  34. machinegnostics/magcal/layer_base.py +42 -0
  35. machinegnostics/magcal/layer_history_base.py +74 -0
  36. machinegnostics/magcal/layer_io_process_base.py +238 -0
  37. machinegnostics/magcal/layer_param_base.py +448 -0
  38. machinegnostics/magcal/mg_weights.py +36 -0
  39. machinegnostics/magcal/sample_characteristics.py +532 -0
  40. machinegnostics/magcal/scale_optimization.py +185 -0
  41. machinegnostics/magcal/scale_param.py +313 -0
  42. machinegnostics/magcal/util/__init__.py +0 -0
  43. machinegnostics/magcal/util/dis_docstring.py +18 -0
  44. machinegnostics/magcal/util/logging.py +24 -0
  45. machinegnostics/magcal/util/min_max_float.py +34 -0
  46. machinegnostics/magnet/__init__.py +0 -0
  47. machinegnostics/metrics/__init__.py +28 -0
  48. machinegnostics/metrics/accu.py +61 -0
  49. machinegnostics/metrics/accuracy.py +67 -0
  50. machinegnostics/metrics/auto_correlation.py +183 -0
  51. machinegnostics/metrics/auto_covariance.py +204 -0
  52. machinegnostics/metrics/cls_report.py +130 -0
  53. machinegnostics/metrics/conf_matrix.py +93 -0
  54. machinegnostics/metrics/correlation.py +178 -0
  55. machinegnostics/metrics/cross_variance.py +167 -0
  56. machinegnostics/metrics/divi.py +82 -0
  57. machinegnostics/metrics/evalmet.py +109 -0
  58. machinegnostics/metrics/f1_score.py +128 -0
  59. machinegnostics/metrics/gmmfe.py +108 -0
  60. machinegnostics/metrics/hc.py +141 -0
  61. machinegnostics/metrics/mae.py +72 -0
  62. machinegnostics/metrics/mean.py +117 -0
  63. machinegnostics/metrics/median.py +122 -0
  64. machinegnostics/metrics/mg_r2.py +167 -0
  65. machinegnostics/metrics/mse.py +78 -0
  66. machinegnostics/metrics/precision.py +119 -0
  67. machinegnostics/metrics/r2.py +122 -0
  68. machinegnostics/metrics/recall.py +108 -0
  69. machinegnostics/metrics/rmse.py +77 -0
  70. machinegnostics/metrics/robr2.py +119 -0
  71. machinegnostics/metrics/std.py +144 -0
  72. machinegnostics/metrics/variance.py +101 -0
  73. machinegnostics/models/__init__.py +2 -0
  74. machinegnostics/models/classification/__init__.py +1 -0
  75. machinegnostics/models/classification/layer_history_log_reg.py +121 -0
  76. machinegnostics/models/classification/layer_io_process_log_reg.py +98 -0
  77. machinegnostics/models/classification/layer_mlflow_log_reg.py +107 -0
  78. machinegnostics/models/classification/layer_param_log_reg.py +275 -0
  79. machinegnostics/models/classification/mg_log_reg.py +273 -0
  80. machinegnostics/models/cross_validation.py +118 -0
  81. machinegnostics/models/data_split.py +106 -0
  82. machinegnostics/models/regression/__init__.py +2 -0
  83. machinegnostics/models/regression/layer_histroy_rob_reg.py +139 -0
  84. machinegnostics/models/regression/layer_io_process_rob_rig.py +88 -0
  85. machinegnostics/models/regression/layer_mlflow_rob_reg.py +134 -0
  86. machinegnostics/models/regression/layer_param_rob_reg.py +212 -0
  87. machinegnostics/models/regression/mg_lin_reg.py +253 -0
  88. machinegnostics/models/regression/mg_poly_reg.py +258 -0
  89. machinegnostics-0.0.1.dist-info/METADATA +246 -0
  90. machinegnostics-0.0.1.dist-info/RECORD +93 -0
  91. machinegnostics-0.0.1.dist-info/WHEEL +5 -0
  92. machinegnostics-0.0.1.dist-info/licenses/LICENSE +674 -0
  93. machinegnostics-0.0.1.dist-info/top_level.txt +2 -0
@@ -0,0 +1,830 @@
1
+ '''
2
+ base ELDF class
3
+ Estimating Local Distribution Functions
4
+
5
+ Author: Nirmal Parmar
6
+ Machine Gnostics
7
+ '''
8
+
9
+ import numpy as np
10
+ import warnings
11
+ import logging
12
+ from machinegnostics.magcal.util.logging import get_logger
13
+ from scipy.optimize import minimize
14
+ from typing import Dict, Any
15
+ from machinegnostics.magcal.characteristics import GnosticsCharacteristics
16
+ from machinegnostics.magcal.data_conversion import DataConversion
17
+ from machinegnostics.magcal.gdf.base_egdf import BaseEGDF
18
+ from machinegnostics.magcal.scale_param import ScaleParam
19
+ from machinegnostics.magcal.gdf.z0_estimator import Z0Estimator
20
+
21
+ class BaseELDF(BaseEGDF):
22
+ '''Base ELDF class'''
23
+ def __init__(self,
24
+ data: np.ndarray,
25
+ DLB: float = None,
26
+ DUB: float = None,
27
+ LB: float = None,
28
+ UB: float = None,
29
+ S = 'auto',
30
+ varS: bool = False,
31
+ z0_optimize: bool = True,
32
+ tolerance: float = 1e-3,
33
+ data_form: str = 'a',
34
+ n_points: int = 500,
35
+ homogeneous: bool = True,
36
+ catch: bool = True,
37
+ weights: np.ndarray = None,
38
+ wedf: bool = True,
39
+ opt_method: str = 'L-BFGS-B',
40
+ verbose: bool = False,
41
+ max_data_size: int = 1000,
42
+ flush: bool = True):
43
+ super().__init__(data=data,
44
+ DLB=DLB,
45
+ DUB=DUB,
46
+ LB=LB,
47
+ UB=UB,
48
+ S=S,
49
+ tolerance=tolerance,
50
+ data_form=data_form,
51
+ n_points=n_points,
52
+ homogeneous=homogeneous,
53
+ catch=catch,
54
+ weights=weights,
55
+ wedf=wedf,
56
+ opt_method=opt_method,
57
+ verbose=verbose,
58
+ max_data_size=max_data_size,
59
+ flush=flush)
60
+
61
+ # Store raw inputs
62
+ self.data = data
63
+ self.DLB = DLB
64
+ self.DUB = DUB
65
+ self.LB = LB
66
+ self.UB = UB
67
+ self.S = S
68
+ self.varS = varS # ELDF specific
69
+ self.z0_optimize = z0_optimize # ELDF specific
70
+ self.tolerance = tolerance
71
+ self.data_form = data_form
72
+ self.n_points = n_points
73
+ self.homogeneous = homogeneous
74
+ self.catch = catch
75
+ self.weights = weights if weights is not None else np.ones_like(data)
76
+ self.wedf = wedf
77
+ self.opt_method = opt_method
78
+ self.verbose = verbose
79
+ self.max_data_size = max_data_size
80
+ self.flush = flush
81
+ self._fitted = False # To track if fit has been called
82
+
83
+ # Store initial parameters if catching
84
+ if self.catch:
85
+ self._store_initial_params()
86
+
87
+ # Validate all inputs
88
+ # self._validate_inputs()
89
+
90
+ # logger
91
+ self.logger = get_logger(self.__class__.__name__, logging.DEBUG if verbose else logging.WARNING)
92
+ self.logger.debug(f"{self.__class__.__name__} initialized:")
93
+
94
+ # if S is float or int and is greater than 2, warn user
95
+ if (isinstance(self.S, float) or isinstance(self.S, int)) and self.S > 2:
96
+ self.logger.warning("S is greater than 2, which may not suitable for local distribution estimation. Consider using in range [0, 2]")
97
+ warnings.warn("S is greater than 2, which may not suitable for local distribution estimation. Consider using in range [0, 2]", UserWarning)
98
+
99
+
100
+
101
+ def _fit_eldf(self, plot: bool = True):
102
+ """Fit the ELDF model to the data."""
103
+ self.logger.info("Starting ELDF fitting process...")
104
+ try:
105
+ # Step 1: Data preprocessing
106
+ self.logger.info("Preprocessing data...")
107
+ self.data = np.sort(self.data)
108
+ self._estimate_data_bounds()
109
+ self._transform_data_to_standard_domain()
110
+ self._estimate_weights()
111
+
112
+ # Step 2: Bounds estimation
113
+ self.logger.info("Estimating initial probable bounds...")
114
+ self._estimate_initial_probable_bounds()
115
+ self._generate_evaluation_points()
116
+
117
+ # Step 3: Get distribution function values for optimization
118
+ self.logger.info("Getting distribution function values...")
119
+ self.df_values = self._get_distribution_function_values(use_wedf=self.wedf)
120
+
121
+ # Step 4: Parameter optimization
122
+ self.logger.info("Optimizing parameters...")
123
+ self._determine_optimization_strategy()
124
+
125
+ # Step 5: Calculate final ELDF and PDF
126
+ self.logger.info("Computing final ELDF and PDF values...")
127
+ self._compute_final_results()
128
+
129
+ # Step 6: Generate smooth curves for plotting and analysis
130
+ self.logger.info("Generating smooth curves for analysis...")
131
+ self._generate_smooth_curves()
132
+
133
+ # Step 7: Transform bounds back to original domain
134
+
135
+ self._transform_bounds_to_original_domain()
136
+ self.logger.info("Transformed bounds back to original data domain.")
137
+ # Mark as fitted (Step 8 is now optional via marginal_analysis())
138
+ self._fitted = True
139
+
140
+ # Step 8: Z0 estimate with Z0Estimator
141
+ self.logger.info("Estimating Z0...")
142
+ self._compute_z0(optimize=self.z0_optimize)
143
+
144
+ # step 9: varS
145
+ if self.varS:
146
+ self.logger.info("Estimating varying S...")
147
+ self._varS_calculation()
148
+ self.logger.info("Computing final results for varying S...")
149
+ self._compute_final_results_varS()
150
+ self.logger.info("Generating smooth curves for varying S...")
151
+ self._generate_smooth_curves_varS()
152
+
153
+ # Step 10: Z0 re-estimate with varS if enabled
154
+ if self.varS:
155
+ self.logger.info("Re-estimating Z0 with varying S...")
156
+ self._compute_z0(optimize=self.z0_optimize)
157
+
158
+ self.logger.info("ELDF fitting completed successfully.")
159
+
160
+ if plot:
161
+ self.logger.info("Plotting results...")
162
+ self._plot()
163
+
164
+ # clean up computation cache
165
+ if self.flush:
166
+ self.logger.info("Cleaning up computation cache...")
167
+ self._cleanup_computation_cache()
168
+
169
+ except Exception as e:
170
+ # log error
171
+ error_msg = f"ELDF fitting failed: {e}"
172
+ self.logger.error(error_msg)
173
+ self.params['errors'].append({
174
+ 'method': '_fit_eldf',
175
+ 'error': error_msg,
176
+ 'exception_type': type(e).__name__
177
+ })
178
+ self.logger.info(f"Error during ELDF fitting: {e}")
179
+ raise e
180
+
181
+
182
+ def _compute_eldf_core(self, S, LB, UB, zi_data=None, zi_eval=None):
183
+ """Core computation for the ELDF model."""
184
+ self.logger.info("Computing core ELDF values...")
185
+ # Use provided data or default to instance data
186
+ if zi_data is None:
187
+ zi_data = self.z
188
+ if zi_eval is None:
189
+ zi_eval = zi_data
190
+
191
+ # Convert to infinite domain
192
+ zi_n = DataConversion._convert_fininf(zi_eval, LB, UB)
193
+ zi_d = DataConversion._convert_fininf(zi_data, LB, UB)
194
+
195
+ # Calculate R matrix with numerical stability
196
+ R = zi_n.reshape(-1, 1) / (zi_d.reshape(1, -1) + self._NUMERICAL_EPS)
197
+
198
+ # Get characteristics
199
+ gc = GnosticsCharacteristics(R=R, verbose=self.verbose)
200
+ q, q1 = gc._get_q_q1(S=S)
201
+
202
+ # Calculate fidelities and irrelevances
203
+ fi = gc._fi(q=q, q1=q1)
204
+ hi = gc._hi(q=q, q1=q1)
205
+ return self._estimate_eldf_from_moments(fi, hi), fi, hi
206
+
207
+ def _estimate_eldf_from_moments(self, fidelity, irrelevance):
208
+ """Estimate the ELDF from moments."""
209
+ self.logger.info("Estimating ELDF from moments...")
210
+ weights = self.weights.reshape(-1, 1)
211
+
212
+ mean_irrelevance = np.sum(weights * irrelevance, axis=0) / np.sum(weights)
213
+
214
+ eldf_values = (1 - mean_irrelevance) / 2
215
+
216
+ return eldf_values.flatten()
217
+
218
+ def _compute_final_results(self):
219
+ """Compute the final results for the ELDF model."""
220
+ self.logger.info("Computing final ELDF and PDF results...")
221
+ # Implement final results computation logic here
222
+ zi_d = DataConversion._convert_fininf(self.z, self.LB_opt, self.UB_opt)
223
+ self.zi = zi_d
224
+
225
+ # Calculate ELDF and get moments
226
+ eldf_values, fi, hi = self._compute_eldf_core(self.S_opt, self.LB_opt, self.UB_opt)
227
+
228
+ # Store for derivative calculations
229
+ self.fi = fi
230
+ self.hi = hi
231
+
232
+ self.eldf = eldf_values
233
+ self.pdf = self._compute_eldf_pdf(self.fi, self.hi)
234
+
235
+ if self.catch:
236
+ self.logger.info("Catching parameters...")
237
+ self.params.update({
238
+ 'eldf': self.eldf.copy(),
239
+ 'pdf': self.pdf.copy(),
240
+ 'zi': self.zi.copy(),
241
+ # 'S_var': self.S_var.copy() if self.varS else None
242
+ })
243
+
244
+ def _compute_eldf_pdf(self, fi, hi):
245
+ """Compute the PDF for the ELDF model."""
246
+ self.logger.info("Computing PDF from ELDF moments...")
247
+ weights = self.weights.reshape(-1, 1)
248
+
249
+ # fi_mean
250
+ fi_mean = np.sum(weights * fi, axis=0) / np.sum(weights)
251
+ pdf_values = ((fi_mean)**2)/(self.S_opt)
252
+ return pdf_values.flatten()
253
+
254
+ def _generate_smooth_curves(self):
255
+ """Generate smooth curves for plotting and analysis - ELDF."""
256
+ self.logger.info("Generating smooth curves for ELDF...")
257
+ try:
258
+ self.logger.info("Generating smooth curves without varying S...")
259
+
260
+ smooth_eldf, self.smooth_fi, self.smooth_hi = self._compute_eldf_core(
261
+ self.S_opt, self.LB_opt, self.UB_opt,
262
+ zi_data=self.z_points_n, zi_eval=self.z
263
+ )
264
+ smooth_pdf = self._compute_eldf_pdf(self.smooth_fi, self.smooth_hi)
265
+
266
+ self.eldf_points = smooth_eldf
267
+ self.pdf_points = smooth_pdf
268
+
269
+ # Store zi_n for derivative calculations
270
+ self.zi_n = DataConversion._convert_fininf(self.z_points_n, self.LB_opt, self.UB_opt)
271
+
272
+ # Mark as generated
273
+ self._computation_cache['smooth_curves_generated'] = True
274
+
275
+ if self.catch:
276
+ self.logger.info("Catching parameters...")
277
+ self.params.update({
278
+ 'eldf_points': self.eldf_points.copy(),
279
+ 'pdf_points': self.pdf_points.copy(),
280
+ 'zi_points': self.zi_n.copy()
281
+ })
282
+
283
+ self.logger.info(f"Generated smooth curves with {self.n_points} points.")
284
+
285
+ except Exception as e:
286
+ # log error
287
+ error_msg = f"Smooth curve generation failed: {e}"
288
+ self.logger.error(error_msg)
289
+ self.params['errors'].append({
290
+ 'method': '_generate_smooth_curves',
291
+ 'error': error_msg,
292
+ 'exception_type': type(e).__name__
293
+ })
294
+
295
+ self.logger.warning(f"Could not generate smooth curves: {e}")
296
+
297
+ # Create fallback points using original data
298
+ self.eldf_points = self.eldf.copy() if hasattr(self, 'eldf') else None
299
+ self.pdf_points = self.pdf.copy() if hasattr(self, 'pdf') else None
300
+ self._computation_cache['smooth_curves_generated'] = False
301
+
302
+
303
+ def _plot(self, plot_smooth: bool = True, plot: str = 'both', bounds: bool = True, extra_df: bool = True, figsize: tuple = (12, 8)):
304
+ """Enhanced plotting with better organization."""
305
+ self.logger.info("Preparing to plot ELDF and PDF...")
306
+ import matplotlib.pyplot as plt
307
+
308
+ if plot_smooth and (len(self.data) > self.max_data_size) and self.verbose:
309
+ self.logger.warning(f"Given data size ({len(self.data)}) exceeds max_data_size ({self.max_data_size}). For optimal compute performance, set 'plot_smooth=False', or 'max_data_size' to a larger value whichever is appropriate.")
310
+
311
+ if not self.catch:
312
+ self.logger.warning("Plot is not available with argument catch=False")
313
+ return
314
+
315
+ if not self._fitted:
316
+ raise RuntimeError("Must fit ELDF before plotting.")
317
+
318
+ # Validate plot parameter
319
+ if plot not in ['gdf', 'pdf', 'both']:
320
+ raise ValueError("plot parameter must be 'gdf', 'pdf', or 'both'")
321
+
322
+ # Check data availability
323
+ if plot in ['gdf', 'both'] and self.params.get('eldf') is None:
324
+ raise ValueError("ELDF must be calculated before plotting GDF")
325
+ if plot in ['pdf', 'both'] and self.params.get('pdf') is None:
326
+ raise ValueError("PDF must be calculated before plotting PDF")
327
+
328
+ # Prepare data
329
+ x_points = self.data
330
+ eldf_plot = self.params.get('eldf')
331
+ pdf_plot = self.params.get('pdf')
332
+ wedf = self.params.get('wedf')
333
+ ksdf = self.params.get('ksdf')
334
+
335
+ # Check smooth plotting availability
336
+ has_smooth = (hasattr(self, 'z_points_n') and hasattr(self, 'eldf_points')
337
+ and hasattr(self, 'pdf_points') and self.z_points_n is not None
338
+ and self.eldf_points is not None and self.pdf_points is not None)
339
+ plot_smooth = plot_smooth and has_smooth
340
+
341
+ # Create figure
342
+ fig, ax1 = plt.subplots(figsize=figsize)
343
+
344
+ # Plot ELDF if requested
345
+ if plot in ['gdf', 'both']:
346
+ self._plot_eldf(ax1, x_points, eldf_plot, plot_smooth, extra_df, wedf, ksdf)
347
+
348
+ # Plot PDF if requested
349
+ if plot in ['pdf', 'both']:
350
+ if plot == 'pdf':
351
+ self._plot_pdf(ax1, x_points, pdf_plot, plot_smooth, is_secondary=False)
352
+ else:
353
+ ax2 = ax1.twinx()
354
+ self._plot_pdf(ax2, x_points, pdf_plot, plot_smooth, is_secondary=True)
355
+
356
+ # Add bounds and formatting
357
+ self._add_plot_formatting(ax1, plot, bounds)
358
+
359
+ # Add Z0 vertical line if available
360
+ if hasattr(self, 'z0') and self.z0 is not None:
361
+ ax1.axvline(x=self.z0, color='magenta', linestyle='-.', linewidth=1,
362
+ alpha=0.8, label=f'Z0={self.z0:.3f}')
363
+ # Update legend to include Z0
364
+ ax1.legend(loc='upper left', bbox_to_anchor=(0, 1))
365
+
366
+ plt.tight_layout()
367
+ plt.show()
368
+
369
+ def _plot_pdf(self, ax, x_points, pdf_plot, plot_smooth, is_secondary=False):
370
+ """Plot PDF components."""
371
+ self.logger.info("Plotting PDF...")
372
+ import numpy as np # Add numpy import
373
+ color = 'red'
374
+
375
+ if plot_smooth and hasattr(self, 'pdf_points') and self.pdf_points is not None:
376
+ ax.plot(x_points, pdf_plot, 'o', color=color, label='PDF', markersize=4)
377
+ ax.plot(self.di_points_n, self.pdf_points, color=color,
378
+ linestyle='-', linewidth=2, alpha=0.8)
379
+ max_pdf = np.max(self.pdf_points)
380
+ else:
381
+ ax.plot(x_points, pdf_plot, 'o-', color=color, label='PDF',
382
+ markersize=4, linewidth=1, alpha=0.8)
383
+ max_pdf = np.max(pdf_plot)
384
+
385
+ ax.set_ylabel('PDF', color=color)
386
+ ax.tick_params(axis='y', labelcolor=color)
387
+ ax.set_ylim(0, max_pdf * 1.1)
388
+
389
+ if is_secondary:
390
+ ax.legend(loc='upper right', bbox_to_anchor=(1, 1))
391
+
392
+ def _plot_eldf(self, ax, x_points, eldf_plot, plot_smooth, extra_df, wedf, ksdf):
393
+ """Plot ELDF components."""
394
+ self.logger.info("Plotting ELDF...")
395
+ if plot_smooth and hasattr(self, 'eldf_points') and self.eldf_points is not None:
396
+ ax.plot(x_points, eldf_plot, 'o', color='blue', label='ELDF', markersize=4)
397
+ ax.plot(self.di_points_n, self.eldf_points, color='blue',
398
+ linestyle='-', linewidth=2, alpha=0.8)
399
+ else:
400
+ ax.plot(x_points, eldf_plot, 'o-', color='blue', label='ELDF',
401
+ markersize=4, linewidth=1, alpha=0.8)
402
+
403
+ if extra_df:
404
+ if wedf is not None:
405
+ ax.plot(x_points, wedf, 's', color='lightblue',
406
+ label='WEDF', markersize=3, alpha=0.8)
407
+ if ksdf is not None:
408
+ ax.plot(x_points, ksdf, 's', color='cyan',
409
+ label='KS Points', markersize=3, alpha=0.8)
410
+
411
+ ax.set_ylabel('ELDF', color='blue')
412
+ ax.tick_params(axis='y', labelcolor='blue')
413
+ ax.set_ylim(0, 1)
414
+
415
+ def _add_plot_formatting(self, ax1, plot, bounds):
416
+ """Add formatting, bounds, and legends to plot."""
417
+ self.logger.info("Adding plot formatting and bounds...")
418
+ ax1.set_xlabel('Data Points')
419
+
420
+ # Add bounds if requested
421
+ if bounds:
422
+ bound_info = [
423
+ (self.params.get('DLB'), 'green', '-', 'DLB'),
424
+ (self.params.get('DUB'), 'orange', '-', 'DUB'),
425
+ (self.params.get('LB'), 'purple', '--', 'LB'),
426
+ (self.params.get('UB'), 'brown', '--', 'UB')
427
+ ]
428
+
429
+ for bound, color, style, name in bound_info:
430
+ if bound is not None:
431
+ ax1.axvline(x=bound, color=color, linestyle=style, linewidth=2,
432
+ alpha=0.8, label=f"{name}={bound:.3f}")
433
+
434
+ # Add shaded regions
435
+ if self.params.get('LB') is not None:
436
+ ax1.axvspan(self.data.min(), self.params['LB'], alpha=0.15, color='purple')
437
+ if self.params.get('UB') is not None:
438
+ ax1.axvspan(self.params['UB'], self.data.max(), alpha=0.15, color='brown')
439
+
440
+ # Set limits and add grid
441
+ data_range = self.params['DUB'] - self.params['DLB']
442
+ padding = data_range * 0.1
443
+ ax1.set_xlim(self.params['DLB'] - padding, self.params['DUB'] + padding)
444
+
445
+ # Set title
446
+ titles = {
447
+ 'gdf': 'ELDF' + (' with Bounds' if bounds else ''),
448
+ 'pdf': 'PDF' + (' with Bounds' if bounds else ''),
449
+ 'both': 'ELDF and PDF' + (' with Bounds' if bounds else '')
450
+ }
451
+
452
+ ax1.set_title(titles[plot])
453
+ ax1.legend(loc='upper left', bbox_to_anchor=(0, 1))
454
+ ax1.grid(True, alpha=0.3)
455
+
456
+ def _get_eldf_second_derivative(self, fi, hi):
457
+ """Calculate second derivative of ELDF from stored fidelities and irrelevances."""
458
+ self.logger.info("Calculating second derivative of ELDF...")
459
+ if fi is None or hi is None:
460
+ fi = self.fi
461
+ hi = self.hi
462
+
463
+ if fi is None or hi is None:
464
+ raise ValueError("Fidelities and irrelevances must be calculated before second derivative estimation.")
465
+
466
+ weights = self.weights.reshape(-1, 1)
467
+
468
+ # Calculate f^2 * h (fidelity squared times irrelevance)
469
+ fh = (self.fi**2) * self.hi
470
+
471
+ # Weight and scale by S^2
472
+ weighted_fh = fh * (weights / (self.S_opt**2))
473
+
474
+ # Sum over data points
475
+ second_derivative = 4 * np.sum(weighted_fh, axis=0) / np.sum(weights)
476
+
477
+ return second_derivative.flatten()
478
+
479
+ def _get_eldf_third_derivative(self, fi, hi):
480
+ """Calculate third derivative of ELDF from stored fidelities and irrelevances."""
481
+ self.logger.info("Calculating third derivative of ELDF...")
482
+ if fi is None or hi is None:
483
+ fi = self.fi
484
+ hi = self.hi
485
+
486
+ if fi is None or hi is None:
487
+ raise ValueError("Fidelities and irrelevances must be calculated before third derivative estimation.")
488
+
489
+ weights = self.weights.reshape(-1, 1)
490
+
491
+ # Calculate components
492
+ h2 = self.hi**2 # h^2
493
+ f2 = self.fi**2 # f^2
494
+ f2h2 = f2 * h2 # f^2 * h^2
495
+ f4 = f2**2 # f^4
496
+
497
+ # Calculate the expression: 8 * (2 * f^2 * h^2 - f^4) * (W / S^3)
498
+ expression = 2 * f2h2 - f4
499
+ weighted_expression = expression * (weights / (self.S_opt**3))
500
+
501
+ # Sum over data points
502
+ third_derivative = 8 * np.sum(weighted_expression, axis=0) / np.sum(weights)
503
+
504
+ return third_derivative.flatten()
505
+
506
+ def _get_eldf_fourth_derivative(self, fi, hi):
507
+ """Calculate fourth derivative of ELDF from stored fidelities and irrelevances."""
508
+ self.logger.info("Calculating fourth derivative of ELDF...")
509
+ if fi is None or hi is None:
510
+ fi = self.fi
511
+ hi = self.hi
512
+
513
+ if self.fi is None or self.hi is None:
514
+ raise ValueError("Fidelities and irrelevances must be calculated before fourth derivative estimation.")
515
+
516
+ weights = self.weights.reshape(-1, 1)
517
+
518
+ # Calculate components
519
+ f2 = self.fi**2 # f^2
520
+ h = self.hi # h
521
+ h3 = h**3 # h^3
522
+
523
+ # Calculate f^2 * h^3 and f^4 * h
524
+ f2h3 = f2 * h3
525
+ f4h = (f2**2) * h # f^4 * h
526
+
527
+ # Weight and scale by (2/S)^4
528
+ scale_factor = (2 / self.S_opt)**4
529
+ weighted_f2h3 = f2h3 * (weights * scale_factor)
530
+ weighted_f4h = f4h * (weights * scale_factor)
531
+
532
+ # Calculate the expression: 4 * (f^2 * h^3 - 2 * f^4 * h)
533
+ sum_f2h3 = np.sum(weighted_f2h3, axis=0) / np.sum(weights)
534
+ sum_f4h = np.sum(weighted_f4h, axis=0) / np.sum(weights)
535
+
536
+ fourth_derivative = 4 * (sum_f2h3 - 2 * sum_f4h)
537
+
538
+ return fourth_derivative.flatten()
539
+
540
+ def _get_results(self)-> dict:
541
+ """Return fitting results."""
542
+ self.logger.info("Retrieving ELDF fitting results...")
543
+ if not self._fitted:
544
+ raise RuntimeError("Must fit ELDF before getting results.")
545
+
546
+ # selected key from params if exists
547
+ keys = ['DLB', 'DUB', 'LB', 'UB', 'S_opt', 'z0', 'eldf', 'pdf',
548
+ 'eldf_points', 'pdf_points', 'zi', 'zi_points', 'weights']
549
+ results = {key: self.params.get(key) for key in keys if key in self.params}
550
+ return results
551
+
552
+ # z0 compute
553
+ def _compute_z0(self, optimize: bool = None):
554
+ """
555
+ Compute the Z0 point where PDF is maximum using the Z0Estimator class.
556
+
557
+ Parameters:
558
+ -----------
559
+ optimize : bool, optional
560
+ If True, use interpolation-based methods for higher accuracy.
561
+ If False, use simple linear search on existing points.
562
+ If None, uses the instance's z0_optimize setting.
563
+ """
564
+ self.logger.info("Computing Z0 point using Z0Estimator...")
565
+ if self.z is None:
566
+ self.logger.error("Data must be transformed (self.z) before Z0 estimation.")
567
+ raise ValueError("Data must be transformed (self.z) before Z0 estimation.")
568
+
569
+ # Use provided optimize parameter or fall back to instance setting
570
+ use_optimize = optimize if optimize is not None else self.z0_optimize
571
+
572
+ self.logger.info('ELDF: Computing Z0 point using Z0Estimator...')
573
+
574
+ try:
575
+ # Create Z0Estimator instance with proper constructor signature
576
+ z0_estimator = Z0Estimator(
577
+ gdf_object=self, # Pass the ELDF object itself
578
+ optimize=use_optimize,
579
+ verbose=self.verbose
580
+ )
581
+
582
+ # Call fit() method to estimate Z0
583
+ self.z0 = z0_estimator.fit()
584
+
585
+ # Get estimation info for debugging and storage
586
+ if self.catch:
587
+ estimation_info = z0_estimator.get_estimation_info()
588
+ self.params.update({
589
+ 'z0': float(self.z0) if self.z0 is not None else None,
590
+ 'z0_method': estimation_info.get('z0_method', 'unknown'),
591
+ 'z0_estimation_info': estimation_info
592
+ })
593
+
594
+ method_used = z0_estimator.get_estimation_info().get('z0_method', 'unknown')
595
+ self.logger.info(f'ELDF: Z0 point computed successfully, (method: {method_used})')
596
+
597
+ except Exception as e:
598
+ # Log the error
599
+ error_msg = f"Z0 estimation failed: {str(e)}"
600
+ self.logger.error(error_msg)
601
+ self.params['errors'].append({
602
+ 'method': '_compute_z0',
603
+ 'error': error_msg,
604
+ 'exception_type': type(e).__name__
605
+ })
606
+
607
+ self.logger.warning(f"Warning: Z0Estimator failed with error: {e}")
608
+ self.logger.info("Falling back to simple maximum finding...")
609
+
610
+ # Fallback to simple maximum finding
611
+ self._compute_z0_fallback()
612
+
613
+ if self.catch:
614
+ self.params.update({
615
+ 'z0': float(self.z0),
616
+ 'z0_method': 'fallback_simple_maximum',
617
+ 'z0_estimation_info': {'error': str(e)}
618
+ })
619
+
620
+ def _compute_z0_fallback(self):
621
+ """
622
+ Fallback method for Z0 computation using simple maximum finding.
623
+ """
624
+ self.logger.info("Using fallback method for Z0 point...")
625
+
626
+ if not hasattr(self, 'di_points_n') or not hasattr(self, 'pdf_points'):
627
+ self.logger.error("Both 'di_points_n' and 'pdf_points' must be defined for Z0 computation.")
628
+ raise ValueError("Both 'di_points_n' and 'pdf_points' must be defined for Z0 computation.")
629
+
630
+ self.logger.info('Using fallback method for Z0 point...')
631
+
632
+ # Find index with maximum PDF
633
+ max_idx = np.argmax(self.pdf_points)
634
+ self.z0 = self.di_points_n[max_idx]
635
+
636
+ self.logger.info(f"Z0 point (fallback method).")
637
+
638
+ def analyze_z0(self, figsize: tuple = (12, 6)) -> Dict[str, Any]:
639
+ """
640
+ Analyze and visualize Z0 estimation results.
641
+
642
+ Parameters:
643
+ -----------
644
+ figsize : tuple
645
+ Figure size for the plot
646
+
647
+ Returns:
648
+ --------
649
+ Dict[str, Any]
650
+ Z0 analysis information
651
+ """
652
+ self.logger.info("Analyzing Z0 estimation results...")
653
+ if not hasattr(self, 'z0') or self.z0 is None:
654
+ self.logger.error("Z0 must be computed before analysis. Call fit() first.")
655
+ raise ValueError("Z0 must be computed before analysis. Call fit() first.")
656
+
657
+ # Create Z0Estimator for analysis
658
+ z0_estimator = Z0Estimator(
659
+ gdf_object=self,
660
+ optimize=self.z0_optimize,
661
+ verbose=self.verbose
662
+ )
663
+
664
+ # Re-estimate for analysis (this is safe since it's already computed)
665
+ z0_estimator.fit()
666
+
667
+ # Get detailed info
668
+ analysis_info = z0_estimator.get_estimation_info()
669
+
670
+ # Create visualization
671
+ z0_estimator.plot_z0_analysis(figsize=figsize)
672
+
673
+ return analysis_info
674
+
675
+ def _calculate_fidelities_irrelevances_at_given_zi(self, zi):
676
+ """Helper method to recalculate fidelities and irrelevances for current zi."""
677
+ self.logger.info("Calculating fidelities and irrelevances at given zi...")
678
+ # Convert to infinite domain
679
+ zi_n = DataConversion._convert_fininf(self.z, self.LB_opt, self.UB_opt)
680
+ # is zi given then use it, else use self.zi
681
+ if zi is None:
682
+ zi_d = self.zi
683
+ else:
684
+ zi_d = zi
685
+
686
+ # Calculate R matrix
687
+ eps = np.finfo(float).eps
688
+ R = zi_n.reshape(-1, 1) / (zi_d + eps).reshape(1, -1)
689
+
690
+ # Get characteristics
691
+ gc = GnosticsCharacteristics(R=R, verbose=self.verbose)
692
+ q, q1 = gc._get_q_q1(S=self.S_opt)
693
+
694
+ # Store fidelities and irrelevances
695
+ self.fi = gc._fi(q=q, q1=q1)
696
+ self.hi = gc._hi(q=q, q1=q1)
697
+
698
+ def _estimate_s0_sigma(self, z0, s_local, s_global, mode="sum"):
699
+ """
700
+ Estimate S0 and sigma given z0 (float), s_local (array), and s_global (float).
701
+
702
+ Parameters
703
+ ----------
704
+ z0 : float
705
+ Mean value of the data.
706
+ s_local : array-like
707
+ Local scale parameters.
708
+ s_global : float
709
+ Global scale parameter.
710
+ mode : str
711
+ "mean" -> match average predicted to s_global
712
+ "sum" -> match sum predicted to s_global
713
+
714
+ Returns
715
+ -------
716
+ S0, sigma : floats
717
+ Estimated parameters.
718
+ """
719
+ self.logger.info("Estimating S0 and sigma...")
720
+ s_local = np.asarray(s_local)
721
+
722
+ def objective(params):
723
+ S0, sigma = params
724
+ preds = S0 * np.exp(sigma * z0) * s_local
725
+ if mode == "mean":
726
+ target = preds.mean()
727
+ elif mode == "sum":
728
+ target = preds.sum()
729
+ else:
730
+ raise ValueError("mode must be 'mean' or 'sum'")
731
+ return (s_global - target) ** 2
732
+
733
+ # Initial guess
734
+ p0 = [1.0, 0.0]
735
+
736
+ res = minimize(objective, p0, method="Nelder-Mead")
737
+ return res.x[0], res.x[1]
738
+
739
+ def _varS_calculation(self):
740
+ """Calculate varS if enabled."""
741
+ self.logger.info("Calculating varying S (varS)...")
742
+ from machinegnostics import variance
743
+
744
+ self.logger.info("Calculating varS for ELDF...")
745
+ # estimate fi hi at z0
746
+ gc, q, q1 = self._calculate_gcq_at_given_zi(self.z0)
747
+
748
+ fi_z0 = gc._fi(q=q, q1=q1)
749
+
750
+ scale = ScaleParam()
751
+ self.S_local = scale._gscale_loc(fi_z0)
752
+
753
+ # # s0 # NOTE for future exploration
754
+ # self.S0, self.sigma = self._estimate_s0_sigma(
755
+ # z0=self.z0,
756
+ # s_local=fi_z0,
757
+ # s_global=self.S_opt,
758
+ # mode="sum"
759
+ # )
760
+
761
+ # Svar
762
+ self.S_var = self.S_local * self.S_opt
763
+ return self.S_var
764
+
765
+ def _compute_final_results_varS(self):
766
+ """Compute the final results for the ELDF model."""
767
+ self.logger.info("Computing final ELDF and PDF results with varying S...")
768
+ # Implement final results computation logic here
769
+ # zi_d = DataConversion._convert_fininf(self.z, self.LB_opt, self.UB_opt)
770
+ # self.zi = zi_d
771
+
772
+ eldf_values, fi, hi = self._compute_eldf_core(self.S_var, self.LB_opt, self.UB_opt)
773
+ self.fi = fi
774
+ self.hi = hi
775
+
776
+ self.eldf = eldf_values
777
+ self.pdf = self._compute_eldf_pdf(self.fi, self.hi)
778
+
779
+ if self.catch:
780
+ self.logger.info("Catching parameters with varying S...")
781
+ self.params.update({
782
+ 'eldf': self.eldf.copy(),
783
+ 'pdf': self.pdf.copy(),
784
+ 'zi': self.zi.copy(),
785
+ 'S_var': self.S_var.copy() if self.varS else None
786
+ })
787
+
788
+ def _generate_smooth_curves_varS(self):
789
+ """Generate smooth curves for plotting and analysis - ELDF."""
790
+ self.logger.info("Generating smooth curves for ELDF with varying S...")
791
+ try:
792
+ self.logger.info("Generating smooth curves with varying S...")
793
+
794
+ smooth_eldf, self.smooth_fi, self.smooth_hi = self._compute_eldf_core(
795
+ self.S_var, self.LB_opt, self.UB_opt,
796
+ zi_data=self.z_points_n, zi_eval=self.z
797
+ )
798
+ smooth_pdf = self._compute_eldf_pdf(self.smooth_fi, self.smooth_hi)
799
+
800
+ self.eldf_points = smooth_eldf
801
+ self.pdf_points = smooth_pdf
802
+
803
+ # Mark as generated
804
+ self._computation_cache['smooth_curves_generated'] = True
805
+
806
+ if self.catch:
807
+ self.params.update({
808
+ 'eldf_points': self.eldf_points.copy(),
809
+ 'pdf_points': self.pdf_points.copy(),
810
+ 'zi_points': self.zi_n.copy()
811
+ })
812
+
813
+ self.logger.info(f"Generated smooth curves with {self.n_points} points.")
814
+
815
+ except Exception as e:
816
+ # log error
817
+ error_msg = f"Smooth curve generation failed: {e}"
818
+ self.logger.error(error_msg)
819
+ self.params['errors'].append({
820
+ 'method': '_generate_smooth_curves_varS',
821
+ 'error': error_msg,
822
+ 'exception_type': type(e).__name__
823
+ })
824
+
825
+ self.logger.warning(f"Could not generate smooth curves: {e}")
826
+
827
+ # Create fallback points using original data
828
+ self.eldf_points = self.eldf.copy() if hasattr(self, 'eldf') else None
829
+ self.pdf_points = self.pdf.copy() if hasattr(self, 'pdf') else None
830
+ self._computation_cache['smooth_curves_generated'] = False