spacr 0.4.0__py3-none-any.whl → 0.4.2__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.
- spacr/__init__.py +2 -2
- spacr/core.py +14 -3
- spacr/deep_spacr.py +2 -95
- spacr/gui_core.py +301 -46
- spacr/gui_elements.py +131 -0
- spacr/gui_utils.py +24 -20
- spacr/io.py +312 -8
- spacr/measure.py +11 -12
- spacr/plot.py +2 -2
- spacr/settings.py +157 -49
- spacr/sp_stats.py +221 -0
- spacr/submodules.py +2 -2
- spacr/utils.py +115 -33
- {spacr-0.4.0.dist-info → spacr-0.4.2.dist-info}/METADATA +2 -1
- {spacr-0.4.0.dist-info → spacr-0.4.2.dist-info}/RECORD +19 -18
- {spacr-0.4.0.dist-info → spacr-0.4.2.dist-info}/LICENSE +0 -0
- {spacr-0.4.0.dist-info → spacr-0.4.2.dist-info}/WHEEL +0 -0
- {spacr-0.4.0.dist-info → spacr-0.4.2.dist-info}/entry_points.txt +0 -0
- {spacr-0.4.0.dist-info → spacr-0.4.2.dist-info}/top_level.txt +0 -0
spacr/__init__.py
CHANGED
@@ -27,7 +27,7 @@ from . import openai
|
|
27
27
|
from . import ml
|
28
28
|
from . import toxo
|
29
29
|
from . import cellpose
|
30
|
-
from . import
|
30
|
+
from . import sp_stats
|
31
31
|
from . import logger
|
32
32
|
|
33
33
|
__all__ = [
|
@@ -58,7 +58,7 @@ __all__ = [
|
|
58
58
|
"ml",
|
59
59
|
"toxo",
|
60
60
|
"cellpose",
|
61
|
-
"
|
61
|
+
"sp_stats",
|
62
62
|
"logger"
|
63
63
|
]
|
64
64
|
|
spacr/core.py
CHANGED
@@ -9,12 +9,11 @@ warnings.filterwarnings("ignore", message="3D stack used, but stitch_threshold=0
|
|
9
9
|
|
10
10
|
def preprocess_generate_masks(settings):
|
11
11
|
|
12
|
-
from .io import preprocess_img_data, _load_and_concatenate_arrays
|
12
|
+
from .io import preprocess_img_data, _load_and_concatenate_arrays, convert_to_yokogawa
|
13
13
|
from .plot import plot_image_mask_overlay, plot_arrays
|
14
|
-
from .utils import _pivot_counts_table, check_mask_folder, adjust_cell_masks, print_progress, save_settings, delete_intermedeate_files
|
14
|
+
from .utils import _pivot_counts_table, check_mask_folder, adjust_cell_masks, print_progress, save_settings, delete_intermedeate_files, format_path_for_system, normalize_src_path
|
15
15
|
from .settings import set_default_settings_preprocess_generate_masks
|
16
16
|
|
17
|
-
|
18
17
|
if 'src' in settings:
|
19
18
|
if not isinstance(settings['src'], (str, list)):
|
20
19
|
ValueError(f'src must be a string or a list of strings')
|
@@ -23,6 +22,8 @@ def preprocess_generate_masks(settings):
|
|
23
22
|
ValueError(f'src is a required parameter')
|
24
23
|
return
|
25
24
|
|
25
|
+
settings['src'] = normalize_src_path(settings['src'])
|
26
|
+
|
26
27
|
if isinstance(settings['src'], str):
|
27
28
|
settings['src'] = [settings['src']]
|
28
29
|
|
@@ -30,9 +31,19 @@ def preprocess_generate_masks(settings):
|
|
30
31
|
source_folders = settings['src']
|
31
32
|
for source_folder in source_folders:
|
32
33
|
print(f'Processing folder: {source_folder}')
|
34
|
+
|
35
|
+
if settings['metadata_type'] == 'auto':
|
36
|
+
convert_to_yokogawa(folder=source_folder)
|
37
|
+
|
38
|
+
source_folder = format_path_for_system(source_folder)
|
33
39
|
settings['src'] = source_folder
|
34
40
|
src = source_folder
|
35
41
|
settings = set_default_settings_preprocess_generate_masks(settings)
|
42
|
+
|
43
|
+
if settings['cell_channel'] is None and settings['nucleus_channel'] is None and settings['pathogen_channel'] is None:
|
44
|
+
print(f'Error: At least one of cell_channel, nucleus_channel or pathogen_channel must be defined')
|
45
|
+
return
|
46
|
+
|
36
47
|
save_settings(settings, name='gen_mask_settings')
|
37
48
|
|
38
49
|
if not settings['pathogen_channel'] is None:
|
spacr/deep_spacr.py
CHANGED
@@ -938,67 +938,8 @@ def deep_spacr(settings={}):
|
|
938
938
|
if os.path.exists(settings['model_path']):
|
939
939
|
apply_model_to_tar(settings)
|
940
940
|
|
941
|
-
def model_knowledge_transfer(
|
942
|
-
teacher_paths,
|
943
|
-
student_save_path,
|
944
|
-
data_loader, # A DataLoader with (images, labels)
|
945
|
-
device='cpu',
|
946
|
-
student_model_name='maxvit_t',
|
947
|
-
pretrained=True,
|
948
|
-
dropout_rate=None,
|
949
|
-
use_checkpoint=False,
|
950
|
-
alpha=0.5,
|
951
|
-
temperature=2.0,
|
952
|
-
lr=1e-4,
|
953
|
-
epochs=10
|
954
|
-
):
|
955
|
-
"""
|
956
|
-
Performs multi-teacher knowledge distillation on a new labeled dataset,
|
957
|
-
producing a single student TorchModel that combines (distills) the
|
958
|
-
teachers' knowledge plus the labeled data.
|
959
|
-
|
960
|
-
Usage:
|
961
|
-
student = model_knowledge_transfer(
|
962
|
-
teacher_paths=[
|
963
|
-
'teacherA.pth',
|
964
|
-
'teacherB.pth',
|
965
|
-
...
|
966
|
-
],
|
967
|
-
student_save_path='distilled_student.pth',
|
968
|
-
data_loader=my_data_loader,
|
969
|
-
device='cuda',
|
970
|
-
student_model_name='maxvit_t',
|
971
|
-
alpha=0.5,
|
972
|
-
temperature=2.0,
|
973
|
-
lr=1e-4,
|
974
|
-
epochs=10
|
975
|
-
)
|
976
|
-
|
977
|
-
Then load it via:
|
978
|
-
fused_student = torch.load('distilled_student.pth')
|
979
|
-
# fused_student is a TorchModel instance, ready for inference.
|
980
|
-
|
981
|
-
Args:
|
982
|
-
teacher_paths (list[str]): List of paths to teacher models (TorchModel
|
983
|
-
or dict with 'model' in it). They must have the same architecture
|
984
|
-
or at least produce the same dimension of output.
|
985
|
-
student_save_path (str): Destination path to save the final student
|
986
|
-
TorchModel.
|
987
|
-
data_loader (DataLoader): Yields (images, labels) from the new dataset.
|
988
|
-
device (str): 'cpu' or 'cuda'.
|
989
|
-
student_model_name (str): Architecture name for the student TorchModel.
|
990
|
-
pretrained (bool): If the student should be initialized as pretrained.
|
991
|
-
dropout_rate (float): If needed by your TorchModel init.
|
992
|
-
use_checkpoint (bool): If needed by your TorchModel init.
|
993
|
-
alpha (float): Weight balancing real-label CE vs. distillation loss
|
994
|
-
(0..1).
|
995
|
-
temperature (float): Distillation temperature (>1 typically).
|
996
|
-
lr (float): Learning rate for the student.
|
997
|
-
epochs (int): Number of training epochs.
|
941
|
+
def model_knowledge_transfer(teacher_paths, student_save_path, data_loader, device='cpu', student_model_name='maxvit_t', pretrained=True, dropout_rate=None, use_checkpoint=False, alpha=0.5, temperature=2.0, lr=1e-4, epochs=10):
|
998
942
|
|
999
|
-
Returns:
|
1000
|
-
TorchModel: The final, trained student model.
|
1001
|
-
"""
|
1002
943
|
from spacr.utils import TorchModel # Adapt if needed
|
1003
944
|
|
1004
945
|
# Adjust filename to reflect knowledge-distillation if desired
|
@@ -1101,42 +1042,8 @@ def model_knowledge_transfer(
|
|
1101
1042
|
|
1102
1043
|
return student_model
|
1103
1044
|
|
1104
|
-
def model_fusion(model_paths,
|
1105
|
-
save_path,
|
1106
|
-
device='cpu',
|
1107
|
-
model_name='maxvit_t',
|
1108
|
-
pretrained=True,
|
1109
|
-
dropout_rate=None,
|
1110
|
-
use_checkpoint=False,
|
1111
|
-
aggregator='mean'):
|
1112
|
-
"""
|
1113
|
-
Fuses an arbitrary number of TorchModel instances by combining their weights
|
1114
|
-
(using mean, geomean, median, sum, max, or min) and saves the entire fused
|
1115
|
-
model object.
|
1116
|
-
|
1117
|
-
You can later load the fused model with:
|
1118
|
-
model = torch.load('fused_model.pth')
|
1119
|
-
|
1120
|
-
which returns a ready-to-use TorchModel instance.
|
1121
|
-
|
1122
|
-
Parameters:
|
1123
|
-
model_paths (list of str): Paths to the model checkpoints to fuse.
|
1124
|
-
Each checkpoint can be:
|
1125
|
-
- A dict with keys ['model', 'model_name', ...]
|
1126
|
-
- A TorchModel instance
|
1127
|
-
save_path (str): Destination path to save the fused model.
|
1128
|
-
device (str): 'cpu' or 'cuda' for loading weights and final model device.
|
1129
|
-
model_name (str): Default model name (used if not in checkpoint).
|
1130
|
-
pretrained (bool): Default if not in checkpoint.
|
1131
|
-
dropout_rate (float): Default if not in checkpoint.
|
1132
|
-
use_checkpoint (bool): Default if not in checkpoint.
|
1133
|
-
aggregator (str): How to combine weights across models:
|
1134
|
-
'mean', 'geomean', 'median', 'sum', 'max', or 'min'.
|
1045
|
+
def model_fusion(model_paths,save_path,device='cpu',model_name='maxvit_t',pretrained=True,dropout_rate=None,use_checkpoint=False,aggregator='mean'):
|
1135
1046
|
|
1136
|
-
Returns:
|
1137
|
-
fused_model (TorchModel): The final fused TorchModel instance
|
1138
|
-
with combined weights.
|
1139
|
-
"""
|
1140
1047
|
from spacr.utils import TorchModel
|
1141
1048
|
|
1142
1049
|
if save_path.endswith('.pth'):
|
spacr/gui_core.py
CHANGED
@@ -170,49 +170,22 @@ def display_figure(fig):
|
|
170
170
|
#flash_feedback("right")
|
171
171
|
show_next_figure()
|
172
172
|
|
173
|
-
def zoom_v1(event):
|
174
|
-
nonlocal scale_factor
|
175
|
-
|
176
|
-
zoom_speed = 0.1 # Adjust the zoom speed for smoother experience
|
177
|
-
|
178
|
-
# Determine the zoom direction based on the scroll event
|
179
|
-
if event.num == 4 or event.delta > 0: # Scroll up (zoom in)
|
180
|
-
scale_factor /= (1 + zoom_speed) # Divide to zoom in
|
181
|
-
elif event.num == 5 or event.delta < 0: # Scroll down (zoom out)
|
182
|
-
scale_factor *= (1 + zoom_speed) # Multiply to zoom out
|
183
|
-
|
184
|
-
# Adjust the axes limits based on the new scale factor
|
185
|
-
for ax in canvas.figure.get_axes():
|
186
|
-
xlim = ax.get_xlim()
|
187
|
-
ylim = ax.get_ylim()
|
188
|
-
|
189
|
-
x_center = (xlim[1] + xlim[0]) / 2
|
190
|
-
y_center = (ylim[1] + ylim[0]) / 2
|
191
|
-
|
192
|
-
x_range = (xlim[1] - xlim[0]) * scale_factor
|
193
|
-
y_range = (ylim[1] - ylim[0]) * scale_factor
|
194
|
-
|
195
|
-
# Set the new limits
|
196
|
-
ax.set_xlim([x_center - x_range / 2, x_center + x_range / 2])
|
197
|
-
ax.set_ylim([y_center - y_range / 2, y_center + y_range / 2])
|
198
|
-
|
199
|
-
# Redraw the figure efficiently
|
200
|
-
canvas.draw_idle()
|
201
|
-
|
202
173
|
def zoom_test(event):
|
203
174
|
if event.num == 4: # Scroll up
|
204
175
|
print("zoom in")
|
205
176
|
elif event.num == 5: # Scroll down
|
206
177
|
print("zoom out")
|
207
|
-
|
208
|
-
def
|
209
|
-
|
178
|
+
|
179
|
+
def zoom_v1(event):
|
180
|
+
# Fixed zoom factors (adjust these if you want faster or slower zoom)
|
181
|
+
zoom_in_factor = 0.9 # When zooming in, ranges shrink by 10%
|
182
|
+
zoom_out_factor = 1.1 # When zooming out, ranges increase by 10%
|
210
183
|
|
211
184
|
# Determine the zoom direction based on the scroll event
|
212
185
|
if event.num == 4 or (hasattr(event, 'delta') and event.delta > 0): # Scroll up = zoom in
|
213
|
-
factor =
|
186
|
+
factor = zoom_in_factor
|
214
187
|
elif event.num == 5 or (hasattr(event, 'delta') and event.delta < 0): # Scroll down = zoom out
|
215
|
-
factor =
|
188
|
+
factor = zoom_out_factor
|
216
189
|
else:
|
217
190
|
return # No recognized scroll direction
|
218
191
|
|
@@ -247,23 +220,28 @@ def display_figure(fig):
|
|
247
220
|
return # No recognized scroll direction
|
248
221
|
|
249
222
|
for ax in canvas.figure.get_axes():
|
223
|
+
# Get the current mouse position in pixel coordinates
|
224
|
+
mouse_x, mouse_y = event.x, event.y
|
225
|
+
|
226
|
+
# Convert pixel coordinates to data coordinates
|
227
|
+
inv = ax.transData.inverted()
|
228
|
+
data_x, data_y = inv.transform((mouse_x, mouse_y))
|
229
|
+
|
230
|
+
# Get the current axis limits
|
250
231
|
xlim = ax.get_xlim()
|
251
232
|
ylim = ax.get_ylim()
|
252
233
|
|
253
|
-
|
254
|
-
y_center = (ylim[1] + ylim[0]) / 2
|
255
|
-
|
234
|
+
# Calculate the zooming range around the cursor position
|
256
235
|
x_range = (xlim[1] - xlim[0]) * factor
|
257
236
|
y_range = (ylim[1] - ylim[0]) * factor
|
258
237
|
|
259
|
-
#
|
260
|
-
ax.set_xlim([
|
261
|
-
ax.set_ylim([
|
238
|
+
# Adjust the limits while keeping the mouse position fixed
|
239
|
+
ax.set_xlim([data_x - (data_x - xlim[0]) * factor, data_x + (xlim[1] - data_x) * factor])
|
240
|
+
ax.set_ylim([data_y - (data_y - ylim[0]) * factor, data_y + (ylim[1] - data_y) * factor])
|
262
241
|
|
263
242
|
# Redraw the figure efficiently
|
264
243
|
canvas.draw_idle()
|
265
244
|
|
266
|
-
|
267
245
|
# Bind events for hover, click interactions, and zoom
|
268
246
|
canvas_widget.bind("<Motion>", on_hover)
|
269
247
|
canvas_widget.bind("<Leave>", on_leave)
|
@@ -854,7 +832,7 @@ def initiate_abort():
|
|
854
832
|
global thread_control, q, parent_frame
|
855
833
|
if thread_control.get("run_thread") is not None:
|
856
834
|
try:
|
857
|
-
q.put("Aborting processes...")
|
835
|
+
#q.put("Aborting processes...")
|
858
836
|
thread_control.get("run_thread").terminate()
|
859
837
|
thread_control["run_thread"] = None
|
860
838
|
q.put("Processes aborted.")
|
@@ -862,22 +840,136 @@ def initiate_abort():
|
|
862
840
|
q.put(f"Error aborting process: {e}")
|
863
841
|
|
864
842
|
thread_control = {"run_thread": None, "stop_requested": False}
|
843
|
+
|
844
|
+
def check_src_folders_files(settings, settings_type, q):
|
845
|
+
"""
|
846
|
+
Checks if 'src' is a key in the settings dictionary and if it exists as a valid path.
|
847
|
+
If 'src' is a list, iterates through the list and checks each path.
|
848
|
+
If any path is missing, prompts the user to edit or remove invalid paths.
|
849
|
+
"""
|
850
|
+
|
851
|
+
request_stop = False
|
852
|
+
|
853
|
+
def _folder_has_images(folder_path, image_extensions = {".png", ".jpg", ".jpeg", ".bmp", ".gif", ".tiff", ".tif", ".webp", ".npy", ".npz", "nd2", "czi", "lif"}):
|
854
|
+
"""Check if a folder contains any image files."""
|
855
|
+
return any(file.lower().endswith(tuple(image_extensions)) for file in os.listdir(folder_path))
|
856
|
+
|
857
|
+
def _has_folder(parent_folder, sub_folder="measure"):
|
858
|
+
"""Check if a specific sub-folder exists inside the given folder."""
|
859
|
+
return os.path.isdir(os.path.join(parent_folder, sub_folder))
|
860
|
+
|
861
|
+
from .utils import normalize_src_path
|
862
|
+
|
863
|
+
settings['src'] = normalize_src_path(settings['src'])
|
864
|
+
|
865
|
+
src_value = settings.get("src")
|
866
|
+
|
867
|
+
# **Skip if 'src' is missing**
|
868
|
+
if src_value is None:
|
869
|
+
return request_stop
|
870
|
+
|
871
|
+
# Convert single string src to a list for uniform handling
|
872
|
+
if isinstance(src_value, str):
|
873
|
+
src_list = [src_value]
|
874
|
+
elif isinstance(src_value, list):
|
875
|
+
src_list = src_value
|
876
|
+
else:
|
877
|
+
request_stop = True
|
878
|
+
return request_stop # Ensure early exit
|
879
|
+
|
880
|
+
# Identify missing paths
|
881
|
+
missing_paths = {i: path for i, path in enumerate(src_list) if not os.path.exists(path)}
|
882
|
+
|
883
|
+
if missing_paths:
|
884
|
+
q.put(f'Error: The following paths are missing: {missing_paths}')
|
885
|
+
request_stop = True
|
886
|
+
return request_stop # Ensure early exit
|
887
|
+
|
888
|
+
conditions = [True] # Initialize conditions list
|
889
|
+
|
890
|
+
for path in src_list: # Fixed: Use src_list instead of src_value
|
891
|
+
if settings_type == 'mask':
|
892
|
+
pictures_continue = _folder_has_images(path)
|
893
|
+
folder_chan_continue = _has_folder(path, "1")
|
894
|
+
folder_stack_continue = _has_folder(path, "stack")
|
895
|
+
folder_npz_continue = _has_folder(path, "norm_channel_stack")
|
896
|
+
|
897
|
+
if not pictures_continue:
|
898
|
+
if not any([folder_chan_continue, folder_stack_continue, folder_npz_continue]):
|
899
|
+
if not folder_chan_continue:
|
900
|
+
q.put(f"Error: Missing channel folder in folder: {path}")
|
901
|
+
|
902
|
+
if not folder_stack_continue:
|
903
|
+
q.put(f"Error: Missing stack folder in folder: {path}")
|
904
|
+
|
905
|
+
if not folder_npz_continue:
|
906
|
+
q.put(f"Error: Missing norm_channel_stack folder in folder: {path}")
|
907
|
+
else:
|
908
|
+
q.put(f"Error: No images in folder: {path}")
|
909
|
+
|
910
|
+
#q.put(f"path:{path}")
|
911
|
+
#q.put(f"pictures_continue:{pictures_continue}, folder_chan_continue:{folder_chan_continue}, folder_stack_continue:{folder_stack_continue}, folder_npz_continue:{folder_npz_continue}")
|
912
|
+
|
913
|
+
conditions = [pictures_continue, folder_chan_continue, folder_stack_continue, folder_npz_continue]
|
914
|
+
|
915
|
+
if settings_type == 'measure':
|
916
|
+
npy_continue = _folder_has_images(path, image_extensions={".npy"})
|
917
|
+
conditions = [npy_continue]
|
918
|
+
|
919
|
+
if settings_type == 'recruitment':
|
920
|
+
db_continue = _folder_has_images(path, image_extensions={".db"})
|
921
|
+
conditions = [db_continue]
|
922
|
+
|
923
|
+
if settings_type == 'umap':
|
924
|
+
db_continue = _folder_has_images(path, image_extensions={".db"})
|
925
|
+
conditions = [db_continue]
|
926
|
+
|
927
|
+
if settings_type == 'analyze_plaques':
|
928
|
+
conditions = [True]
|
929
|
+
|
930
|
+
if settings_type == 'map_barcodes':
|
931
|
+
conditions = [True]
|
932
|
+
|
933
|
+
if settings_type == 'regression':
|
934
|
+
db_continue = _folder_has_images(path, image_extensions={".db"})
|
935
|
+
conditions = [db_continue]
|
936
|
+
|
937
|
+
if settings_type == 'classify':
|
938
|
+
db_continue = _folder_has_images(path, image_extensions={".db"})
|
939
|
+
conditions = [db_continue]
|
940
|
+
|
941
|
+
if settings_type == 'analyze_plaques':
|
942
|
+
db_continue = _folder_has_images(path, image_extensions={".db"})
|
943
|
+
conditions = [db_continue]
|
944
|
+
|
945
|
+
if not any(conditions):
|
946
|
+
q.put(f"Error: The following path(s) is missing images or folders: {path}")
|
947
|
+
request_stop = True
|
948
|
+
|
949
|
+
return request_stop
|
865
950
|
|
866
951
|
def start_process(q=None, fig_queue=None, settings_type='mask'):
|
867
952
|
global thread_control, vars_dict, parent_frame
|
868
953
|
from .settings import check_settings, expected_types
|
869
954
|
from .gui_utils import run_function_gui, set_cpu_affinity, initialize_cuda, display_gif_in_plot_frame, print_widget_structure
|
870
|
-
|
955
|
+
|
871
956
|
if q is None:
|
872
957
|
q = Queue()
|
873
958
|
if fig_queue is None:
|
874
959
|
fig_queue = Queue()
|
875
960
|
try:
|
876
|
-
settings = check_settings(vars_dict, expected_types, q)
|
961
|
+
settings, errors = check_settings(vars_dict, expected_types, q)
|
962
|
+
|
963
|
+
if len(errors) > 0:
|
964
|
+
return
|
965
|
+
|
966
|
+
if check_src_folders_files(settings, settings_type, q):
|
967
|
+
return
|
968
|
+
|
877
969
|
except ValueError as e:
|
878
970
|
q.put(f"Error: {e}")
|
879
971
|
return
|
880
|
-
|
972
|
+
|
881
973
|
if isinstance(thread_control, dict) and thread_control.get("run_thread") is not None:
|
882
974
|
initiate_abort()
|
883
975
|
|
@@ -902,13 +994,176 @@ def start_process(q=None, fig_queue=None, settings_type='mask'):
|
|
902
994
|
|
903
995
|
# Store the process in thread_control for future reference
|
904
996
|
thread_control["run_thread"] = process
|
997
|
+
|
905
998
|
else:
|
906
999
|
q.put(f"Error: Unknown settings type '{settings_type}'")
|
907
1000
|
return
|
908
|
-
|
1001
|
+
|
909
1002
|
def process_console_queue():
|
910
1003
|
global q, console_output, parent_frame, progress_bar, process_console_queue
|
911
1004
|
|
1005
|
+
# Initialize function attribute if it doesn't exist
|
1006
|
+
if not hasattr(process_console_queue, "completed_tasks"):
|
1007
|
+
process_console_queue.completed_tasks = []
|
1008
|
+
if not hasattr(process_console_queue, "current_maximum"):
|
1009
|
+
process_console_queue.current_maximum = None
|
1010
|
+
|
1011
|
+
ansi_escape_pattern = re.compile(r'\x1B\[[0-?]*[ -/]*[@-~]')
|
1012
|
+
|
1013
|
+
spacing = 5
|
1014
|
+
|
1015
|
+
# **Configure styles for different message types**
|
1016
|
+
console_output.tag_configure("error", foreground="red", spacing3 = spacing)
|
1017
|
+
console_output.tag_configure("warning", foreground="orange", spacing3 = spacing)
|
1018
|
+
console_output.tag_configure("normal", foreground="white", spacing3 = spacing)
|
1019
|
+
|
1020
|
+
while not q.empty():
|
1021
|
+
message = q.get_nowait()
|
1022
|
+
clean_message = ansi_escape_pattern.sub('', message)
|
1023
|
+
|
1024
|
+
# **Detect Error Messages (Red)**
|
1025
|
+
if clean_message.startswith("Error:"):
|
1026
|
+
console_output.insert(tk.END, clean_message + "\n", "error")
|
1027
|
+
console_output.see(tk.END)
|
1028
|
+
#print("Run aborted due to error:", clean_message) # Debug message
|
1029
|
+
#return # **Exit immediately to stop further execution**
|
1030
|
+
|
1031
|
+
# **Detect Warning Messages (Orange)**
|
1032
|
+
elif clean_message.startswith("Warning:"):
|
1033
|
+
console_output.insert(tk.END, clean_message + "\n", "warning")
|
1034
|
+
|
1035
|
+
# **Process Progress Messages Normally**
|
1036
|
+
elif clean_message.startswith("Progress:"):
|
1037
|
+
try:
|
1038
|
+
# Extract the progress information
|
1039
|
+
match = re.search(r'Progress: (\d+)/(\d+), operation_type: ([\w\s]*),(.*)', clean_message)
|
1040
|
+
|
1041
|
+
if match:
|
1042
|
+
current_progress = int(match.group(1))
|
1043
|
+
total_progress = int(match.group(2))
|
1044
|
+
operation_type = match.group(3).strip()
|
1045
|
+
additional_info = match.group(4).strip() # Capture everything after operation_type
|
1046
|
+
|
1047
|
+
# Check if the maximum value has changed
|
1048
|
+
if process_console_queue.current_maximum != total_progress:
|
1049
|
+
process_console_queue.current_maximum = total_progress
|
1050
|
+
process_console_queue.completed_tasks = []
|
1051
|
+
|
1052
|
+
# Add the task to the completed set
|
1053
|
+
process_console_queue.completed_tasks.append(current_progress)
|
1054
|
+
|
1055
|
+
# Calculate the unique progress count
|
1056
|
+
unique_progress_count = len(np.unique(process_console_queue.completed_tasks))
|
1057
|
+
|
1058
|
+
# Update the progress bar
|
1059
|
+
if progress_bar:
|
1060
|
+
progress_bar['maximum'] = total_progress
|
1061
|
+
progress_bar['value'] = unique_progress_count
|
1062
|
+
|
1063
|
+
# Store operation type and additional info
|
1064
|
+
if operation_type:
|
1065
|
+
progress_bar.operation_type = operation_type
|
1066
|
+
progress_bar.additional_info = additional_info
|
1067
|
+
|
1068
|
+
# Update the progress label
|
1069
|
+
if progress_bar.progress_label:
|
1070
|
+
progress_bar.update_label()
|
1071
|
+
|
1072
|
+
# Clear completed tasks when progress is complete
|
1073
|
+
if unique_progress_count >= total_progress:
|
1074
|
+
process_console_queue.completed_tasks.clear()
|
1075
|
+
|
1076
|
+
except Exception as e:
|
1077
|
+
print(f"Error parsing progress message: {e}")
|
1078
|
+
|
1079
|
+
# **Insert Normal Messages with Extra Line Spacing**
|
1080
|
+
else:
|
1081
|
+
console_output.insert(tk.END, clean_message + "\n", "normal")
|
1082
|
+
|
1083
|
+
console_output.see(tk.END)
|
1084
|
+
|
1085
|
+
# **Continue processing if no error was detected**
|
1086
|
+
after_id = console_output.after(uppdate_frequency, process_console_queue)
|
1087
|
+
parent_frame.after_tasks.append(after_id)
|
1088
|
+
|
1089
|
+
def process_console_queue_v2():
|
1090
|
+
global q, console_output, parent_frame, progress_bar, process_console_queue
|
1091
|
+
|
1092
|
+
# Initialize function attribute if it doesn't exist
|
1093
|
+
if not hasattr(process_console_queue, "completed_tasks"):
|
1094
|
+
process_console_queue.completed_tasks = []
|
1095
|
+
if not hasattr(process_console_queue, "current_maximum"):
|
1096
|
+
process_console_queue.current_maximum = None
|
1097
|
+
|
1098
|
+
ansi_escape_pattern = re.compile(r'\x1B\[[0-?]*[ -/]*[@-~]')
|
1099
|
+
|
1100
|
+
while not q.empty():
|
1101
|
+
message = q.get_nowait()
|
1102
|
+
clean_message = ansi_escape_pattern.sub('', message)
|
1103
|
+
|
1104
|
+
# **Abort Execution if an Error Message is Detected**
|
1105
|
+
if clean_message.startswith("Error:"):
|
1106
|
+
console_output.insert(tk.END, clean_message + "\n", "error")
|
1107
|
+
console_output.see(tk.END)
|
1108
|
+
print("Run aborted due to error:", clean_message) # Debug message
|
1109
|
+
return # **Exit immediately to stop further execution**
|
1110
|
+
|
1111
|
+
# Check if the message contains progress information
|
1112
|
+
if clean_message.startswith("Progress:"):
|
1113
|
+
try:
|
1114
|
+
# Extract the progress information
|
1115
|
+
match = re.search(r'Progress: (\d+)/(\d+), operation_type: ([\w\s]*),(.*)', clean_message)
|
1116
|
+
|
1117
|
+
if match:
|
1118
|
+
current_progress = int(match.group(1))
|
1119
|
+
total_progress = int(match.group(2))
|
1120
|
+
operation_type = match.group(3).strip()
|
1121
|
+
additional_info = match.group(4).strip() # Capture everything after operation_type
|
1122
|
+
|
1123
|
+
# Check if the maximum value has changed
|
1124
|
+
if process_console_queue.current_maximum != total_progress:
|
1125
|
+
process_console_queue.current_maximum = total_progress
|
1126
|
+
process_console_queue.completed_tasks = []
|
1127
|
+
|
1128
|
+
# Add the task to the completed set
|
1129
|
+
process_console_queue.completed_tasks.append(current_progress)
|
1130
|
+
|
1131
|
+
# Calculate the unique progress count
|
1132
|
+
unique_progress_count = len(np.unique(process_console_queue.completed_tasks))
|
1133
|
+
|
1134
|
+
# Update the progress bar
|
1135
|
+
if progress_bar:
|
1136
|
+
progress_bar['maximum'] = total_progress
|
1137
|
+
progress_bar['value'] = unique_progress_count
|
1138
|
+
|
1139
|
+
# Store operation type and additional info
|
1140
|
+
if operation_type:
|
1141
|
+
progress_bar.operation_type = operation_type
|
1142
|
+
progress_bar.additional_info = additional_info
|
1143
|
+
|
1144
|
+
# Update the progress label
|
1145
|
+
if progress_bar.progress_label:
|
1146
|
+
progress_bar.update_label()
|
1147
|
+
|
1148
|
+
# Clear completed tasks when progress is complete
|
1149
|
+
if unique_progress_count >= total_progress:
|
1150
|
+
process_console_queue.completed_tasks.clear()
|
1151
|
+
|
1152
|
+
except Exception as e:
|
1153
|
+
print(f"Error parsing progress message: {e}")
|
1154
|
+
|
1155
|
+
else:
|
1156
|
+
# Insert non-progress messages into the console
|
1157
|
+
console_output.insert(tk.END, clean_message + "\n")
|
1158
|
+
console_output.see(tk.END)
|
1159
|
+
|
1160
|
+
# **Continue processing if no error was detected**
|
1161
|
+
after_id = console_output.after(uppdate_frequency, process_console_queue)
|
1162
|
+
parent_frame.after_tasks.append(after_id)
|
1163
|
+
|
1164
|
+
def process_console_queue_v1():
|
1165
|
+
global q, console_output, parent_frame, progress_bar, process_console_queue
|
1166
|
+
|
912
1167
|
# Initialize function attribute if it doesn't exist
|
913
1168
|
if not hasattr(process_console_queue, "completed_tasks"):
|
914
1169
|
process_console_queue.completed_tasks = []
|