RayDraping

class PlasmaCalcs.tools.sci_tools.ray_draping.RayDraping(*, scene, anchor, fov=None, ray_s=None)

Bases: RayDrapingCore

representation of the entire ray draping problem, including mapping calculations.

Consider using self.wbox(), to get “the answer”, i.e. ray points mapped into box system.
scene: dict or RayDrapeScene
scene conditions for ray draping problem.
dict –> convert to RayDrapeScene.
Should have keys: R0, XEXT, YEXT, ZEXT, T.
anchor: dict or RayDrapeAnchor
anchor point for ray draping problem.
dict –> convert to RayDrapeAnchor.
May have any of these keys: Asph_theta, Asph_phi, Abox_x, Abox_y.
(Missing keys use RayDrapeAnchor defaults.)
fov: 3-tuple of (“standard”, Nx, Ny), None, xr.DataArray, or RayDrapeFOV
field-of-view grid for ray draping problem.
None –> specify it later, by setting self.fov = value.
xr.DataArray –> convert to RayDrapeFOV.
Should have ‘component’ dimension associated with x, y, z coords of each point,
in sph system cartesian coords centered at r=0.
(“standard”, Nx, Ny) –> use self.standard_fov(Nx, Ny) to create reasonable FOV,
chosen such as the rectangle at zsph=R0+ZEXT) which fully covers entire box
(without any smart rotations yet; edges parallel to x sph and y sph axes).
ray_s: None, int, dict, or xr.DataArray including ‘ray_s’ or ‘ray_s_dim’ dimension.
Distance along each ray, from FOV. Rays are parametrized (in sph system) as:
w(s) = F + rayhat * s, where:
rayhat = (F - T)/|F - T|,
F is the point in FOV corresponding to this ray (there is one ray per FOV point),
T is the telescope/observer position.
Must include the ‘ray_s’ or ‘ray_s_dim’ dimension. Can be 1D if all rays have same spacing,
but might also have same dims as FOV, to specify different spacing for each ray.
None –> specify later (getting self.ray_s before specifying will just cause crash).
int –> number of points along each ray, choosing self.standard_ray_s_limits() for min and max s,
while using inclusive=(False,False) when generating the grid (to avoid including edge points).
standard_ray_s_limits() picks s limits to precisely cover full paths through the draped box region.
dict –> kwargs for pc.xarray_grid(). Uses self.standard_ray_s_limits() for min and max, unless provided.
Also uses name=’ray_s’ unless provided, and inclusive=(False,False) unless provided.
[TODO] add “maxstep” option to xarray_grid(), to enable “maxstep = simulation dx”.
Workaround to solve that problem for now: specify ray_s=None when making drape, then later do:
smin, smax = drape.standard_ray_s_limits()
ray_s=pc.xarray_grid(smin, smax, N=1+int(np.max((smax-smin)/dx)))
Examples: 100; dict(N=100); pc.xr1d([100, 200, 300, 400], name=’ray_s’).
(Use self.ray_s to see ray_s value, and self.ray_s_array to see the actual ray_s xr.DataArray.)
EXAMPLE
——-
# Below is a somewhat exaggerated example for visualization purposes;
# if the 3D simulation box is really that large compared to the Sun,
# then curvature effects might be physically significant within the plasma,
# so maybe a plane-parallel simulation isn’t the best idea on that scale.
import PlasmaCalcs as pc
import numpy as np
Rsun = 6.957e8 # [m]
AU = pc.DEFAULTS.PHYSICAL.CONSTANTS_SI[‘AU’] # 1.495978707e11 [m]
scene = pc.RayDrapeScene(R0=Rsun,
XEXT=1.0*Rsun,
YEXT=0.7*Rsun,
ZEXT=1.0*Rsun,
T=1*AU)
anchor = pc.RayDrapeAnchor(Asph_theta=np.pi*3/8,
Asph_phi=np.pi/4)
drape = pc.RayDraping(scene=scene, anchor=anchor,
fov=(“standard”, 7, 4),
ray_s=30)
drape.plot(‘both’, periodic=False)

Methods

box_to_sph(wbox)

return wsph corresponding to any wbox point or points.

from_dict(d)

create RayDraping instance from dict, with keys corresponding to __init__ kwargs,

get_post(x, y[, system, dim, N])

returns this post, i.e. points corresponding to line from (x,y,0) to (x,y,ZEXT) in box system.

keep_in_box(wsph, *[, in_box])

