Source code for pypolar.poincare

"""
Poincaré sphere visualization routines.

Functions for drawing Poincaré  representations::

   * draw_empty_sphere(ax=None, **kwargs)
   * draw_jones_poincare(J, ax=None, label=None, normalize="s0", text_kwargs=None, **kwargs)
   * draw_stokes_poincare(S, ax=None, label=None, normalize="s0", text_kwargs=None, **kwargs)
   * join_jones_poincare(J1, J2, ax=None, normalize="s0", **kwargs)
   * join_stokes_poincare(S1, S2, ax=None, normalize="s0", **kwargs)

Poincaré coordinates use reduced Stokes values (S1/S0, S2/S0, S3/S0),
so partially polarized states lie inside the unit sphere.

Set `normalize="unit"` to project states onto the unit sphere using
`(S1,S2,S3) / sqrt(S1^2+S2^2+S3^2)`.
"""

import numpy as np
import matplotlib.pyplot as plt

import pypolar.jones

__all__ = (
    "draw_empty_sphere",
    "draw_jones_poincare",
    "draw_stokes_poincare",
    "join_jones_poincare",
    "join_stokes_poincare",
)


def _jones_for_visualization(J):
    """Return a Jones vector in the plotting convention used by this package."""
    if pypolar.jones.alternate_sign_convention:
        return np.conjugate(J)
    return J


