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,1019 @@
1
+ '''
2
+ base QLDF class
3
+ Quantifying Local Distribution Functions
4
+
5
+ Author: Nirmal Parmar
6
+ Machine Gnostics
7
+ '''
8
+
9
+ import numpy as np
10
+ import warnings
11
+ from scipy.optimize import minimize
12
+ from typing import Dict, Any
13
+ import logging
14
+ from machinegnostics.magcal.util.logging import get_logger
15
+ from machinegnostics.magcal.characteristics import GnosticsCharacteristics
16
+ from machinegnostics.magcal.data_conversion import DataConversion
17
+ from machinegnostics.magcal.gdf.base_qgdf import BaseQGDF
18
+ from machinegnostics.magcal.scale_param import ScaleParam
19
+ from machinegnostics.magcal.gdf.z0_estimator import Z0Estimator
20
+
21
+ class BaseQLDF(BaseQGDF):
22
+ '''Base QLDF 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 # QLDF specific
69
+ self.z0_optimize = z0_optimize # QLDF 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 setup
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 quantifying local distribution estimation. Consider using in range [0, 2]")
97
+ warnings.warn("S is greater than 2, which may not suitable for quantifying local distribution estimation. Consider using in range [0, 2]", UserWarning)
98
+
99
+
100
+
101
+ def _fit_qldf(self, plot: bool = True):
102
+ """Fit the QLDF model to the data."""
103
+ self.logger.debug("Starting QLDF 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("Calculating 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(egdf=False) # QLDF does not use egdf
124
+
125
+ # Step 5: Calculate final QLDF and PDF
126
+ self.logger.info("Computing final QLDF and PDF...")
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
+ self.logger.info("Transforming bounds back to original domain...")
135
+ self._transform_bounds_to_original_domain()
136
+ # Mark as fitted (Step 8 is now optional via marginal_analysis())
137
+ self._fitted = True
138
+
139
+ # Step 8: Z0 estimate with Z0Estimator
140
+ self.logger.info("Estimating Z0...")
141
+ self._compute_z0(optimize=self.z0_optimize)
142
+ # derivatives
143
+ # self._calculate_all_derivatives()
144
+
145
+ # # Step 9: varS
146
+ if self.varS:
147
+ self.logger.info("Calculating variable S parameter...")
148
+ self._varS_calculation()
149
+ self.logger.info("Recomputing final results with variable S...")
150
+ self._compute_final_results_varS()
151
+ self.logger.info("Generating smooth curves with variable S...")
152
+ self._generate_smooth_curves_varS()
153
+
154
+ # Step 10: Z0 re-estimate with varS if enabled
155
+ if self.varS:
156
+ self.logger.info("Re-estimating Z0 with variable S...")
157
+ self._compute_z0(optimize=self.z0_optimize)
158
+
159
+ self.logger.info("QLDF fitting completed successfully.")
160
+
161
+ if plot:
162
+ self.logger.info("Plotting results...")
163
+ self._plot()
164
+
165
+ # clean up computation cache
166
+ if self.flush:
167
+ self.logger.info("Flushing computation cache to free memory...")
168
+ self._cleanup_computation_cache()
169
+
170
+ except Exception as e:
171
+ # log error
172
+ error_msg = f"QLDF fitting failed: {e}"
173
+ self.logger.error(error_msg)
174
+ self.params['errors'].append({
175
+ 'method': '_fit_qldf',
176
+ 'error': error_msg,
177
+ 'exception_type': type(e).__name__
178
+ })
179
+ self.logger.info(f"Error during QLDF fitting: {e}")
180
+ raise e
181
+
182
+
183
+ def _compute_qldf_core(self, S, LB, UB, zi_data=None, zi_eval=None):
184
+ """Core computation for the QLDF model."""
185
+ self.logger.debug("Computing core QLDF values...")
186
+
187
+ # Use provided data or default to instance data
188
+ if zi_data is None:
189
+ zi_data = self.z
190
+ if zi_eval is None:
191
+ zi_eval = zi_data
192
+
193
+ # Convert to infinite domain
194
+ zi_n = DataConversion._convert_fininf(zi_eval, LB, UB)
195
+ zi_d = DataConversion._convert_fininf(zi_data, LB, UB)
196
+
197
+ # Calculate R matrix with numerical stability
198
+ R = zi_n.reshape(-1, 1) / (zi_d.reshape(1, -1) + self._NUMERICAL_EPS)
199
+
200
+ # Get characteristics
201
+ gc = GnosticsCharacteristics(R=R, verbose=self.verbose)
202
+ q, q1 = gc._get_q_q1(S=S)
203
+
204
+ # Calculate quantifying fidelities and irrelevances
205
+ fj = gc._fj(q=q, q1=q1) # quantifying fidelities
206
+ hj = gc._hj(q=q, q1=q1) # quantifying irrelevances
207
+ return self._estimate_qldf_from_moments(fj, hj), fj, hj
208
+
209
+ def _estimate_qldf_from_moments(self, fidelity, irrelevance):
210
+ """Estimate the QLDF from moments using equation (15.33): QLDF = (1 - h_QL)/2."""
211
+ self.logger.debug("Estimating QLDF from moments...")
212
+
213
+ weights = self.weights.reshape(-1, 1)
214
+
215
+ # Calculate weighted mean of quantifying irrelevances (h_QL)
216
+ mean_irrelevance = np.sum(weights * irrelevance, axis=0) / np.sum(weights)
217
+ # hQL
218
+ hQL = mean_irrelevance / (np.sqrt(1 + mean_irrelevance**2) + self._NUMERICAL_EPS)
219
+ # Apply equation (15.33): QLDF = (1 - h_QL)/2
220
+ qldf_values = (1 - hQL) / 2
221
+
222
+ return qldf_values.flatten()
223
+
224
+ def _compute_final_results(self):
225
+ """Compute the final results for the QLDF model."""
226
+ self.logger.debug("Computing final QLDF results...")
227
+
228
+ # Convert data to infinite domain
229
+ zi_d = DataConversion._convert_fininf(self.z, self.LB_opt, self.UB_opt)
230
+ self.zi = zi_d
231
+
232
+ # Calculate QLDF and get moments
233
+ qldf_values, fj, hj = self._compute_qldf_core(self.S_opt, self.LB_opt, self.UB_opt)
234
+
235
+ # Store for derivative calculations
236
+ self.fj = fj # quantifying fidelities
237
+ self.hj = hj # quantifying irrelevances
238
+
239
+ # # varS - Variable S parameter
240
+ # if self.varS:
241
+ # fj_m = np.sum(self.fj * self.weights, axis=0) / np.sum(self.weights)
242
+ # scale = ScaleParam()
243
+ # self.S_var = np.abs(scale._gscale_loc(fj_m) * self.S_opt) # NOTE fi or fj?
244
+ # # cap value for minimum S_var array
245
+ # self.S_var = np.maximum(self.S_var, 0.1)
246
+ # qldf_values, fj, hj = self._compute_qldf_core(self.S_var, self.LB_opt, self.UB_opt)
247
+ # self.fj = fj
248
+ # self.hj = hj
249
+
250
+ self.qldf = qldf_values
251
+ self.pdf = self._compute_qldf_pdf(self.fj, self.hj)
252
+
253
+ if self.catch:
254
+ self.params.update({
255
+ 'qldf': self.qldf.copy(),
256
+ 'pdf': self.pdf.copy(),
257
+ 'zi': self.zi.copy(),
258
+ # 'S_var': self.S_var.copy() if self.varS else None
259
+ })
260
+
261
+ def _compute_qldf_pdf(self, fj, hj):
262
+ """Compute the PDF for the QLDF model using equation (15.34): dQL/dZ₀ = (1/SZ₀) * f̄Q/((1 + (h̄Q)²)^(3/2))."""
263
+ self.logger.debug("Computing PDF for QLDF...")
264
+
265
+ weights = self.weights.reshape(-1, 1)
266
+
267
+ # Calculate weighted means of quantifying fidelities and irrelevances
268
+ fQ_mean = np.sum(weights * fj, axis=0) / np.sum(weights) # f̄Q
269
+ hQ_mean = np.sum(weights * hj, axis=0) / np.sum(weights) # h̄Q
270
+
271
+ # hQL
272
+ hQL = hQ_mean / (np.sqrt(1 + hQ_mean**2) + self._NUMERICAL_EPS)
273
+
274
+ # Apply equation (15.34): dQL/dZ₀ = (1/SZ₀) * f̄Q/((1 + (h̄Q)²)^(3/2))
275
+ # Note: We use S instead of SZ₀ for the scaling factor
276
+ denominator = (1 + hQL**2)**(3/2)
277
+
278
+ # Handle division by zero
279
+ eps = np.finfo(float).eps
280
+ denominator = np.where(denominator == 0, eps, denominator)
281
+
282
+ pdf_values = (1 / self.S_opt) * fQ_mean / denominator
283
+
284
+ return pdf_values.flatten()
285
+
286
+ def _generate_smooth_curves(self):
287
+ """Generate smooth curves for plotting and analysis - QLDF."""
288
+ self.logger.debug("Generating smooth curves for QLDF...")
289
+
290
+ try:
291
+ if self.verbose and not self.varS:
292
+ self.logger.info("Generating smooth curves without varying S...")
293
+
294
+ smooth_qldf, self.smooth_fj, self.smooth_hj = self._compute_qldf_core(
295
+ self.S_opt, self.LB_opt, self.UB_opt,
296
+ zi_data=self.z_points_n, zi_eval=self.z
297
+ )
298
+ smooth_pdf = self._compute_qldf_pdf(self.smooth_fj, self.smooth_hj)
299
+
300
+ self.qldf_points = smooth_qldf
301
+ self.pdf_points = smooth_pdf
302
+
303
+ # Store zi_n for derivative calculations
304
+ self.zi_n = DataConversion._convert_fininf(self.z_points_n, self.LB_opt, self.UB_opt)
305
+
306
+ # Mark as generated
307
+ self._computation_cache['smooth_curves_generated'] = True
308
+
309
+ if self.catch:
310
+ self.params.update({
311
+ 'qldf_points': self.qldf_points.copy(),
312
+ 'pdf_points': self.pdf_points.copy(),
313
+ 'zi_points': self.zi_n.copy()
314
+ })
315
+
316
+ if self.verbose and not self.varS:
317
+ self.logger.info(f"Generated smooth curves with {self.n_points} points.")
318
+
319
+ except Exception as e:
320
+ # log error
321
+ error_msg = f"Smooth curve generation failed: {e}"
322
+ self.logger.error(error_msg)
323
+ self.params['errors'].append({
324
+ 'method': '_generate_smooth_curves',
325
+ 'error': error_msg,
326
+ 'exception_type': type(e).__name__
327
+ })
328
+
329
+ self.logger.warning(f"Warning: Could not generate smooth curves: {e}")
330
+
331
+ # Create fallback points using original data
332
+ self.qldf_points = self.qldf.copy() if hasattr(self, 'qldf') else None
333
+ self.pdf_points = self.pdf.copy() if hasattr(self, 'pdf') else None
334
+ self._computation_cache['smooth_curves_generated'] = False
335
+
336
+
337
+ def _plot(self, plot_smooth: bool = True, plot: str = 'both', bounds: bool = True, extra_df: bool = True, figsize: tuple = (12, 8)):
338
+ """Enhanced plotting with better organization."""
339
+ self.logger.info("Plotting QLDF results...")
340
+
341
+ import matplotlib.pyplot as plt
342
+
343
+ if plot_smooth and (len(self.data) > self.max_data_size) and self.verbose:
344
+ self.logger.info(f"Warning: 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.")
345
+
346
+ if not self.catch:
347
+ self.logger.info("Plot is not available with argument catch=False")
348
+ return
349
+
350
+ if not self._fitted:
351
+ self.logger.info("QLDF is not fitted yet.")
352
+ raise RuntimeError("Must fit QLDF before plotting.")
353
+
354
+ # Validate plot parameter
355
+ if plot not in ['gdf', 'pdf', 'both']:
356
+ self.logger.error("Invalid plot parameter. Must be 'gdf', 'pdf', or 'both'.")
357
+ raise ValueError("plot parameter must be 'gdf', 'pdf', or 'both'")
358
+
359
+ # Check data availability
360
+ if plot in ['gdf', 'both'] and self.params.get('qldf') is None:
361
+ self.logger.error("QLDF must be calculated before plotting GDF")
362
+ raise ValueError("QLDF must be calculated before plotting GDF")
363
+ if plot in ['pdf', 'both'] and self.params.get('pdf') is None:
364
+ self.logger.error("PDF must be calculated before plotting PDF")
365
+ raise ValueError("PDF must be calculated before plotting PDF")
366
+
367
+ # Prepare data
368
+ x_points = self.data
369
+ qldf_plot = self.params.get('qldf')
370
+ pdf_plot = self.params.get('pdf')
371
+ wedf = self.params.get('wedf')
372
+ ksdf = self.params.get('ksdf')
373
+
374
+ # Check smooth plotting availability
375
+ has_smooth = (hasattr(self, 'z_points_n') and hasattr(self, 'qldf_points')
376
+ and hasattr(self, 'pdf_points') and self.z_points_n is not None
377
+ and self.qldf_points is not None and self.pdf_points is not None)
378
+ plot_smooth = plot_smooth and has_smooth
379
+
380
+ # Create figure
381
+ fig, ax1 = plt.subplots(figsize=figsize)
382
+
383
+ # Plot QLDF if requested
384
+ if plot in ['gdf', 'both']:
385
+ self._plot_qldf(ax1, x_points, qldf_plot, plot_smooth, extra_df, wedf, ksdf)
386
+
387
+ # Plot PDF if requested
388
+ if plot in ['pdf', 'both']:
389
+ if plot == 'pdf':
390
+ self._plot_pdf(ax1, x_points, pdf_plot, plot_smooth, is_secondary=False)
391
+ else:
392
+ ax2 = ax1.twinx()
393
+ self._plot_pdf(ax2, x_points, pdf_plot, plot_smooth, is_secondary=True)
394
+
395
+ # Add bounds and formatting
396
+ self._add_plot_formatting(ax1, plot, bounds)
397
+
398
+ # Add Z0 vertical line if available
399
+ if hasattr(self, 'z0') and self.z0 is not None:
400
+ ax1.axvline(x=self.z0, color='magenta', linestyle='-.', linewidth=1,
401
+ alpha=0.8, label=f'Z0={self.z0:.3f}')
402
+ # Update legend to include Z0
403
+ ax1.legend(loc='upper left', bbox_to_anchor=(0, 1))
404
+
405
+ plt.tight_layout()
406
+ plt.show()
407
+
408
+ def _plot_pdf(self, ax, x_points, pdf_plot, plot_smooth, is_secondary=False):
409
+ """Plot PDF components."""
410
+ self.logger.debug("Plotting PDF...")
411
+ import numpy as np # Add numpy import
412
+ color = 'red'
413
+
414
+ if plot_smooth and hasattr(self, 'pdf_points') and self.pdf_points is not None:
415
+ ax.plot(x_points, pdf_plot, 'o', color=color, label='PDF', markersize=4)
416
+ ax.plot(self.di_points_n, self.pdf_points, color=color,
417
+ linestyle='-', linewidth=2, alpha=0.8)
418
+ max_pdf = np.max(self.pdf_points)
419
+ else:
420
+ ax.plot(x_points, pdf_plot, 'o-', color=color, label='PDF',
421
+ markersize=4, linewidth=1, alpha=0.8)
422
+ max_pdf = np.max(pdf_plot)
423
+
424
+ ax.set_ylabel('PDF', color=color)
425
+ ax.tick_params(axis='y', labelcolor=color)
426
+ ax.set_ylim(0, max_pdf * 1.1)
427
+
428
+ if is_secondary:
429
+ ax.legend(loc='upper right', bbox_to_anchor=(1, 1))
430
+
431
+ def _plot_qldf(self, ax, x_points, qldf_plot, plot_smooth, extra_df, wedf, ksdf):
432
+ """Plot QLDF components."""
433
+ self.logger.debug("Plotting QLDF...")
434
+ if plot_smooth and hasattr(self, 'qldf_points') and self.qldf_points is not None:
435
+ ax.plot(x_points, qldf_plot, 'o', color='blue', label='QLDF', markersize=4)
436
+ ax.plot(self.di_points_n, self.qldf_points, color='blue',
437
+ linestyle='-', linewidth=2, alpha=0.8)
438
+ else:
439
+ ax.plot(x_points, qldf_plot, 'o-', color='blue', label='QLDF',
440
+ markersize=4, linewidth=1, alpha=0.8)
441
+
442
+ if extra_df:
443
+ if wedf is not None:
444
+ ax.plot(x_points, wedf, 's', color='lightblue',
445
+ label='WEDF', markersize=3, alpha=0.8)
446
+ if ksdf is not None:
447
+ ax.plot(x_points, ksdf, 's', color='cyan',
448
+ label='KS Points', markersize=3, alpha=0.8)
449
+
450
+ ax.set_ylabel('QLDF', color='blue')
451
+ ax.tick_params(axis='y', labelcolor='blue')
452
+ ax.set_ylim(0, 1)
453
+
454
+ def _add_plot_formatting(self, ax1, plot, bounds):
455
+ """Add formatting, bounds, and legends to plot."""
456
+ self.logger.debug("Adding plot formatting...")
457
+ ax1.set_xlabel('Data Points')
458
+
459
+ # Add bounds if requested
460
+ if bounds:
461
+ bound_info = [
462
+ (self.params.get('DLB'), 'green', '-', 'DLB'),
463
+ (self.params.get('DUB'), 'orange', '-', 'DUB'),
464
+ (self.params.get('LB'), 'purple', '--', 'LB'),
465
+ (self.params.get('UB'), 'brown', '--', 'UB')
466
+ ]
467
+
468
+ for bound, color, style, name in bound_info:
469
+ if bound is not None:
470
+ ax1.axvline(x=bound, color=color, linestyle=style, linewidth=2,
471
+ alpha=0.8, label=f"{name}={bound:.3f}")
472
+
473
+ # Add shaded regions
474
+ if self.params.get('LB') is not None:
475
+ ax1.axvspan(self.data.min(), self.params['LB'], alpha=0.15, color='purple')
476
+ if self.params.get('UB') is not None:
477
+ ax1.axvspan(self.params['UB'], self.data.max(), alpha=0.15, color='brown')
478
+
479
+ # Set limits and add grid
480
+ data_range = self.params['DUB'] - self.params['DLB']
481
+ padding = data_range * 0.1
482
+ ax1.set_xlim(self.params['DLB'] - padding, self.params['DUB'] + padding)
483
+
484
+ # Set title
485
+ titles = {
486
+ 'gdf': 'QLDF' + (' with Bounds' if bounds else ''),
487
+ 'pdf': 'PDF' + (' with Bounds' if bounds else ''),
488
+ 'both': 'QLDF and PDF' + (' with Bounds' if bounds else '')
489
+ }
490
+
491
+ ax1.set_title(titles[plot])
492
+ ax1.legend(loc='upper left', bbox_to_anchor=(0, 1))
493
+ ax1.grid(True, alpha=0.3)
494
+
495
+ def _get_qldf_second_derivative(self, fj=None, hj=None):
496
+ """
497
+ Calculate second derivative of QLDF using mathematical derivation.
498
+
499
+ Starting from: dQL/dZ₀ = (1/S) * f̄Q/((1 + (h̄Q)²)^(3/2))
500
+
501
+ Second derivative: d²QL/dZ₀² = (1/S) * d/dZ₀[f̄Q/((1 + (h̄Q)²)^(3/2))]
502
+ """
503
+ self.logger.debug("Calculating second derivative of QLDF...")
504
+ if fj is None or hj is None:
505
+ fj = self.fj
506
+ hj = self.hj
507
+
508
+ if fj is None or hj is None:
509
+ self.logger.error("Quantifying fidelities and irrelevances must be calculated before second derivative estimation.")
510
+ raise ValueError("Quantifying fidelities and irrelevances must be calculated before second derivative estimation.")
511
+
512
+ weights = self.weights.reshape(-1, 1)
513
+
514
+ # Calculate weighted means and their derivatives
515
+ fQ_mean = np.sum(weights * fj, axis=0) / np.sum(weights) # f̄Q
516
+ hQ_mean = np.sum(weights * hj, axis=0) / np.sum(weights) # h̄Q
517
+
518
+ # For derivatives, we need: d(f̄Q)/dZ₀ and d(h̄Q)/dZ₀
519
+ # These are approximated by the variance-like terms
520
+ dfQ_dz = np.sum(weights * (fj - fQ_mean) * self.zi.reshape(-1, 1), axis=0) / np.sum(weights)
521
+ dhQ_dz = np.sum(weights * (hj - hQ_mean) * self.zi.reshape(-1, 1), axis=0) / np.sum(weights)
522
+
523
+ # Apply quotient rule: d/dx[u/v] = (v*du - u*dv)/v²
524
+ # where u = f̄Q and v = (1 + (h̄Q)²)^(3/2)
525
+
526
+ u = fQ_mean
527
+ v = (1 + hQ_mean**2)**(3/2)
528
+ du_dz = dfQ_dz
529
+ dv_dz = (3/2) * (1 + hQ_mean**2)**(1/2) * 2 * hQ_mean * dhQ_dz
530
+
531
+ # Second derivative using quotient rule
532
+ second_derivative = (1 / self.S_opt) * (v * du_dz - u * dv_dz) / (v**2)
533
+
534
+ return second_derivative.flatten()
535
+
536
+ def _get_qldf_third_derivative(self, fj=None, hj=None):
537
+ """
538
+ Calculate third derivative of QLDF using mathematical derivation.
539
+
540
+ This involves differentiating the second derivative expression.
541
+ """
542
+ self.logger.debug("Calculating third derivative of QLDF...")
543
+
544
+ if fj is None or hj is None:
545
+ fj = self.fj
546
+ hj = self.hj
547
+
548
+ if fj is None or hj is None:
549
+ self.logger.error("Quantifying fidelities and irrelevances must be calculated before third derivative estimation.")
550
+ raise ValueError("Quantifying fidelities and irrelevances must be calculated before third derivative estimation.")
551
+
552
+ weights = self.weights.reshape(-1, 1)
553
+
554
+ # Calculate weighted means and derivatives
555
+ fQ_mean = np.sum(weights * fj, axis=0) / np.sum(weights)
556
+ hQ_mean = np.sum(weights * hj, axis=0) / np.sum(weights)
557
+
558
+ # First derivatives
559
+ dfQ_dz = np.sum(weights * (fj - fQ_mean) * self.zi.reshape(-1, 1), axis=0) / np.sum(weights)
560
+ dhQ_dz = np.sum(weights * (hj - hQ_mean) * self.zi.reshape(-1, 1), axis=0) / np.sum(weights)
561
+
562
+ # Second derivatives (approximated)
563
+ d2fQ_dz2 = np.sum(weights * (fj - fQ_mean) * (self.zi.reshape(-1, 1))**2, axis=0) / np.sum(weights)
564
+ d2hQ_dz2 = np.sum(weights * (hj - hQ_mean) * (self.zi.reshape(-1, 1))**2, axis=0) / np.sum(weights)
565
+
566
+ # Complex expression for third derivative - this is a simplified approximation
567
+ # Full derivation would be extremely complex
568
+
569
+ # Terms for the third derivative calculation
570
+ term1 = d2fQ_dz2 / (1 + hQ_mean**2)**(3/2)
571
+ term2 = -3 * dfQ_dz * hQ_mean * dhQ_dz / (1 + hQ_mean**2)**(5/2)
572
+ term3 = -3 * fQ_mean * d2hQ_dz2 * hQ_mean / (1 + hQ_mean**2)**(5/2)
573
+ term4 = 15 * fQ_mean * hQ_mean**2 * (dhQ_dz)**2 / (1 + hQ_mean**2)**(7/2)
574
+
575
+ third_derivative = (1 / self.S_opt) * (term1 + term2 + term3 + term4)
576
+
577
+ return third_derivative.flatten()
578
+
579
+ def _get_qldf_fourth_derivative(self, fj=None, hj=None):
580
+ """Calculate fourth derivative of QLDF using numerical differentiation."""
581
+ self.logger.debug("Calculating fourth derivative of QLDF...")
582
+
583
+ if fj is None or hj is None:
584
+ fj = self.fj
585
+ hj = self.hj
586
+
587
+ if fj is None or hj is None:
588
+ self.logger.error("Quantifying fidelities and irrelevances must be calculated before fourth derivative estimation.")
589
+ raise ValueError("Quantifying fidelities and irrelevances must be calculated before fourth derivative estimation.")
590
+
591
+ # For fourth derivative, use numerical differentiation as it's extremely complex
592
+ dz = 1e-7
593
+
594
+ # Get third derivatives at slightly shifted points
595
+ zi_plus = self.zi + dz
596
+ zi_minus = self.zi - dz
597
+
598
+ # Store original zi
599
+ original_zi = self.zi.copy()
600
+
601
+ # Calculate third derivative at zi + dz
602
+ self.zi = zi_plus
603
+ self._calculate_fidelities_irrelevances_at_given_zi(self.zi)
604
+ third_plus = self._get_qldf_third_derivative()
605
+
606
+ # Calculate third derivative at zi - dz
607
+ self.zi = zi_minus
608
+ self._calculate_fidelities_irrelevances_at_given_zi(self.zi)
609
+ third_minus = self._get_qldf_third_derivative()
610
+
611
+ # Restore original zi and recalculate fj, hj
612
+ self.zi = original_zi
613
+ self._calculate_fidelities_irrelevances_at_given_zi(self.zi)
614
+
615
+ # Numerical derivative
616
+ fourth_derivative = (third_plus - third_minus) / (2 * dz) * self.zi
617
+
618
+ return fourth_derivative.flatten()
619
+
620
+ def _get_qldf_derivatives_numerical(self, order=2, h=1e-6):
621
+ """
622
+ Calculate QLDF derivatives using numerical differentiation.
623
+ This is more reliable for higher-order derivatives.
624
+
625
+ Parameters:
626
+ -----------
627
+ order : int
628
+ Order of derivative (2, 3, or 4)
629
+ h : float
630
+ Step size for numerical differentiation
631
+ """
632
+ self.logger.debug(f"Calculating {order}th derivative of QLDF using numerical differentiation...")
633
+ if not hasattr(self, 'pdf_points') or self.pdf_points is None:
634
+ self.logger.error("PDF must be calculated before derivative estimation.")
635
+ raise ValueError("PDF must be calculated before derivative estimation.")
636
+
637
+ from scipy.misc import derivative
638
+
639
+ # Create interpolation function for PDF
640
+ from scipy.interpolate import interp1d
641
+ pdf_interp = interp1d(self.di_points_n, self.pdf_points,
642
+ kind='cubic', bounds_error=False, fill_value=0)
643
+
644
+ # Calculate derivatives at data points
645
+ derivatives = []
646
+ for z_val in self.data:
647
+ if order == 2:
648
+ deriv = derivative(pdf_interp, z_val, dx=h, n=1, order=3)
649
+ elif order == 3:
650
+ deriv = derivative(pdf_interp, z_val, dx=h, n=2, order=5)
651
+ elif order == 4:
652
+ deriv = derivative(pdf_interp, z_val, dx=h, n=3, order=7)
653
+ else:
654
+ raise ValueError("Order must be 2, 3, or 4")
655
+
656
+ derivatives.append(deriv)
657
+
658
+ return np.array(derivatives)
659
+
660
+ def _calculate_fidelities_irrelevances_at_given_zi(self, zi):
661
+ """Helper method to recalculate quantifying fidelities and irrelevances for current zi."""
662
+ self.logger.debug("Recalculating quantifying fidelities and irrelevances for given zi...")
663
+
664
+ # Convert to infinite domain
665
+ zi_n = DataConversion._convert_fininf(self.z, self.LB_opt, self.UB_opt)
666
+ # Use given zi if provided, else use self.zi
667
+ if zi is None:
668
+ zi_d = self.zi
669
+ else:
670
+ zi_d = zi
671
+
672
+ # Calculate R matrix
673
+ eps = np.finfo(float).eps
674
+ R = zi_n.reshape(-1, 1) / (zi_d + eps).reshape(1, -1)
675
+
676
+ # Get characteristics
677
+ gc = GnosticsCharacteristics(R=R, verbose=self.verbose)
678
+ q, q1 = gc._get_q_q1(S=self.S_opt)
679
+
680
+ # Store quantifying fidelities and irrelevances
681
+ self.fj = gc._fj(q=q, q1=q1) # quantifying fidelities
682
+ self.hj = gc._hj(q=q, q1=q1) # quantifying irrelevances
683
+
684
+ def _get_results(self)-> dict:
685
+ """Return fitting results."""
686
+ self.logger.debug("Retrieving QLDF results...")
687
+
688
+ if not self._fitted:
689
+ self.logger.error("QLDF must be fitted before getting results.")
690
+ raise RuntimeError("Must fit QLDF before getting results.")
691
+
692
+ # selected key from params if exists
693
+ keys = ['DLB', 'DUB', 'LB', 'UB', 'S_opt', 'z0', 'qldf', 'pdf',
694
+ 'qldf_points', 'pdf_points', 'zi', 'zi_points', 'weights']
695
+ results = {key: self.params.get(key) for key in keys if key in self.params}
696
+ return results
697
+
698
+ # z0 compute
699
+ def _compute_z0(self, optimize: bool = None):
700
+ """
701
+ Compute the Z0 point where PDF is maximum using the Z0Estimator class.
702
+
703
+ Parameters:
704
+ -----------
705
+ optimize : bool, optional
706
+ If True, use interpolation-based methods for higher accuracy.
707
+ If False, use simple linear search on existing points.
708
+ If None, uses the instance's z0_optimize setting.
709
+ """
710
+ self.logger.debug("Computing Z0 point...")
711
+ if self.z is None:
712
+ self.logger.error("Data must be transformed (self.z) before Z0 estimation.")
713
+ raise ValueError("Data must be transformed (self.z) before Z0 estimation.")
714
+
715
+ # Use provided optimize parameter or fall back to instance setting
716
+ use_optimize = optimize if optimize is not None else self.z0_optimize
717
+
718
+ self.logger.info('QLDF: Computing Z0 point using Z0Estimator...')
719
+
720
+ try:
721
+ # Create Z0Estimator instance with proper constructor signature
722
+ z0_estimator = Z0Estimator(
723
+ gdf_object=self, # Pass the QLDF object itself
724
+ optimize=use_optimize,
725
+ verbose=self.verbose
726
+ )
727
+
728
+ # Call fit() method to estimate Z0
729
+ self.z0 = z0_estimator.fit()
730
+
731
+ # Get estimation info for debugging and storage
732
+ if self.catch:
733
+ estimation_info = z0_estimator.get_estimation_info()
734
+ self.params.update({
735
+ 'z0': float(self.z0) if self.z0 is not None else None,
736
+ 'z0_method': estimation_info.get('z0_method', 'unknown'),
737
+ 'z0_estimation_info': estimation_info
738
+ })
739
+
740
+ method_used = z0_estimator.get_estimation_info().get('z0_method', 'unknown')
741
+ self.logger.info(f'QLDF: Z0 point computed successfully, (method: {method_used})')
742
+
743
+ except Exception as e:
744
+ # Log the error
745
+ error_msg = f"Z0 estimation failed: {str(e)}"
746
+ self.logger.error(error_msg)
747
+ self.params['errors'].append({
748
+ 'method': '_compute_z0',
749
+ 'error': error_msg,
750
+ 'exception_type': type(e).__name__
751
+ })
752
+
753
+ self.logger.warning(f"Warning: Z0Estimator failed with error: {e}")
754
+ self.logger.info("Falling back to simple maximum finding...")
755
+
756
+ # Fallback to simple maximum finding
757
+ self._compute_z0_fallback()
758
+
759
+ if self.catch:
760
+ self.params.update({
761
+ 'z0': float(self.z0),
762
+ 'z0_method': 'fallback_simple_maximum',
763
+ 'z0_estimation_info': {'error': str(e)}
764
+ })
765
+
766
+ def _compute_z0_fallback(self):
767
+ """
768
+ Fallback method for Z0 computation using simple maximum finding.
769
+ """
770
+ self.logger.info("Computing Z0 point using fallback method (simple maximum finding)...")
771
+
772
+ if not hasattr(self, 'di_points_n') or not hasattr(self, 'pdf_points'):
773
+ self.logger.error("Both 'di_points_n' and 'pdf_points' must be defined for Z0 computation.")
774
+ raise ValueError("Both 'di_points_n' and 'pdf_points' must be defined for Z0 computation.")
775
+
776
+ self.logger.info('Using fallback method for Z0 point...')
777
+
778
+ # Find index with maximum PDF
779
+ max_idx = np.argmax(self.pdf_points)
780
+ self.z0 = self.di_points_n[max_idx]
781
+
782
+ self.logger.info(f"Z0 point (fallback method).")
783
+
784
+ def analyze_z0(self, figsize: tuple = (12, 6)) -> Dict[str, Any]:
785
+ """
786
+ Analyze and visualize Z0 estimation results.
787
+
788
+ Parameters:
789
+ -----------
790
+ figsize : tuple
791
+ Figure size for the plot
792
+
793
+ Returns:
794
+ --------
795
+ Dict[str, Any]
796
+ Z0 analysis information
797
+ """
798
+ self.logger.debug("Analyzing Z0 estimation...")
799
+
800
+ if not hasattr(self, 'z0') or self.z0 is None:
801
+ self.logger.error("Z0 must be computed before analysis. Call fit() first.")
802
+ raise ValueError("Z0 must be computed before analysis. Call fit() first.")
803
+
804
+ # Create Z0Estimator for analysis
805
+ z0_estimator = Z0Estimator(
806
+ gdf_object=self,
807
+ optimize=self.z0_optimize,
808
+ verbose=self.verbose
809
+ )
810
+
811
+ # Re-estimate for analysis (this is safe since it's already computed)
812
+ z0_estimator.fit()
813
+
814
+ # Get detailed info
815
+ analysis_info = z0_estimator.get_estimation_info()
816
+
817
+ # Create visualization
818
+ z0_estimator.plot_z0_analysis(figsize=figsize)
819
+
820
+ return analysis_info
821
+
822
+ def _calculate_all_derivatives(self):
823
+ """Calculate all derivatives and store in params."""
824
+ self.logger.debug("Calculating all QLDF derivatives...")
825
+
826
+ if not self._fitted:
827
+ self.logger.error("QLDF must be fitted before calculating derivatives.")
828
+ raise RuntimeError("Must fit QLDF before calculating derivatives.")
829
+
830
+ try:
831
+ # Calculate derivatives using analytical methods
832
+ second_deriv = self._get_qldf_second_derivative()
833
+ third_deriv = self._get_qldf_third_derivative()
834
+ fourth_deriv = self._get_qldf_fourth_derivative()
835
+
836
+ # Store in params
837
+ if self.catch:
838
+ self.params.update({
839
+ 'second_derivative': second_deriv.copy(),
840
+ 'third_derivative': third_deriv.copy(),
841
+ 'fourth_derivative': fourth_deriv.copy()
842
+ })
843
+
844
+ self.logger.info("QLDF derivatives calculated and stored successfully.")
845
+
846
+ except Exception as e:
847
+ # Log error
848
+ error_msg = f"Derivative calculation failed: {e}"
849
+ self.logger.error(error_msg)
850
+ self.params['errors'].append({
851
+ 'method': '_calculate_all_derivatives',
852
+ 'error': error_msg,
853
+ 'exception_type': type(e).__name__
854
+ })
855
+
856
+ self.logger.error(f"Warning: Could not calculate derivatives: {e}")
857
+
858
+ # Fallback to numerical differentiation
859
+ try:
860
+ second_deriv_num = self._get_qldf_derivatives_numerical(order=2)
861
+ third_deriv_num = self._get_qldf_derivatives_numerical(order=3)
862
+ fourth_deriv_num = self._get_qldf_derivatives_numerical(order=4)
863
+
864
+ if self.catch:
865
+ self.params.update({
866
+ 'second_derivative': second_deriv_num.copy(),
867
+ 'third_derivative': third_deriv_num.copy(),
868
+ 'fourth_derivative': fourth_deriv_num.copy()
869
+ })
870
+
871
+ self.logger.info("QLDF derivatives calculated using numerical differentiation and stored successfully.")
872
+
873
+ except Exception as ne:
874
+ # Log numerical differentiation error
875
+ num_error_msg = f"Numerical derivative calculation failed: {ne}"
876
+ self.logger.error(num_error_msg)
877
+ self.params['errors'].append({
878
+ 'method': '_calculate_all_derivatives_numerical',
879
+ 'error': num_error_msg,
880
+ 'exception_type': type(ne).__name__
881
+ })
882
+
883
+ self.logger.warning(f"Warning: Could not calculate numerical derivatives: {ne}")
884
+
885
+ def _estimate_s0_sigma(self, z0, s_local, s_global, mode="sum"):
886
+ """
887
+ Estimate S0 and sigma given z0 (float), s_local (array), and s_global (float).
888
+
889
+ Parameters
890
+ ----------
891
+ z0 : float
892
+ Mean value of the data.
893
+ s_local : array-like
894
+ Local scale parameters.
895
+ s_global : float
896
+ Global scale parameter.
897
+ mode : str
898
+ "mean" -> match average predicted to s_global
899
+ "sum" -> match sum predicted to s_global
900
+
901
+ Returns
902
+ -------
903
+ S0, sigma : floats
904
+ Estimated parameters.
905
+ """
906
+ self.logger.info("Estimating S0 and sigma...")
907
+ s_local = np.asarray(s_local)
908
+
909
+ def objective(params):
910
+ S0, sigma = params
911
+ preds = S0 * np.exp(sigma * z0) * s_local
912
+ if mode == "mean":
913
+ target = preds.mean()
914
+ elif mode == "sum":
915
+ target = preds.sum()
916
+ else:
917
+ raise ValueError("mode must be 'mean' or 'sum'")
918
+ return (s_global - target) ** 2
919
+
920
+ # Initial guess
921
+ p0 = [1.0, 0.0]
922
+
923
+ res = minimize(objective, p0, method="Nelder-Mead")
924
+ return res.x[0], res.x[1]
925
+
926
+ def _varS_calculation(self):
927
+ """Calculate varS if enabled."""
928
+ self.logger.debug("Calculating varS for QLDF...")
929
+
930
+ from machinegnostics import variance
931
+
932
+ self.logger.info("Calculating varS for QLDF...")
933
+ # estimate fi hi at z0
934
+ gc, q, q1 = self._calculate_gcq_at_given_zi(self.z0)
935
+
936
+ fi_z0 = gc._fj(q=q, q1=q1)
937
+
938
+ scale = ScaleParam()
939
+ self.S_local = scale._gscale_loc(fi_z0)
940
+
941
+ self.S_local = np.maximum(self.S_local, 0.1) # cap value for minimum S_local array
942
+
943
+ # # s0 # NOTE for future exploration
944
+ # self.S0, self.sigma = self._estimate_s0_sigma(
945
+ # z0=self.z0,
946
+ # s_local=fi_z0,
947
+ # s_global=self.S_opt,
948
+ # mode="sum"
949
+ # )
950
+
951
+ # Svar
952
+ self.S_var = self.S_local * self.S_opt
953
+ return self.S_var
954
+
955
+ def _compute_final_results_varS(self):
956
+ """Compute the final results for the QLDF model."""
957
+ self.logger.info("Computing final results for QLDF with varS...")
958
+ # Implement final results computation logic here
959
+ # zi_d = DataConversion._convert_fininf(self.z, self.LB_opt, self.UB_opt)
960
+ # self.zi = zi_d
961
+
962
+ qldf_values, fj, hj = self._compute_qldf_core(self.S_var, self.LB_opt, self.UB_opt)
963
+ self.fj = fj
964
+ self.hj = hj
965
+
966
+ self.qldf = qldf_values
967
+ self.pdf = self._compute_qldf_pdf(self.fj, self.hj)
968
+
969
+ if self.catch:
970
+ self.params.update({
971
+ 'qldf': self.qldf.copy(),
972
+ 'pdf': self.pdf.copy(),
973
+ 'zi': self.zi.copy(),
974
+ 'S_var': self.S_var.copy() if self.varS else None
975
+ })
976
+
977
+ def _generate_smooth_curves_varS(self):
978
+ """Generate smooth curves for plotting and analysis - QLDF."""
979
+ self.logger.info("Generating smooth curves for QLDF with varS...")
980
+ try:
981
+ self.logger.info("Generating smooth curves with varying S...")
982
+
983
+ smooth_qldf, self.smooth_fj, self.smooth_hj = self._compute_qldf_core(
984
+ self.S_var, self.LB_opt, self.UB_opt,
985
+ zi_data=self.z_points_n, zi_eval=self.z
986
+ )
987
+ smooth_pdf = self._compute_qldf_pdf(self.smooth_fj, self.smooth_hj)
988
+
989
+ self.qldf_points = smooth_qldf
990
+ self.pdf_points = smooth_pdf
991
+
992
+ # Mark as generated
993
+ self._computation_cache['smooth_curves_generated'] = True
994
+
995
+ if self.catch:
996
+ self.params.update({
997
+ 'qldf_points': self.qldf_points.copy(),
998
+ 'pdf_points': self.pdf_points.copy(),
999
+ 'zi_points': self.zi_n.copy()
1000
+ })
1001
+
1002
+ self.logger.info(f"Generated smooth curves with {self.n_points} points.")
1003
+
1004
+ except Exception as e:
1005
+ # log error
1006
+ error_msg = f"Smooth curve generation failed: {e}"
1007
+ self.logger.error(error_msg)
1008
+ self.params['errors'].append({
1009
+ 'method': '_generate_smooth_curves_varS',
1010
+ 'error': error_msg,
1011
+ 'exception_type': type(e).__name__
1012
+ })
1013
+
1014
+ self.logger.warning(f"Warning: Could not generate smooth curves: {e}")
1015
+
1016
+ # Create fallback points using original data
1017
+ self.qldf_points = self.qldf.copy() if hasattr(self, 'qldf') else None
1018
+ self.pdf_points = self.pdf.copy() if hasattr(self, 'pdf') else None
1019
+ self._computation_cache['smooth_curves_generated'] = False