From 4e2269bdaee01d7cab7ab0766604a08f25d778ec Mon Sep 17 00:00:00 2001 From: Artur Glavic Date: Thu, 9 Oct 2025 13:46:52 +0200 Subject: [PATCH] Add Jochen colormap, LT and YT map automatic colorscale adjustment --- eos/options.py | 14 ++++- eos/projection.py | 123 +++++++++++++++++++++++++++++++++++-------- eos/reduction_e2h.py | 33 ++++++++++-- 3 files changed, 140 insertions(+), 30 deletions(-) diff --git a/eos/options.py b/eos/options.py index 3db56c6..6ae0b7c 100644 --- a/eos/options.py +++ b/eos/options.py @@ -422,6 +422,7 @@ class PlotColormaps(StrEnum): inferno = "inferno" gist_rainbow = "gist_rainbow" nipy_spectral = "nipy_spectral" + jochen_deluxe = "jochen_deluxe" @dataclass class ReflectivityOutputConfig(ArgParsable): @@ -458,7 +459,7 @@ class ReflectivityOutputConfig(ArgParsable): ) plot_colormap: PlotColormaps = field( - default=PlotColormaps.gist_ncar, + default=PlotColormaps.jochen_deluxe, metadata={ 'short': 'pcmap', 'group': 'output', @@ -594,6 +595,7 @@ class E2HPlotSelection(StrEnum): All = 'all' YZ = 'Iyz' LT = 'Ilt' + YT = 'Iyt' TZ = 'Itz' Q = 'Iq' L = 'Il' @@ -673,7 +675,7 @@ class E2HReductionConfig(ArgParsable): ) plot_colormap: PlotColormaps = field( - default=PlotColormaps.gist_ncar, + default=PlotColormaps.jochen_deluxe, metadata={ 'short': 'pcmap', 'group': 'output', @@ -689,6 +691,14 @@ class E2HReductionConfig(ArgParsable): }, ) + thetaRangeR: Tuple[float, float] = field( + default_factory=lambda: [-0.75, 0.75], + metadata={ + 'short': 'T', + 'group': 'region of interest', + 'help': 'theta region of interest w.r.t. beam center', + }, + ) @dataclass class E2HConfig: diff --git a/eos/projection.py b/eos/projection.py index b1e5b8e..06afa18 100644 --- a/eos/projection.py +++ b/eos/projection.py @@ -3,12 +3,15 @@ Classes used to calculate projections/binnings from event data onto given grids. """ import logging +import warnings from abc import ABC, abstractmethod from typing import List, Tuple, Union import numpy as np from dataclasses import dataclass +from matplotlib.colors import LogNorm + from .event_data_types import EventDatasetProtocol from .instrument import Detector, LZGrid from .normalization import LZNormalisation @@ -275,16 +278,25 @@ class LZProjection(ProjectionInterface): else: cmap=False - if not 'norm' in kwargs: - kwargs['norm'] = LogNorm() - if self.is_normalized: - self._graph = plt.pcolormesh(self.lamda, self.alphaF, self.data.ref, **kwargs) - if cmap: - plt.colorbar(label='R') + I = self.data.ref else: - self._graph = plt.pcolormesh(self.lamda, self.alphaF, self.data.I, **kwargs) - if cmap: + I = self.data.I + + + if not 'norm' in kwargs: + vmin = I[(I>0)].min() + vmax = np.nanmax(I) + kwargs['norm'] = LogNorm(vmin, vmax, clip=True) + + + # suppress warning for wrongly sorted y-axis pixels (blades overlap) + with warnings.catch_warnings(action='ignore', category=UserWarning): + self._graph = plt.pcolormesh(self.lamda, self.alphaF, I, **kwargs) + if cmap: + if self.is_normalized: + plt.colorbar(label='R') + else: plt.colorbar(label='I / cpm') plt.xlabel('$\\lambda$ / $\\AA$') plt.ylabel('$\\Theta$ / °') @@ -300,6 +312,20 @@ class LZProjection(ProjectionInterface): """ Inline update of previous plot by just updating the data. """ + if self.is_normalized: + I = self.data.ref + else: + I = self.data.I + + if isinstance(self._graph.norm, LogNorm): + vmin = I[(I>0)].min()*0.5 + else: + vmin = 0 + vmax = np.nanmax(I) + self._graph.set_array(I) + self._graph.norm.vmin = vmin + self._graph.norm.vmax = vmax + if self.is_normalized: self._graph.set_array(self.data.ref) else: @@ -408,23 +434,21 @@ class YZProjection(ProjectionInterface): del(kwargs['colorbar']) else: cmap=False - if not 'aspect' in kwargs: - kwargs['aspect'] = 'auto' + + vmax = self.data.I.max() if not 'norm' in kwargs: - kwargs['norm'] = LogNorm() + vmin = self.data.I[(self.data.I>0)].min()*0.5 + kwargs['norm'] = LogNorm(vmin, vmax) - self._graph = plt.imshow(self.data.I.T, - extent=(float(self.y[0]), float(self.y[-1]), - float(self.z[0]), float(self.z[-1])), - **kwargs) + self._graph = plt.pcolormesh(self.y, self.z, self.data.I.T, **kwargs) if cmap: plt.colorbar(label='I / cpm') plt.xlabel('Y') plt.ylabel('Z') plt.xlim(self.y[0], self.y[-1]) - plt.ylim(self.z[0], self.z[-1]) + plt.ylim(self.z[-1], self.z[0]) plt.title('Horizontal Pixel vs. Vertical Pixel') self._graph_axis = plt.gca() @@ -434,7 +458,14 @@ class YZProjection(ProjectionInterface): """ Inline update of previous plot by just updating the data. """ + if isinstance(self._graph.norm, LogNorm): + vmin = self.data.I[(self.data.I>0)].min()*0.5 + else: + vmin = 0 + vmax = self.data.I.max() self._graph.set_array(self.data.I.T) + self._graph.norm.vmin = vmin + self._graph.norm.vmax = vmax def draw_yzcross(self, event): if event.inaxes is not self._graph_axis: @@ -451,6 +482,57 @@ class YZProjection(ProjectionInterface): art.remove() plt.draw() +class YTProjection(YZProjection): + theta: np.ndarray + + def __init__(self, tthh: float): + dd = Detector.delta_z[1]-Detector.delta_z[0] + delta = np.hstack([Detector.delta_z, Detector.delta_z[-1]+dd])-dd/2. + self.theta = tthh + delta + super().__init__() + + def plot(self, **kwargs): + from matplotlib import pyplot as plt + from matplotlib.colors import LogNorm + + if 'colorbar' in kwargs: + cmap=True + del(kwargs['colorbar']) + else: + cmap=False + + if not 'norm' in kwargs: + kwargs['norm'] = LogNorm() + + self._graph = plt.pcolormesh(self.y, self.theta, self.data.I.T, **kwargs) + if cmap: + plt.colorbar(label='I / cpm') + + plt.xlabel('Y') + plt.ylabel('Theta / °') + plt.xlim(self.y[0], self.y[-1]) + plt.ylim(self.theta[-1], self.theta[0]) + plt.title('Horizontal Pixel vs. Angle') + + self._graph_axis = plt.gca() + plt.connect('button_press_event', self.draw_tzcross) + + def draw_tzcross(self, event): + if event.inaxes is not self._graph_axis: + return + from matplotlib import pyplot as plt + tbm = self._graph_axis.figure.canvas.manager.toolbar.mode + if event.button is plt.MouseButton.LEFT and tbm=='': + self._graph_axis.plot([event.xdata, event.xdata], [self.theta[0], self.theta[-1]], '-', color='grey') + self._graph_axis.plot([self.y[0], self.y[-1]], [event.ydata, event.ydata], '-', color='grey') + self._graph_axis.text(event.xdata, event.ydata, f'({event.xdata:.1f}, {event.ydata:.1f})', backgroundcolor='white') + plt.draw() + if event.button is plt.MouseButton.RIGHT and tbm=='': + for art in list(self._graph_axis.lines)+list(self._graph_axis.texts): + art.remove() + plt.draw() + + class TofZProjection(ProjectionInterface): tof: np.ndarray z: np.ndarray @@ -494,23 +576,18 @@ class TofZProjection(ProjectionInterface): del(kwargs['colorbar']) else: cmap=False - if not 'aspect' in kwargs: - kwargs['aspect'] = 'auto' if not 'norm' in kwargs: kwargs['norm'] = LogNorm() - self._graph = plt.imshow(self.data.I.T, - extent=(float(self.tof[0])*1e3, float(self.tof[-1])*1e3, - float(self.z[0]), float(self.z[-1])), - **kwargs) + self._graph = plt.pcolormesh(self.tof*1e3, self.z, self.data.I.T, **kwargs) if cmap: plt.colorbar(label='I / cpm') plt.xlabel('Time of Flight / ms') plt.ylabel('Z') plt.xlim(self.tof[0]*1e3, self.tof[-1]*1e3) - plt.ylim(self.z[0], self.z[-1]) + plt.ylim(self.z[-1], self.z[0]) plt.title('Time of Flight vs. Vertical Pixel') self._graph_axis = plt.gca() diff --git a/eos/reduction_e2h.py b/eos/reduction_e2h.py index 5560764..30e500a 100644 --- a/eos/reduction_e2h.py +++ b/eos/reduction_e2h.py @@ -5,8 +5,9 @@ Can be used as a live preview with automatic update when files are modified. import logging import os -import matplotlib.pyplot as plt import numpy as np +import matplotlib.pyplot as plt +from matplotlib.colors import ListedColormap from orsopy import fileio from datetime import datetime @@ -20,7 +21,7 @@ from . import event_handling as eh from .path_handling import PathResolver from .projection import CombinedProjection, LZProjection, ProjectionInterface, ReflectivityProjector, TofProjection, \ TofZProjection, \ - YZProjection + YTProjection, YZProjection NEEDS_LAMDA = (E2HPlotSelection.All, E2HPlotSelection.LT, E2HPlotSelection.Q, E2HPlotSelection.L) @@ -70,10 +71,12 @@ class E2HReduction: self.event_actions |= eh.FilterMonitorThreshold(self.config.experiment.lowCurrentThreshold) if not self.config.reduction.fast: self.event_actions |= eh.FilterStrangeTimes() - if self.config.reduction.plot==E2HPlotSelection.TZ: - # perform time fold-back and corrections for tof if not fast mode + if self.config.reduction.plot in [E2HPlotSelection.YT, E2HPlotSelection.YZ]: + # perform time fold-back and apply yRange filter if not fast mode self.event_actions |= ea.MergeFrames() self.event_actions |= ea.AnalyzePixelIDs(self.config.experiment.yRange) + if self.config.reduction.plot==E2HPlotSelection.YT: + # perform corrections for tof if not fast mode self.event_actions |= eh.TofTimeCorrection(self.config.experiment.incidentAngle==IncidentAngle.alphaF) # select needed actions in depenence of plots if self.config.reduction.plot in NEEDS_LAMDA: @@ -87,12 +90,14 @@ class E2HReduction: if self.config.reduction.plot in [E2HPlotSelection.All, E2HPlotSelection.LT, E2HPlotSelection.Q]: self.grid = LZGrid(0.01, [0.0, 0.25]) - if self.config.reduction.plot in [E2HPlotSelection.All, E2HPlotSelection.LT, E2HPlotSelection.YZ, E2HPlotSelection.TZ]: + if self.config.reduction.plot in [E2HPlotSelection.All, E2HPlotSelection.LT, E2HPlotSelection.YZ, E2HPlotSelection.YT]: self.plot_kwds['colorbar'] = True self.plot_kwds['cmap'] = str(self.config.reduction.plot_colormap) if self.config.reduction.plotArgs==E2HPlotArguments.Linear: self.plot_kwds['norm'] = None + self.register_colormap() + def reduce(self): if self.config.reduction.plot in [E2HPlotSelection.All, E2HPlotSelection.LT, E2HPlotSelection.Q]: if self.config.reduction.normalizationModel: @@ -121,16 +126,29 @@ class E2HReduction: if self.config.reduction.show_plot: plt.show() + def register_colormap(self): + cmap = plt.colormaps['gnuplot'](np.arange(256)) + cmap[:1, :] = np.array([256/256, 255/256, 236/256, 1]) + cmap = ListedColormap(cmap, name='jochen_deluxe', N=cmap.shape[0]) + #cmap.set_bad((1.,1.,0.9)) + plt.colormaps.register(cmap) + def prepare_graphs(self): last_file_header = AmorHeader(self.file_list[-1]) tthh = last_file_header.geometry.nu - last_file_header.geometry.mu + if not self.config.reduction.is_default('thetaRangeR'): + # adjust range based on detector center + thetaRange = [ti+tthh for ti in self.config.reduction.thetaRangeR] + else: + thetaRange = [tthh - last_file_header.geometry.div/2, tthh + last_file_header.geometry.div/2] if self.config.reduction.plot==E2HPlotSelection.LT: self.projection = LZProjection(tthh, self.grid) if not self.config.reduction.fast: self.projection.correct_gravity(last_file_header.geometry.detectorDistance) self.projection.apply_lamda_mask(self.config.experiment.lambdaRange) + self.projection.apply_theta_mask(thetaRange) self.projection.apply_norm_mask(self.norm) if self.config.reduction.plot==E2HPlotSelection.Q: @@ -139,12 +157,16 @@ class E2HReduction: plz.correct_gravity(last_file_header.geometry.detectorDistance) plz.calculate_q() plz.apply_lamda_mask(self.config.experiment.lambdaRange) + plz.apply_theta_mask(thetaRange) plz.apply_norm_mask(self.norm) self.projection = ReflectivityProjector(plz, self.norm) if self.config.reduction.plot==E2HPlotSelection.YZ: self.projection = YZProjection() + if self.config.reduction.plot==E2HPlotSelection.YT: + self.projection = YTProjection(tthh) + if self.config.reduction.plot==E2HPlotSelection.TZ: self.projection = TofZProjection(last_file_header.timing.tau, foldback=not self.config.reduction.fast) @@ -157,6 +179,7 @@ class E2HReduction: plz.correct_gravity(last_file_header.geometry.detectorDistance) plz.calculate_q() plz.apply_lamda_mask(self.config.experiment.lambdaRange) + plz.apply_theta_mask(thetaRange) plz.apply_norm_mask(self.norm) pr = ReflectivityProjector(plz, self.norm) pyz = YZProjection()