Skip to content

Commit 6307577

Browse files
committed
element lines and help
1 parent f5e5f75 commit 6307577

File tree

7 files changed

+187
-21
lines changed

7 files changed

+187
-21
lines changed

omc3_gui/plotting/element_lines.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
"""
2+
Element Line Plotter
3+
--------------------
4+
5+
This module contains functions to plot element lines with pyqtgraph.
6+
"""
7+
import numpy as np
8+
import pandas as pd
9+
import tfs
10+
import pyqtgraph as pg
11+
import logging
12+
from omc3.optics_measurements.constants import S
13+
from qtpy.QtCore import Qt
14+
15+
LOGGER = logging.getLogger(__name__)
16+
LENGTH: str = "LENGTH"
17+
18+
def plot_element_lines(plot: pg.PlotWidget, data_frame: tfs.TfsDataFrame, ranges: list[tuple[str, str]], start_zero: bool):
19+
"""
20+
Plot vertical lines on the plot for elements in the data_frame in the given ranges.
21+
22+
Args:
23+
plot (pg.PlotWidget): The plot to plot the lines into.
24+
data_frame (tfs.TfsDataFrame): The data_frame to plot the lines from.
25+
ranges (list[tuple[str, str]]): A list of tuples of the form (start_element, end_element).
26+
start_zero (bool): Whether to start the plot from zero or not.
27+
"""
28+
if start_zero and not all(ranges[0][0] == r[0] for r in ranges):
29+
LOGGER.warning("Not all ranges start at the same element. Using only the first!")
30+
ranges = [ranges[0]]
31+
32+
plotItem = plot.plotItem
33+
34+
# Find start and end elements
35+
start = min(ranges, key=lambda r: data_frame.loc[r[0], S])[0]
36+
end = max(ranges, key=lambda r: data_frame.loc[r[1], S])[1]
37+
38+
# Select element range and do some wrapping gymnastics if needed
39+
if data_frame.loc[start, S] <= data_frame.loc[end, S]:
40+
s_elements = data_frame.loc[start:end, S]
41+
else:
42+
s_elements = data_frame.loc[start:, S] + data_frame.loc[:end, S]
43+
44+
if start_zero:
45+
s_elements = s_elements - s_elements.loc[start]
46+
s_elements = s_elements + np.where(s_elements < 0, data_frame.headers[LENGTH], 0)
47+
48+
# Plot the lines - this takes a while, not sure how to improve
49+
plotItem.disableAutoRange() # speeds it up a bit
50+
pen = pg.mkPen(color="grey", width=1, style=Qt.PenStyle.DotLine)
51+
for element, x in s_elements.items():
52+
plotItem.addLine(x=x, z=-10, pen=pen, label=element)
53+
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
"""
2+
Help Dialogs
3+
------------
4+
"""
5+
6+
from qtpy.QtCore import Qt
7+
from qtpy.QtWidgets import QMessageBox
8+
9+
def show_help_dialog():
10+
help_text = """
11+
<h3> Frequently Asked Questions </h3><br>
12+
13+
<br>
14+
<b> How do I open a Measurement ?</b><br>
15+
16+
One way to open measurements automatically, is to give them as command line
17+
arguments when starting the sbs_gui, either <i> -m </i> or <i> --measurements </i>.<br>
18+
If you want to load them manually, you click the <i>Load</i> button.<br><br>
19+
20+
The given/selected measurements can be either <i>omc3 optics folders, folders containing sbs-json files
21+
or sbs-json files</i> directly.
22+
The latter are created automatically in the measurement-output folder when editing a loaded measurement.<br>
23+
24+
<br>
25+
26+
<b> Do I have to invert my corrections when using them in the machine? </b><br>
27+
28+
YES! <i>(but it depends)</i><br>
29+
The "corrections" here are actually used to match the model opttics to the
30+
mesured optics (see the info about the dashed "corr" line below).
31+
Therefore you have to invert them, to actually use them as corrections in the machine. <br>
32+
<b> NOTE </b> that these are the MAD-X values. Make how the signs are actually
33+
mapped in the machine! <br>
34+
35+
<br>
36+
37+
<b> What is the solid line ?</b><br>
38+
39+
The solid line is the difference between the Measurement and
40+
the propagated model, i.e. the Measurement at the start (or end) of the segment
41+
propagated through the nominal model via MAD-X.<br>
42+
This line therefore shows you how much the optics deviate through the segment
43+
from the nominal model.<br>
44+
45+
<br>
46+
47+
<b> What is the dashed line that says "corr" ?</b><br>
48+
49+
This is the difference between the <i>"corrected"</i> propagated model and the
50+
nominal propagated model.<br>
51+
This means in both cases the measured values are used as initial conditions.
52+
What you are trying to achieve is a match between the dashed and the solid line,
53+
because that means that now your model matches the optics in the measured data.<br>
54+
55+
<br>
56+
57+
<b> What is the dashed line that says "expct" ?</b><br>
58+
59+
This is the difference between the Measurement and the "corrected" propagated model
60+
and is therefore the <i>expected</i> measured difference to the nominal model after
61+
applying the correction in the machine (same as in global correction).<br>
62+
You can activate this view via the plot-settings <i>"Expectation"</i>.<br>
63+
64+
<br>
65+
66+
<b> Shortcuts </b><br>
67+
68+
In Graph:<br>
69+
<i>Double-Click</i> : Zoom history back one step. <br>
70+
<i>Right-Click</i> : Zoom history back one step. <br>
71+
<i>Shift + Right-Click</i> : Zoom history back all steps. <br>
72+
<i>Alt + Right-Click</i> : pyqtgraph context menu. <br>
73+
74+
<br>
75+
76+
In Measurements-List:<br>
77+
<i>Double-Click</i> : Edit the Measurement.<br>
78+
79+
<br>
80+
81+
"""
82+
83+
msg_box = QMessageBox(icon=QMessageBox.Information)
84+
msg_box.setWindowTitle("Help")
85+
msg_box.setTextFormat(Qt.TextFormat.RichText)
86+
msg_box.setText(help_text)
87+
msg_box.exec_()