return wsph points masked to only those inside the draped box region.

periodic_wrap(wbox)

return wbox points wrapped periodically into x and y box extents.

plot([system, box, fov, rays, max_n_rays, ...])

visualize self by plotting.

plot_post(x, y[, ax_sph, ax_box])

plot a post (i.e. line from z=0 to z=ZEXT in box system) on the indicated ax(es).

ray_s_array()

self.ray_s as an xr.DataArray including 'ray_s' or 'ray_s_dim' dimension.

ray_tangents([system, unit_vector])

return array of vectors pointing in ray directions (i.e. tangent to ray path).

rayhat()

rayhat, i.e. unit vector along each ray in sph system cartesian coords.

rays([system, in_box, periodic])

return rays, i.e. points along each ray, in desired coordinate system.

sph_to_box(wsph, *[, in_box, periodic])

return wbox corresponding to any wsph point or points.

standard_FOV(Nx, Ny, *[, xdim, ydim])

return standard FOV (array) with (Nx, Ny) points.

standard_fov(Nx, Ny, *[, xdim, ydim])

return standard fov (RayDrapeFOV) with (Nx, Ny) points.

standard_fov_limits()

return (xmin, xmax, ymin, ymax) for standard FOV.

standard_ray_s(N)

return standard ray_s with N points along each ray.

standard_ray_s_limits()

return (min, max) s to use for self.ray_s, corresponding to full overlap with the box.

wbox(*[, in_box, periodic])

return wbox, i.e. the points along each ray, in box system cartesian coords.

wsph(*[, in_box])

return wsph, i.e. the points along each ray in sph system cartesian coords centered at r=0.

_get_box_corners([system])

return all 8 corners of the box, as xr.DataArray with 'corner' and 'component' dims.

_get_box_plot_values(*[, system, include])

return dict of values related to box, for self.plot(box=True).

_get_box_top([system, Nu, Nv])

return top surface of box, with Nu x Nv points,

_get_fov_value(value)

convert value to RayDrapeFOV (unless None) appropriate for storage in self.fov.

_repr_contents()

list of contents for self.__repr__

Attributes

Abox_x

alias to self.anchor.Abox_x

Abox_y

alias to self.anchor.Abox_y

Asph

alias to self.anchor.Asph

FOV

alias to self.fov.FOV

PLOT_DEFAULTS

R0

alias to self.scene.R0

T

alias to self.scene.T

X0

alias to self.scene.X0

X1

x-coordinate of right edge of box in box system.

XEXT

alias to self.scene.XEXT

Y0

alias to self.scene.Y0

Y1

y-coordinate of top edge of box in box system.

YEXT

alias to self.scene.YEXT

ZEXT

alias to self.scene.ZEXT

fov

field-of-view grid for ray draping problem.

n_rays

number of rays, i.e. number of points in self.FOV.

ray_s_dim

return name of ray_s dimension, i.e. 'ray_s' or 'ray_s_dim', depending on which one self.ray_s_array() has.

property Abox_x

alias to self.anchor.Abox_x

property Abox_y

alias to self.anchor.Abox_y

property Asph

alias to self.anchor.Asph

property FOV

alias to self.fov.FOV

property R0

alias to self.scene.R0

property T

alias to self.scene.T

property X0

alias to self.scene.X0

property X1

x-coordinate of right edge of box in box system. == X0 + XEXT.

property XEXT

alias to self.scene.XEXT

property Y0

alias to self.scene.Y0

property Y1

y-coordinate of top edge of box in box system. == Y0 + YEXT.

property YEXT

alias to self.scene.YEXT

property ZEXT

alias to self.scene.ZEXT

_get_box_corners(system='box')

return all 8 corners of the box, as xr.DataArray with ‘corner’ and ‘component’ dims.

Corners are in box system unless system==’sph’, in which case converted via self.box_to_sph().
_get_box_plot_values(*, system='box', include=['o', 'x', 'A', 'posts', 'bottom', 'top'])

return dict of values related to box, for self.plot(box=True).

