llwp 2.1.0__tar.gz → 2.1.2__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: llwp
3
- Version: 2.1.0
3
+ Version: 2.1.2
4
4
  Summary: LLWP is a fast, efficient and easy solution for exploring and assigning spectra - relying on Loomis-Wood plots.
5
5
  Author-email: Luis Bonah <bonah@ph1.uni-koeln.de>
6
6
  License-Expression: GPL-3.0-only
@@ -158,6 +158,10 @@ def status_d(func):
158
158
  return _wrapper
159
159
 
160
160
 
161
+ # Guards all modifications of matplotlib artists as well as the rendering of the figures
162
+ matplotlib_lock = threading.RLock()
163
+
164
+
161
165
  def lock_d(lock):
162
166
  @wrapt.decorator
163
167
  def _wrapper(wrapped, instance, args, kwargs):
@@ -357,6 +361,7 @@ class Config(dict):
357
361
  "flag_showseriesarrows": (True, bool),
358
362
  "flag_keeponlylastassignment": (False, bool),
359
363
  "flag_autoreloadfiles": (True, bool),
364
+ "flag_reloadlinafterdelete": (True, bool),
360
365
  "flag_lincustomfreqformat": ("", str),
361
366
  "flag_allowdocking": (True, bool),
362
367
  "flag_docksalwaysontop": (True, bool),
@@ -682,7 +687,11 @@ class PlotWidget(QWidget):
682
687
  np.zeros(shape=(0, 2, 2)), colors=config["color_cat"], capstyle="round"
683
688
  )
684
689
  self.lin_coll = self.ax.scatter(
685
- [], [], color=config["color_ref"], marker=config["plot_linmarker"], zorder=100
690
+ [],
691
+ [],
692
+ color=config["color_ref"],
693
+ marker=config["plot_linmarker"],
694
+ zorder=100,
686
695
  )
687
696
  self.ax.add_collection(self.exp_coll, autolim=False)
688
697
  self.ax.add_collection(self.cat_coll, autolim=False)
@@ -702,6 +711,7 @@ class PlotWidget(QWidget):
702
711
 
703
712
  self.update_plot()
704
713
 
714
+ @lock_d(matplotlib_lock)
705
715
  def update_plot(self):
706
716
  scaling = config["plot_yscale"]
707
717
 
@@ -877,18 +887,15 @@ class PlotWidget(QWidget):
877
887
  "s": lambda: self.move_plot("out"),
878
888
  "a": lambda: self.move_plot("left"),
879
889
  "d": lambda: self.move_plot("right"),
880
-
881
890
  "Shift+w": lambda: self.move_plot("sin"),
882
891
  "Shift+s": lambda: self.move_plot("sout"),
883
892
  "Shift+a": lambda: self.move_plot("sleft"),
884
893
  "Shift+d": lambda: self.move_plot("sright"),
885
-
886
894
  # We cannot use shortcuts that are also used in the menu bar here, as these are global shortcuts on MacOS, as the MenuBar is always active
887
895
  "Alt+w": lambda: self.change_index(0, +1),
888
896
  "Alt+s": lambda: self.change_index(0, -1),
889
897
  "Alt+a": lambda: self.change_index(1, -1),
890
898
  "Alt+d": lambda: self.change_index(1, +1),
891
-
892
899
  "Alt+Up": lambda: self.change_index(0, +1),
893
900
  "Alt+Down": lambda: self.change_index(0, -1),
894
901
  "Alt+Left": lambda: self.change_index(1, -1),
@@ -910,16 +917,17 @@ class PlotWidget(QWidget):
910
917
 
911
918
 
912
919
  drawplot_decorator = DynamicDecorator()
913
- matplotlib_lock = threading.RLock()
914
920
 
915
921
 
916
922
  ##
917
923
  ## Customized PyQt classes
918
924
  ##
919
925
  class QThread(QThread):
926
+ # Maps each function to the sequence number of its most recently created thread
920
927
  active_runnables = {}
921
928
  lock = threading.RLock()
922
929
  threads = set()
930
+ _sequence = AtomicCounter()
923
931
 
924
932
  @classmethod
925
933
  def threaded_d(cls, func):
@@ -930,26 +938,37 @@ class QThread(QThread):
930
938
 
931
939
  return wrapper
932
940
 
941
+ @classmethod
942
+ def _release_finished(cls):
943
+ # Python references are only dropped once Qt reports the thread as finished.
944
+ # Dropping them earlier (e.g. from the finished signal, which is emitted
945
+ # before the thread has fully stopped) can destroy a running QThread.
946
+ with cls.lock:
947
+ for thread in [t for t in cls.threads if t.isFinished()]:
948
+ thread.wait()
949
+ cls.threads.discard(thread)
950
+
933
951
  def __init__(self, function, *args, **kwargs):
934
952
  super().__init__()
935
953
  self.function, self.args, self.kwargs = function, args, kwargs
936
- self.threads.add(self)
937
- self.finished.connect(lambda x=self: self.threads.remove(x))
954
+ with self.lock:
955
+ self._release_finished()
956
+ # The sequence number is assigned at creation (not when the thread
957
+ # starts running), so the newest request always wins and the result
958
+ # does not depend on OS scheduling or on reused thread idents.
959
+ self.sequence = self._sequence.increase()
960
+ self.active_runnables[self.function] = self.sequence
961
+ self.threads.add(self)
938
962
 
939
963
  def earlyreturn(self):
940
- if self.active_runnables[self.function] != self.thread_id:
964
+ if self.active_runnables.get(self.function) != self.sequence:
941
965
  raise EarlyReturnError
942
966
 
943
967
  def run(self):
944
968
  try:
945
- with self.lock:
946
- self.thread_id = threading.current_thread().ident
947
- self.active_runnables[self.function] = self.thread_id
948
969
  self.function(*self.args, **self.kwargs, thread=self)
949
970
  except EarlyReturnError:
950
971
  pass
951
- except Exception:
952
- raise
953
972
 
954
973
 
955
974
  class NTimesBehavior(QObject):
@@ -968,7 +987,7 @@ class NTimesBehavior(QObject):
968
987
  event.button() == Qt.MouseButton.LeftButton
969
988
  and event.modifiers() & Qt.KeyboardModifier.AltModifier
970
989
  ):