omc3_gui/segment_by_segment/main_controller.py

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
from typing import TYPE_CHECKING
1414

1515
from omc3.sbs_propagation import segment_by_segment
16-
from omc3.segment_by_segment.constants import corrections_madx
1716
from qtpy import QtWidgets
1817
from qtpy.QtCore import Slot
1918

@@ -723,12 +722,6 @@ def load_segments_for_measurement(self, measurement: OpticsMeasurement):
723722
segment = SegmentDataModel(measurement, *segment_tuple)
724723
measurement.try_add_segment(segment)
725724

726-
for segment in measurement.segments:
727-
corrections = measurement.output_dir / corrections_madx.format(segment.name)
728-
if corrections.exists():
729-
measurement.corrections = corrections
730-
break # for now they should all be the same corrections
731-
732725
@Slot()
733726
def save_segments(self):
734727
LOGGER.debug("Saving segments to a file.")
@@ -844,7 +837,7 @@ def plot(self, fail_ok: bool = True):
844837
log_function("Not plotting, no segments have been run.")
845838
return
846839

847-
if settings.same_start:
840+
if settings.same_start and not settings.model_s: # only an issue if they all start at 0
848841
starts = {re.sub(r"\.B[12]$", "", s.start, flags=re.IGNORECASE) for s in segments_data}
849842
if len(starts) > 1:
850843
log_function("Not plotting, segments have different start BPMs (see 'Same Start' in settings).")

omc3_gui/segment_by_segment/main_view.py

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
from qtpy.QtCore import QItemSelectionModel, QModelIndex, Qt, Signal, Slot
2323

