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.
- __init__.py +0 -0
- machinegnostics/__init__.py +24 -0
- machinegnostics/magcal/__init__.py +37 -0
- machinegnostics/magcal/characteristics.py +460 -0
- machinegnostics/magcal/criteria_eval.py +268 -0
- machinegnostics/magcal/criterion.py +140 -0
- machinegnostics/magcal/data_conversion.py +381 -0
- machinegnostics/magcal/gcor.py +64 -0
- machinegnostics/magcal/gdf/__init__.py +2 -0
- machinegnostics/magcal/gdf/base_df.py +39 -0
- machinegnostics/magcal/gdf/base_distfunc.py +1202 -0
- machinegnostics/magcal/gdf/base_egdf.py +823 -0
- machinegnostics/magcal/gdf/base_eldf.py +830 -0
- machinegnostics/magcal/gdf/base_qgdf.py +1234 -0
- machinegnostics/magcal/gdf/base_qldf.py +1019 -0
- machinegnostics/magcal/gdf/cluster_analysis.py +456 -0
- machinegnostics/magcal/gdf/data_cluster.py +975 -0
- machinegnostics/magcal/gdf/data_intervals.py +853 -0
- machinegnostics/magcal/gdf/data_membership.py +536 -0
- machinegnostics/magcal/gdf/der_egdf.py +243 -0
- machinegnostics/magcal/gdf/distfunc_engine.py +841 -0
- machinegnostics/magcal/gdf/egdf.py +324 -0
- machinegnostics/magcal/gdf/eldf.py +297 -0
- machinegnostics/magcal/gdf/eldf_intv.py +609 -0
- machinegnostics/magcal/gdf/eldf_ma.py +627 -0
- machinegnostics/magcal/gdf/homogeneity.py +1218 -0
- machinegnostics/magcal/gdf/intv_engine.py +1523 -0
- machinegnostics/magcal/gdf/marginal_intv_analysis.py +558 -0
- machinegnostics/magcal/gdf/qgdf.py +289 -0
- machinegnostics/magcal/gdf/qldf.py +296 -0
- machinegnostics/magcal/gdf/scedasticity.py +197 -0
- machinegnostics/magcal/gdf/wedf.py +181 -0
- machinegnostics/magcal/gdf/z0_estimator.py +1047 -0
- machinegnostics/magcal/layer_base.py +42 -0
- machinegnostics/magcal/layer_history_base.py +74 -0
- machinegnostics/magcal/layer_io_process_base.py +238 -0
- machinegnostics/magcal/layer_param_base.py +448 -0
- machinegnostics/magcal/mg_weights.py +36 -0
- machinegnostics/magcal/sample_characteristics.py +532 -0
- machinegnostics/magcal/scale_optimization.py +185 -0
- machinegnostics/magcal/scale_param.py +313 -0
- machinegnostics/magcal/util/__init__.py +0 -0
- machinegnostics/magcal/util/dis_docstring.py +18 -0
- machinegnostics/magcal/util/logging.py +24 -0
- machinegnostics/magcal/util/min_max_float.py +34 -0
- machinegnostics/magnet/__init__.py +0 -0
- machinegnostics/metrics/__init__.py +28 -0
- machinegnostics/metrics/accu.py +61 -0
- machinegnostics/metrics/accuracy.py +67 -0
- machinegnostics/metrics/auto_correlation.py +183 -0
- machinegnostics/metrics/auto_covariance.py +204 -0
- machinegnostics/metrics/cls_report.py +130 -0
- machinegnostics/metrics/conf_matrix.py +93 -0
- machinegnostics/metrics/correlation.py +178 -0
- machinegnostics/metrics/cross_variance.py +167 -0
- machinegnostics/metrics/divi.py +82 -0
- machinegnostics/metrics/evalmet.py +109 -0
- machinegnostics/metrics/f1_score.py +128 -0
- machinegnostics/metrics/gmmfe.py +108 -0
- machinegnostics/metrics/hc.py +141 -0
- machinegnostics/metrics/mae.py +72 -0
- machinegnostics/metrics/mean.py +117 -0
- machinegnostics/metrics/median.py +122 -0
- machinegnostics/metrics/mg_r2.py +167 -0
- machinegnostics/metrics/mse.py +78 -0
- machinegnostics/metrics/precision.py +119 -0
- machinegnostics/metrics/r2.py +122 -0
- machinegnostics/metrics/recall.py +108 -0
- machinegnostics/metrics/rmse.py +77 -0
- machinegnostics/metrics/robr2.py +119 -0
- machinegnostics/metrics/std.py +144 -0
- machinegnostics/metrics/variance.py +101 -0
- machinegnostics/models/__init__.py +2 -0
- machinegnostics/models/classification/__init__.py +1 -0
- machinegnostics/models/classification/layer_history_log_reg.py +121 -0
- machinegnostics/models/classification/layer_io_process_log_reg.py +98 -0
- machinegnostics/models/classification/layer_mlflow_log_reg.py +107 -0
- machinegnostics/models/classification/layer_param_log_reg.py +275 -0
- machinegnostics/models/classification/mg_log_reg.py +273 -0
- machinegnostics/models/cross_validation.py +118 -0
- machinegnostics/models/data_split.py +106 -0
- machinegnostics/models/regression/__init__.py +2 -0
- machinegnostics/models/regression/layer_histroy_rob_reg.py +139 -0
- machinegnostics/models/regression/layer_io_process_rob_rig.py +88 -0
- machinegnostics/models/regression/layer_mlflow_rob_reg.py +134 -0
- machinegnostics/models/regression/layer_param_rob_reg.py +212 -0
- machinegnostics/models/regression/mg_lin_reg.py +253 -0
- machinegnostics/models/regression/mg_poly_reg.py +258 -0
- machinegnostics-0.0.1.dist-info/METADATA +246 -0
- machinegnostics-0.0.1.dist-info/RECORD +93 -0
- machinegnostics-0.0.1.dist-info/WHEEL +5 -0
- machinegnostics-0.0.1.dist-info/licenses/LICENSE +674 -0
- 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
|