Проблема, с которой я столкнулся, связана с обновлением цветовой панели графика plt.imshow. Вот код, который я постараюсь разобрать и кое-что объяснить позже.
Для первой ячейки в Jupyter у меня есть функции, импорт и входные параметры:
# Imports ##############################################################
import numpy as np
%matplotlib notebook
import matplotlib.pyplot as plt
import matplotlib.colors as colors
from matplotlib.widgets import Slider
from scipy.ndimage import label, find_objects
# Functions ############################################################
def intensity_distribution(r, z, PMax, w0, zR):
wZ = w0 * np.sqrt(1 + (z / zR)**2) # beam radius at z (Gaussian beam spreading)
I0 = 2 * PMax / (np.pi * wZ**2) # peak intensity at radius wZ
return I0 * np.exp(-2 * r**2 / wZ**2), wZ # Gaussian intensity distribution, beam radius
def get_circle_ROI(I, r, threshold):
ROI = (I > threshold).astype("uint8") # binary mask for regions above the threshold
labels, features = label(ROI) # label connected regions
slices = find_objects(labels) # get bounding box slices for labeled regions
xSlice, ySlice = slices[0] # extract x and y slices of the largest feature
ROIHeight = (xSlice.stop - xSlice.start) * (r[1] - r[0]) * 1e6 # convert height to micrometers
ROIWidth = (ySlice.stop - ySlice.start) * (r[1] - r[0]) * 1e6 # convert width to micrometers
cx = (ySlice.start + ySlice.stop) // 2 # x-coordinate of the center
cy = (xSlice.start + xSlice.stop) // 2 # y-coordinate of the center
centre = (r[cy] * 1e6, r[cx] * 1e6) # convert center coordinates to micrometers
radius = min(ROIWidth, ROIHeight) / 2 # radius is the smaller dimension's half-width
return centre, radius
def update_plot(PMax, zOffset):
"""Update the heatmap based on new parameters."""
global colorbar
# Calculate intensity distribution at given z offset
I, wZ = intensity_distribution(np.sqrt(R**2 + Z**2), zOffset, PMax, BEAM_RADIUS_AT_FOCUS, zR)
I /= 1e6 # convert intensity from W/m² to W/mm²
I += 0.01 # small offset for better visualization contrast
max_intensity = I.max() # maximum intensity in the current distribution
# Calculate the on-axis peak intensity at focus in W/mm²
I0 = (2 * PMax) / (np.pi * BEAM_RADIUS_AT_FOCUS**2) # peak intensity in W/m² at z = 0
I0 /= 1e6 # convert peak intensity to W/mm²
# Calculate the Full Width at Half Maximum (FWHM) in micrometers
centre, fwhm = get_circle_ROI(I, r, max_intensity / 2)
_, tenth = get_circle_ROI(I, r, max_intensity / 10)
# Clear and update plot
ax.clear() # clear current axes
# Display the updated intensity distribution as a heatmap
im = ax.imshow(I, extent=[r[0]*1e6, r[-1]*1e6, r[0]*1e6, r[-1]*1e6], norm=colors.LogNorm(vmax=14000))
ax.set_xlabel("x (μm)") # label for x-axis
ax.set_ylabel("y (μm)") # label for y-axis
# Add plot title with z offset, FWHM, and max intensity in W/mm²
ax.set_title(f"FWHM = {fwhm:.1f} μm\n"
f"Radius at 10% of total power = {tenth:.2f} μm\n"
f"Max power = {I.max():.2f} W/mm²",
loc="left")
# Draw a circle representing the FWHM boundary
cirlcefwhm = plt.Circle(centre, fwhm, color='white', fill=False, linestyle='--', linewidth=2, label="FWHM")
cirlce10 = plt.Circle(centre, tenth, color='white', fill=False, linestyle='--', linewidth=2, label="10% of I$_max$")
ax.add_patch(cirlcefwhm) # add the FWHM circle to the plot
ax.add_patch(cirlce10) # add the circle where power is 10% of max
#### Problematic starts here ####
if colorbar is not None: # if colorbar already exists, remove it
colorbar.remove()
colorbar = plt.colorbar(im, ax=ax, label="Intensity (W/mm²)") # create new colorbar in W/mm²
fig.draw_without_rendering() # redraw based on the recommendation of matplotlib instead of colorbar.draw_all()
fig.canvas.draw() # redraw figure to reflect updates
#### Problematic ends here ####
def sliders_on_changed(val):
''' Slider update function '''
power = power_slider.val * MAX_LASER_POWER / 100 # calculate current power level in watts
z_offset = z_offset_slider.val / 1000 # convert slider z offset from mm to meters
update_plot(power, z_offset) # update the plot with new parameters
# Inputs ###############################################################
WAVELENGTH = 10.6e-6 # wavelength in meters
MAX_LASER_POWER = 80 # max laser power in watts
BEAM_WIDTH_AT_FOCUS = 120e-6 # beam width at focus in meters
BEAM_RADIUS_AT_FOCUS = BEAM_WIDTH_AT_FOCUS / 2 # beam radius at focus in meters
zR = np.pi * BEAM_RADIUS_AT_FOCUS**2 / WAVELENGTH # Rayleigh range in meters
gridSize = 100 # resolution
r = np.linspace(-500e-6, 500e-6, gridSize) # range for spatial coordinates in meters
R, Z = np.meshgrid(r, r) # create grid for spatial coordinates
colorbar = None # init colorbar
#### Problematic starts here ####
if colorbar is not None: # if colorbar already exists, remove it
colorbar.remove()
colorbar = plt.colorbar(im, ax=ax, label="Intensity (W/mm²)") # create new colorbar in W/mm²
fig.draw_without_rendering() # redraw based on the recommendation of matplotlib instead of colorbar.draw_all()
fig.canvas.draw() # redraw figure to reflect updates
#### Problematic ends here ####
Не совсем понимаю, почему это не работает. Происходит следующее: цветная полоса появляется при первом обновлении, а затем исчезает после изменения смещения по оси Z или процента мощности. Обычно после изменения любого из значений.
Во второй ячейке блокнота Jupyter содержится вызов функций:
Выполнив это, вы увидите пользовательский интерфейс, который выглядит следующим образом:
А затем, когда я перемещаю любой из ползунков, цветная полоса исчезает:
Обновление цветовой панели кажется распространенной проблемой, я видел много вопросов по stackoverflow по этому поводу, я даже ответил один раз.
Однако решение на самом деле не работает со слушателями слайдера.
P.S.: Изменение серверной части на самом деле не помогает. Однако использование %matplotlib qt обнаружило обратную трассировку:
Traceback (most recent call last):
File "C:\ProgramData\anaconda3\Lib\site-packages\matplotlib\cbook\__init__.py", line 309, in process
func(*args, **kwargs)
File "C:\ProgramData\anaconda3\Lib\site-packages\matplotlib\widgets.py", line 603, in
return self._observers.connect('changed', lambda val: func(val))
^^^^^^^^^
File "C:\Users\User\AppData\Local\Temp\ipykernel_26252\2170461726.py", line 66, in sliders_on_changed
update_plot(power, z_offset) # update the plot with new parameters
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "C:\Users\User\AppData\Local\Temp\ipykernel_26252\2170461726.py", line 57, in update_plot
colorbar.remove()
File "C:\ProgramData\anaconda3\Lib\site-packages\matplotlib\colorbar.py", line 1041, in remove
self.ax.remove()
File "C:\ProgramData\anaconda3\Lib\site-packages\matplotlib\artist.py", line 242, in remove
self._remove_method(self)
File "C:\ProgramData\anaconda3\Lib\site-packages\matplotlib\figure.py", line 944, in delaxes
self._axstack.remove(ax)
File "C:\ProgramData\anaconda3\Lib\site-packages\matplotlib\figure.py", line 92, in remove
self._axes.pop(a)
KeyError:
Некоторые вещи, которые я пробовал:
Существует метод для цветной панели, называемый draw_all()
Matplotlib также предлагает использовать fig.draw_without_rendering()
Мое собственное решение
Установка пределов цветной панели без удаления colorbar
Может кто-нибудь помочь мне понять, в чем дело? Я понимаю, что код довольно длинный, при необходимости могу сократить его до простого примера.
Проблема, с которой я столкнулся, связана с обновлением цветовой панели графика plt.imshow. Вот код, который я постараюсь разобрать и кое-что объяснить позже. [list] [*]Для первой ячейки в Jupyter у меня есть функции, импорт и входные параметры: [/list] [code]# Imports ############################################################## import numpy as np %matplotlib notebook import matplotlib.pyplot as plt import matplotlib.colors as colors from matplotlib.widgets import Slider from scipy.ndimage import label, find_objects # Functions ############################################################ def intensity_distribution(r, z, PMax, w0, zR): wZ = w0 * np.sqrt(1 + (z / zR)**2) # beam radius at z (Gaussian beam spreading) I0 = 2 * PMax / (np.pi * wZ**2) # peak intensity at radius wZ return I0 * np.exp(-2 * r**2 / wZ**2), wZ # Gaussian intensity distribution, beam radius def get_circle_ROI(I, r, threshold): ROI = (I > threshold).astype("uint8") # binary mask for regions above the threshold labels, features = label(ROI) # label connected regions slices = find_objects(labels) # get bounding box slices for labeled regions xSlice, ySlice = slices[0] # extract x and y slices of the largest feature ROIHeight = (xSlice.stop - xSlice.start) * (r[1] - r[0]) * 1e6 # convert height to micrometers ROIWidth = (ySlice.stop - ySlice.start) * (r[1] - r[0]) * 1e6 # convert width to micrometers cx = (ySlice.start + ySlice.stop) // 2 # x-coordinate of the center cy = (xSlice.start + xSlice.stop) // 2 # y-coordinate of the center centre = (r[cy] * 1e6, r[cx] * 1e6) # convert center coordinates to micrometers radius = min(ROIWidth, ROIHeight) / 2 # radius is the smaller dimension's half-width return centre, radius def update_plot(PMax, zOffset): """Update the heatmap based on new parameters.""" global colorbar # Calculate intensity distribution at given z offset I, wZ = intensity_distribution(np.sqrt(R**2 + Z**2), zOffset, PMax, BEAM_RADIUS_AT_FOCUS, zR) I /= 1e6 # convert intensity from W/m² to W/mm² I += 0.01 # small offset for better visualization contrast max_intensity = I.max() # maximum intensity in the current distribution # Calculate the on-axis peak intensity at focus in W/mm² I0 = (2 * PMax) / (np.pi * BEAM_RADIUS_AT_FOCUS**2) # peak intensity in W/m² at z = 0 I0 /= 1e6 # convert peak intensity to W/mm² # Calculate the Full Width at Half Maximum (FWHM) in micrometers centre, fwhm = get_circle_ROI(I, r, max_intensity / 2) _, tenth = get_circle_ROI(I, r, max_intensity / 10) # Clear and update plot ax.clear() # clear current axes # Display the updated intensity distribution as a heatmap im = ax.imshow(I, extent=[r[0]*1e6, r[-1]*1e6, r[0]*1e6, r[-1]*1e6], norm=colors.LogNorm(vmax=14000)) ax.set_xlabel("x (μm)") # label for x-axis ax.set_ylabel("y (μm)") # label for y-axis # Add plot title with z offset, FWHM, and max intensity in W/mm² ax.set_title(f"FWHM = {fwhm:.1f} μm\n" f"Radius at 10% of total power = {tenth:.2f} μm\n" f"Max power = {I.max():.2f} W/mm²", loc="left") # Draw a circle representing the FWHM boundary cirlcefwhm = plt.Circle(centre, fwhm, color='white', fill=False, linestyle='--', linewidth=2, label="FWHM") cirlce10 = plt.Circle(centre, tenth, color='white', fill=False, linestyle='--', linewidth=2, label="10% of I$_max$") ax.add_patch(cirlcefwhm) # add the FWHM circle to the plot ax.add_patch(cirlce10) # add the circle where power is 10% of max #### Problematic starts here #### if colorbar is not None: # if colorbar already exists, remove it colorbar.remove() colorbar = plt.colorbar(im, ax=ax, label="Intensity (W/mm²)") # create new colorbar in W/mm² fig.draw_without_rendering() # redraw based on the recommendation of matplotlib instead of colorbar.draw_all() fig.canvas.draw() # redraw figure to reflect updates #### Problematic ends here #### def sliders_on_changed(val): ''' Slider update function ''' power = power_slider.val * MAX_LASER_POWER / 100 # calculate current power level in watts z_offset = z_offset_slider.val / 1000 # convert slider z offset from mm to meters update_plot(power, z_offset) # update the plot with new parameters # Inputs ############################################################### WAVELENGTH = 10.6e-6 # wavelength in meters MAX_LASER_POWER = 80 # max laser power in watts BEAM_WIDTH_AT_FOCUS = 120e-6 # beam width at focus in meters BEAM_RADIUS_AT_FOCUS = BEAM_WIDTH_AT_FOCUS / 2 # beam radius at focus in meters zR = np.pi * BEAM_RADIUS_AT_FOCUS**2 / WAVELENGTH # Rayleigh range in meters gridSize = 100 # resolution r = np.linspace(-500e-6, 500e-6, gridSize) # range for spatial coordinates in meters R, Z = np.meshgrid(r, r) # create grid for spatial coordinates colorbar = None # init colorbar [/code] Проблема заключается в следующем: [code]#### Problematic starts here #### if colorbar is not None: # if colorbar already exists, remove it colorbar.remove() colorbar = plt.colorbar(im, ax=ax, label="Intensity (W/mm²)") # create new colorbar in W/mm² fig.draw_without_rendering() # redraw based on the recommendation of matplotlib instead of colorbar.draw_all() fig.canvas.draw() # redraw figure to reflect updates #### Problematic ends here #### [/code] Не совсем понимаю, почему это не работает. Происходит следующее: цветная полоса появляется при первом обновлении, а затем исчезает после изменения смещения по оси Z или процента мощности. Обычно после изменения любого из значений. [list] [*]Во второй ячейке блокнота Jupyter содержится вызов функций: [/list] [code]fig, ax = plt.subplots() plt.subplots_adjust(left=0.25, bottom=0.35) # leave space for sliders update_plot(50, 0) ax_power = plt.axes([0.25, 0.2, 0.65, 0.03], facecolor = "lightgray") ax_z_offset = plt.axes([0.25, 0.15, 0.65, 0.03], facecolor = "lightgray") power_slider = Slider(ax_power, 'Power (%)', 0.1, 100, valinit=50) z_offset_slider = Slider(ax_z_offset, 'Z-Offset (mm)', 0, 5.0, valinit=0) power_slider.on_changed(sliders_on_changed) z_offset_slider.on_changed(sliders_on_changed) [/code] Выполнив это, вы увидите пользовательский интерфейс, который выглядит следующим образом: [img]https://i.sstatic.net/Kn9Wpj6G.png[/img]
А затем, когда я перемещаю любой из ползунков, цветная полоса исчезает: [img]https://i.sstatic.net/Uoa16OED.png[/img]
Обновление цветовой панели кажется распространенной проблемой, я видел много вопросов по stackoverflow по этому поводу, я даже ответил один раз. Однако решение на самом деле не работает со слушателями слайдера. P.S.: Изменение серверной части на самом деле не помогает. Однако использование %matplotlib qt обнаружило обратную трассировку: [code]Traceback (most recent call last): File "C:\ProgramData\anaconda3\Lib\site-packages\matplotlib\cbook\__init__.py", line 309, in process func(*args, **kwargs) File "C:\ProgramData\anaconda3\Lib\site-packages\matplotlib\widgets.py", line 603, in return self._observers.connect('changed', lambda val: func(val)) ^^^^^^^^^ File "C:\Users\User\AppData\Local\Temp\ipykernel_26252\2170461726.py", line 66, in sliders_on_changed update_plot(power, z_offset) # update the plot with new parameters ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "C:\Users\User\AppData\Local\Temp\ipykernel_26252\2170461726.py", line 57, in update_plot colorbar.remove() File "C:\ProgramData\anaconda3\Lib\site-packages\matplotlib\colorbar.py", line 1041, in remove self.ax.remove() File "C:\ProgramData\anaconda3\Lib\site-packages\matplotlib\artist.py", line 242, in remove self._remove_method(self) File "C:\ProgramData\anaconda3\Lib\site-packages\matplotlib\figure.py", line 944, in delaxes self._axstack.remove(ax) File "C:\ProgramData\anaconda3\Lib\site-packages\matplotlib\figure.py", line 92, in remove self._axes.pop(a) KeyError: [/code] Некоторые вещи, которые я пробовал: [list] [*]Существует метод для цветной панели, называемый draw_all() [*]Matplotlib также предлагает использовать fig.draw_without_rendering() [*]Мое собственное решение [*]Установка пределов цветной панели без удаления colorbar [/list] Может кто-нибудь помочь мне понять, в чем дело? Я понимаю, что код довольно длинный, при необходимости могу сократить его до простого примера.
Я использую [list] [*]Jupyter 6.5.4 [*]Matplotlib 3.7.2 [/list]