from .optical_component import *
from .component_group import *
[docs]
class Monitor(OpticalComponent):
[docs]
def __init__(self, origin, width, height, **kwargs):
super().__init__(origin, **kwargs)
self.width = width
self.height = height
self.surface = Rectangle(width, height)
self._edge_color = "orange"
self.name = kwargs.get("name", None)
self._initialize()
def _initialize(self):
self._data_raw = [] # list of tuples (P, intensity)
self._sorted_data = []
self._updated = False
self._sortYZindex = None
self._sort_method = None
[docs]
def clear(self):
self._initialize()
@property
def ndata(self):
return len(self._data_raw)
@property
def data(self):
return self.get_data()
[docs]
def get_data(self, sort="YZ"):
if (not self._sorted_data) or (self._updated) or (self._sort_method != sort):
if sort == "YZ":
self._sort_method = "YZ"
self._sorted_data = [self._data_raw[i] for i in self.sortYZIndex]
elif sort == "ID":
self._sort_method = "ID"
self._sorted_data = [self._data_raw[i] for i in self.sortIDindex]
self._updated = False
return self._sorted_data
@property
def rays(self):
return self.get_rays()
[docs]
def get_rays(self, sort="YZ"):
if self.ndata == 0:
return []
return [data[3] for data in self.get_data(sort=sort)]
@property
def raw_yList(self):
if self.ndata == 0:
return np.array([])
return np.array([data[0][1] for data in self._data_raw])
@property
def raw_zList(self):
if self.ndata == 0:
return np.array([])
return np.array([data[0][2] for data in self._data_raw])
@property
def PList(self):
return self.get_PList()
[docs]
def get_PList(self, sort="YZ"):
if self.ndata == 0:
return np.array([])
return np.array([data[0] for data in self.get_data(sort=sort)])
@property
def yList(self):
return self.get_yList()
[docs]
def get_yList(self, sort="YZ"):
if self.ndata == 0:
return np.array([])
tangent_Y = self.tangent_Y # 3
return np.array(
[np.dot(data[0], tangent_Y) for data in self.get_data(sort=sort)]
)
@property
def zList(self):
return self.get_zList()
[docs]
def get_zList(self, sort="YZ"):
if self.ndata == 0:
return np.array([])
tangent_Z = self.tangent_Z # 3
return np.array(
[np.dot(data[0], tangent_Z) for data in self.get_data(sort=sort)]
)
@property
def IList(self):
return self.get_IList()
[docs]
def get_IList(self, sort="YZ"):
"""intensity list"""
if self.ndata == 0:
return np.array([])
return np.array([data[1] for data in self.get_data(sort=sort)])
@property
def tList(self):
return self.get_tList()
[docs]
def get_tList(self, sort="YZ"):
"""tilt list"""
if self.ndata == 0:
return np.array([])
return np.array([data[2] for data in self.get_data(sort=sort)])
@property
def directionList(self):
return self.get_directionList()
[docs]
def get_directionList(self, sort="YZ"):
if self.ndata == 0:
return np.array([])
return np.array([r.direction for r in self.get_rays(sort=sort)])
@property
def sortYZIndex(self):
self._sortYZindex = np.lexsort((self.raw_zList, self.raw_yList))
return self._sortYZindex
@property
def sortIDindex(self):
self._sortIDindex = np.argsort([data[3]._id for data in self._data_raw])
return self._sortIDindex
@property
def tYList(self):
return self.get_tYList()
[docs]
def get_tYList(self, sort="YZ"):
directionList = self.get_directionList(sort=sort) # n*3
# y component of the direction vector is the second component
# tYList = directionList[:, 1] # n
tangent_Y = self.tangent_Y # 3
tYList = np.dot(directionList, tangent_Y) # n
return tYList
@property
def tZList(self, sort="YZ"):
directionList = self.get_directionList(sort=sort) # n*3
# z component of the direction vector is the third component
# tZList = directionList[:, 2] # n
tangent_Z = self.tangent_Z # 3
tZList = np.dot(directionList, tangent_Z) # n
return tZList
[docs]
def get_ray_i(self, idx):
rayi = self.rays[idx]
hity = self.yList[idx]
hitz = self.zList[idx]
tilty = self.tYList[idx]
tiltz = self.tZList[idx]
intensity = self.IList[idx]
hit_point = [hity, hitz, tilty, tiltz, intensity]
return rayi, hit_point
[docs]
def get_ray_id(self, ray_id):
for idx, r in enumerate(self.rays):
if r._id == ray_id:
return self.get_ray_i(idx)
return None, None
[docs]
def interact_local(self, ray):
return [ray]
[docs]
def render(self, ax, type: str, **kwargs):
return super().render(ax, type, color=self._edge_color, **kwargs)
[docs]
def get_bbox(self):
return self.surface.get_bbox()
[docs]
def record(self, rays: List[Ray]):
Pts = []
for r in rays:
local_ray = self.ray_to_local_coordinates(r)
P, t = self.intersect_point_local(local_ray)
if P is not None:
Pts.append(
(P, local_ray.intensity, t, r)
) # intersection point; intensity; distance along ray; original ray
self._data_raw.extend(Pts)
self._updated = True
def _get_hist_y(self):
yList = self.yList
counts, bins = np.histogram(
yList, bins=30, range=(-self.width / 2, self.width / 2)
)
return counts, bins
[docs]
def get_waist_distance(self):
tList = self.tList
rays = self.rays
# account for beam propagation direction
waist_distance_List = []
for r, t in zip(rays, tList):
q = r.q_at_z(t)
distance_raw = r.distance_to_waist(q)
if np.dot(r.direction, self.normal) > 0:
# if the ray is propagating towards the monitor, we take the negative distance
distance_raw = -distance_raw
waist_distance_List.append(distance_raw)
waist_distance_List = np.array(waist_distance_List)
return waist_distance_List
[docs]
def get_beam_waist(self):
tList = self.tList
rList = self.rList
waist_List = []
for r, t in zip(rList, tList):
q = r.q_at_z(t)
waist_List.append(r.waist(q))
return np.array(waist_List)
[docs]
def get_delta_pos(self):
yList = self.yList
zList = self.zList
if len(yList) == 0 or len(zList) == 0:
return np.array([0.0]), np.array([0.0])
idx_y = np.argsort(yList)
y_sorted = yList[idx_y]
dy = np.diff(y_sorted)
z_sorted = zList[idx_y]
dz = np.diff(z_sorted)
return dy, dz
@property
def sum_intensity(self):
return np.sum([data[1] for data in self.get_data()])
@property
def avg_intensity(self):
return np.mean([data[1] for data in self.get_data()])
@property
def std_histy(self):
counts, bins = self._get_hist_y()
Ix = np.sum(counts * bins[:-1]) / np.sum(counts)
Ixx = np.sum(counts * bins[:-1] ** 2) / np.sum(counts)
return np.sqrt(Ixx - Ix**2)
[docs]
def export_rays_npz(self, filename: str):
print(f"Exporting {self.ndata} rays to {filename} ...")
xList = self.yList
yList = self.zList
tXList = self.tYList
tYList = self.tZList
IList = self.IList
np.savez(
filename,
xList=xList,
yList=yList,
tXList=tXList,
tYList=tYList,
IList=IList,
)
[docs]
def render_hist(self, ax, type="Y", **kwargs):
if type == "Y":
counts, bins = self._get_hist_y()
ax.bar(bins[:-1], counts, width=(bins[1] - bins[0]), **kwargs)
ax.set_xlabel("Y")
elif type == "YZ":
ax.hist2d(
self.yList,
self.zList,
bins=30,
density=False,
range=(
(-self.width / 2, self.width / 2),
(-self.height / 2, self.height / 2),
),
**kwargs,
)
ax.set_xlabel("Y")
ax.set_ylabel("Z")
[docs]
def render_scatter(self, ax, **kwargs):
if self.ndata == 0:
return
yList = self.yList
zList = self.zList
tList = self.tList
IList = self.IList
alpha = np.clip(IList, 0.1, 1)
ax.scatter(yList, zList, marker="+", alpha=alpha, c="blue")
#
annote_delta_pos = kwargs.get("annote_delta_pos", False)
if annote_delta_pos:
dy, dz = self.get_delta_pos()
std_y = np.std(dy) if len(dy) > 0 else 0.0
std_z = np.std(dz) if len(dz) > 0 else 0.0
# annote the std of scatter points in Y and Z
ax.text(
0,
self.height / 2 * 0.6,
f"SX={std_y:.4f}, SY={std_z:.4f}",
)
# annote the mean of scatter points in Y and Z
mean_dy = np.mean(dy) if len(dy) > 0 else 0.0
mean_dz = np.mean(dz) if len(dz) > 0 else 0.0
ax.text(
0,
self.height / 2 * 0.4,
f"MX={mean_dy:.4f}, MY={mean_dz:.4f}",
)
#
annote_yz_index = kwargs.get("annote_yz_index", False)
if annote_yz_index:
sort_index = self.sortYZIndex
for i, (y, z) in enumerate(zip(yList[sort_index], zList[sort_index])):
ax.text(
y,
z,
f"{i}",
fontsize=8,
color="black",
)
#
annote_id = kwargs.get("annote_id", False)
if annote_id:
for r, y, z in zip(self.rays, yList, zList):
ax.text(
y,
z,
f"{r._id}",
fontsize=8,
color="black",
)
#
annote_pathlength = kwargs.get("annote_pathlength", False)
if annote_pathlength:
for r, y, z, t in zip(self.rays, yList, zList, tList):
ax.text(
y,
z,
f"L={r.pathlength(t):.2f}",
fontsize=8,
color="black",
)
#
annote_phase = kwargs.get("annote_phase", False)
if annote_phase:
for r, y, z, t in zip(self.rays, yList, zList, tList):
ax.text(
y,
z,
f"Phi={r.phase(t):.2f}",
fontsize=8,
color="black",
)
#
# GAUSSIAN BEAM
gaussian_beam = kwargs.get("gaussian_beam", False)
if gaussian_beam:
spot_size_scale = kwargs.get("spot_size_scale", 1.0)
tList = self.tList
rList = self.rays
spotsizeList = (
np.array([r.spot_size(t) for r, t in zip(rList, tList)])
* spot_size_scale
)
waistList = (
np.array([r.waist(r.q_at_z(t)) for r, t in zip(rList, tList)])
* spot_size_scale
)
annote_spotsize = kwargs.get("annote_spotsize", False)
annote_waist = kwargs.get("annote_waist", False)
_sign = 1
for spotsize, waist, y, z, a in zip(
spotsizeList, waistList, yList, zList, alpha
):
circle = plt.Circle((y, z), spotsize, color="red", alpha=a / 2, ec=None)
ax.add_artist(circle)
if annote_spotsize:
ax.text(
y,
z + 0.1 * self.height * _sign,
f"sp={spotsize:.2e}",
fontsize=8,
color="black",
)
if annote_waist:
ax.text(
y,
z + 0.1 * self.height * _sign,
f"w={waist:.2e}",
fontsize=8,
color="black",
)
_sign = (_sign + 1 + 7) % 7 - 3 # alternate sign for waist annotation
#
annote_dis_std = kwargs.get("annote_dis_std", False)
if annote_dis_std:
waist_distance_List = self.get_waist_distance()
std_distance = np.std(waist_distance_List)
ax.text(0, self.height / 2 * 0.8, f"Std(z)={std_distance:.4f}")
#
annote_waist_std = kwargs.get("annote_waist_std", False)
if annote_waist_std:
waist_List = self.get_beam_waist()
std_waist = np.std(waist_List)
mean_waist = np.mean(waist_List)
ax.text(
0,
self.height / 2 * 0.2,
f"Std(w)={std_waist*1e4:.4f}, M(w)={mean_waist*1e4:.4f}",
)
#
annote_beam_tilt = kwargs.get("annote_beam_tilt", False)
if annote_beam_tilt:
tYList = self.tYList
tZList = self.tZList
if len(tYList) > 0 and len(tZList) > 0:
std_tY = np.std(tYList)
std_tZ = np.std(tZList)
ax.text(
0,
self.height / 2 * 0.0,
f"Std(tY)={std_tY:.4f}, Std(tZ)={std_tZ:.4f}",
)
ax.set_xlim(-self.width / 2, self.width / 2)
ax.set_ylim(-self.height / 2, self.height / 2)
ax.set_xlabel("Y")
ax.set_ylabel("Z")
if hasattr(self, "name") and self.name is not None:
ax.set_title(str(self.name))
ax.set_aspect("auto")
[docs]
def render_delta_pos_y(self, ax, **kwargs):
dy, dz = self.get_delta_pos()
# bar plot of dy
ax.bar(np.arange(len(dy)), dy, **kwargs)
ax.set_ylim(np.min(dy) * 0.8, np.max(dy) * 1.2)
ax.set_xlabel("Index")
ax.set_ylabel("Delta Y")
ax.set_title("Delta Y Position")
ax.set_xticks(np.arange(len(dy)))
ax.set_xticklabels([f"{i}" for i in range(len(dy))])
[docs]
def render_tilt_y(self, ax, **kwargs):
tYList = self.tYList
ax.bar(np.arange(len(tYList)), tYList, **kwargs)
ax.set_xlabel("Index")
ax.set_ylabel("Tilt Y")
ax.set_title("Tilt Y Position")
ax.set_xticks(np.arange(len(tYList)))
ax.set_xticklabels([f"{i}" for i in range(len(tYList))])
[docs]
def render_projection(self, ax, comp, **kwargs):
# Get edge color and line width from kwargs
color = kwargs.get("color", "black")
linewidth = kwargs.get("linewidth", 2)
tangent_Y = self.tangent_Y
tangent_Z = self.tangent_Z
origin = np.array(self.origin)
def render_comp(c):
global_x, global_y, global_z = c._get_boundary_points("3D")
# Stack into (N, 3) array of global points
pts = np.column_stack([global_x, global_y, global_z])
_render_proj_pts(pts)
def _render_proj_pts(pts):
rel = pts - origin # (N, 3)
local_y = rel @ tangent_Y # projection onto tangent_Y
local_z = rel @ tangent_Z # projection onto tangent_Z
ax.plot(local_y, local_z, color=color, linewidth=linewidth)
def _render_proj_pt(pt):
rel = pt - origin # (1, 3)
local_y = rel @ tangent_Y # projection onto tangent_Y
local_z = rel @ tangent_Z # projection onto tangent_Z
ax.scatter(local_y, local_z, color=color, linewidth=linewidth, marker="+")
if isinstance(comp, ComponentGroup):
for c in comp.components:
render_comp(c)
elif isinstance(comp, OpticalComponent):
render_comp(comp)
elif isinstance(comp, np.ndarray):
_render_proj_pt(comp)
else:
raise TypeError(f"Unsupported component type: {type(comp)}")