barsukov 1.3.4__tar.gz → 1.3.7__tar.gz
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.
Potentially problematic release.
This version of barsukov might be problematic. Click here for more details.
- {barsukov-1.3.4/src/barsukov.egg-info → barsukov-1.3.7}/PKG-INFO +9 -3
- {barsukov-1.3.4 → barsukov-1.3.7}/README.md +1 -1
- {barsukov-1.3.4 → barsukov-1.3.7}/pyproject.toml +8 -0
- {barsukov-1.3.4 → barsukov-1.3.7}/src/barsukov/data/Lock_in_emulator.py +1 -9
- {barsukov-1.3.4 → barsukov-1.3.7}/src/barsukov/data/__init__.py +2 -1
- barsukov-1.3.7/src/barsukov/data/color_map_app.py +127 -0
- barsukov-1.3.7/src/barsukov/data/color_map_core.py +283 -0
- barsukov-1.3.7/src/barsukov/data/color_map_widget.py +614 -0
- {barsukov-1.3.4/src/barsukov/app → barsukov-1.3.7/src/barsukov/data}/lock_in_emulator_app.py +26 -22
- {barsukov-1.3.4 → barsukov-1.3.7/src/barsukov.egg-info}/PKG-INFO +9 -3
- {barsukov-1.3.4 → barsukov-1.3.7}/src/barsukov.egg-info/SOURCES.txt +4 -1
- barsukov-1.3.7/src/barsukov.egg-info/requires.txt +8 -0
- barsukov-1.3.4/src/barsukov.egg-info/requires.txt +0 -3
- {barsukov-1.3.4 → barsukov-1.3.7}/.github/workflows/versioning.yml +0 -0
- {barsukov-1.3.4 → barsukov-1.3.7}/.gitignore +0 -0
- {barsukov-1.3.4 → barsukov-1.3.7}/MANIFEST.in +0 -0
- {barsukov-1.3.4 → barsukov-1.3.7}/setup.cfg +0 -0
- {barsukov-1.3.4 → barsukov-1.3.7}/src/barsukov/__init__.py +0 -0
- {barsukov-1.3.4 → barsukov-1.3.7}/src/barsukov/data/Change_phase.py +0 -0
- {barsukov-1.3.4 → barsukov-1.3.7}/src/barsukov/data/constants.py +0 -0
- {barsukov-1.3.4 → barsukov-1.3.7}/src/barsukov/data/fft.py +0 -0
- {barsukov-1.3.4 → barsukov-1.3.7}/src/barsukov/data/noise.py +0 -0
- {barsukov-1.3.4 → barsukov-1.3.7}/src/barsukov/exp/__init__.py +0 -0
- {barsukov-1.3.4 → barsukov-1.3.7}/src/barsukov/exp/exp_utils.py +0 -0
- {barsukov-1.3.4 → barsukov-1.3.7}/src/barsukov/exp/mwHP.py +0 -0
- {barsukov-1.3.4 → barsukov-1.3.7}/src/barsukov/exp/smKE.py +0 -0
- {barsukov-1.3.4 → barsukov-1.3.7}/src/barsukov/logger.py +0 -0
- {barsukov-1.3.4 → barsukov-1.3.7}/src/barsukov/obj2file.py +0 -0
- {barsukov-1.3.4 → barsukov-1.3.7}/src/barsukov/script.py +0 -0
- {barsukov-1.3.4 → barsukov-1.3.7}/src/barsukov/time.py +0 -0
- {barsukov-1.3.4 → barsukov-1.3.7}/src/barsukov.egg-info/dependency_links.txt +0 -0
- {barsukov-1.3.4 → barsukov-1.3.7}/src/barsukov.egg-info/top_level.txt +0 -0
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: barsukov
|
|
3
|
-
Version: 1.3.
|
|
3
|
+
Version: 1.3.7
|
|
4
4
|
Summary: Experiment Automation Package
|
|
5
|
-
Author-email: Igor Barsukov <igorb@ucr.edu
|
|
5
|
+
Author-email: Igor Barsukov <igorb@ucr.edu>
|
|
6
|
+
Maintainer-email: Steven Castaneda <scast206@ucr.edu>, Marlon Lopez <mlope589@ucr.edu>
|
|
6
7
|
Project-URL: Homepage, https://barsukov.ucr.edu
|
|
7
8
|
Classifier: Programming Language :: Python :: 3
|
|
8
9
|
Classifier: Operating System :: OS Independent
|
|
@@ -11,6 +12,11 @@ Description-Content-Type: text/markdown
|
|
|
11
12
|
Requires-Dist: pytz>=2014.10
|
|
12
13
|
Requires-Dist: numpy>=1.0.0
|
|
13
14
|
Requires-Dist: scipy>=0.9.0
|
|
15
|
+
Requires-Dist: matplotlib>=3.8
|
|
16
|
+
Requires-Dist: sympy>=1.2
|
|
17
|
+
Requires-Dist: PyQt5>=5.15
|
|
18
|
+
Requires-Dist: pyqtgraph>=0.13.7
|
|
19
|
+
Requires-Dist: dill>=0.4.0
|
|
14
20
|
|
|
15
21
|
# Barsukov
|
|
16
22
|
|
|
@@ -27,7 +33,7 @@ Use the package manager [pip](https://pip.pypa.io/en/stable/) to install barsuko
|
|
|
27
33
|
```bash
|
|
28
34
|
pip install barsukov
|
|
29
35
|
```
|
|
30
|
-
|
|
36
|
+
Dependencies: pytz, dill, numpy, scipy, matplotlib, pyvisa, IPython, PyQt5, pyqtgraph
|
|
31
37
|
## Usage
|
|
32
38
|
|
|
33
39
|
```python
|
|
@@ -9,10 +9,18 @@ dependencies = [
|
|
|
9
9
|
"pytz>=2014.10",
|
|
10
10
|
"numpy>=1.0.0",
|
|
11
11
|
"scipy>=0.9.0",
|
|
12
|
+
"matplotlib>=3.8",
|
|
13
|
+
"sympy >=1.2",
|
|
14
|
+
"PyQt5 >=5.15",
|
|
15
|
+
"pyqtgraph >=0.13.7",
|
|
16
|
+
"dill >=0.4.0",
|
|
12
17
|
]
|
|
13
18
|
authors = [
|
|
14
19
|
{ name = "Igor Barsukov", email = "igorb@ucr.edu" },
|
|
20
|
+
]
|
|
21
|
+
maintainers = [
|
|
15
22
|
{ name = "Steven Castaneda", email = "scast206@ucr.edu" },
|
|
23
|
+
{ name = "Marlon Lopez", email = "mlope589@ucr.edu"},
|
|
16
24
|
]
|
|
17
25
|
requires-python = ">=3.6"
|
|
18
26
|
description = "Experiment Automation Package"
|
|
@@ -44,16 +44,9 @@ class Lock_in_emulator:
|
|
|
44
44
|
self.x_plot = self.x_arr(self.t_plot)
|
|
45
45
|
|
|
46
46
|
self.original_signal = self.signal_arr(self.x_plot)
|
|
47
|
-
self.
|
|
47
|
+
self.expected_signal = 0.5 * self.x_amp * np.gradient(self.original_signal, self.x_plot)
|
|
48
48
|
self.output_signal = self.signal_output_arr(self.t_plot)
|
|
49
49
|
|
|
50
|
-
#Symbolic feature:
|
|
51
|
-
x, p, w, a = sp.symbols('x p w a')
|
|
52
|
-
f = a / ((x - p)**2 + w**2)
|
|
53
|
-
dfdx = sp.diff(f, x)
|
|
54
|
-
f_prime = sp.lambdify((x, p, w, a), dfdx, 'numpy')
|
|
55
|
-
self.expected_signal = 0.5 * self.x_amp * f_prime(self.x_plot, 2, 1, 1e-6)
|
|
56
|
-
|
|
57
50
|
self.fit()
|
|
58
51
|
#self.plot()
|
|
59
52
|
|
|
@@ -165,7 +158,6 @@ class Lock_in_emulator:
|
|
|
165
158
|
self.axes[0].plot(self.x_plot, self.original_signal, 'r-', label='Original Signal')[0],
|
|
166
159
|
self.axes[1].plot(self.x_plot, self.output_signal, 'b-', label='Demodulated Signal (Lock-In)')[0],
|
|
167
160
|
self.axes[1].plot(self.x_plot, self.expected_signal, 'r-', label='Demodulated Signal (Expected)')[0],
|
|
168
|
-
self.axes[1].plot(self.x_plot, self.expected_signal_num, 'm-', label='Demodulated Signal (Expected Numerical)')[0],
|
|
169
161
|
self.axes[1].plot(self.x_plot, self.adjusted_signal, 'g-', label=f'Demodulated Signal (Adjusted)\n Diminish: {self.diminish}\n Stretch:{self.stretch}\n Shift: {self.shift}')[0],
|
|
170
162
|
]
|
|
171
163
|
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import os
|
|
3
|
+
import argparse
|
|
4
|
+
import numpy as np
|
|
5
|
+
|
|
6
|
+
try:
|
|
7
|
+
from PyQt5 import QtCore, QtWidgets
|
|
8
|
+
except ImportError:
|
|
9
|
+
from PySide6 import QtCore, QtWidgets
|
|
10
|
+
|
|
11
|
+
from color_map_core import (
|
|
12
|
+
DataLoader,
|
|
13
|
+
DEFAULT_XY_FILE,
|
|
14
|
+
DEFAULT_Z_FILE,
|
|
15
|
+
DEFAULT_CMAP
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
from color_map_widget import InteractiveHeatmapWidget
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def build_arg_parser():
|
|
22
|
+
"""Build command line argument parser"""
|
|
23
|
+
p = argparse.ArgumentParser(
|
|
24
|
+
description="Interactive heatmap viewer (CSV/TSV/space-delimited supported)."
|
|
25
|
+
)
|
|
26
|
+
p.add_argument("--xy", dest="xy_file", default=DEFAULT_XY_FILE,
|
|
27
|
+
help="Path to XY file (X,Y vectors; header optional).")
|
|
28
|
+
p.add_argument("--z", dest="z_file", default=DEFAULT_Z_FILE,
|
|
29
|
+
help="Path to Z matrix file (rows form image lines).")
|
|
30
|
+
p.add_argument("--x-col", type=int, default=1,
|
|
31
|
+
help="Zero-based column index to use for X (default: 1).")
|
|
32
|
+
p.add_argument("--y-col", type=int, default=0,
|
|
33
|
+
help="Zero-based column index to use for Y (default: 0).")
|
|
34
|
+
p.add_argument("--cmap", default=DEFAULT_CMAP,
|
|
35
|
+
help="Colormap to use (e.g., viridis, plasma, inferno, magma, cividis, gray, jet).")
|
|
36
|
+
return p
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def main():
|
|
40
|
+
"""Main CLI entry point"""
|
|
41
|
+
# Parse command line arguments
|
|
42
|
+
parser = build_arg_parser()
|
|
43
|
+
args = parser.parse_args()
|
|
44
|
+
|
|
45
|
+
# Load data with data loader
|
|
46
|
+
loader = DataLoader()
|
|
47
|
+
|
|
48
|
+
# Use command line arguments
|
|
49
|
+
xy_file = args.xy_file
|
|
50
|
+
z_file = args.z_file
|
|
51
|
+
x_col = args.x_col
|
|
52
|
+
y_col = args.y_col
|
|
53
|
+
cmap = args.cmap
|
|
54
|
+
|
|
55
|
+
try:
|
|
56
|
+
# Check if file exists and detect columns
|
|
57
|
+
if os.path.exists(xy_file):
|
|
58
|
+
num_cols, col_names = loader.detect_columns(xy_file)
|
|
59
|
+
if num_cols > 0:
|
|
60
|
+
# Load with specified columns
|
|
61
|
+
y, x = loader.load_xy_data(xy_file, x_col=x_col, y_col=y_col)
|
|
62
|
+
# Use column names for labels if available
|
|
63
|
+
if col_names and len(col_names) > max(x_col, y_col):
|
|
64
|
+
x_label = f'X ({col_names[x_col]})'
|
|
65
|
+
y_label = f'Y ({col_names[y_col]})'
|
|
66
|
+
else:
|
|
67
|
+
x_label = 'X'
|
|
68
|
+
y_label = 'Y'
|
|
69
|
+
else:
|
|
70
|
+
raise ValueError("No columns detected")
|
|
71
|
+
else:
|
|
72
|
+
raise FileNotFoundError(f"{xy_file} not found")
|
|
73
|
+
|
|
74
|
+
Z = loader.load_matrix_data(z_file)
|
|
75
|
+
except Exception as e:
|
|
76
|
+
print(f"Error loading files: {e}")
|
|
77
|
+
print("Creating demo data...")
|
|
78
|
+
# Create demo data if files not found
|
|
79
|
+
x = np.linspace(0, 10, 100)
|
|
80
|
+
y = np.linspace(0, 10, 100)
|
|
81
|
+
xx, yy = np.meshgrid(x, y)
|
|
82
|
+
Z = np.sin(xx) * np.cos(yy)
|
|
83
|
+
x_label = 'X'
|
|
84
|
+
y_label = 'Y'
|
|
85
|
+
|
|
86
|
+
# Calculate ranges
|
|
87
|
+
xmin, xmax = float(np.nanmin(x)), float(np.nanmax(x))
|
|
88
|
+
ymin, ymax = float(np.nanmin(y)), float(np.nanmax(y))
|
|
89
|
+
|
|
90
|
+
# Create the app
|
|
91
|
+
app = QtWidgets.QApplication.instance()
|
|
92
|
+
created_app = False
|
|
93
|
+
if app is None:
|
|
94
|
+
app = QtWidgets.QApplication(sys.argv)
|
|
95
|
+
created_app = True
|
|
96
|
+
# Enable high DPI support
|
|
97
|
+
try:
|
|
98
|
+
app.setAttribute(QtCore.Qt.AA_EnableHighDpiScaling, True)
|
|
99
|
+
app.setAttribute(QtCore.Qt.AA_UseHighDpiPixmaps, True)
|
|
100
|
+
except:
|
|
101
|
+
pass # Older Qt version
|
|
102
|
+
|
|
103
|
+
# Initial vmin/vmax from data
|
|
104
|
+
vmin = float(np.nanmin(Z))
|
|
105
|
+
vmax = float(np.nanmax(Z))
|
|
106
|
+
|
|
107
|
+
# Create widget
|
|
108
|
+
win = InteractiveHeatmapWidget(
|
|
109
|
+
data=Z,
|
|
110
|
+
x_range=(xmin, xmax),
|
|
111
|
+
y_range=(ymin, ymax),
|
|
112
|
+
x_label=x_label,
|
|
113
|
+
y_label=y_label,
|
|
114
|
+
cmap=cmap,
|
|
115
|
+
vmin=vmin,
|
|
116
|
+
vmax=vmax
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
win.show()
|
|
120
|
+
|
|
121
|
+
# For CLI, run the event loop
|
|
122
|
+
if created_app:
|
|
123
|
+
sys.exit(app.exec_())
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
if __name__ == "__main__":
|
|
127
|
+
main()
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Core computation and data handling for color map analysis
|
|
3
|
+
"""
|
|
4
|
+
import re
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
# ---------------------------
|
|
8
|
+
# CONSTANTS
|
|
9
|
+
# ---------------------------
|
|
10
|
+
DEFAULT_XY_FILE = "table1.txt"
|
|
11
|
+
DEFAULT_Z_FILE = "table2.txt"
|
|
12
|
+
DEFAULT_KC = -0.01
|
|
13
|
+
DEFAULT_KB = 0.002
|
|
14
|
+
DEFAULT_CMAP = 'viridis'
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# ---------------------------
|
|
18
|
+
# DATA I/O
|
|
19
|
+
# ---------------------------
|
|
20
|
+
class DataLoader:
|
|
21
|
+
"""Handles file I/O operations for XY and matrix data"""
|
|
22
|
+
|
|
23
|
+
@staticmethod
|
|
24
|
+
def detect_columns(path):
|
|
25
|
+
"""Detect the number and names of columns in a data file"""
|
|
26
|
+
try:
|
|
27
|
+
with open(path, 'r', encoding='utf-8', errors='ignore') as f:
|
|
28
|
+
# Read header line
|
|
29
|
+
header = f.readline().strip()
|
|
30
|
+
|
|
31
|
+
# Read first data line to determine actual column count
|
|
32
|
+
first_data_line = None
|
|
33
|
+
for line in f:
|
|
34
|
+
line = line.strip()
|
|
35
|
+
if line and not line.startswith('#'):
|
|
36
|
+
first_data_line = line
|
|
37
|
+
break
|
|
38
|
+
|
|
39
|
+
# Determine delimiter from first data line (more reliable)
|
|
40
|
+
if first_data_line:
|
|
41
|
+
if '\t' in first_data_line:
|
|
42
|
+
delim = '\t'
|
|
43
|
+
splitter = re.compile(r'\t+')
|
|
44
|
+
elif ',' in first_data_line:
|
|
45
|
+
delim = ','
|
|
46
|
+
splitter = re.compile(r',')
|
|
47
|
+
else:
|
|
48
|
+
delim = None
|
|
49
|
+
splitter = re.compile(r'\s+')
|
|
50
|
+
|
|
51
|
+
# Count columns in data line
|
|
52
|
+
data_parts = [p for p in splitter.split(first_data_line) if p.strip()]
|
|
53
|
+
num_cols = len(data_parts)
|
|
54
|
+
|
|
55
|
+
# Parse header if present
|
|
56
|
+
if header:
|
|
57
|
+
header_parts = [p.strip() for p in splitter.split(header) if p.strip()]
|
|
58
|
+
# Check if header contains numbers (likely not a header)
|
|
59
|
+
try:
|
|
60
|
+
float(header_parts[0])
|
|
61
|
+
# First line is data, not header
|
|
62
|
+
return num_cols, [f"Column {i}" for i in range(num_cols)]
|
|
63
|
+
except:
|
|
64
|
+
# First line is header, but might have fewer columns
|
|
65
|
+
# Use actual data column count
|
|
66
|
+
col_names = header_parts[:num_cols]
|
|
67
|
+
# Pad with generic names if header has fewer columns
|
|
68
|
+
while len(col_names) < num_cols:
|
|
69
|
+
col_names.append(f"Column {len(col_names)}")
|
|
70
|
+
return num_cols, col_names
|
|
71
|
+
else:
|
|
72
|
+
return num_cols, [f"Column {i}" for i in range(num_cols)]
|
|
73
|
+
|
|
74
|
+
# Fallback: try to parse header
|
|
75
|
+
if header:
|
|
76
|
+
parts = re.split(r'[,\t\s]+', header)
|
|
77
|
+
parts = [p.strip() for p in parts if p.strip()]
|
|
78
|
+
try:
|
|
79
|
+
float(parts[0])
|
|
80
|
+
return len(parts), [f"Column {i}" for i in range(len(parts))]
|
|
81
|
+
except:
|
|
82
|
+
return len(parts), parts
|
|
83
|
+
|
|
84
|
+
return 0, []
|
|
85
|
+
except:
|
|
86
|
+
return 0, []
|
|
87
|
+
|
|
88
|
+
@staticmethod
|
|
89
|
+
def load_xy_data(path, x_col=1, y_col=0):
|
|
90
|
+
"""Load X/Y data with column selection
|
|
91
|
+
|
|
92
|
+
Uses np.genfromtxt to handle missing values gracefully (converts to NaN).
|
|
93
|
+
This matches legacy behavior where missing first column values become NaN in y,
|
|
94
|
+
but the rest of the row still contributes to x.
|
|
95
|
+
"""
|
|
96
|
+
delim = None # Initialize delim before try block
|
|
97
|
+
skip_header = 0
|
|
98
|
+
|
|
99
|
+
# First detect delimiter and header from first data line (more reliable)
|
|
100
|
+
try:
|
|
101
|
+
with open(path, 'r', encoding='utf-8', errors='ignore') as f:
|
|
102
|
+
first_line = f.readline().strip()
|
|
103
|
+
|
|
104
|
+
# Check if first line is header by trying to parse first element as float
|
|
105
|
+
try:
|
|
106
|
+
# Try to parse first element as number
|
|
107
|
+
float(first_line.split()[0].strip())
|
|
108
|
+
skip_header = 0
|
|
109
|
+
# First line is data, use it for delimiter detection
|
|
110
|
+
test_line = first_line
|
|
111
|
+
except (ValueError, IndexError):
|
|
112
|
+
# First line is likely header, read next line for delimiter detection
|
|
113
|
+
skip_header = 1
|
|
114
|
+
test_line = None
|
|
115
|
+
for line in f:
|
|
116
|
+
line = line.strip()
|
|
117
|
+
if line and not line.startswith('#'):
|
|
118
|
+
test_line = line
|
|
119
|
+
break
|
|
120
|
+
|
|
121
|
+
# Detect delimiter from actual data line (more reliable)
|
|
122
|
+
if test_line:
|
|
123
|
+
if '\t' in test_line:
|
|
124
|
+
delim = '\t'
|
|
125
|
+
elif ',' in test_line:
|
|
126
|
+
delim = ','
|
|
127
|
+
else:
|
|
128
|
+
delim = None # whitespace
|
|
129
|
+
except:
|
|
130
|
+
pass
|
|
131
|
+
|
|
132
|
+
# Try np.loadtxt first (faster for well-formed data)
|
|
133
|
+
try:
|
|
134
|
+
data = np.loadtxt(path, skiprows=skip_header, dtype=np.float64, ndmin=2, delimiter=delim)
|
|
135
|
+
if data.size == 0:
|
|
136
|
+
raise ValueError("No data found in file")
|
|
137
|
+
|
|
138
|
+
# Handle single column case
|
|
139
|
+
if data.ndim == 1:
|
|
140
|
+
data = data.reshape(-1, 1)
|
|
141
|
+
|
|
142
|
+
# Validate column indices
|
|
143
|
+
num_cols = data.shape[1]
|
|
144
|
+
if x_col >= num_cols:
|
|
145
|
+
x_col = min(x_col, num_cols - 1)
|
|
146
|
+
if y_col >= num_cols:
|
|
147
|
+
y_col = min(y_col, num_cols - 1)
|
|
148
|
+
|
|
149
|
+
return data[:, y_col], data[:, x_col] # y, x
|
|
150
|
+
except (ValueError, IndexError, UnicodeDecodeError):
|
|
151
|
+
# np.loadtxt failed (likely due to missing values), fall back to np.genfromtxt
|
|
152
|
+
# np.genfromtxt handles missing values by converting them to NaN
|
|
153
|
+
pass
|
|
154
|
+
|
|
155
|
+
# Use np.genfromtxt (handles missing values gracefully)
|
|
156
|
+
# If delim wasn't set, try to detect it from first data line
|
|
157
|
+
if delim is None:
|
|
158
|
+
try:
|
|
159
|
+
with open(path, 'r', encoding='utf-8', errors='ignore') as f:
|
|
160
|
+
# Skip header
|
|
161
|
+
first_line = f.readline().strip()
|
|
162
|
+
try:
|
|
163
|
+
float(first_line.split()[0].strip())
|
|
164
|
+
test_line = first_line
|
|
165
|
+
skip_header = 0
|
|
166
|
+
except (ValueError, IndexError):
|
|
167
|
+
skip_header = 1
|
|
168
|
+
test_line = None
|
|
169
|
+
for line in f:
|
|
170
|
+
line = line.strip()
|
|
171
|
+
if line and not line.startswith('#'):
|
|
172
|
+
test_line = line
|
|
173
|
+
break
|
|
174
|
+
|
|
175
|
+
if test_line:
|
|
176
|
+
if '\t' in test_line:
|
|
177
|
+
delim = '\t'
|
|
178
|
+
elif ',' in test_line:
|
|
179
|
+
delim = ','
|
|
180
|
+
else:
|
|
181
|
+
delim = None
|
|
182
|
+
except:
|
|
183
|
+
delim = None
|
|
184
|
+
|
|
185
|
+
# Use np.genfromtxt which handles missing values (converts to NaN)
|
|
186
|
+
# This matches legacy behavior: missing first column -> NaN in y, but row still contributes to x
|
|
187
|
+
try:
|
|
188
|
+
y, x = np.genfromtxt(
|
|
189
|
+
path,
|
|
190
|
+
delimiter=delim,
|
|
191
|
+
dtype=np.float64,
|
|
192
|
+
skip_header=skip_header,
|
|
193
|
+
usecols=(y_col, x_col),
|
|
194
|
+
unpack=True,
|
|
195
|
+
autostrip=True,
|
|
196
|
+
invalid_raise=False, # Don't raise on invalid values, convert to NaN
|
|
197
|
+
)
|
|
198
|
+
return y, x
|
|
199
|
+
except Exception:
|
|
200
|
+
# If all else fails, try default columns
|
|
201
|
+
y, x = np.genfromtxt(
|
|
202
|
+
path,
|
|
203
|
+
delimiter=delim,
|
|
204
|
+
dtype=np.float64,
|
|
205
|
+
skip_header=skip_header,
|
|
206
|
+
usecols=(0, 1),
|
|
207
|
+
unpack=True,
|
|
208
|
+
autostrip=True,
|
|
209
|
+
invalid_raise=False,
|
|
210
|
+
)
|
|
211
|
+
return y, x
|
|
212
|
+
|
|
213
|
+
@staticmethod
|
|
214
|
+
def load_matrix_data(path):
|
|
215
|
+
"""Optimized matrix loading with better memory usage"""
|
|
216
|
+
# First pass: determine dimensions
|
|
217
|
+
with open(path, "r", encoding="utf-8", errors="ignore") as f:
|
|
218
|
+
lines = [line.strip() for line in f if line.strip() and not line.strip().startswith("#")]
|
|
219
|
+
|
|
220
|
+
if not lines:
|
|
221
|
+
raise ValueError("No numeric rows found in file")
|
|
222
|
+
|
|
223
|
+
first = lines[0]
|
|
224
|
+
if ',' in first:
|
|
225
|
+
sep = ','
|
|
226
|
+
splitter = re.compile(r"\s*,\s*")
|
|
227
|
+
elif '\t' in first:
|
|
228
|
+
sep = '\t'
|
|
229
|
+
splitter = re.compile(r"\t+")
|
|
230
|
+
else:
|
|
231
|
+
sep = ' ' # whitespace
|
|
232
|
+
splitter = re.compile(r"\s+")
|
|
233
|
+
|
|
234
|
+
rows = []
|
|
235
|
+
for line in lines:
|
|
236
|
+
try:
|
|
237
|
+
row = np.fromstring(line, sep=sep, dtype=np.float64)
|
|
238
|
+
if row.size:
|
|
239
|
+
rows.append(row)
|
|
240
|
+
continue
|
|
241
|
+
except Exception:
|
|
242
|
+
pass
|
|
243
|
+
# Fallback parser (handles stray spaces/commas)
|
|
244
|
+
parts = [p for p in splitter.split(line) if p]
|
|
245
|
+
vals = []
|
|
246
|
+
for p in parts:
|
|
247
|
+
try:
|
|
248
|
+
vals.append(float(p))
|
|
249
|
+
except ValueError:
|
|
250
|
+
pass
|
|
251
|
+
if vals:
|
|
252
|
+
rows.append(np.asarray(vals, dtype=np.float64))
|
|
253
|
+
|
|
254
|
+
if not rows:
|
|
255
|
+
raise ValueError("No numeric rows found in file")
|
|
256
|
+
|
|
257
|
+
widths = np.array([r.size for r in rows], dtype=int)
|
|
258
|
+
modal_w = np.bincount(widths).argmax()
|
|
259
|
+
good_rows = [r for r in rows if r.size == modal_w]
|
|
260
|
+
A = np.vstack(good_rows).astype(np.float64)
|
|
261
|
+
if A.shape[0] > A.shape[1]:
|
|
262
|
+
A = A.T
|
|
263
|
+
return A
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
# ---------------------------
|
|
267
|
+
# COMPUTATION UTILITIES
|
|
268
|
+
# ---------------------------
|
|
269
|
+
def calculate_effective_range(vmin, vmax, brightness, contrast):
|
|
270
|
+
"""Calculate effective value range after brightness/contrast adjustment"""
|
|
271
|
+
vmin_p = (vmin - brightness) / max(1e-12, contrast)
|
|
272
|
+
vmax_p = (vmax - brightness) / max(1e-12, contrast)
|
|
273
|
+
return vmin_p, vmax_p
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def calculate_marker_position(brightness, contrast, span, kc=DEFAULT_KC):
|
|
277
|
+
"""Calculate control pad marker position from brightness/contrast values"""
|
|
278
|
+
import math
|
|
279
|
+
u = np.clip(0.5 + brightness / max(1e-9, span), 0.0, 1.0)
|
|
280
|
+
v = 0.5
|
|
281
|
+
if contrast > 0:
|
|
282
|
+
v = np.clip(0.5 - math.log(contrast) / (2 * max(1e-9, abs(kc))), 0.0, 1.0)
|
|
283
|
+
return u, v
|