2424
from omc3_gui.plotting.classes import DualPlotWidget
25+
from omc3_gui.segment_by_segment.help_view import show_help_dialog
2526
from omc3_gui.segment_by_segment.main_model import (
2627
MeasurementListModel,
2728
SegmentTableModel,
@@ -153,16 +154,25 @@ def _add_menus(self):
153154

154155
# insert before the last entry (which is "Exit")
155156
file_menu.insertAction(file_menu.actions()[-1], menu_settings)
156-
157-
# Clear All ---
157+
158+
# Help ---
158159
help_menu: QtWidgets.QMenu = self.get_action_by_title("Help") # defined in View-class
160+
161+
# Clear All -
159162
menu_clear_all = QtWidgets.QAction("Reload Data", self)
160163
menu_clear_all.setIcon(
161164
QtWidgets.QApplication.style().standardIcon(QtWidgets.QStyle.StandardPixmap.SP_DialogResetButton)
162165
)
163166
menu_clear_all.triggered.connect(self.sig_menu_clear_all.emit)
164167
help_menu.insertAction(help_menu.actions()[-1], menu_clear_all)
165168

169+
menu_show_help = QtWidgets.QAction("Show Help", self)
170+
menu_show_help.setIcon(
171+
QtWidgets.QApplication.style().standardIcon(QtWidgets.QStyle.StandardPixmap.SP_MessageBoxQuestion)
172+
)
173+
menu_show_help.triggered.connect(show_help_dialog) # more of a controller thing, but OK for this one
174+
help_menu.insertAction(help_menu.actions()[-1], menu_show_help)
175+
166176
def add_settings_to_menu(self, menu: str, settings: object, names: Sequence[str] | None = None, hook: callable = None):
167177
""" Add quick-access checkboxes to the menu which are connected to the respective attributes in settings.
168178
@@ -433,13 +443,6 @@ def __init__(self):
433443
self.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
434444
self.setShowGrid(True)
435445
self.setStyleSheet(MONOSPACED_TOOLTIP)
436-
437-
# def mousePressEvent(self, e: QtGui.QMouseEvent) -> None:
438-
# idx = self.indexAt(e.pos())
439-
# if e.button() == Qt.RightButton:
440-
# self.model().toggle_row(idx) # rather a controller thing?
441-
# return
442-
# super().mousePressEvent(e)
443446

444447

445448
class ColoredItemDelegate(QtWidgets.QStyledItemDelegate):

omc3_gui/segment_by_segment/measurement_model.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
MODEL_DIRECTORY,
2222
PHASE_NAME,
2323
)
24+
from omc3.segment_by_segment.constants import corrections_madx
2425
from tfs.reader import read_headers
2526

2627
from omc3_gui.ui_components.dataclass_ui import DirectoryPath, FilePath, metafield
@@ -228,16 +229,19 @@ def from_path(cls, path: Path) -> OpticsMeasurement:
228229
LOGGER.error(f"JSON errror: {e!s}\nTrying to load as optics-measurement folder.")
229230

230231
# Try to load from optics-measurement folder ---
231-
model_dir = None
232232
info = {}
233233
try:
234234
model_dir = _parse_model_dir_from_optics_measurement(path)
235235
except FileNotFoundError as e:
236236
LOGGER.error(str(e))
237237
else:
238238
info = _parse_info_from_model_dir(model_dir)
239+
info["model_dir"] = model_dir
239240

240-
return cls(measurement_dir=path, model_dir=model_dir, **info)
241+
if (path / corrections_madx).is_file():
242+
info["corrections"] = path / corrections_madx
243+
244+
return cls(measurement_dir=path, **info)
241245

242246
@classmethod
243247
def from_json(cls, path: Path, measurement_dir: Path | None = None) -> OpticsMeasurement:

omc3_gui/segment_by_segment/plotting.py

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@
77
from __future__ import annotations
88

99
from dataclasses import dataclass
10+
from functools import cache
1011
import logging
12+
from pathlib import Path
1113

1214
from omc3.definitions.optics import (
1315
S_COLUMN,
@@ -19,9 +21,13 @@
1921
ColumnsAndLabels,
2022
)
2123
from omc3.segment_by_segment.propagables import PropagableColumns
24+
from omc3.model.constants import TWISS_ELEMENTS_DAT
25+
from omc3.optics_measurements.constants import NAME
2226
from qtpy.QtCore import Qt
27+
import tfs
2328

2429
from omc3_gui.plotting.classes import DualPlotWidget
30+
from omc3_gui.plotting.element_lines import plot_element_lines
2531
from omc3_gui.plotting.latex_to_html import latex_to_html_converter
2632
from omc3_gui.plotting.tfs_plotter import plot_dataframes
2733
from omc3_gui.segment_by_segment.segment_model import SegmentDataModel
@@ -173,8 +179,7 @@ def get_data(segment: SegmentDataModel, file_name: str):
173179
x_column = S_COLUMN
174180
if settings.model_s:
175181
x_column = S_MODEL_COLUMN
176-
177-
182+
178183
# Loop over top/bottom plots ---
179184
for definition, plot in zip(definitions.plots, widget.plots):
180185
definition: PlotDefinition
@@ -189,6 +194,17 @@ def get_data(segment: SegmentDataModel, file_name: str):
189194
# continue anyway
190195
dataframes = {label: df for label, df in dataframes.items() if df is not None}
191196

197+
# Plot Model Elements ---
198+
if settings.show_model:
199+
model_dir = segments[0].measurement.model_dir
200+
bpm_ranges = [(s.start, s.end) for s in segments]
201+
plot_element_lines(
202+
plot=plot,
203+
data_frame=load_twiss_elements(model_dir),
204+
ranges=bpm_ranges,
205+
start_zero=not settings.model_s,
206+
)
207+
192208
# Loop over forward/backward plots ---
193209
for direction in ("forward", "backward"):
194210
if not getattr(settings, direction): # user activated
@@ -218,3 +234,12 @@ def get_data(segment: SegmentDataModel, file_name: str):
218234
if settings.reset_zoom:
219235
plot.enableAutoRange()
220236

237+
238+
@cache
239+
def load_twiss_elements(model_dir: Path) -> tfs.TfsDataFrame:
240+
""" Load the twiss elements from the model directory.
241+
Cache here, because that might take a moment, so better to keep the DataFrame in memory.
242+
"""
243+
df = tfs.read_tfs(model_dir / TWISS_ELEMENTS_DAT, index=NAME)
244+
df = df.loc[df.index.str.match(r"^(?!DRIFT).*"), [S_COLUMN.column]] # we actually only need the s column and headers
245+
return df

omc3_gui/segment_by_segment/settings.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ class MainSettings:
2020

2121
@dataclass(slots=True)
2222
class PlotSettings:
23+
show_model: bool = metafield("Show Model", "Show markers for the elements of the Model.", default=False)
2324
show_legend: bool = metafield("Show Legend", "Show legend.", default=True)
2425
marker_size: float = metafield("Marker Size", "Size of the markers.", default=8.5)
2526
expected: bool = metafield("Expectation", "Show expected value after correction instead of correction itself.", default=False)

0 commit comments

Comments
 (0)