Results are xr.DataArrays with ‘component’ dim telling x,y,z coords.
system: ‘box’ or ‘sph’
which system to get box values for.
‘box’ –> returns values in box system cartesian coords. No conversion necessary.
‘sph’ –> convert to sph system cartesian coords centered at r=0, via self.box_to_sph().
include: list of strs, tells which keys to include. (Use ‘posts’ to include all posts):
‘o’: point at xbox=X0, ybox=Y0, zbox=0. This is the lower-left point in the box.
‘x’: point at xbox=X1, ybox=Y0, zbox=0
‘A’: the anchor point
‘post00’: (xbox=X0,ybox=Y0), zbox from 0 to ZEXT, with dim ‘_pc_post_i_’
‘post10’: (xbox=X1,ybox=Y0), zbox from 0 to ZEXT, with dim ‘_pc_post_i_’
‘post01’: (xbox=X0,ybox=Y1), zbox from 0 to ZEXT, with dim ‘_pc_post_i_’
‘post11’: (xbox=X1,ybox=Y1), zbox from 0 to ZEXT, with dim ‘_pc_post_i_’
‘bottom’: zbox=0 surface, with dims ‘_pc_surface_u_’, and ‘_pc_surface_v_’
‘top’: zbox=ZEXT surface, with dims ‘_pc_surface_u_’, and ‘_pc_surface_v_’
(above, X1=X0+XEXT; Y1=Y0+YEXT)
The number of points along the posts and surface dims are determined by
self.PLOT_DEFAULTS[‘post_points’] and self.PLOT_DEFAULTS[‘surface_points’].
_get_box_top(system='box', *, Nu=21, Nv=21)

return top surface of box, with Nu x Nv points,

along dims _pc_surface_u_ and _pc_surface_v_.
Points are in box system unless system==’sph’, in which case converted via self.box_to_sph().
_get_fov_value(value)

convert value to RayDrapeFOV (unless None) appropriate for storage in self.fov.

_repr_contents()

list of contents for self.__repr__

anchor_cls

alias of RayDrapeAnchor

box_to_sph(wbox)

return wsph corresponding to any wbox point or points.

i.e., returns w expressed in sph system cartesian coords centered at r=0.
wbox: xarray object (e.g. DataArray)
points in box system cartesian coords,
with ‘component’ dimension associated with x, y, z coords of each point.
(Possibly useful: wbox=xr.concat([xbox, ybox, zbox], dim=’component’))
property fov

field-of-view grid for ray draping problem.