971
- i, ok = QInputDialog.getInt(self.widget, "Repat how many times", "N: ")
990
+ i, ok = QInputDialog.getInt(self.widget, "Repeat how many times", "N: ")
972
991
  if not ok:
973
992
  return True
974
993
 
@@ -1138,6 +1157,21 @@ class Figure(Figure):
1138
1157
 
1139
1158
 
1140
1159
  class FigureCanvas(FigureCanvas):
1160
+ # Rendering and painting run in the GUI thread, while worker threads modify the
1161
+ # artists of the figure. Both have to hold the matplotlib_lock, otherwise the
1162
+ # renderer might traverse artists that are removed at the same time.
1163
+ def draw(self):
1164
+ with matplotlib_lock:
1165
+ super().draw()
1166
+
1167
+ def paintEvent(self, event):
1168
+ with matplotlib_lock:
1169
+ super().paintEvent(event)
1170
+
1171
+ def blit(self, *args, **kwargs):
1172
+ with matplotlib_lock:
1173
+ super().blit(*args, **kwargs)
1174
+
1141
1175
  def __init__(self, *args, **kwargs):
1142
1176
  super().__init__(*args, **kwargs)
1143
1177
  self.wheelEvent = lambda event: event.ignore()
@@ -2342,6 +2376,47 @@ class NewAssignments(LinFile):
2342
2376
  file.write(pyckett.df_to_lin(df, custom_freq_format=custom_freq_format))
2343
2377
 
2344
2378
 
2379
+ def delete_assignments(assignments):
2380
+ new_assignments = None
2381
+ for lin_fname in assignments["filename"].unique():
2382
+ to_delete = assignments[assignments["filename"] == lin_fname]
2383
+ to_delete = to_delete[list(pyckett.lin_dtypes.keys())]
2384
+
2385
+ if lin_fname == "__newassignments__":
2386
+ new_assignments = NewAssignments.get_instance()
2387
+ lin = new_assignments.get_new_assignments_df().copy()
2388
+ else:
2389
+ lin = pyckett.lin_to_df(lin_fname, sort=False)
2390
+
2391
+ lin = pd.merge(lin, to_delete, how="left", indicator=True)
2392
+ lin = lin[lin["_merge"] == "left_only"]
2393
+ lin = lin.drop("_merge", axis="columns").reset_index(drop=True)
2394
+
2395
+ if lin_fname == "__newassignments__":
2396
+ new_assignments.new_assignments_df = lin
2397
+ new_assignments.load_file()
2398
+
2399
+ new_assignments_window = NewAssignmentsWindow.instance
2400
+ new_assignments_window.model.update()
2401
+ else:
2402
+ # Saving the *.lin file after removing the transitions
2403
+ # can overwrite custom formats for the x and error values
2404
+ with open(lin_fname, "w+") as file:
2405
+ custom_freq_format = config["flag_lincustomfreqformat"]
2406
+ file.write(
2407
+ pyckett.df_to_lin(lin, custom_freq_format=custom_freq_format)
2408
+ )
2409
+
2410
+ # Without the file watcher the file would not be updated otherwise
2411
+ if (
2412
+ config["flag_reloadlinafterdelete"]
2413
+ and not config["flag_autoreloadfiles"]
2414
+ ):
2415
+ lin_file = LinFile.ids.get(lin_fname)
2416
+ if lin_file:
2417
+ lin_file.load_file()
2418
+
2419
+
2345
2420
  class FileAdditionalSettingsDialog(QDialog):
2346
2421
  open_dialogs = {}
2347
2422
 
@@ -2560,6 +2635,7 @@ class LWPAx:
2560
2635
  self.indices = None
2561
2636
  self.annotation = None
2562
2637
  self.qns = None
2638
+ self.lin_df = None
2563
2639
 
2564
2640
  with matplotlib_lock:
2565
2641
  self.span = matplotlib.widgets.SpanSelector(
@@ -2577,7 +2653,11 @@ class LWPAx:
2577
2653
  np.zeros(shape=(0, 2, 2)), colors=config["color_cat"], capstyle="round"
2578
2654
  )
2579
2655
  self.lin_coll = ax.scatter(
2580
- [], [], color=config["color_ref"], marker=config["plot_linmarker"], zorder=100
2656
+ [],
2657
+ [],
2658
+ color=config["color_ref"],
2659
+ marker=config["plot_linmarker"],
2660
+ zorder=100,
2581
2661
  )
2582
2662
 
2583
2663
  with matplotlib_lock:
@@ -2591,6 +2671,7 @@ class LWPAx:
2591
2671
  ax.set_xticks([])
2592
2672
 
2593
2673
  @drawplot_decorator.d
2674
+ @lock_d(matplotlib_lock)
2594
2675
  def update(self):
2595
2676
  ax = self.ax
2596
2677
  ax.set_xlim(self.xrange)
@@ -2705,7 +2786,7 @@ class LWPAx:
2705
2786
 
2706
2787
  else:
2707
2788
  segs = np.array(((xs, xs), (ys * 0, ys))).T
2708
- colors = dataframe["color"].to_numpy()
2789
+ colors = dataframe["color"].to_numpy().copy()
2709
2790
 
2710
2791
  mask = xs == self.ref_position
2711
2792
  colors[mask] = config["color_ref"]
@@ -2713,6 +2794,7 @@ class LWPAx:
2713
2794
  self.cat_coll.set(segments=segs, colors=colors)
2714
2795
 
2715
2796
  elif datatype == "lin":
2797
+ self.lin_df = dataframe
2716
2798
  tuples = list(zip(xs, ys))
2717
2799
  tuples = tuples if len(tuples) != 0 else [[None, None]]
2718
2800
  colors = dataframe["color"].to_numpy()
@@ -2874,12 +2956,13 @@ class LWPAx:
2874
2956
 
2875
2957
  def fit_data(self, xmin, xmax):
2876
2958
  # Delete artists highlighting previous fit
2877
- if self.__class__.fit_vline is not None:
2878
- self.__class__.fit_vline.remove()
2879
- self.__class__.fit_vline = None
2880
- if self.__class__.fit_curve is not None:
2881
- self.__class__.fit_curve.remove()
2882
- self.__class__.fit_curve = None
2959
+ with matplotlib_lock:
2960
+ if self.__class__.fit_vline is not None:
2961
+ self.__class__.fit_vline.remove()
2962
+ self.__class__.fit_vline = None
2963
+ if self.__class__.fit_curve is not None:
2964
+ self.__class__.fit_curve.remove()
2965
+ self.__class__.fit_curve = None
2883
2966
 
