aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSam Scholten2025-10-28 12:48:42 +1000
committerSam Scholten2025-10-28 12:48:42 +1000
commita6770b50fcbc49bbbdd25ed4b7511fdd1498e864 (patch)
tree046f943665eb15fbccad65c35a95eada961d7ba1
parent20320a87d98c8517869f5e7c0fcd66bfdc090713 (diff)
downloadtransivent-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__.py2
-rw-r--r--src/transivent/event_processor.py58
-rw-r--r--src/transivent/utils.py16
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