[docs] def draw_empty_sphere(ax=None, **kwargs): """ Plot an empty Poincaré sphere. Args: ax: pyplot axis **kwargs: style arguments passed to guide line artists. Returns: tuple: `(fig, ax, artists)` with surface, line, and text handles. """ if ax is None: fig = plt.figure(figsize=(8, 8)) ax = fig.add_subplot(111, projection="3d") else: fig = ax.figure ax.view_init(elev=30, azim=45) try: ax.set_box_aspect((1, 1, 1)) except AttributeError: try: ax.set_aspect("equal") except NotImplementedError: pass u = np.radians(np.linspace(0, 360, 90)) v = np.radians(np.linspace(0, 180, 90)) zz = np.zeros_like(u) x = np.outer(np.cos(u), np.sin(v)) y = np.outer(np.sin(u), np.sin(v)) z = np.outer(np.ones_like(u), np.cos(v)) surface = ax.plot_surface(x, y, z, alpha=0.1, color="blue") # Draw circumferences. lines = [] circle_kwargs = dict(kwargs) if "lw" not in circle_kwargs and "linewidth" not in circle_kwargs: circle_kwargs["lw"] = 0.5 lines.append(ax.plot(np.sin(u), np.cos(u), zz, "k", **circle_kwargs)[0]) lines.append(ax.plot(np.sin(u), zz, np.cos(u), "k", **circle_kwargs)[0]) lines.append(ax.plot(zz, np.sin(u), np.cos(u), "k", **circle_kwargs)[0]) # Draw x,y,z axes. axis_kwargs = dict(kwargs) if "lw" not in axis_kwargs and "linewidth" not in axis_kwargs: axis_kwargs["lw"] = 1 if "alpha" not in axis_kwargs: axis_kwargs["alpha"] = 0.5 lines.append(ax.plot([-1, 1], [0, 0], [0, 0], "k--", **axis_kwargs)[0]) lines.append(ax.plot([0, 0], [-1, 1], [0, 0], "k--", **axis_kwargs)[0]) lines.append(ax.plot([0, 0], [0, 0], [-1, 1], "k--", **axis_kwargs)[0]) # Label directions. texts = [] texts.append(ax.text(1.15, 0, 0, "0°", fontsize=12, color="black", ha="center")) texts.append(ax.text(0, 1.25, 0, "45°", fontsize=12, color="black", ha="center")) texts.append(ax.text(0, 0, 1.15, "RCP", fontsize=12, color="black", ha="center")) texts.append(ax.text(0, 0, -1.15, "LCP", fontsize=12, color="black", ha="center")) texts.append(ax.text(-1.15, 0, 0, "90°", fontsize=12, color="black", ha="center")) # Stokes parameters. ax.set_xlabel("S₁", fontsize=14, labelpad=-10) ax.set_ylabel("S₂", fontsize=14, labelpad=-10) ax.set_zlabel("S₃", fontsize=14, labelpad=-10) # Hide grid and ticks. ax.set_xticks([]) ax.set_yticks([]) ax.set_zticks([]) artists = {"surface": surface, "lines": lines, "texts": texts} return fig, ax, artists
def great_circle_points(ax, ay, az, bx, by, bz): """ Create a list of points along the great circle between a and b. The great circle is assumed to lie on the unit sphere with center at (0,0,0) The points a=(ax,ay,az) and b=(bx,by,bz) are the beginning and end of the arc. Algorithm is from https://www.physicsforums.com / threads / 571535 """ a = np.array([ax, ay, az], dtype=float) b = np.array([bx, by, bz], dtype=float) na = np.linalg.norm(a) nb = np.linalg.norm(b) if np.isclose(na, 0.0) or np.isclose(nb, 0.0): raise ValueError("Great-circle endpoints must be non-zero vectors.") a /= na b /= nb dot = np.clip(np.dot(a, b), -1.0, 1.0) delta = np.arccos(dot) psi = np.linspace(0.0, delta) # Identical points: the arc degenerates to a single point. if np.isclose(delta, 0.0): p = np.repeat(a[np.newaxis, :], psi.size, axis=0) return p[:, 0], p[:, 1], p[:, 2] # Antipodal points: choose a deterministic orthogonal direction. if np.isclose(delta, np.pi): i = int(np.argmin(np.abs(a))) ref = np.eye(3)[i] u = np.cross(a, ref) u /= np.linalg.norm(u) p = np.cos(psi)[:, np.newaxis] * a + np.sin(psi)[:, np.newaxis] * u return p[:, 0], p[:, 1], p[:, 2] # Spherical linear interpolation (SLERP) on the unit sphere. sindelta = np.sin(delta) w1 = np.sin(delta - psi) / sindelta w2 = np.sin(psi) / sindelta p = w1[:, np.newaxis] * a + w2[:, np.newaxis] * b p /= np.linalg.norm(p, axis=1, keepdims=True) return p[:, 0], p[:, 1], p[:, 2] def spherical_angles(x, y, z): """Azimuth and elevation for a point on a sphere.""" phi = np.arctan2(y, x) theta = np.arctan2(np.sqrt(x * x + y * y), z) return phi, theta def _stokes_xyz_for_poincare(S, normalize="s0"): """Return Stokes coordinates for Poincaré plotting.""" SS = np.asarray(S, dtype=float) if SS.shape != (4,): raise ValueError("Stokes vector must have shape (4,).") if normalize == "s0": s0 = SS[0] if np.isclose(s0, 0.0): raise ValueError("Stokes vector with S0=0 cannot be mapped onto the Poincaré sphere.") return SS[1] / s0, SS[2] / s0, SS[3] / s0 if normalize == "unit": sp = np.sqrt(SS[1] ** 2 + SS[2] ** 2 + SS[3] ** 2) if np.isclose(sp, 0.0): raise ValueError("Unpolarized Stokes vector cannot be projected onto the unit Poincaré sphere.") return SS[1] / sp, SS[2] / sp, SS[3] / sp raise ValueError("normalize must be either 's0' or 'unit'.") _LEGACY_POINCARE_TEXT_KWARGS = ( "ha", "horizontalalignment", "va", "verticalalignment", "fontsize", "fontfamily", "fontname", "fontstyle", "fontvariant", "fontweight", "fontstretch", "fontproperties", "multialignment", "rotation_mode", "linespacing", "bbox", )
[docs] def draw_stokes_poincare(S, ax=None, label=None, normalize="s0", text_kwargs=None, **kwargs): """ Plot one Stokes state on or inside the Poincaré sphere. Coordinates are controlled by `normalize`: * `normalize="s0"` uses reduced Stokes values `(S1/S0, S2/S0, S3/S0)`. * `normalize="unit"` uses pure-state projection `(S1,S2,S3) / sqrt(S1^2+S2^2+S3^2)`. Any keyword arguments for point styling should use standard Matplotlib names (for example `linewidth`, `lw`, `color`, `linestyle`, `markersize`). Label styling should use `text_kwargs`; legacy text keys like `ha`, `va`, and `fontsize` in `**kwargs` are still accepted when `label` is provided. Args: S: Stokes vector with shape `(4,)` ax: optional matplotlib 3D axis label: optional text label normalize: either `"s0"` or `"unit"` text_kwargs: optional style args for label text **kwargs: style arguments passed directly to `matplotlib.axes.Axes.plot` Returns: tuple: `(fig, ax, artists)` with point and optional label handles. """ if ax is None: fig = plt.figure(figsize=(8, 8)) ax = fig.add_subplot(111, projection="3d") draw_empty_sphere(ax) else: fig = ax.figure x, y, z = _stokes_xyz_for_poincare(S, normalize=normalize) plot_kwargs = dict(kwargs) resolved_text_kwargs = text_kwargs if label is not None: if resolved_text_kwargs is None: resolved_text_kwargs = {} else: resolved_text_kwargs = dict(resolved_text_kwargs) # Backward compatibility: route legacy text kwargs to the label artist. for key in _LEGACY_POINCARE_TEXT_KWARGS: if key in plot_kwargs: if key not in resolved_text_kwargs: resolved_text_kwargs[key] = plot_kwargs[key] plot_kwargs.pop(key) if "color" in plot_kwargs and "color" not in resolved_text_kwargs: resolved_text_kwargs["color"] = plot_kwargs["color"] point = ax.plot([x], [y], [z], "o", **plot_kwargs)[0] label_artist = None if label is not None: label_artist = ax.text(x, y, z, label, **resolved_text_kwargs) return fig, ax, {"point": point, "label": label_artist}
[docs] def draw_jones_poincare(J, ax=None, label=None, normalize="s0", text_kwargs=None, **kwargs): """ Plot one Jones state on or inside the Poincaré sphere. Args: J: Jones vector with shape `(2,)` ax: optional matplotlib 3D axis label: optional text label normalize: either `"s0"` or `"unit"` text_kwargs: optional style args for label text **kwargs: style arguments passed to `draw_stokes_poincare` Returns: tuple: `(fig, ax, artists)` as returned by :func:`draw_stokes_poincare`. """ JJ = _jones_for_visualization(J) S = pypolar.jones.jones_to_stokes(JJ) return draw_stokes_poincare(S, ax=ax, label=label, normalize=normalize, text_kwargs=text_kwargs, **kwargs)
[docs] def join_stokes_poincare(S1, S2, ax=None, normalize="s0", **kwargs): """ Plot a connection between two Stokes vectors on or inside the Poincaré sphere. The direction follows a great-circle path for non-zero-radius endpoints and uses linear interpolation when an endpoint is at the origin. Args: S1: first Stokes vector with shape `(4,)` S2: second Stokes vector with shape `(4,)` ax: optional matplotlib 3D axis normalize: either `"s0"` or `"unit"` **kwargs: style arguments passed to `matplotlib.axes.Axes.plot` Returns: tuple: `(fig, ax, line)` where `line` is the connecting arc/segment. """ if ax is None: fig = plt.figure(figsize=(8, 8)) ax = fig.add_subplot(111, projection="3d") draw_empty_sphere(ax) else: fig = ax.figure p1 = np.array(_stokes_xyz_for_poincare(S1, normalize=normalize), dtype=float) p2 = np.array(_stokes_xyz_for_poincare(S2, normalize=normalize), dtype=float) r1 = np.linalg.norm(p1) r2 = np.linalg.norm(p2) # If either endpoint is at the origin, connect points with a straight segment. if np.isclose(r1, 0.0) or np.isclose(r2, 0.0): t = np.linspace(0.0, 1.0, 50) p = (1.0 - t)[:, np.newaxis] * p1 + t[:, np.newaxis] * p2 line = ax.plot(p[:, 0], p[:, 1], p[:, 2], **kwargs)[0] return fig, ax, line u1 = p1 / r1 u2 = p2 / r2 ux, uy, uz = great_circle_points(u1[0], u1[1], u1[2], u2[0], u2[1], u2[2]) u = np.column_stack((ux, uy, uz)) # On the sphere, this is a great-circle arc; inside the sphere, scale radius between endpoints. radii = np.linspace(r1, r2, u.shape[0]) p = u * radii[:, np.newaxis] line = ax.plot(p[:, 0], p[:, 1], p[:, 2], **kwargs)[0] return fig, ax, line
[docs] def join_jones_poincare(J1, J2, ax=None, normalize="s0", **kwargs): """ Plot a connection between two Jones vectors on or inside the Poincaré sphere. Args: J1: first Jones vector with shape `(2,)` J2: second Jones vector with shape `(2,)` ax: optional matplotlib 3D axis normalize: either `"s0"` or `"unit"` **kwargs: style arguments passed to `join_stokes_poincare` Returns: tuple: `(fig, ax, line)` as returned by :func:`join_stokes_poincare`. """ JJ1 = _jones_for_visualization(J1) JJ2 = _jones_for_visualization(J2) S1 = pypolar.jones.jones_to_stokes(JJ1) S2 = pypolar.jones.jones_to_stokes(JJ2) return join_stokes_poincare(S1, S2, ax=ax, normalize=normalize, **kwargs)