2884
2967
  # Fit the data
2885
2968
  xmiddle, xuncert, fit_xs, fit_ys = self.fit_peak(xmin, xmax)
@@ -2887,12 +2970,13 @@ class LWPAx:
2887
2970
  QApplication.clipboard().setText(str(xmiddle))
2888
2971
 
2889
2972
  # Highlight fit in plot
2890
- self.__class__.fit_curve = self.ax.plot(
2891
- fit_xs, fit_ys, color=config["color_fit"], alpha=0.7, linewidth=1
2892
- )[0]
2893
- self.__class__.fit_vline = self.ax.axvline(
2894
- x=xmiddle, color=config["color_fit"], ls="--", alpha=1, linewidth=1
2895
- )
2973
+ with matplotlib_lock:
2974
+ self.__class__.fit_curve = self.ax.plot(
2975
+ fit_xs, fit_ys, color=config["color_fit"], alpha=0.7, linewidth=1
2976
+ )[0]
2977
+ self.__class__.fit_vline = self.ax.axvline(
2978
+ x=xmiddle, color=config["color_fit"], ls="--", alpha=1, linewidth=1
2979
+ )
2896
2980
 
2897
2981
  # Create assignment object
2898
2982
  new_assignment = {
@@ -2941,8 +3025,8 @@ class LWPAx:
2941
3025
  xmiddle, xuncert = fit_results["xmiddle"], fit_results["xuncert"]
2942
3026
  fit_xs, fit_ys = fit_results["fit_xs"], fit_results["fit_ys"]
2943
3027
  except Exception as E:
2944
- self.fitcurve = None
2945
- self.fitline = None
3028
+ self.fit_curve = None
3029
+ self.fit_line = None
2946
3030
  notify_error.emit(
2947
3031
  f"The fitting failed with the following error message : {str(E)}"
2948
3032
  )
@@ -3275,7 +3359,7 @@ class LWPWidget(QGroupBox):
3275
3359
 
3276
3360
  for i_col in range(n_widgets):
3277
3361
  thread.earlyreturn()
3278
- if i_col > n_cols:
3362
+ if i_col >= n_cols:
3279
3363
  continue
3280
3364
 
3281
3365
  refwidget = tab_widget.widget(i_col)
@@ -3341,24 +3425,28 @@ class LWPWidget(QGroupBox):
3341
3425
  thread.earlyreturn()
3342
3426
 
3343
3427
  # Set the correct values to the LWPAxes
3344
- if self.lwpaxes.shape != (n_rows, n_cols):
3345
- notify_error.emit("Shape of LWPAxes is out of sync with requested values.")
3346
- return
3428
+ # The lock makes sure that the axes are not replaced while they are updated
3429
+ with matplotlib_lock:
3430
+ if self.lwpaxes.shape != (n_rows, n_cols):
3431
+ notify_error.emit(
3432
+ "Shape of LWPAxes is out of sync with requested values."
3433
+ )
3434
+ return
3347
3435
 
3348
- for i_row in range(n_rows):
3349
- for i_col in range(n_cols):
3350
- ax = self.lwpaxes[i_row, i_col]
3351
- ax.ref_position = positions[i_row, i_col]
3352
- ax.xrange = (xmins[i_row, i_col], xmaxs[i_row, i_col])
3353
- ax.qns = qns[i_row, i_col]
3354
- ax.indices = {
3355
- label: (
3356
- min_indices[label][i_row, i_col],
3357
- max_indices[label][i_row, i_col],
3358
- )
3359
- for label in ("exp", "cat", "lin")
3360
- }
3361
- ax.update()
3436
+ for i_row in range(n_rows):
3437
+ for i_col in range(n_cols):
3438
+ ax = self.lwpaxes[i_row, i_col]
3439
+ ax.ref_position = positions[i_row, i_col]
3440
+ ax.xrange = (xmins[i_row, i_col], xmaxs[i_row, i_col])
3441
+ ax.qns = qns[i_row, i_col]
3442
+ ax.indices = {
3443
+ label: (
3444
+ min_indices[label][i_row, i_col],
3445
+ max_indices[label][i_row, i_col],
3446
+ )
3447
+ for label in ("exp", "cat", "lin")
3448
+ }
3449
+ ax.update()
3362
3450
 
3363
3451
  def get_current_ax(self):
3364
3452
  shape = self.lwpaxes.shape
@@ -3368,6 +3456,27 @@ class LWPWidget(QGroupBox):
3368
3456
  else:
3369
3457
  return self.lwpaxes[0, 0]
3370
3458
 
3459
+ def get_assignments_at(self, lwpax, event):
3460
+ """Return the assignments whose markers are under the cursor."""
3461
+ lin_df = lwpax.lin_df
3462
+ if lin_df is None or not len(lin_df):
3463
+ return pd.DataFrame()
3464
+
3465
+ ratio = self.plotcanvas.device_pixel_ratio
3466
+ height = self.plotcanvas.get_width_height(physical=True)[1]
3467
+ mouseevent = matplotlib.backend_bases.MouseEvent(
3468
+ "button_press_event",
3469
+ self.plotcanvas,
3470
+ event.x() * ratio,
3471
+ height - event.y() * ratio,
3472
+ )
3473
+ contained, info = lwpax.lin_coll.contains(mouseevent)
3474
+ if not contained:
3475
+ return pd.DataFrame()
3476
+
3477
+ assignments = lin_df.iloc[info["ind"]].copy()
3478
+ return assignments
3479
+
3371
3480
  def contextMenuCanvas(self, event):
3372
3481
  x, y = event.x(), event.y()
3373
3482
  geometry = self.plotcanvas.geometry()
@@ -3389,8 +3498,20 @@ class LWPWidget(QGroupBox):
3389
3498
  open_blend_action = menu.addAction("Open in Blended Lines")
3390
3499
  fit_all_action = menu.addAction("Fit all")
3391
3500
 
3501
+ assignments = self.get_assignments_at(lwpax, event)
3502
+ delete_assignment_action = None
3503
+ if len(assignments):
3504
+ menu.addSeparator()
3505
+ delete_assignment_action = menu.addAction(
3506
+ f"Delete assignment{'s' if len(assignments) > 1 else ''}"
3507
+ )
3508
+
3392
3509
  action = menu.exec(self.plotcanvas.mapToGlobal(event.pos()))
3393
- if action == get_position_action:
3510
+ if action is None:
3511
+ return
3512
+ if action == delete_assignment_action:
3513
+ delete_assignments(assignments)
3514
+ elif action == get_position_action:
3394
3515
  QApplication.clipboard().setText(str(lwpax.ref_position))
3395
3516
  elif action == get_qns_action:
3396
3517
  output_string = []
@@ -3761,6 +3882,12 @@ class MainWindow(QMainWindow):
3761
3882
  config.register("flag_allowdocking", self.update_docking)
3762
3883
  config.register("flag_docksalwaysontop", self.update_docksalwaysontop)
3763
3884
 
3885
+ self.setDockOptions(
3886
+ QMainWindow.DockOption.AnimatedDocks
3887
+ | QMainWindow.DockOption.AllowNestedDocks
3888
+ | QMainWindow.DockOption.AllowTabbedDocks
3889
+ )
3890
+
3764
3891
  try:
3765
3892
  # Set LLWP logo as icon
3766
3893
  possible_folders = [
@@ -6018,6 +6145,14 @@ class ReferenceSelector(QTabWidget):
6018
6145
  self.values_changed.emit()
6019
6146
  mainwindow.lwpwidget.set_data()
6020
6147
 
6148
+ def change_series_qns(self):
6149
+ i, ok = QInputDialog.getInt(
6150
+ self, "Set Number of Quantum Numbers", "# Quantum Numbers: "
6151
+ )
6152
+ if not ok:
6153
+ return
6154
+ config["series_qns"] = i
6155
+
6021
6156
  def contextMenuEvent(self, event):
6022
6157
  menu = QMenu(self)
6023
6158
  get_positions_action = menu.addAction("Copy Reference Positions")
@@ -6027,8 +6162,15 @@ class ReferenceSelector(QTabWidget):
6027
6162
 
6028
6163
  change_qns_templates = config["series_changeqnsactions"]
6029
6164
  change_qns_actions = {}
6165
+ is_transition_active = self.state["method"] == "Transition"
6166
+
6167
+ if is_transition_active:
6168
+ menu.addSeparator()
6169
+ change_series_qns_action = menu.addAction(
6170
+ "Change number of quantum numbers"
6171
+ )
6030
6172
 
6031
- if self.state["method"] == "Transition" and len(change_qns_templates):
6173
+ if is_transition_active and len(change_qns_templates):
6032
6174
  menu.addSeparator()
6033
6175
 
6034
6176
  for label in change_qns_templates.keys():
@@ -6065,6 +6207,8 @@ class ReferenceSelector(QTabWidget):
6065
6207
  self.series_selector.change_qns_action(label)
6066
6208
  elif action == list_from_cat_trend_action:
6067
6209
  self.list_from_cat_trend()
6210
+ elif is_transition_active and action == change_series_qns_action:
6211
+ self.change_series_qns()
6068
6212
 
6069
6213
 
6070
6214
  class SeriesSelector(QWidget):
@@ -6620,7 +6764,11 @@ class NewAssignmentsWindow(EQDockWidget):
6620
6764
  # tooltip_append = "Append to file if checked or overwrite content if unchecked"
6621
6765
  new_assignments = self.new_assignments = NewAssignments.get_instance()
6622
6766
 
6623
- save_label = "Save && Update" if (not config["onsave_skipall"]) and config["onsave_consent"] else "Save"
6767
+ save_label = (
6768
+ "Save && Update"
6769
+ if (not config["onsave_skipall"]) and config["onsave_consent"]
6770
+ else "Save"
6771
+ )
6624
6772
  widgets = self.widgets = {
6625
6773
  "save_settings": QQ(
6626
6774
  QToolButton,
@@ -6991,7 +7139,9 @@ class OnSaveOptionsDialog(QDialog):
6991
7139
  for widget in self.widgets:
6992
7140
  widget.setEnabled(enabled)
6993
7141
 
6994
- NewAssignmentsWindow.instance.widgets["save"].setText("Save && Update" if enabled else "Save")
7142
+ NewAssignmentsWindow.instance.widgets["save"].setText(
7143
+ "Save && Update" if enabled else "Save"
7144
+ )
6995
7145
 
6996
7146
  def on_exit(self, _=None):
6997
7147
  self.__class__.open_instance = None
@@ -7637,35 +7787,36 @@ class ResidualsWindow(EQDockWidget):
7637
7787
  colors = df["color"].to_numpy()
7638
7788
  tuples = list(zip(xs, ys))
7639
7789
  tuples = tuples if len(tuples) != 0 else [[None, None]]
7640
- self.points.set_offsets(tuples)
7641
- self.points.set_color(colors)
7642
- if len(xs) and config["residuals_autoscale"]:
7643
- xmin, xmax = np.min(xs), np.max(xs)
7644
- if xmin == xmax:
7645
- xmin -= 1
7646
- xmax += 1
7647
- self.ax.set_xlim(
7648
- [
7649
- xmin - 0.02 * (xmax - xmin),
7650
- xmax + 0.02 * (xmax - xmin),
7651
- ]
7652
- )
7653
- ymin, ymax = np.min(ys), np.max(ys)
7654
- if ymin == ymax:
7655
- ymin -= 1
7656
- ymax += 1
7657
- y_range = [ymin, ymax]
7658
- self.ax.set_ylim(
7659
- y_range[0] - config["plot_ymargin"] * (y_range[1] - y_range[0]),
7660
- y_range[1] + config["plot_ymargin"] * (y_range[1] - y_range[0]),
7661
- )
7790
+ with matplotlib_lock:
7791
+ self.points.set_offsets(tuples)
7792
+ self.points.set_color(colors)
7793
+ if len(xs) and config["residuals_autoscale"]:
7794
+ xmin, xmax = np.min(xs), np.max(xs)
7795
+ if xmin == xmax:
7796
+ xmin -= 1
7797
+ xmax += 1
7798
+ self.ax.set_xlim(
7799
+ [
7800
+ xmin - 0.02 * (xmax - xmin),
7801
+ xmax + 0.02 * (xmax - xmin),
7802
+ ]
7803
+ )
7804
+ ymin, ymax = np.min(ys), np.max(ys)
7805
+ if ymin == ymax:
7806
+ ymin -= 1
7807
+ ymax += 1
7808
+ y_range = [ymin, ymax]
7809
+ self.ax.set_ylim(
7810
+ y_range[0] - config["plot_ymargin"] * (y_range[1] - y_range[0]),
7811
+ y_range[1] + config["plot_ymargin"] * (y_range[1] - y_range[0]),
7812
+ )
7813
+
7662
7814
  notify_info.emit("<br/>".join(message))
7663
7815
  except Exception:
7664
7816
  notify_warning.emit("There was an error in your Residuals window input")
7665
7817
  finally:
7666
7818
  self.plotting_finished.emit()
7667
7819
 
7668
-
7669
7820
  def save_residuals(self):
7670
7821
  df = self.get_residuals()
7671
7822
  fname = QFileDialog.getSaveFileName(
@@ -7734,52 +7885,12 @@ class ResidualsWindow(EQDockWidget):
7734
7885
  mainwindow.raise_()
7735
7886
  mainwindow.activateWindow()
7736
7887
  elif action_to_perform == "deletefromfile":
7737
- unique_lin_fnames = tmp_transitions["filename_lin"].unique()
7738
- for lin_fname in unique_lin_fnames:
7739
- if lin_fname == "__newassignments__":
7740
- new_assignments = NewAssignments.get_instance()
7741
- lin = new_assignments.get_new_assignments_df().copy()
7742
- else:
7743
- lin = pyckett.lin_to_df(lin_fname, sort=False)
7744
-
7745
- transitions_to_delete = tmp_transitions.query(
7746
- "filename_lin == @lin_fname"
7747
- )
7748
- columns = {
7749
- x: x.replace("_lin", "").replace("_x", "")
7750
- for x in transitions_to_delete.columns
7751
- if (x.endswith("_lin") or x.endswith("_x"))
7752
- }
7753
- transitions_to_delete = transitions_to_delete.rename(
7754
- columns=columns
7755
- )
7756
- transitions_to_delete = transitions_to_delete[
7757
- list(pyckett.lin_dtypes.keys())
7758
- ]
7759
-
7760
- lin = pd.merge(
7761
- lin, transitions_to_delete, how="left", indicator=True
7762
- )
7763
- lin = lin[lin["_merge"] == "left_only"]
7764
- lin = lin.drop("_merge", axis="columns").reset_index(drop=True)
7765
-
7766
- if lin_fname == "__newassignments__":
7767
- new_assignments.new_assignments_df = lin
7768
- new_assignments.load_file()
7769
-
7770
- new_assignments_window = NewAssignmentsWindow.instance
7771
- new_assignments_window.model.update()
7772
- # new_assignments_window.model.resize_columns()
7773
- else:
7774
- # Saving the *.lin file after removing the transitions
7775
- # can overwrite custom formats for the x and error values
7776
- with open(lin_fname, "w+") as file:
7777
- custom_freq_format = config["flag_lincustomfreqformat"]
7778
- file.write(
7779
- pyckett.df_to_lin(
7780
- lin, custom_freq_format=custom_freq_format
7781
- )
7782
- )
7888
+ columns = {
7889
+ x: x.replace("_lin", "").replace("_x", "")
7890
+ for x in tmp_transitions.columns
7891
+ if (x.endswith("_lin") or x.endswith("_x"))
7892
+ }
7893
+ delete_assignments(tmp_transitions.rename(columns=columns))
7783
7894
 
7784
7895
  def fit_file_context_menu(self, pos):
7785
7896
  menu = QMenu(self)
@@ -7835,7 +7946,6 @@ class BlendedLinesWindow(EQDockWidget):
7835
7946
  [], [], color=config["blendedlines_color_total"]
7836
7947
  )[0]
7837
7948
  self.plot_parts = []
7838
- self.plot_parts_lock = threading.Lock()
7839
7949
  self.update_plot_requested.connect(super().update_plot)
7840
7950
 
7841
7951
  def update_plot(self):
@@ -7976,7 +8086,7 @@ class BlendedLinesWindow(EQDockWidget):
7976
8086
  x, profile, derivative, 0, *args, fixedwidth=fixedwidth
7977
8087
  )
7978
8088
 
7979
- with plot_widget.plot_parts_lock:
8089
+ with matplotlib_lock:
7980
8090
  for part in plot_widget.plot_parts:
7981
8091
  part.remove()
7982
8092
  plot_widget.plot_parts = []
@@ -8076,8 +8186,9 @@ class BlendedLinesWindow(EQDockWidget):
8076
8186
 
8077
8187
  ax = plot_widget.ax
8078
8188
 
8079
- plot_widget.fit_line.set_data(res_xs, res_ys)
8080
- plot_widget.fit_line.set_color(config["blendedlines_color_total"])
8189
+ with matplotlib_lock:
8190
+ plot_widget.fit_line.set_data(res_xs, res_ys)
8191
+ plot_widget.fit_line.set_color(config["blendedlines_color_total"])
8081
8192
 
8082
8193
  opt_param = []
8083
8194
  err_param = []
@@ -8101,7 +8212,7 @@ class BlendedLinesWindow(EQDockWidget):
8101
8212
  tmp_ys = fitfunction_no_baseline(res_xs, *tmp_params)
8102
8213
  tmp_ys += exp_mean
8103
8214
 
8104
- with plot_widget.plot_parts_lock:
8215
+ with matplotlib_lock:
8105
8216
  plot_widget.plot_parts.append(
8106
8217
  ax.plot(
8107
8218
  res_xs,
@@ -8114,7 +8225,7 @@ class BlendedLinesWindow(EQDockWidget):
8114
8225
  opt_param.append(tmp_params)
8115
8226
  err_param.append(tmp_errors)
8116
8227
 
8117
- with plot_widget.plot_parts_lock:
8228
+ with matplotlib_lock:
8118
8229
  plot_widget.plot_parts.append(
8119
8230
  ax.scatter(
8120
8231
  [x[0] for x in opt_param],
@@ -8125,7 +8236,7 @@ class BlendedLinesWindow(EQDockWidget):
8125
8236
 
8126
8237
  if polynomrank > 0 and config["blendedlines_showbaseline"]:
8127
8238
  baseline_args = popt[-polynomrank:]
8128
- with plot_widget.plot_parts_lock:
8239
+ with matplotlib_lock:
8129
8240
  plot_widget.plot_parts.append(
8130
8241
  ax.plot(
8131
8242
  res_xs,
@@ -8164,7 +8275,7 @@ class BlendedLinesWindow(EQDockWidget):
8164
8275
 
8165
8276
  peaks = self.peaks.copy()
8166
8277
  fixedwidth = config["blendedlines_fixedwidth"]
8167
- with plot_widget.plot_parts_lock:
8278
+ with matplotlib_lock:
8168
8279
  for part in plot_widget.plot_parts:
8169
8280
  part.remove()
8170
8281
  plot_widget.plot_parts = []
@@ -8337,7 +8448,7 @@ class BlendedLinesWindow(EQDockWidget):
8337
8448
 
8338
8449
  idx += delta_idx
8339
8450
 
8340
- with plot_widget.plot_parts_lock:
8451
+ with matplotlib_lock:
8341
8452
  plot_widget.plot_parts.append(
8342
8453
  ax.scatter(
8343
8454
  [x[0] for x in opt_param],
@@ -8366,8 +8477,9 @@ class BlendedLinesWindow(EQDockWidget):
8366
8477
 
8367
8478
  thread.earlyreturn()
8368
8479
 
8369
- plot_widget.fit_line.set_data(res_xs, res_ys)
8370
- plot_widget.fit_line.set_color(config["blendedlines_color_total"])
8480
+ with matplotlib_lock:
8481
+ plot_widget.fit_line.set_data(res_xs, res_ys)
8482
+ plot_widget.fit_line.set_color(config["blendedlines_color_total"])
8371
8483
 
8372
8484
  self.fill_table_requested.emit()
8373
8485
  self.set_indicator_text.emit("Ready")
@@ -9097,28 +9209,29 @@ class EnergyLevelsWindow(EQDockWidget):
9097
9209
  colors = df["color"].to_numpy()
9098
9210
  tuples = list(zip(xs, ys))
9099
9211
  tuples = tuples if len(tuples) != 0 else [[None, None]]
9100
- self.points.set_offsets(tuples)
9101
- self.points.set_color(colors)
9102
- if len(xs) and config["energylevels_autoscale"]:
9103
- xmin, xmax = np.min(xs), np.max(xs)
9104
- if xmin == xmax:
9105
- xmin -= 1
9106
- xmax += 1
9107
- self.ax.set_xlim(
9108
- [
9109
- xmin - 0.02 * (xmax - xmin),
9110
- xmax + 0.02 * (xmax - xmin),
9111
- ]
9112
- )
9113
- ymin, ymax = np.min(ys), np.max(ys)
9114
- if ymin == ymax:
9115
- ymin -= 1
9116
- ymax += 1
9117
- y_range = [ymin, ymax]
9118
- self.ax.set_ylim(
9119
- y_range[0] - config["plot_ymargin"] * (y_range[1] - y_range[0]),
9120
- y_range[1] + config["plot_ymargin"] * (y_range[1] - y_range[0]),
9121
- )
9212
+ with matplotlib_lock:
9213
+ self.points.set_offsets(tuples)
9214
+ self.points.set_color(colors)
9215
+ if len(xs) and config["energylevels_autoscale"]:
9216
+ xmin, xmax = np.min(xs), np.max(xs)
9217
+ if xmin == xmax:
9218
+ xmin -= 1
9219
+ xmax += 1
9220
+ self.ax.set_xlim(
9221
+ [
9222
+ xmin - 0.02 * (xmax - xmin),
9223
+ xmax + 0.02 * (xmax - xmin),
9224
+ ]
9225
+ )
9226
+ ymin, ymax = np.min(ys), np.max(ys)
9227
+ if ymin == ymax:
9228
+ ymin -= 1
9229
+ ymax += 1
9230
+ y_range = [ymin, ymax]
9231
+ self.ax.set_ylim(
9232
+ y_range[0] - config["plot_ymargin"] * (y_range[1] - y_range[0]),
9233
+ y_range[1] + config["plot_ymargin"] * (y_range[1] - y_range[0]),
9234
+ )
9122
9235
  except Exception:
9123
9236
  notify_error.emit("There was an error in your Energy Levels window inputs")
9124
9237
  raise
@@ -9197,6 +9310,7 @@ class PeakfinderWindow(EQDockWidget):
9197
9310
  [], [], color=config["peakfinder_peakcolor"], marker="*"
9198
9311
  )
9199
9312
 
9313
+ @lock_d(matplotlib_lock)
9200
9314
  def update_plot(self):
9201
9315
  xmin, xmax = self.xrange
9202
9316
  tuples = list(filter(lambda x: xmin < x[0] < xmax, self.parent.peaks))
@@ -9221,7 +9335,7 @@ class PeakfinderWindow(EQDockWidget):
9221
9335
  layout.addLayout(tmp_layout)
9222
9336
 
9223
9337
  self.run_button = QQ(
9224
- QPushButton, text="Run Peakfinder", change=lambda: self.find_peaks()
9338
+ QPushButton, text="Run Peakfinder", change=lambda: self.on_run_clicked()
9225
9339
  )
9226
9340
  tmp_layout.addWidget(self.run_button)
9227
9341
  tmp_layout.addWidget(
@@ -9345,11 +9459,15 @@ class PeakfinderWindow(EQDockWidget):
9345
9459
  config["peakfinder_kwargs"] = kwargs
9346
9460
  return kwargs.copy()
9347
9461
 
9462
+ def on_run_clicked(self):
9463
+ self.run_button.setEnabled(False)
9464
+ kwargs = self.get_kwargs()
9465
+ self.find_peaks(kwargs)
9466
+
9348
9467
  @QThread.threaded_d
9349
9468
  @peak_locked
9350
- def find_peaks(self, thread=None):
9469
+ def find_peaks(self, kwargs, thread=None):
9351
9470
  self.peakfinding_started.emit()
9352
- kwargs = self.get_kwargs()
9353
9471
 
9354
9472
  if "frequency" in kwargs:
9355
9473
  val = kwargs["frequency"]
@@ -10164,9 +10282,9 @@ def bin_data(dataframe, binwidth, range):
10164
10282
  bin_array = ((dataframe["x"].to_numpy() - range[0]) // binwidth).astype(np.int64)
10165
10283
  value_column = "x" if "y" not in dataframe else "y"
10166
10284
 
10167
- index_ = dataframe.groupby(
10168
- [bin_array, dataframe["filename"]], observed=True
10169
- )[value_column].idxmax()
10285
+ index_ = dataframe.groupby([bin_array, dataframe["filename"]], observed=True)[
10286
+ value_column
10287
+ ].idxmax()
10170
10288
  return dataframe.loc[index_.to_numpy()]
10171
10289
 
10172
10290
 
@@ -10575,7 +10693,11 @@ class ASAPAx(LWPAx):
10575
10693
  np.zeros(shape=(0, 2, 2)), colors=config["color_exp"], capstyle="round"
10576
10694
  )
10577
10695
  self.lin_coll = self.ax.scatter(
10578
- [], [], color=config["color_ref"], marker=config["plot_linmarker"], zorder=100
10696
+ [],
10697
+ [],
10698
+ color=config["color_ref"],
10699
+ marker=config["plot_linmarker"],
10700
+ zorder=100,
10579
10701
  )
10580
10702
 
10581
10703
  with matplotlib_lock:
@@ -10617,6 +10739,7 @@ class ASAPAx(LWPAx):
10617
10739
  self.exp_coll.set(segments=segs, color=color)
10618
10740
 
10619
10741
  @drawplot_decorator.d
10742
+ @lock_d(matplotlib_lock)
10620
10743
  def update(self):
10621
10744
  ax = self.ax
10622
10745
 
@@ -10786,12 +10909,13 @@ class ASAPAx(LWPAx):
10786
10909
  return
10787
10910
 
10788
10911
  # Delete artists highlighting previous fit
10789
- if self.__class__.fit_vline is not None:
10790
- self.__class__.fit_vline.remove()
10791
- self.__class__.fit_vline = None
10792
- if self.__class__.fit_curve is not None:
10793
- self.__class__.fit_curve.remove()
10794
- self.__class__.fit_curve = None
10912
+ with matplotlib_lock:
10913
+ if self.__class__.fit_vline is not None:
10914
+ self.__class__.fit_vline.remove()
10915
+ self.__class__.fit_vline = None
10916
+ if self.__class__.fit_curve is not None:
10917
+ self.__class__.fit_curve.remove()
10918
+ self.__class__.fit_curve = None
10795
10919
 
10796
10920
  if onclick:
10797
10921
  xmiddle = xmin
@@ -10800,14 +10924,16 @@ class ASAPAx(LWPAx):
10800
10924
  else:
10801
10925
  # Fit the data
10802
10926
  xmiddle, xuncert, fit_xs, fit_ys = self.fit_peak(xmin, xmax)
10803
- self.__class__.fit_curve = self.ax.plot(
10804
- fit_xs, fit_ys, color=config["color_fit"], alpha=0.7, linewidth=1
10805
- )[0]
10927
+ with matplotlib_lock:
10928
+ self.__class__.fit_curve = self.ax.plot(
10929
+ fit_xs, fit_ys, color=config["color_fit"], alpha=0.7, linewidth=1
10930
+ )[0]
10806
10931
 
10807
10932
  # Highlight fit in plot
10808
- self.__class__.fit_vline = self.ax.axvline(
10809
- x=xmiddle, color=config["color_fit"], ls="--", alpha=1, linewidth=1
10810
- )
10933
+ with matplotlib_lock:
10934
+ self.__class__.fit_vline = self.ax.axvline(
10935
+ x=xmiddle, color=config["color_fit"], ls="--", alpha=1, linewidth=1
10936
+ )
10811
10937
 
10812
10938
  # Emit signal for detail viewer
10813
10939
  self.x_fit = xmiddle
@@ -10904,8 +11030,8 @@ class ASAPAx(LWPAx):
10904
11030
  fit_xs, fit_ys = fit_results["fit_xs"], fit_results["fit_ys"]
10905
11031
 
10906
11032
  except Exception as E:
10907
- self.fitcurve = None
10908
- self.fitline = None
11033
+ self.fit_curve = None
11034
+ self.fit_line = None
10909
11035
  notify_error.emit(
10910
11036
  f"The fitting failed with the following error message : {str(E)}"
10911
11037
  )
@@ -11259,12 +11385,13 @@ class ASAPWidget(LWPWidget):
11259
11385
  @status_d
11260
11386
  @drawplot_decorator.d
11261
11387
  def calc_correlation_plots(self, thread=None):
11262
- if self._ax_class.fit_vline is not None:
11263
- self._ax_class.fit_vline.remove()
11264
- self._ax_class.fit_vline = None
11265
- if self._ax_class.fit_curve is not None:
11266
- self._ax_class.fit_curve.remove()
11267
- self._ax_class.fit_curve = None
11388
+ with matplotlib_lock:
11389
+ if self._ax_class.fit_vline is not None:
11390
+ self._ax_class.fit_vline.remove()
11391
+ self._ax_class.fit_vline = None
11392
+ if self._ax_class.fit_curve is not None:
11393
+ self._ax_class.fit_curve.remove()
11394
+ self._ax_class.fit_curve = None
11268
11395
 
11269
11396
  if not hasattr(ASAPSettingsWindow, "instance"):
11270
11397
  return
@@ -11294,7 +11421,7 @@ class ASAPWidget(LWPWidget):
11294
11421
 
11295
11422
  for i_col in range(n_widgets):
11296
11423
  thread.earlyreturn()
11297
- if i_col > n_cols:
11424
+ if i_col >= n_cols:
11298
11425
  continue
11299
11426
 
11300
11427
  refwidget = tab_widget.widget(i_col)
@@ -11803,10 +11930,22 @@ class ASAPSquaredWindow(EQDockWidget):
11803
11930
 
11804
11931
  tmp_layout = QHBoxLayout()
11805
11932
  tmp_layout.addWidget(QQ(QLabel, text="Width: "))
11806
- tmp_layout.addWidget(QQ(QDoubleSpinBoxFullPrec, "asap_squaredwidth", width=120))
11933
+ tmp_layout.addWidget(
11934
+ QQ(
11935
+ QDoubleSpinBoxFullPrec,
11936
+ "asap_squaredwidth",
11937
+ width=120,
11938
+ range=(None, None),
11939
+ )
11940
+ )
11807
11941
  tmp_layout.addWidget(QQ(QLabel, text="Stepsize: "))
11808
11942
  tmp_layout.addWidget(
11809
- QQ(QDoubleSpinBoxFullPrec, "asap_squaredstepsize", width=120)
11943
+ QQ(
11944
+ QDoubleSpinBoxFullPrec,
11945
+ "asap_squaredstepsize",
11946
+ width=120,
11947
+ range=(None, None),
11948
+ )
11810
11949
  )
11811
11950
  tmp_layout.addWidget(QQ(QLabel, text="Threshold: "))
11812
11951
  tmp_layout.addWidget(
@@ -11870,12 +12009,15 @@ class ASAPSquaredWindow(EQDockWidget):
11870
12009
  self.noq = config["series_qns"]
11871
12010
  assignments = []
11872
12011
 
11873
- for ax in self.fig.get_axes():
11874
- self.fig.delaxes(ax)
11875
- self.ax = self.fig.subplots()
11876
- self.span_selector = matplotlib.widgets.SpanSelector(
11877
- self.ax, self.on_range, "horizontal", useblit=True
11878
- )
12012
+ with matplotlib_lock:
12013
+ for ax in self.fig.get_axes():
12014
+ self.fig.delaxes(ax)
12015
+ self.fit_vline = None
12016
+ self.fit_curve = None
12017
+ self.ax = self.fig.subplots()
12018
+ self.span_selector = matplotlib.widgets.SpanSelector(
12019
+ self.ax, self.on_range, "horizontal", useblit=True
12020
+ )
11879
12021
 
11880
12022
  width = config["asap_squaredwidth"] or config["plot_width"]
11881
12023
  stepsize = config["asap_squaredstepsize"] or config["asap_stepsize"]
@@ -11943,9 +12085,10 @@ class ASAPSquaredWindow(EQDockWidget):
11943
12085
  tot_ys /= ys_max
11944
12086
  log_y_max += np.log10(ys_max)
11945
12087
 
11946
- self.ax.plot(rel_xs, tot_ys, color=config["color_exp"])
11947
- if config["asap_squaredplotlog"]:
11948
- self.ax.set_yscale("log")
12088
+ with matplotlib_lock:
12089
+ self.ax.plot(rel_xs, tot_ys, color=config["color_exp"])
12090
+ if config["asap_squaredplotlog"]:
12091
+ self.ax.set_yscale("log")
11949
12092
 
11950
12093
  self.data = np.vstack((rel_xs, tot_ys)).T
11951
12094
  self.drawplot.emit()
@@ -12016,12 +12159,13 @@ class ASAPSquaredWindow(EQDockWidget):
12016
12159
  @status_d
12017
12160
  def fit_data(self, xmin, xmax, thread=None):
12018
12161
  # Delete artists highlighting previous fit
12019
- if self.fit_vline is not None:
12020
- self.fit_vline.remove()
12021
- self.fit_vline = None
12022
- if self.fit_curve is not None:
12023
- self.fit_curve.remove()
12024
- self.fit_curve = None
12162
+ with matplotlib_lock:
12163
+ if self.fit_vline is not None:
12164
+ self.fit_vline.remove()
12165
+ self.fit_vline = None
12166
+ if self.fit_curve is not None:
12167
+ self.fit_curve.remove()
12168
+ self.fit_curve = None
12025
12169
 
12026
12170
  if self.data is None:
12027
12171
  notify_warning.emit("No cross-correlation data available.")
@@ -12054,8 +12198,8 @@ class ASAPSquaredWindow(EQDockWidget):
12054
12198
  f"The following parameters were determined: \n{popt=}\n{perr=}"
12055
12199
  )
12056
12200
  except Exception as E:
12057
- self.fitcurve = None
12058
- self.fitline = None
12201
+ self.fit_curve = None
12202
+ self.fit_vline = None
12059
12203
  notify_error.emit(
12060
12204
  f"The fitting failed with the following error message : {str(E)}"
12061
12205
  )
@@ -12065,12 +12209,13 @@ class ASAPSquaredWindow(EQDockWidget):
12065
12209
  self.clipboard_requested.emit(str(xmiddle))
12066
12210
 
12067
12211
  # Highlight fit in plot
12068
- self.fit_curve = self.ax.plot(
12069
- fit_xs, fit_ys, color=config["color_fit"], alpha=0.7, linewidth=1
12070
- )[0]
12071
- self.fit_vline = self.ax.axvline(
12072
- x=xmiddle, color=config["color_fit"], ls="--", alpha=1, linewidth=1
12073
- )
12212
+ with matplotlib_lock:
12213
+ self.fit_curve = self.ax.plot(
12214
+ fit_xs, fit_ys, color=config["color_fit"], alpha=0.7, linewidth=1
12215
+ )[0]
12216
+ self.fit_vline = self.ax.axvline(
12217
+ x=xmiddle, color=config["color_fit"], ls="--", alpha=1, linewidth=1
12218
+ )
12074
12219
  self.xmiddle = xmiddle
12075
12220
  self.xuncert = xuncert
12076
12221
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: llwp
3
- Version: 2.1.0
3
+ Version: 2.1.2
4
4
  Summary: LLWP is a fast, efficient and easy solution for exploring and assigning spectra - relying on Loomis-Wood plots.
5
5
  Author-email: Luis Bonah <bonah@ph1.uni-koeln.de>
6
6
  License-Expression: GPL-3.0-only
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "llwp"
7
- version = "2.1.0"
7
+ version = "2.1.2"
8
8
  authors = [
9
9
  { name="Luis Bonah", email="bonah@ph1.uni-koeln.de" },
10
10
  ]
@@ -30,3 +30,6 @@ dev = [
30
30
  [project.scripts]
31
31
  llwp = "llwp:start_llwp"
32
32
  asap = "llwp:start_asap"
33
+
34
+ [tool.setuptools]
35
+ include-package-data = true
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes