|
4 | 4 | import sys |
5 | 5 |
|
6 | 6 | from PySide6.QtCore import QThreadPool, QTimer, Signal, Qt |
7 | | -from PySide6.QtWidgets import QFileDialog, QMessageBox |
| 7 | +from PySide6.QtWidgets import QFileDialog, QMessageBox, QProgressDialog |
8 | 8 |
|
| 9 | +from matplotlib.axes import Axes |
9 | 10 | from matplotlib.backends.backend_qtagg import FigureCanvas |
10 | 11 | from matplotlib.figure import Figure |
11 | 12 | from matplotlib.lines import Line2D |
|
20 | 21 |
|
21 | 22 | from hexrd import distortion as distortion_pkg |
22 | 23 |
|
| 24 | +from hexrdgui import utils |
23 | 25 | from hexrdgui.async_worker import AsyncWorker |
24 | 26 | from hexrdgui.blit_manager import BlitManager |
25 | 27 | from hexrdgui.calibration.cartesian_plot import cartesian_viewer |
|
33 | 35 | from hexrdgui.masking.create_polar_mask import create_polar_line_data_from_raw |
34 | 36 | from hexrdgui.masking.mask_manager import MaskManager |
35 | 37 | from hexrdgui.snip_viewer_dialog import SnipViewerDialog |
36 | | -from hexrdgui import utils |
| 38 | +from hexrdgui.waterfall_plot import WaterfallPlotDialog |
37 | 39 | from hexrdgui.utils.array import split_array |
38 | 40 | from hexrdgui.utils.conversions import ( |
39 | 41 | angles_to_stereo, cart_to_angles, cart_to_pixels, q_to_tth, tth_to_q, |
@@ -76,6 +78,7 @@ def __init__(self, parent=None, image_names=None): |
76 | 78 | self.raw_view_images_dict = {} |
77 | 79 | self._mask_boundary_artists = [] |
78 | 80 | self._latest_compute_view_worker = None |
| 81 | + self._waterfall_plot_dialog = None |
79 | 82 |
|
80 | 83 | # Track the current mode so that we can more lazily clear on change. |
81 | 84 | self.mode = None |
@@ -1157,55 +1160,10 @@ def finish_show_polar(self, iviewer): |
1157 | 1160 | HexrdConfig().last_unscaled_azimuthal_integral_data = unscaled |
1158 | 1161 |
|
1159 | 1162 | self.azimuthal_integral_axis = axis |
1160 | | - axis.set_ylabel(r'Azimuthal Average', **self.label_kwargs) |
1161 | 1163 | self.update_azimuthal_plot_overlays() |
1162 | 1164 | self.update_wppf_plot() |
1163 | 1165 |
|
1164 | | - # Set up formatting for the x-axis |
1165 | | - default_formatter = axis.xaxis.get_major_formatter() |
1166 | | - f = self.format_polar_x_major_ticks |
1167 | | - formatter = PolarXAxisFormatter(default_formatter, f) |
1168 | | - axis.xaxis.set_major_formatter(formatter) |
1169 | | - |
1170 | | - axis.yaxis.set_major_locator(AutoLocator()) |
1171 | | - axis.yaxis.set_minor_locator(AutoMinorLocator()) |
1172 | | - |
1173 | | - axis.xaxis.set_major_locator(PolarXAxisTickLocator(self)) |
1174 | | - self.axis.xaxis.set_minor_locator( |
1175 | | - PolarXAxisMinorTickLocator(self) |
1176 | | - ) |
1177 | | - |
1178 | | - # change property of ticks |
1179 | | - axis.tick_params(**self.major_tick_kwargs) |
1180 | | - axis.tick_params(**self.minor_tick_kwargs) |
1181 | | - |
1182 | | - # add grid lines parallel to x-axis in azimuthal average |
1183 | | - kwargs = { |
1184 | | - 'visible': True, |
1185 | | - 'which': 'major', |
1186 | | - 'axis': 'y', |
1187 | | - 'linewidth': 0.25, |
1188 | | - 'linestyle': '-', |
1189 | | - 'color': 'k', |
1190 | | - 'alpha': 0.75, |
1191 | | - } |
1192 | | - axis.grid(**kwargs) |
1193 | | - |
1194 | | - kwargs = { |
1195 | | - 'visible': True, |
1196 | | - 'which': 'minor', |
1197 | | - 'axis': 'y', |
1198 | | - 'linewidth': 0.075, |
1199 | | - 'linestyle': '--', |
1200 | | - 'color': 'k', |
1201 | | - 'alpha': 0.9, |
1202 | | - } |
1203 | | - axis.grid(**kwargs) |
1204 | | - |
1205 | | - # add grid lines parallel to y-axis |
1206 | | - kwargs['which'] = 'both' |
1207 | | - kwargs['axis'] = 'x' |
1208 | | - axis.grid(**kwargs) |
| 1166 | + self._setup_azimuthal_axis(axis) |
1209 | 1167 | else: |
1210 | 1168 | self.update_azimuthal_integral_plot() |
1211 | 1169 | axis = self.azimuthal_integral_axis |
@@ -1331,6 +1289,65 @@ def on_beam_energy_modified(self): |
1331 | 1289 | # Update the beam energy on the instrument |
1332 | 1290 | self.iviewer.instr.beam_energy = HexrdConfig().beam_energy |
1333 | 1291 |
|
| 1292 | + def _setup_azimuthal_axis(self, axis: Axes): |
| 1293 | + # Set the labels |
| 1294 | + axis.set_xlabel(self.polar_xlabel, **self.label_kwargs) |
| 1295 | + axis.set_ylabel(r'Azimuthal Average', **self.label_kwargs) |
| 1296 | + |
| 1297 | + # Set up formatting for the x-axis |
| 1298 | + # This is important in case "Q" is on the x axis instead |
| 1299 | + # of two theta. |
| 1300 | + default_formatter = axis.xaxis.get_major_formatter() |
| 1301 | + f = self.format_polar_x_major_ticks |
| 1302 | + formatter = PolarXAxisFormatter(default_formatter, f) |
| 1303 | + axis.xaxis.set_major_formatter(formatter) |
| 1304 | + |
| 1305 | + axis.yaxis.set_major_locator(AutoLocator()) |
| 1306 | + axis.yaxis.set_minor_locator(AutoMinorLocator()) |
| 1307 | + |
| 1308 | + axis.xaxis.set_major_locator(PolarXAxisTickLocator(self)) |
| 1309 | + self.axis.xaxis.set_minor_locator( |
| 1310 | + PolarXAxisMinorTickLocator(self) |
| 1311 | + ) |
| 1312 | + |
| 1313 | + # change property of ticks |
| 1314 | + axis.tick_params(**self.major_tick_kwargs) |
| 1315 | + axis.tick_params(**self.minor_tick_kwargs) |
| 1316 | + |
| 1317 | + # Set up the grids |
| 1318 | + # These are default kwargs for the grids. |
| 1319 | + default_kwargs = { |
| 1320 | + 'visible': True, |
| 1321 | + 'linewidth': 0.075, |
| 1322 | + 'linestyle': '--', |
| 1323 | + 'color': 'k', |
| 1324 | + 'alpha': 0.9, |
| 1325 | + } |
| 1326 | + |
| 1327 | + # Grid for minor y tickers |
| 1328 | + axis.grid(**{ |
| 1329 | + **default_kwargs, |
| 1330 | + 'which': 'minor', |
| 1331 | + 'axis': 'y', |
| 1332 | + 'linewidth': 0.25, |
| 1333 | + 'linestyle': '-', |
| 1334 | + 'alpha': 0.75, |
| 1335 | + }) |
| 1336 | + |
| 1337 | + # Grid for major y tickers |
| 1338 | + axis.grid(**{ |
| 1339 | + **default_kwargs, |
| 1340 | + 'which': 'major', |
| 1341 | + 'axis': 'y', |
| 1342 | + }) |
| 1343 | + |
| 1344 | + # Grid for all x tickers |
| 1345 | + axis.grid(**{ |
| 1346 | + **default_kwargs, |
| 1347 | + 'which': 'both', |
| 1348 | + 'axis': 'x', |
| 1349 | + }) |
| 1350 | + |
1334 | 1351 | @property |
1335 | 1352 | def polar_x_axis_type(self): |
1336 | 1353 | return HexrdConfig().polar_x_axis_type |
@@ -1510,10 +1527,14 @@ def compute_azimuthal_integral_sum(self, scaled=True): |
1510 | 1527 | pimg = self.scaled_images[0] |
1511 | 1528 | else: |
1512 | 1529 | pimg = self.unscaled_images[0] |
| 1530 | + |
| 1531 | + return self._compute_azimuthal_integral_sum(pimg) |
| 1532 | + |
| 1533 | + def _compute_azimuthal_integral_sum(self, pimg: np.ndarray) -> np.ndarray: |
1513 | 1534 | # !!! NOTE: visible polar masks have already been applied |
1514 | 1535 | # in polarview.py |
1515 | | - masked = np.ma.masked_array(pimg, mask=np.isnan(pimg)) |
1516 | 1536 | offset = HexrdConfig().azimuthal_offset |
| 1537 | + masked = np.ma.masked_array(pimg, mask=np.isnan(pimg)) |
1517 | 1538 | return masked.sum(axis=0) / np.sum(~masked.mask, axis=0) + offset |
1518 | 1539 |
|
1519 | 1540 | def clear_azimuthal_overlay_artists(self): |
@@ -1767,6 +1788,126 @@ def export_current_plot(self, filename): |
1767 | 1788 |
|
1768 | 1789 | self.iviewer.write_image(filename) |
1769 | 1790 |
|
| 1791 | + def create_waterfall_plot(self): |
| 1792 | + if self.mode != ViewType.polar: |
| 1793 | + msg = 'Cannot create waterfall plot if we are not in polar mode' |
| 1794 | + raise Exception(msg) |
| 1795 | + |
| 1796 | + if not self.iviewer: |
| 1797 | + msg = 'Cannot create waterfall plot without an iviewer' |
| 1798 | + raise Exception(msg) |
| 1799 | + |
| 1800 | + if self._waterfall_plot_dialog is not None: |
| 1801 | + self._waterfall_plot_dialog.hide() |
| 1802 | + self._waterfall_plot_dialog = None |
| 1803 | + |
| 1804 | + # Determine the number of lineouts |
| 1805 | + num_lineouts = HexrdConfig().imageseries_length |
| 1806 | + |
| 1807 | + # Display a progress dialog indicating that we are |
| 1808 | + # generating intensities... |
| 1809 | + progress = QProgressDialog( |
| 1810 | + 'Generating azimuthal lineouts...', |
| 1811 | + None, |
| 1812 | + 0, |
| 1813 | + num_lineouts, |
| 1814 | + self, |
| 1815 | + ) |
| 1816 | + progress.setWindowTitle('HEXRD') |
| 1817 | + progress.setValue(1) |
| 1818 | + |
| 1819 | + # No close button in the corner |
| 1820 | + flags = progress.windowFlags() |
| 1821 | + progress.setWindowFlags( |
| 1822 | + (flags | Qt.CustomizeWindowHint) & |
| 1823 | + ~Qt.WindowCloseButtonHint |
| 1824 | + ) |
| 1825 | + |
| 1826 | + self._create_waterfall_progress = progress |
| 1827 | + |
| 1828 | + # Compute azimuthal lineouts in a background thread |
| 1829 | + worker = AsyncWorker(self._create_waterfall_lineouts) |
| 1830 | + self.thread_pool.start(worker) |
| 1831 | + self._latest_compute_view_worker = worker |
| 1832 | + |
| 1833 | + def on_finished(): |
| 1834 | + progress.reject() |
| 1835 | + |
| 1836 | + # Get the results and close the progress dialog when finished |
| 1837 | + worker.signals.result.connect(self._finish_create_waterfall) |
| 1838 | + worker.signals.finished.connect(on_finished) |
| 1839 | + |
| 1840 | + progress.exec() |
| 1841 | + |
| 1842 | + def _create_waterfall_lineouts(self) -> list[np.ndarray]: |
| 1843 | + progress = self._create_waterfall_progress |
| 1844 | + |
| 1845 | + # Determine the number of lineouts |
| 1846 | + num_lineouts = HexrdConfig().imageseries_length |
| 1847 | + lineouts = [None] * num_lineouts |
| 1848 | + |
| 1849 | + # We can already compute the lineout for the current frame |
| 1850 | + current_idx = HexrdConfig().current_imageseries_idx |
| 1851 | + lineouts[current_idx] = self.compute_azimuthal_integral_sum() |
| 1852 | + |
| 1853 | + # Make a deep copy of the iviewer, since we will modify it |
| 1854 | + iviewer = copy.deepcopy(self.iviewer) |
| 1855 | + |
| 1856 | + # Now generate the lineouts for the other frames |
| 1857 | + for i in range(num_lineouts): |
| 1858 | + if i == current_idx: |
| 1859 | + # We already generated this one |
| 1860 | + continue |
| 1861 | + |
| 1862 | + # Create the new imageseries dict |
| 1863 | + HexrdConfig().current_imageseries_idx = i |
| 1864 | + try: |
| 1865 | + new_images_dict = HexrdConfig().images_dict |
| 1866 | + finally: |
| 1867 | + # Always restore the previous index |
| 1868 | + HexrdConfig().current_imageseries_idx = current_idx |
| 1869 | + |
| 1870 | + # Now force the image dict to change |
| 1871 | + iviewer.pv.images_dict = new_images_dict |
| 1872 | + |
| 1873 | + # Generate the new image |
| 1874 | + iviewer.pv.warp_all_images() |
| 1875 | + |
| 1876 | + # Grab the new image |
| 1877 | + polar_img = iviewer.img |
| 1878 | + if HexrdConfig().polar_apply_scaling_to_lineout: |
| 1879 | + # Apply the transform |
| 1880 | + polar_img = self.transform(polar_img) |
| 1881 | + |
| 1882 | + # Compute the integration |
| 1883 | + lineouts[i] = self._compute_azimuthal_integral_sum(polar_img) |
| 1884 | + |
| 1885 | + progress.setValue(progress.value() + 1) |
| 1886 | + |
| 1887 | + return lineouts |
| 1888 | + |
| 1889 | + def _finish_create_waterfall(self, lineouts: list[np.ndarray]): |
| 1890 | + # Now create the waterfall plot dialog with the lineouts |
| 1891 | + # Create a matplotlib figure and set up everything |
| 1892 | + figure = plt.figure() |
| 1893 | + ax = figure.add_subplot() |
| 1894 | + |
| 1895 | + # Grab tth |
| 1896 | + angular_grid = self.iviewer.angular_grid |
| 1897 | + tth = np.degrees(angular_grid[1][0]) |
| 1898 | + line_data = [(tth, lineout.filled(np.nan)) for lineout in lineouts] |
| 1899 | + |
| 1900 | + # Set up the same azimuthal axes parameters as the polar view |
| 1901 | + self._setup_azimuthal_axis(ax) |
| 1902 | + |
| 1903 | + # Disable the tick labels |
| 1904 | + ax.set_yticklabels([]) |
| 1905 | + |
| 1906 | + # Now create and show the waterfall plot |
| 1907 | + dialog = WaterfallPlotDialog(ax, line_data) |
| 1908 | + dialog.show() |
| 1909 | + self._waterfall_plot_dialog = dialog |
| 1910 | + |
1770 | 1911 | def export_to_maud(self, filename): |
1771 | 1912 | if self.mode != ViewType.polar: |
1772 | 1913 | msg = 'Must be in polar mode. Cannot export.' |
|
0 commit comments