pyrpoc 0.1.0__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.
- pyrpoc-0.1.0/LICENSE +7 -0
- pyrpoc-0.1.0/PKG-INFO +40 -0
- pyrpoc-0.1.0/README.md +13 -0
- pyrpoc-0.1.0/pyproject.toml +32 -0
- pyrpoc-0.1.0/pyrpoc/helpers/calibration.py +125 -0
- pyrpoc-0.1.0/pyrpoc/helpers/galvo_funcs.py +136 -0
- pyrpoc-0.1.0/pyrpoc/helpers/prior_stage/functions.py +176 -0
- pyrpoc-0.1.0/pyrpoc/helpers/prior_stage/prior_interface.py +129 -0
- pyrpoc-0.1.0/pyrpoc/helpers/run_image_2d.py +126 -0
- pyrpoc-0.1.0/pyrpoc/helpers/utils.py +64 -0
- pyrpoc-0.1.0/pyrpoc/helpers/widgets.py +78 -0
- pyrpoc-0.1.0/pyrpoc/helpers/zaber.py +44 -0
- pyrpoc-0.1.0/pyrpoc/main.py +7 -0
- pyrpoc-0.1.0/pyrpoc/mains/acquisition.py +204 -0
- pyrpoc-0.1.0/pyrpoc/mains/display.py +258 -0
- pyrpoc-0.1.0/pyrpoc/mains/gui.py +976 -0
- pyrpoc-0.1.0/pyrpoc/mains/pyqt_rpoc.py +662 -0
- pyrpoc-0.1.0/pyrpoc/misc/simulations/organelle_crosstalk.py +63 -0
- pyrpoc-0.1.0/pyrpoc/misc/test_scripts/calib_galvos.py +169 -0
- pyrpoc-0.1.0/pyrpoc/misc/test_scripts/fake_spt.py +58 -0
- pyrpoc-0.1.0/pyrpoc/misc/test_scripts/second_lti.py +227 -0
- pyrpoc-0.1.0/pyrpoc/misc/test_scripts/send_sine.py +77 -0
- pyrpoc-0.1.0/pyrpoc/misc/test_scripts/send_spiral.py +104 -0
- pyrpoc-0.1.0/pyrpoc.egg-info/PKG-INFO +40 -0
- pyrpoc-0.1.0/pyrpoc.egg-info/SOURCES.txt +28 -0
- pyrpoc-0.1.0/pyrpoc.egg-info/dependency_links.txt +1 -0
- pyrpoc-0.1.0/pyrpoc.egg-info/entry_points.txt +2 -0
- pyrpoc-0.1.0/pyrpoc.egg-info/requires.txt +10 -0
- pyrpoc-0.1.0/pyrpoc.egg-info/top_level.txt +1 -0
- pyrpoc-0.1.0/setup.cfg +4 -0
pyrpoc-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
Copyright 2025, Ishaan Singh
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
4
|
+
|
|
5
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
6
|
+
|
|
7
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
pyrpoc-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pyrpoc
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Real-time Precision Opto-Control Toolkit
|
|
5
|
+
Author-email: Ishaan Singh <sing1125@purdue.com>
|
|
6
|
+
License: Copyright 2025, Ishaan Singh
|
|
7
|
+
|
|
8
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
9
|
+
|
|
10
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
11
|
+
|
|
12
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
13
|
+
Requires-Python: >=3.8
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
License-File: LICENSE
|
|
16
|
+
Requires-Dist: numpy
|
|
17
|
+
Requires-Dist: scipy
|
|
18
|
+
Requires-Dist: opencv-python
|
|
19
|
+
Requires-Dist: pillow
|
|
20
|
+
Requires-Dist: matplotlib
|
|
21
|
+
Requires-Dist: pyqt5
|
|
22
|
+
Requires-Dist: superqt
|
|
23
|
+
Requires-Dist: cellpose
|
|
24
|
+
Requires-Dist: nidaqmx
|
|
25
|
+
Requires-Dist: pyvisa
|
|
26
|
+
Dynamic: license-file
|
|
27
|
+
|
|
28
|
+
# pyrpoc
|
|
29
|
+
|
|
30
|
+
Author: Ishaan Singh, Zhang Group (https://sites.google.com/view/zhangresearchgroup)
|
|
31
|
+
|
|
32
|
+
## Basic Installation
|
|
33
|
+
|
|
34
|
+
Navigate to the directory that contains ```setup.py```. Run
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
pip install -e .
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Please reach out to sing1125@purdue.edu with any suggestions or feedback on the GUI.
|
pyrpoc-0.1.0/README.md
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# pyrpoc
|
|
2
|
+
|
|
3
|
+
Author: Ishaan Singh, Zhang Group (https://sites.google.com/view/zhangresearchgroup)
|
|
4
|
+
|
|
5
|
+
## Basic Installation
|
|
6
|
+
|
|
7
|
+
Navigate to the directory that contains ```setup.py```. Run
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
pip install -e .
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Please reach out to sing1125@purdue.edu with any suggestions or feedback on the GUI.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "pyrpoc"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Real-time Precision Opto-Control Toolkit"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.8"
|
|
7
|
+
license = { file = "LICENSE" }
|
|
8
|
+
|
|
9
|
+
authors = [
|
|
10
|
+
{ name = "Ishaan Singh", email = "sing1125@purdue.com" }
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
dependencies = [
|
|
14
|
+
"numpy",
|
|
15
|
+
"scipy",
|
|
16
|
+
"opencv-python",
|
|
17
|
+
"pillow",
|
|
18
|
+
"matplotlib",
|
|
19
|
+
"pyqt5",
|
|
20
|
+
"superqt",
|
|
21
|
+
"cellpose",
|
|
22
|
+
"nidaqmx",
|
|
23
|
+
"pyvisa",
|
|
24
|
+
# Add any others your app depends on
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
[project.scripts]
|
|
28
|
+
pyrpoc = "pyrpoc.main:main"
|
|
29
|
+
|
|
30
|
+
[build-system]
|
|
31
|
+
requires = ["setuptools>=61.0"]
|
|
32
|
+
build-backend = "setuptools.build_meta"
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import tkinter as tk
|
|
2
|
+
from tkinter import ttk, messagebox
|
|
3
|
+
import time, threading
|
|
4
|
+
from matplotlib.figure import Figure
|
|
5
|
+
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
|
|
6
|
+
from pyrpoc.helpers.galvo_funcs import Galvo
|
|
7
|
+
from pyrpoc.helpers.run_image_2d import run_scan
|
|
8
|
+
from pyrpoc.helpers.utils import generate_data
|
|
9
|
+
|
|
10
|
+
# TODO: make the calibration plot dark mode because pretty
|
|
11
|
+
def calibrate_stage(gui):
|
|
12
|
+
cal_win = tk.Toplevel(gui.root)
|
|
13
|
+
cal_win.title("Stage Calibration")
|
|
14
|
+
cal_win.geometry("900x600")
|
|
15
|
+
|
|
16
|
+
config_frame = ttk.Frame(cal_win, padding=10)
|
|
17
|
+
config_frame.pack(side=tk.TOP, fill=tk.X)
|
|
18
|
+
|
|
19
|
+
ttk.Label(config_frame, text='Start Position (µm)').grid(row=0, column=0, sticky='w', padx=5, pady=3)
|
|
20
|
+
start_entry = ttk.Entry(config_frame, width=12, font=('Calibri', 14))
|
|
21
|
+
start_entry.insert(0, str(gui.hyper_config['start_um']))
|
|
22
|
+
start_entry.grid(row=0, column=1, padx=5, pady=3)
|
|
23
|
+
|
|
24
|
+
ttk.Label(config_frame, text='Stop Position (µm)').grid(row=1, column=0, sticky='w', padx=5, pady=3)
|
|
25
|
+
stop_entry = ttk.Entry(config_frame, width=12, font=('Calibri', 14))
|
|
26
|
+
stop_entry.insert(0, str(gui.hyper_config['stop_um']))
|
|
27
|
+
stop_entry.grid(row=1, column=1, padx=5, pady=3)
|
|
28
|
+
|
|
29
|
+
ttk.Label(config_frame, text='Number of Steps').grid(row=2, column=0, sticky='w', padx=5, pady=3)
|
|
30
|
+
cal_steps_entry = ttk.Entry(config_frame, width=10, font=('Calibri', 14))
|
|
31
|
+
cal_steps_entry.insert(0, '10')
|
|
32
|
+
cal_steps_entry.grid(row=2, column=1, padx=5, pady=3)
|
|
33
|
+
|
|
34
|
+
start_button = ttk.Button(config_frame, text='Start Calibration')
|
|
35
|
+
start_button.grid(row=3, column=0, columnspan=2, padx=5, pady=10)
|
|
36
|
+
|
|
37
|
+
fig = Figure(figsize=(5, 3), dpi=100)
|
|
38
|
+
ax = fig.add_subplot(111)
|
|
39
|
+
ax.set_title('Calibration Data')
|
|
40
|
+
ax.set_xlabel('Stage Position (µm)')
|
|
41
|
+
ax.set_ylabel('Average Intensity')
|
|
42
|
+
|
|
43
|
+
canvas = FigureCanvasTkAgg(fig, master=cal_win)
|
|
44
|
+
canvas_widget = canvas.get_tk_widget()
|
|
45
|
+
canvas_widget.pack(side=tk.TOP, fill=tk.BOTH, expand=True)
|
|
46
|
+
|
|
47
|
+
cal_running = [False]
|
|
48
|
+
|
|
49
|
+
def run_calibration():
|
|
50
|
+
cal_running[0] = True
|
|
51
|
+
try:
|
|
52
|
+
start_val = float(start_entry.get().strip())
|
|
53
|
+
stop_val = float(stop_entry.get().strip())
|
|
54
|
+
n_steps = int(cal_steps_entry.get().strip())
|
|
55
|
+
if n_steps < 1 or start_val >= stop_val:
|
|
56
|
+
raise ValueError
|
|
57
|
+
except ValueError:
|
|
58
|
+
messagebox.showerror("Error", "Invalid calibration settings.")
|
|
59
|
+
cal_running[0] = False
|
|
60
|
+
return
|
|
61
|
+
|
|
62
|
+
try:
|
|
63
|
+
gui.zaber_stage.connect()
|
|
64
|
+
except Exception as e:
|
|
65
|
+
messagebox.showerror("Zaber Error", str(e))
|
|
66
|
+
cal_running[0] = False
|
|
67
|
+
return
|
|
68
|
+
|
|
69
|
+
positions_to_scan = ([start_val]
|
|
70
|
+
if n_steps == 1 else
|
|
71
|
+
[start_val + i * (stop_val - start_val) / (n_steps - 1)
|
|
72
|
+
for i in range(n_steps)]
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
positions, intensities = [], []
|
|
76
|
+
|
|
77
|
+
for pos in positions_to_scan:
|
|
78
|
+
if not cal_running[0]:
|
|
79
|
+
break
|
|
80
|
+
try:
|
|
81
|
+
gui.zaber_stage.move_absolute_um(pos)
|
|
82
|
+
except Exception as e:
|
|
83
|
+
messagebox.showerror("Zaber Error", str(e))
|
|
84
|
+
cal_running[0] = False
|
|
85
|
+
break
|
|
86
|
+
|
|
87
|
+
# TODO: calibration can still acquire simulation - probably good to display a warning about that though
|
|
88
|
+
if gui.simulation_mode.get():
|
|
89
|
+
data_list = generate_data(len(gui.config['ai_chans']), config=gui.config)
|
|
90
|
+
data = data_list[0]
|
|
91
|
+
else:
|
|
92
|
+
galvo = Galvo(gui.config)
|
|
93
|
+
data_list = raster_scan(
|
|
94
|
+
[f"{gui.config['device']}/{ch}" for ch in gui.config['ai_chans']],
|
|
95
|
+
galvo
|
|
96
|
+
)
|
|
97
|
+
data = data_list[0]
|
|
98
|
+
|
|
99
|
+
avg_val = data.mean()
|
|
100
|
+
positions.append(pos)
|
|
101
|
+
intensities.append(avg_val)
|
|
102
|
+
|
|
103
|
+
ax.clear()
|
|
104
|
+
ax.set_title('Calibration Data')
|
|
105
|
+
ax.set_xlabel('Stage Position (µm)')
|
|
106
|
+
ax.set_ylabel('Average Intensity')
|
|
107
|
+
ax.plot(positions, intensities, '-o', color='blue')
|
|
108
|
+
canvas.draw()
|
|
109
|
+
canvas.flush_events()
|
|
110
|
+
|
|
111
|
+
time.sleep(0.2)
|
|
112
|
+
|
|
113
|
+
cal_running[0] = False
|
|
114
|
+
|
|
115
|
+
def start_cal():
|
|
116
|
+
if not cal_running[0]:
|
|
117
|
+
threading.Thread(target=run_calibration, daemon=True).start()
|
|
118
|
+
|
|
119
|
+
start_button.configure(command=start_cal)
|
|
120
|
+
|
|
121
|
+
stop_button = ttk.Button(
|
|
122
|
+
config_frame, text='Stop Calibration',
|
|
123
|
+
command=lambda: cal_running.__setitem__(0, False)
|
|
124
|
+
)
|
|
125
|
+
stop_button.grid(row=4, column=0, columnspan=2, padx=5, pady=5)
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import nidaqmx
|
|
2
|
+
from nidaqmx.constants import AcquisitionType
|
|
3
|
+
import numpy as np
|
|
4
|
+
from PIL import Image
|
|
5
|
+
from tkinter import messagebox
|
|
6
|
+
|
|
7
|
+
class Galvo:
|
|
8
|
+
def __init__(self, config, rpoc_mask=None, rpoc_do_chan=None, rpoc_mode=None, dwell_multiplier=2.0, **kwargs):
|
|
9
|
+
defaults = {
|
|
10
|
+
"numsteps_x": 400,
|
|
11
|
+
"numsteps_y": 400,
|
|
12
|
+
"extrasteps_left": 50,
|
|
13
|
+
"extrasteps_right": 50,
|
|
14
|
+
"offset_x": 0.0,
|
|
15
|
+
"offset_y": 0.0,
|
|
16
|
+
"dwell": 10e-6,
|
|
17
|
+
"amp_x": 0.5,
|
|
18
|
+
"amp_y": 0.5,
|
|
19
|
+
"rate": 10000,
|
|
20
|
+
"device": 'Dev1',
|
|
21
|
+
"ao_chans": ['ao1', 'ao0']
|
|
22
|
+
}
|
|
23
|
+
if config:
|
|
24
|
+
defaults.update(config)
|
|
25
|
+
defaults.update(kwargs)
|
|
26
|
+
for key, val in defaults.items():
|
|
27
|
+
setattr(self, key, val)
|
|
28
|
+
|
|
29
|
+
self.rpoc_mask = rpoc_mask
|
|
30
|
+
self.rpoc_do_chan = rpoc_do_chan
|
|
31
|
+
self.rpoc_mode = rpoc_mode # "standard" or "variable" so far
|
|
32
|
+
self.dwell_multiplier = dwell_multiplier
|
|
33
|
+
|
|
34
|
+
self.pixel_samples = max(1, int(self.dwell * self.rate))
|
|
35
|
+
self.total_x = self.numsteps_x + self.extrasteps_left + self.extrasteps_right
|
|
36
|
+
self.total_y = self.numsteps_y
|
|
37
|
+
self.total_samples = self.total_x * self.total_y * self.pixel_samples
|
|
38
|
+
|
|
39
|
+
if self.rpoc_mode == "variable":
|
|
40
|
+
self.waveform = None # or maybe generate it here...
|
|
41
|
+
else:
|
|
42
|
+
self.waveform = self.gen_raster()
|
|
43
|
+
|
|
44
|
+
def gen_raster(self):
|
|
45
|
+
total_rowsamples = self.pixel_samples * self.total_x
|
|
46
|
+
self.total_samples = total_rowsamples * self.total_y
|
|
47
|
+
|
|
48
|
+
single_row_ramp = np.linspace(
|
|
49
|
+
self.offset_x - self.amp_x,
|
|
50
|
+
self.offset_x + self.amp_x,
|
|
51
|
+
total_rowsamples,
|
|
52
|
+
endpoint=False
|
|
53
|
+
)
|
|
54
|
+
x_waveform = np.tile(single_row_ramp, self.total_y)
|
|
55
|
+
|
|
56
|
+
y_steps = np.linspace(
|
|
57
|
+
self.offset_y + self.amp_y,
|
|
58
|
+
self.offset_y - self.amp_y,
|
|
59
|
+
self.total_y
|
|
60
|
+
)
|
|
61
|
+
y_waveform = np.repeat(y_steps, total_rowsamples)
|
|
62
|
+
|
|
63
|
+
composite = np.vstack([x_waveform, y_waveform])
|
|
64
|
+
|
|
65
|
+
if self.rpoc_mask is not None and self.rpoc_do_chan is not None:
|
|
66
|
+
rpoc_wave = build_rpoc_wave(
|
|
67
|
+
self.rpoc_mask,
|
|
68
|
+
self.pixel_samples,
|
|
69
|
+
self.total_x,
|
|
70
|
+
self.total_y,
|
|
71
|
+
high_voltage=5.0
|
|
72
|
+
)
|
|
73
|
+
if rpoc_wave.size != y_waveform.size:
|
|
74
|
+
raise ValueError("RPOC wave length does not match total scan length!")
|
|
75
|
+
composite = np.vstack([composite, rpoc_wave])
|
|
76
|
+
|
|
77
|
+
if x_waveform.size < self.total_samples:
|
|
78
|
+
x_waveform = np.pad(
|
|
79
|
+
x_waveform,
|
|
80
|
+
(0, self.total_samples - x_waveform.size),
|
|
81
|
+
constant_values=x_waveform[-1]
|
|
82
|
+
)
|
|
83
|
+
else:
|
|
84
|
+
x_waveform = x_waveform[:self.total_samples]
|
|
85
|
+
composite[0] = x_waveform
|
|
86
|
+
|
|
87
|
+
return composite
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def gen_variable_waveform(self, mask, dwell_multiplier):
|
|
91
|
+
dwell = self.dwell
|
|
92
|
+
rate = self.rate
|
|
93
|
+
num_y = self.numsteps_y
|
|
94
|
+
num_x = self.numsteps_x + self.extrasteps_left + self.extrasteps_right
|
|
95
|
+
|
|
96
|
+
mask_shape = np.shape(mask)
|
|
97
|
+
if mask_shape[1] != num_y or mask_shape[0] != (num_x - self.extrasteps_left - self.extrasteps_right):
|
|
98
|
+
raise ValueError(f'Error in galvo_funcs.gen_variable_waveform(). Mask is not the right size. Load a mask of dimensions {num_x - self.extrasteps_left - self.extrasteps_right} by {num_y}.')
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
dwell_on = dwell * dwell_multiplier
|
|
102
|
+
dwell_off = dwell
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
x_min = self.offset_x - self.amp_x
|
|
106
|
+
x_max = self.offset_x + self.amp_x
|
|
107
|
+
x_positions = np.linspace(x_min, x_max, num_x, endpoint=False)
|
|
108
|
+
y_positions = np.linspace(self.offset_y + self.amp_y,
|
|
109
|
+
self.offset_y - self.amp_y,
|
|
110
|
+
num_y)
|
|
111
|
+
|
|
112
|
+
x_wave_list = []
|
|
113
|
+
y_wave_list = []
|
|
114
|
+
pixel_map = np.zeros((num_y, num_x), dtype=int)
|
|
115
|
+
|
|
116
|
+
for row_idx in range(num_y):
|
|
117
|
+
row_y = y_positions[row_idx]
|
|
118
|
+
for col_idx in range(num_x):
|
|
119
|
+
if col_idx < self.extrasteps_left or col_idx >= (num_x - self.extrasteps_right): # for clarity im leaving this out of the else - we need to remember to account for extrasteps here
|
|
120
|
+
this_dwell = dwell_off
|
|
121
|
+
elif mask[row_idx, col_idx - self.extrasteps_left]:
|
|
122
|
+
this_dwell = dwell_on
|
|
123
|
+
else:
|
|
124
|
+
this_dwell = dwell_off
|
|
125
|
+
|
|
126
|
+
pixel_samps = max(1, int(this_dwell * rate))
|
|
127
|
+
|
|
128
|
+
x_val = x_positions[col_idx]
|
|
129
|
+
x_wave_list.append(np.full(pixel_samps, x_val))
|
|
130
|
+
|
|
131
|
+
y_wave_list.append(np.full(pixel_samps, row_y))
|
|
132
|
+
|
|
133
|
+
pixel_map[row_idx, col_idx] = pixel_samps
|
|
134
|
+
x_wave = np.concatenate(x_wave_list)
|
|
135
|
+
y_wave = np.concatenate(y_wave_list)
|
|
136
|
+
return x_wave, y_wave, pixel_map
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
from ctypes import WinDLL, create_string_buffer
|
|
2
|
+
import os
|
|
3
|
+
import time
|
|
4
|
+
import numpy as np
|
|
5
|
+
from pyrpoc.mains import acquisition
|
|
6
|
+
import cv2
|
|
7
|
+
|
|
8
|
+
DLL_PATH = os.path.join(os.path.dirname(__file__), "PriorScientificSDK.dll")
|
|
9
|
+
SDKPrior = None
|
|
10
|
+
sessionID = None
|
|
11
|
+
_prior_connected = False # Track connection state
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def connect_prior(port=4):
|
|
15
|
+
global _prior_connected
|
|
16
|
+
if _prior_connected:
|
|
17
|
+
return
|
|
18
|
+
|
|
19
|
+
ret, _ = send_command(f"controller.connect {port}")
|
|
20
|
+
if ret == 0:
|
|
21
|
+
_prior_connected = True
|
|
22
|
+
print(f"Connected to Prior stage on COM{port}")
|
|
23
|
+
else:
|
|
24
|
+
raise RuntimeError(f"Failed to connect to Prior stage on COM{port}")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def initialize_sdk():
|
|
28
|
+
global SDKPrior, sessionID
|
|
29
|
+
|
|
30
|
+
if SDKPrior is None:
|
|
31
|
+
if os.path.exists(DLL_PATH):
|
|
32
|
+
SDKPrior = WinDLL(DLL_PATH)
|
|
33
|
+
else:
|
|
34
|
+
raise RuntimeError("DLL could not be loaded.")
|
|
35
|
+
|
|
36
|
+
ret = SDKPrior.PriorScientificSDK_Initialise()
|
|
37
|
+
if ret != 0:
|
|
38
|
+
raise RuntimeError(f"Failed to initialize Prior SDK. Error code: {ret}")
|
|
39
|
+
|
|
40
|
+
print("Prior SDK Initialized.")
|
|
41
|
+
|
|
42
|
+
if sessionID is None:
|
|
43
|
+
sessionID = SDKPrior.PriorScientificSDK_OpenNewSession()
|
|
44
|
+
if sessionID < 0:
|
|
45
|
+
raise RuntimeError(f"Failed to open Prior SDK session. SessionID: {sessionID}")
|
|
46
|
+
|
|
47
|
+
print(f"SDK Session Opened. Session ID: {sessionID}")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def send_command(command):
|
|
51
|
+
initialize_sdk()
|
|
52
|
+
|
|
53
|
+
rx = create_string_buffer(1000)
|
|
54
|
+
ret = SDKPrior.PriorScientificSDK_cmd(
|
|
55
|
+
sessionID, create_string_buffer(command.encode()), rx
|
|
56
|
+
)
|
|
57
|
+
response = rx.value.decode().strip()
|
|
58
|
+
|
|
59
|
+
if ret != 0:
|
|
60
|
+
print(f"Error executing command: {command} (Return Code: {ret})")
|
|
61
|
+
|
|
62
|
+
return ret, response
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def wait_for_z_motion():
|
|
66
|
+
while True:
|
|
67
|
+
_, response = send_command("controller.z.busy.get")
|
|
68
|
+
|
|
69
|
+
if response:
|
|
70
|
+
try:
|
|
71
|
+
status = int(response)
|
|
72
|
+
if status == 0:
|
|
73
|
+
break
|
|
74
|
+
except ValueError:
|
|
75
|
+
print(f"Invalid response from controller: '{response}'")
|
|
76
|
+
else:
|
|
77
|
+
print("No response from controller, is it connected?")
|
|
78
|
+
|
|
79
|
+
time.sleep(0.1)
|
|
80
|
+
|
|
81
|
+
def auto_focus(gui, port: int, channel_name: str, step_size=10):
|
|
82
|
+
connect_prior(port)
|
|
83
|
+
|
|
84
|
+
gui.simulation_mode.set(False)
|
|
85
|
+
gui.acquiring = True
|
|
86
|
+
|
|
87
|
+
try:
|
|
88
|
+
channel_index = gui.config["channel_names"].index(channel_name)
|
|
89
|
+
except ValueError:
|
|
90
|
+
raise RuntimeError(f"Invalid channel name: '{channel_name}'")
|
|
91
|
+
|
|
92
|
+
ret, current_z = send_command("controller.z.position.get")
|
|
93
|
+
if ret != 0:
|
|
94
|
+
raise RuntimeError("Failed to retrieve current Z position.")
|
|
95
|
+
try:
|
|
96
|
+
current_z = int(current_z)
|
|
97
|
+
except ValueError:
|
|
98
|
+
raise RuntimeError(f"Invalid Z position response: '{current_z}'")
|
|
99
|
+
|
|
100
|
+
z_positions = [current_z + i * step_size for i in range(-10, 11)] # total of 21 points
|
|
101
|
+
best_focus = -1
|
|
102
|
+
best_z = current_z
|
|
103
|
+
|
|
104
|
+
gui.progress_label.config(text=f'(0/{len(z_positions)})')
|
|
105
|
+
gui.root.update_idletasks()
|
|
106
|
+
|
|
107
|
+
for i, z in enumerate(z_positions):
|
|
108
|
+
if not gui.acquiring:
|
|
109
|
+
print("[Autofocus] Interrupted by Stop.")
|
|
110
|
+
break
|
|
111
|
+
|
|
112
|
+
try:
|
|
113
|
+
send_command(f"controller.z.goto-position {z}")
|
|
114
|
+
wait_for_z_motion()
|
|
115
|
+
except Exception as e:
|
|
116
|
+
gui.acquiring = False
|
|
117
|
+
raise RuntimeError(f"Stage move to Z={z} failed: {e}")
|
|
118
|
+
|
|
119
|
+
acquisition.acquire(gui, auxilary=True)
|
|
120
|
+
gui.root.update_idletasks()
|
|
121
|
+
gui.root.update()
|
|
122
|
+
|
|
123
|
+
image = gui.data[channel_index]
|
|
124
|
+
x, y = np.shape(image)
|
|
125
|
+
metric = cv2.Laplacian(image[int(3*x/8):int(5*x/8), int(3*y/8):int(5*y/8)], cv2.CV_64F).var() # only use the middle eighth for focus
|
|
126
|
+
print(f"Z={z} → Focus Metric={metric:.2f}")
|
|
127
|
+
|
|
128
|
+
if metric > best_focus:
|
|
129
|
+
best_focus = metric
|
|
130
|
+
best_z = z
|
|
131
|
+
|
|
132
|
+
gui.progress_label.config(text=f'({i + 1}/{len(z_positions)})')
|
|
133
|
+
gui.root.update_idletasks()
|
|
134
|
+
|
|
135
|
+
if gui.acquiring:
|
|
136
|
+
send_command(f"controller.z.goto-position {best_z}")
|
|
137
|
+
wait_for_z_motion()
|
|
138
|
+
|
|
139
|
+
acquisition.acquire(gui, auxilary=True)
|
|
140
|
+
gui.root.update_idletasks()
|
|
141
|
+
gui.root.update()
|
|
142
|
+
print(f"[Autofocus] Best Z = {best_z}, Metric = {best_focus:.2f}")
|
|
143
|
+
|
|
144
|
+
gui.acquiring = False
|
|
145
|
+
return best_z, best_focus
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def move_z(port: int, z_height: int):
|
|
149
|
+
connect_prior(port)
|
|
150
|
+
|
|
151
|
+
if not (0 <= z_height <= 50000):
|
|
152
|
+
raise ValueError("Z height must be between 0 and 50,000 µm.")
|
|
153
|
+
|
|
154
|
+
ret, _ = send_command(f"controller.z.goto-position {z_height}")
|
|
155
|
+
if ret != 0:
|
|
156
|
+
raise RuntimeError(f"Could not move Prior stage to {z_height} µm.")
|
|
157
|
+
wait_for_z_motion()
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def move_xy(port: int, x: int, y: int):
|
|
161
|
+
connect_prior(port)
|
|
162
|
+
|
|
163
|
+
if not (0 <= x <= 50000) or not (0 <= y <= 50000):
|
|
164
|
+
raise ValueError("X and Y positions must be between 0 and 50,000 µm.")
|
|
165
|
+
|
|
166
|
+
ret, _ = send_command(f"controller.stage.goto-position {x} {y}")
|
|
167
|
+
if ret != 0:
|
|
168
|
+
raise RuntimeError(f"Could not move Prior stage to {x}, {y}.")
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
if __name__ == "__main__":
|
|
172
|
+
print("connecting")
|
|
173
|
+
connect_prior(4)
|
|
174
|
+
|
|
175
|
+
move_xy(4, 1000, 1000)
|
|
176
|
+
move_z(4, 2500)
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# Created by Diego Alonso Alvarez on the 12th July 2020
|
|
2
|
+
#
|
|
3
|
+
# Copyright (c) 2020 Imperial College London
|
|
4
|
+
#
|
|
5
|
+
#Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
#of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
#in the Software without restriction, including without limitation the rights
|
|
8
|
+
#to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
#copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
#furnished to do so, subject to the following conditions:
|
|
11
|
+
#
|
|
12
|
+
#The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
#copies or substantial portions of the Software.
|
|
14
|
+
#
|
|
15
|
+
#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
#IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
#FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
#AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
#LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
#OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
#SOFTWARE.
|
|
22
|
+
|
|
23
|
+
from ctypes import WinDLL, create_string_buffer
|
|
24
|
+
import os
|
|
25
|
+
import sys
|
|
26
|
+
|
|
27
|
+
path = r"C:\Users\Lab Admin\Documents\PythonStuff\pysrs\pysrs\instruments\prior_stage\PriorScientificSDK.dll"
|
|
28
|
+
|
|
29
|
+
if os.path.exists(path):
|
|
30
|
+
SDKPrior = WinDLL(path)
|
|
31
|
+
else:
|
|
32
|
+
raise RuntimeError("DLL could not be loaded.")
|
|
33
|
+
|
|
34
|
+
rx = create_string_buffer(1000)
|
|
35
|
+
realhw = True
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def cmd(msg):
|
|
39
|
+
print(msg)
|
|
40
|
+
ret = SDKPrior.PriorScientificSDK_cmd(
|
|
41
|
+
sessionID, create_string_buffer(msg.encode()), rx
|
|
42
|
+
)
|
|
43
|
+
if ret:
|
|
44
|
+
print(f"Api error {ret}")
|
|
45
|
+
else:
|
|
46
|
+
print(f"46 OK {rx.value.decode()}")
|
|
47
|
+
|
|
48
|
+
input("48 Press ENTER to continue...")
|
|
49
|
+
return ret, rx.value.decode()
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
ret = SDKPrior.PriorScientificSDK_Initialise()
|
|
53
|
+
if ret:
|
|
54
|
+
print(f"54 Error initialising {ret}")
|
|
55
|
+
sys.exit()
|
|
56
|
+
else:
|
|
57
|
+
print(f"57 Ok initialising {ret}")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
ret = SDKPrior.PriorScientificSDK_Version(rx)
|
|
61
|
+
print(f"61 dll version api ret={ret}, version={rx.value.decode()}")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
sessionID = SDKPrior.PriorScientificSDK_OpenNewSession()
|
|
65
|
+
if sessionID < 0:
|
|
66
|
+
print(f"66 Error getting sessionID {ret}")
|
|
67
|
+
else:
|
|
68
|
+
print(f"68 SessionID = {sessionID}")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
ret = SDKPrior.PriorScientificSDK_cmd(
|
|
72
|
+
sessionID, create_string_buffer(b"72 dll.apitest 33 goodresponse"), rx
|
|
73
|
+
)
|
|
74
|
+
print(f"74 api response {ret}, rx = {rx.value.decode()}")
|
|
75
|
+
input("75 Press ENTER to continue...")
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
ret = SDKPrior.PriorScientificSDK_cmd(
|
|
79
|
+
sessionID, create_string_buffer(b"dll.apitest -300 stillgoodresponse"), rx
|
|
80
|
+
)
|
|
81
|
+
print(f"81 api response {ret}, rx = {rx.value.decode()}")
|
|
82
|
+
input("82 Press ENTER to continue...")
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
if realhw:
|
|
86
|
+
print("Connecting...")
|
|
87
|
+
# substitute 3 with your com port Id
|
|
88
|
+
cmd("controller.connect 4")
|
|
89
|
+
|
|
90
|
+
# test an illegal command
|
|
91
|
+
cmd("controller.stage.position.getx")
|
|
92
|
+
|
|
93
|
+
# get current XY position in default units of microns
|
|
94
|
+
cmd("controller.stage.position.get")
|
|
95
|
+
|
|
96
|
+
# re-define this current position as 1234,5678
|
|
97
|
+
cmd("controller.stage.position.set 1234 5678")
|
|
98
|
+
|
|
99
|
+
# check it worked
|
|
100
|
+
cmd("controller.stage.position.get")
|
|
101
|
+
|
|
102
|
+
# set it back to 0,0
|
|
103
|
+
cmd("controller.stage.position.set 0 0")
|
|
104
|
+
cmd("controller.stage.position.get")
|
|
105
|
+
|
|
106
|
+
# start a move to a new position, normally you would poll
|
|
107
|
+
# 'controller.stage.busy.get' until response = 0
|
|
108
|
+
cmd("controller.stage.goto-position 1234 5678")
|
|
109
|
+
|
|
110
|
+
# example velocity move of 10u/s in both x and y
|
|
111
|
+
cmd("controller.stage.move-at-velocity 10 10")
|
|
112
|
+
|
|
113
|
+
# see busy status
|
|
114
|
+
cmd("controller.stage.busy.get")
|
|
115
|
+
|
|
116
|
+
# stop velocity move
|
|
117
|
+
cmd("controller.stage.move-at-velocity 0 0")
|
|
118
|
+
|
|
119
|
+
# see busy status */
|
|
120
|
+
cmd("controller.stage.busy.get")
|
|
121
|
+
|
|
122
|
+
# see new position
|
|
123
|
+
cmd("controller.stage.position.get")
|
|
124
|
+
|
|
125
|
+
# disconnect cleanly from controller
|
|
126
|
+
cmd("controller.disconnect")
|
|
127
|
+
|
|
128
|
+
else:
|
|
129
|
+
input("Press ENTER to continue...")
|