diff options
| author | Sam Scholten | 2025-10-28 12:48:42 +1000 |
|---|---|---|
| committer | Sam Scholten | 2025-10-28 12:48:42 +1000 |
| commit | a6770b50fcbc49bbbdd25ed4b7511fdd1498e864 (patch) | |
| tree | 046f943665eb15fbccad65c35a95eada961d7ba1 | |
| parent | 20320a87d98c8517869f5e7c0fcd66bfdc090713 (diff) | |
| download | transivent-a6770b50fcbc49bbbdd25ed4b7511fdd1498e864.tar.gz transivent-a6770b50fcbc49bbbdd25ed4b7511fdd1498e864.zip | |
Remove time array tolerance check
- Simplified monotonic check to just require positive differences
- Removed machine epsilon tolerance that was too strict for ns-scale timing
- Increased marker size in diffusion scatter plots
- Added version bump to 2.1.0
- Fixed edge cases in ACF calculation and diffusion processing
| -rw-r--r-- | src/transivent/__init__.py | 2 | ||||
| -rw-r--r-- | src/transivent/event_processor.py | 58 | ||||
| -rw-r--r-- | src/transivent/utils.py | 16 |
3 files changed, 52 insertions, 24 deletions
diff --git a/src/transivent/__init__.py b/src/transivent/__init__.py index 558b2a3..1f270ea 100644 --- a/src/transivent/__init__.py +++ b/src/transivent/__init__.py @@ -13,6 +13,8 @@ For advanced usage, building blocks are available in submodules: - transivent.diffusion: Diffusion analysis tools (optional) """ +__version__ = "2.1.0" + from .analysis import detect, detect_from_wfm from .event_detector import detect_events, merge_overlapping_events from .event_plotter import EventPlotter diff --git a/src/transivent/event_processor.py b/src/transivent/event_processor.py index 734fae8..be89513 100644 --- a/src/transivent/event_processor.py +++ b/src/transivent/event_processor.py @@ -182,12 +182,22 @@ def calculate_acf( x = np.asarray(x, dtype=np.float64) n = x.size + # Ensure max_lag is not larger than waveform length + if max_lag >= n: + max_lag = n - 1 + if max_lag <= 0: + # Return zero values for empty or single-point data + return np.array([0.0]), np.array([0.0]) + # Remove mean for proper autocorrelation calculation x = x - x.mean() acf = np.zeros(max_lag + 1) for lag in range(max_lag + 1): - acf[lag] = np.dot(x[:n - lag], x[lag:]) / (n - lag) + if lag >= n or (n - lag) == 0: + acf[lag] = 0.0 + else: + acf[lag] = np.dot(x[:n - lag], x[lag:]) / (n - lag) lags = np.arange(len(acf)) * dt return lags, acf @@ -273,8 +283,12 @@ def calculate_diffusion_statistics( - 'eigenvectors': Eigenvectors of covariance matrix """ # Convert to log space for statistical calculations (as in original script) - log_D = np.log10(diffusion_coeffs) - log_acf = np.log10(acf_values) + # Filter out non-positive values before log10 + valid_positive = (diffusion_coeffs > 0) & (acf_values > 0) + log_D = np.full_like(diffusion_coeffs, np.nan) + log_acf = np.full_like(acf_values, np.nan) + log_D[valid_positive] = np.log10(diffusion_coeffs[valid_positive]) + log_acf[valid_positive] = np.log10(acf_values[valid_positive]) # Remove any invalid values valid_mask = np.isfinite(log_D) & np.isfinite(log_acf) @@ -378,7 +392,7 @@ def create_diffusion_scatter( label: Optional[str] = None, color: Optional[str] = None, marker: str = "o", - size: int = 8, + size: int = 60, ax: Optional[plt.Axes] = None, show_stats: bool = True, **plot_kwargs, @@ -398,7 +412,7 @@ def create_diffusion_scatter( Color for the scatter plot. marker : str, default="o" Marker style. - size : int, default=8 + size : int, default=60 Marker size. ax : plt.Axes, optional Matplotlib axes to plot on. If None, creates new figure. @@ -415,9 +429,9 @@ def create_diffusion_scatter( if ax is None: fig, ax = plt.subplots(figsize=(7, 6)) - # Convert to appropriate units for display (m²/s from pm²/s) - D_display = diffusion_coeffs * 1e24 # pm²/s to m²/s - acf_display = acf_values * 1e24 # pm²/s to m²/s + # The values are already in the correct units (m²/s), just use them directly + D_display = diffusion_coeffs + acf_display = acf_values # Plot scatter ax.scatter( @@ -434,8 +448,15 @@ def create_diffusion_scatter( if show_stats and label is not None and len(D_display) > 1: stats = calculate_diffusion_statistics(diffusion_coeffs, acf_values) - # Create confidence ellipse in log space - center = (np.mean(np.log10(D_display)), np.mean(np.log10(acf_display))) + # Create confidence ellipse in log space - filter non-positive values + D_log = np.log10(D_display[D_display > 0]) if np.any(D_display > 0) else np.array([]) + acf_log = np.log10(acf_display[acf_display > 0]) if np.any(acf_display > 0) else np.array([]) + + if len(D_log) > 0 and len(acf_log) > 0: + center = (np.mean(D_log), np.mean(acf_log)) + else: + # Fall back to linear space if no valid log values + center = (np.mean(D_display), np.mean(acf_display)) ellipse = create_confidence_ellipse( center=center, cov_matrix=stats["covariance_matrix"], @@ -628,9 +649,20 @@ def process_events_for_diffusion( D = fit_diffusion_linear(taus, msds, time_limit=fit_time_limit) diffusion_coeffs.append(D) - # ACF calculation - lags, acf = calculate_acf(wf, dt=sampling_interval, max_lag=max_lag) - acf_values.append(acf[0]) + # ACF calculation - ensure max_lag is not larger than waveform + actual_max_lag = min(max_lag, len(wf) - 1) + if actual_max_lag > 0: + try: + lags, acf = calculate_acf(wf, dt=sampling_interval, max_lag=actual_max_lag) + if len(acf) > 0: + acf_values.append(acf[0]) + else: + acf_values.append(0.0) + except Exception as e: + print(f"Warning: ACF calculation failed: {e}") + acf_values.append(0.0) + else: + acf_values.append(0.0) # Convert to arrays diffusion_coeffs = np.array(diffusion_coeffs) diff --git a/src/transivent/utils.py b/src/transivent/utils.py index 77dda73..96874c5 100644 --- a/src/transivent/utils.py +++ b/src/transivent/utils.py @@ -93,18 +93,12 @@ def validate_detection_inputs( "Validation Warning: Input arrays are empty. This may lead to unexpected behaviour." ) - # Check time monotonicity with a small tolerance for floating-point comparisons + # Check that time is monotonic if len(time) > 1: - # Use a small epsilon for floating-point comparison - # np.finfo(time.dtype).eps is the smallest representable positive number such that 1.0 + eps != 1.0 - # Multiplying by a small factor (e.g., 10) provides a reasonable tolerance. - tolerance = np.finfo(time.dtype).eps * 10 - if not np.all(np.diff(time) > tolerance): - # Log the problematic differences for debugging - problematic_diffs = np.diff(time)[np.diff(time) <= tolerance] - warnings.warn( - f"Validation Warning: Time array is not strictly monotonic increasing within tolerance {tolerance}. " - f"Problematic diffs (first 10): {problematic_diffs[:10]}. This may lead to unexpected behaviour." + if not np.all(np.diff(time) > 0): + raise ValueError( + f"Time array is not monotonic increasing. " + f"This indicates a bug in time array generation." ) # Check parameter validity |