For efficiency, RayDrapeFOV is created immediately when setting self.fov (unless None).
(E.g., self.fov=arr; self.fov # == RayDrapeFOV(arr))
See help(type(self)) for more details about possible values.
fov_cls

alias of RayDrapeFOV

classmethod from_dict(d)

create RayDraping instance from dict, with keys corresponding to __init__ kwargs,

or to kwargs used to initialize scene, anchor, and fov, e.g. can provide “T” here.
Cannot overlap __init__ and scene/anchor/fov kwargs;
e.g. cannot provide both “T” and “scene”.
Keys allowed are:
‘scene’ or scene kwargs: {‘ZEXT’, ‘XEXT’, ‘R0’, ‘Y0’, ‘X0’, ‘T’, ‘YEXT’}
‘anchor’ or anchor kwargs: {‘Abox_y’, ‘Asph_phi’, ‘Asph_theta’, ‘Abox_x’}
‘fov’ or fov kwargs: {‘FOV’}
‘ray_s’
get_post(x, y, system='box', *, dim='_pc_post_i_', N=2)

returns this post, i.e. points corresponding to line from (x,y,0) to (x,y,ZEXT) in box system.

Result is an xarray.DataArray with ‘component’ dimension, converted to the indicated system.
x, y: scalars
coordinates of post in box system.
In box system, the post is a line from (x,y,0) to (x,y,ZEXT).
dim: str
name of dimension for points along post.
N: int
number of points along post (including endpoints).
keep_in_box(wsph, *, in_box=True)

return wsph points masked to only those inside the draped box region.

i.e., only keep points where R0 <= |wsph| <= R0 + ZEXT.
wsph: xarray object (e.g. DataArray)
points in sph system cartesian coords centered at r=0,
with ‘component’ dimension associated with x, y, z coords of each point.
(Possibly useful: wsph=xr.concat([xsph, ysph, zsph], dim=’component’))
in_box: bool, ‘lower’, or ‘upper’
whether to check that points will fall inside the draped box region,
masking all other points using NaNs.
‘lower’ –> only check lower bound: R0 <= |wsph|.
‘upper’ –> only check upper bound: |wsph| <= R0 + ZEXT.
property n_rays

number of rays, i.e. number of points in self.FOV

periodic_wrap(wbox)

return wbox points wrapped periodically into x and y box extents.

The wrapping is done using modulus operation, accounting for X0 and Y0 if nonzero:
xbox = (xbox - X0) % XEXT + X0
ybox = (ybox - Y0) % YEXT + Y0
wbox: xarray object (e.g. DataArray)
points in box system cartesian coords,
with ‘component’ dimension associated with x, y, z coords of each point.
(Possibly useful: wbox=xr.concat([xbox, ybox, zbox], dim=’component’))
plot(system='both', *, box=True, fov=True, rays=True, max_n_rays=100, in_box='lower', periodic=True, ax=None, axsize=(6, 6), aspect=True, view_init=True, extra_points=None, extra_lines=None)

visualize self by plotting. Useful for sanity checks!

Returns matplotlib ax (or tuple of (ax_sph, ax_box) if system=’both’).
If using Jupyter, for easier 3D visualization consider running a cell with command:
%matplotlib notebook
(Below, False-ness checked via “is False”, to avoid ambiguity with empty dicts)
system: ‘both’, ‘sph’ or ‘box’
which system(s) to visualize.
‘both’ –> make a figure with two plots, one for each system.
‘sph’ –> ‘sph’ system, where rays are straight, and box top and bottom are curved.
‘box’ –> ‘box’ system, where rays are curved, and box top and bottom are flat.
box: bool or dict of dicts
whether to plot the box boundaries.
True –> use default box plotting parameters (self.PLOT_DEFAULTS[‘box’]).
dict of dicts –> customize box plotting parameters. vals are dict or False.
Use False to indicate “don’t plot this part”.
Skip key to indicate “use default kwargs for this part”.
‘bottom’: kwargs for ax.plot_surface() for bottom of box (zbox=0 plane)
‘top’: kwargs for ax.plot_surface() for top of box (zbox=ZEXT plane)
‘posts’: kwargs for ax.plot() for vertical posts connecting bottom/top corners
‘A’: kwargs for ax.scatter() for anchor point
‘o’: kwargs for ax.scatter() for point at xbox=X0, ybox=Y0, zbox=0
‘x’: kwargs for ax.scatter() for point at xbox=XEXT, ybox=Y0, zbox=0
fov: bool or dict
whether to plot the FOV points.
True –> use default FOV plotting parameters (self.PLOT_DEFAULTS[‘fov’]).
dict –> customize FOV plotting parameters. Can provide any kwargs for ax.scatter().
rays: bool or dict
whether to plot the rays.
True –> use default ray plotting parameters (self.PLOT_DEFAULTS[‘box’]).
dict –> customize ray plotting parameters. Can provide any kwargs for ax.plot().
Some kwargs are treated specially: ‘colors’ and ‘cmap’
if ‘colors’ is provided, cycle these colors if there are more colors than rays,
else use ‘cmap’, or self.PLOT_DEFAULTS[‘rays_cmap’] if ‘cmap’ not provided.
max_n_rays: int or None
maximum number of rays to plot, else crash (if doing fov and/or rays).
prevents accidentally plotting way too much if doing production-grade RayDraping.
None –> no limit.
in_box: bool, ‘lower’, or ‘upper’
whether to check that points will fall inside the draped box region,
masking all other points using NaNs.
‘lower’ –> only check lower bound: R0 <= |wsph|.
‘upper’ –> only check upper bound: |wsph| <= R0 + ZEXT.
(The check here applies only to plotted rays)
periodic: bool
whether to wrap points periodically into x and y box extents.
True –> like draping the box periodically around the sphere.
False –> mainly useful for debugging… are there other use cases?
(The wrapping here applies only to plotted rays and FOV points when system==’box’)
(If periodic wrapping leads to weird-looking lines crossing the box,
consider using rays={‘linestyle’:’none’, ‘marker’:’.’} to just plot points.)
ax: None or matplotlib Axes3D object
Axes to plot on. None –> create new figure and axes.
Incompatible with system=’both’.
axsize: tuple of two ints
(x,y) size of new figure, per axes (only if making new axes because ax is None).
figsize = (2x, y) if system=’both’, else (x,y).
aspect: bool or 3-tuple of numbers
whether to set aspect ratio for 3D axes.
True –> set aspect based on self.PLOT_DEFAULTS[‘view_init’]. (default: (1,1,1))
3-tuple –> (x,y,z) scaling to use.
(Matplotlib is annoying about this, so we have to do:
sizes=[xmax-xmin, ymax-ymin, zmax-zmin] from get_xlim(), get_ylim(), get_zlim(),
ax.set_box_aspect(sizes[0]*aspect[0], sizes[1]*aspect[1], sizes[2]*aspect[2]))
view_init: bool or dict
whether to ax.view_init().
True –> use self.PLOT_DEFAULTS[‘view_init’].
dict –> dict of kwargs for ax.view_init(). (Consider: ‘elev’, ‘azim’, ‘roll’.)
plot_post(x, y, ax_sph=None, ax_box=None, **kw_plot)

plot a post (i.e. line from z=0 to z=ZEXT in box system) on the indicated ax(es).

x, y: scalars

coordinates of post in box system.
In box system, the post is a line from (x,y,0) to (x,y,ZEXT).
ax_sph, ax_box: None or matplotlib axes objects
plots post in sph system on ax_sph, in box system on ax_box.
None –> don’t plot on that system.
At least one of these two inputs must be provided, else crash.
Additional kwargs go to matplotlib ax.plot() calls.
Example (plots post at x=5, y=7 in both systems):
draper = RayDraping(…)
axes = draper.plot(system=’both’)
draper.plot_post(5, 7, *axes)
# equivalent: draper.plot_post(5, 7, ax_sph=axes[0], ax_box=axes[1])
ray_s_array()

self.ray_s as an xr.DataArray including ‘ray_s’ or ‘ray_s_dim’ dimension.

Behavior depends on self.ray_s:
xr.DataArray –> if has ‘ray_s’ or ‘ray_s_dim’ dim, return as-is, else crash (DimensionalityError)
None –> crash (InputMissingError)
int –> use this many points along each ray, with s_min, s_max = self.standard_ray_s_limits()
dict –> pass kwargs to pc.xarray_grid(), with min, max = self.standard_ray_s_limits() unless provided.
property ray_s_dim

return name of ray_s dimension, i.e. ‘ray_s’ or ‘ray_s_dim’, depending on which one self.ray_s_array() has.

[EFF] for now this just checks dims of self.ray_s_array() (i.e. it recomputes that array every time),
rather than guessing properly before the array is computed.
ray_tangents(system='box', *, unit_vector=False)

return array of vectors pointing in ray directions (i.e. tangent to ray path).

The implementation here takes derivatives along the ‘ray_s’ or ‘ray_s_dim’ dimension,
which may produce artifacts at edges of ray_s.
system: ‘box’ or ‘sph’
‘box’ –> return directions along rays in box system
‘sph’ –> equivalent to self.rayhat() (which is a constant for each ray)
unit_vector: bool
whether to convert to unit vector.
(‘sph’ result will always be unit vector, regardless.)
False –> ‘box’ result will just be derivative of ray with respect to ray_s.
rayhat()

rayhat, i.e. unit vector along each ray in sph system cartesian coords.

rayhat has dimensions of self.FOV,

as well as ‘component’ dimension associated with x, y, z coords of each point.
rayhat = (F - T)/|F - T|, where:
F is the point in FOV corresponding to this ray (there is one ray per FOV point),
T is the telescope/observer position.
rays(system='box', *, in_box=True, periodic=True)

return rays, i.e. points along each ray, in desired coordinate system.

Result is an xr.DataArray with dims from self.FOV, ‘ray_s’ or ‘ray_s_dim’,
and ‘component’ dim associated with x, y, z coords of each point.
system: ‘box’ or ‘sph’
‘box’ –> return points in box system cartesian coords.
‘sph’ –> return points in sph system cartesian coords centered at r=0.
in_box: bool, ‘lower’, or ‘upper’
whether to check that points will fall inside the draped box region,
masking all other points using NaNs.
‘lower’ –> only check lower bound: R0 <= |wsph|.
‘upper’ –> only check upper bound: |wsph| <= R0 + ZEXT.
periodic: bool
whether to wrap points periodically into x and y box extents.
True –> like draping the box periodically around the sphere.
False –> mainly useful for debugging… are there other use cases?
(only used if system==’box’)
Equivalent to self.wbox() or self.wsph(), depending on system.
scene_cls

alias of RayDrapeScene

sph_to_box(wsph, *, in_box=False, periodic=True)

return wbox corresponding to any wsph point or points.

i.e., returns w expressed in box system cartesian coords.
wsph: xarray object (e.g. DataArray)
points in sph system cartesian coords centered at r=0,
with ‘component’ dimension associated with x, y, z coords of each point.
(Possibly useful: wsph=xr.concat([xsph, ysph, zsph], dim=’component’))
in_box: bool, ‘lower’, or ‘upper’
whether to check that points will fall inside the draped box region,
masking all other points using NaNs.
‘lower’ –> only check lower bound: R0 <= |wsph|.
‘upper’ –> only check upper bound: |wsph| <= R0 + ZEXT.
(Use False for efficiency if this was already checked elsewhere!)
periodic: bool
whether to wrap points periodically into x and y box extents.
True –> like draping the box periodically around the sphere.
False –> mainly useful for debugging… are there other use cases?
standard_FOV(Nx, Ny, *, xdim='FOVx', ydim='FOVy')

return standard FOV (array) with (Nx, Ny) points.

This is chosen from scene and anchor params,
as rectangle (at zsph=R0+ZEXT) with size chosen to cover entire box,
and edges parallel to x sph and y sph axes.
It assumes that T is along sph z axis (else crash with NotImplementedError)
See help(PlasmaCalcs.ray_draping) FOV POSITION AND SIZE for more details.
See also: self.standard_fov(), self.standard_fov_limits()
standard_fov(Nx, Ny, *, xdim='FOVx', ydim='FOVy')

return standard fov (RayDrapeFOV) with (Nx, Ny) points.

This is chosen from scene and anchor params,
as rectangle (at zsph=R0+ZEXT) with size chosen to cover entire box,
and edges parallel to x sph and y sph axes.
It assumes that T is along sph z axis (else crash with NotImplementedError)
See help(PlasmaCalcs.ray_draping) FOV POSITION AND SIZE for more details.
See also: self.standard_FOV(), self.standard_fov_limits()
standard_fov_limits()

return (xmin, xmax, ymin, ymax) for standard FOV.

This is chosen from scene and anchor params,
as rectangle (at zsph=R0+ZEXT) with size chosen to cover entire box,
and edges parallel to x sph and y sph axes.
It assumes that T is along sph z axis (else crash with NotImplementedError)
See help(PlasmaCalcs.ray_draping) FOV POSITION AND SIZE for more details.
See also: self.standard_FOV(), self.standard_fov()
standard_ray_s(N)

return standard ray_s with N points along each ray.

Equivalent: self.ray_s_array() when self.ray_s == N.
Equivalent: xarray_grid(*self.standard_ray_s_limits(), N=N, **self._RAY_S_GRID_DEFAULTS)
(self._RAY_S_GRID_DEFAULTS is by default equivalent to: name=’ray_s’, inclusive=(False, False))
standard_ray_s_limits()

return (min, max) s to use for self.ray_s, corresponding to full overlap with the box.

Each ray may have its own (min, max) pair, so each is an xarray with same dims as self.FOV.
See help(PlasmaCalcs.ray_draping) RAY BOUNDARIES section for more details.
Value will be NaN for rays which never enter the draped box region.
wbox(*, in_box=True, periodic=True)

return wbox, i.e. the points along each ray, in box system cartesian coords.

These are the curved paths which are the “final answer” for the ray draping problem.
wbox has dimensions of self.FOV plus ‘ray_s’ or ‘ray_s_dim’ dimension,
as well as ‘component’ dimension associated with x, y, z coords of each point.
in_box: bool, ‘lower’, or ‘upper’
whether to check that points will fall inside the draped box region,
masking all other points using NaNs.
‘lower’ –> only check lower bound: R0 <= |wsph|.
‘upper’ –> only check upper bound: |wsph| <= R0 + ZEXT.
periodic: bool
whether to wrap points periodically into x and y box extents.
True –> like draping the box periodically around the sphere.
False –> mainly useful for debugging… are there other use cases?
wsph(*, in_box=True)

return wsph, i.e. the points along each ray in sph system cartesian coords centered at r=0.

wsph has dimensions of self.FOV plus ‘ray_s’ or ‘ray_s_dim’ dimension,

as well as ‘component’ dimension associated with x, y, z coords of each point.
wsph = F + rayhat * s, where:
rayhat = (F - T)/|F - T|,
F is the point in FOV corresponding to this ray (there is one ray per FOV point),
T is the telescope/observer position.
in_box: bool, ‘lower’, or ‘upper’
whether to check that points will fall inside the draped box region,
masking all other points using NaNs.
‘lower’ –> only check lower bound: R0 <= |wsph|.
‘upper’ –> only check upper bound: |wsph| <= R0 + ZEXT.