k1lib.kop module

This module is for optics simulation. This is exposed automatically with:

from k1lib.imports import *
kop.Rays.parallel(...) # exposed

A comprehensive introduction is available here: https://mlexps.com/optics/1-kop-intro/

class k1lib.kop.Drawable[source]

Bases: object

Base class to do common drawing routines

bounds()[source]

Should return (x1, y1, x2, y2). Base classes should always implement this method

img() PIL.Image.Image[source]

Returns PIL image

svg() str[source]

Returns svg string

class k1lib.kop.RandomPoints(data)[source]

Bases: Drawable

__init__(data)[source]

Container to place random points to be graphed either alone or inside a Drawables

bounds()[source]

Should return (x1, y1, x2, y2). Base classes should always implement this method

class k1lib.kop.Drawables(drawables: List[Drawable], config=None)[source]

Bases: Drawable

__init__(drawables: List[Drawable], config=None)[source]

A container with multiple :class:`Drawable`s, to draw out everything possible within a single coordinate frame

bounds()[source]

Should return (x1, y1, x2, y2). Base classes should always implement this method

class k1lib.kop.RaysFocus(rays: Rays)[source]

Bases: Drawable

bounds()[source]

Should return (x1, y1, x2, y2). Base classes should always implement this method

class k1lib.kop.Rays(data, prevRays: Optional[Rays] = None, ogSurface: Optional[Surface] = None)[source]

Bases: Drawable

__init__(data, prevRays: Optional[Rays] = None, ogSurface: Optional[Surface] = None)[source]

Represents a bunch of rays, stored in a numpy array so that all operations are fast

data format: array of shape (N, 6) - 0) x coordinate of starting point - 1) y coordinate of starting point - 2) angle counter clockwise from positive x axis - 3) length (inf for forever) - 4) wavelength in nm - 5) #transforms - 6) power in Watts, defaulted to 1W - 7) intersected?: used for outgoing rays exiting out of a surface. If surface._cast(prevRay) shows that it does intersect, then add that to the outgoing rays

The #transforms is a little complicated: - Original ray is 0, then each time a glass/mirror surface does something interesting,

the outgoing ray is incremented by 1. If it doesn’t touch the optic element, then it keeps the same number as before

Parameters
  • prevRays – a reference to the previous Rays object, in order to limit the length of all previous rays!

  • ogSurface – the surface that generates this Rays

static parallel(x=0, y=0, theta=0, N=10, height=10, color=700, power=1)[source]

Creates parallel rays starting from a particular point with a particular angle

Parameters
  • theta – angle from positive x axis counterclockwise

  • N – how many rays to create total?

  • height – the height of the parallel rays if it were to have no angle

static parallelToBounds(x=0, y=0, bounds=None, N=10, color=700, power=1, coverage=0.9, angleOffset=0)[source]

Creates parallel rays starting from a particular point to the center of some bounds. The bounds should have the format (xmin, ymin, xmax, ymax). :class:`Surface`s, :class:`OpticElement`s all have the .bounds() method, so you can use them

See also: parallel()

Parameters

coverage – number from 0 to 1. 1 meaning the parallel lines should cover the entire (rotated) height of the bounds, 0.5 means it covers only half of the height, and so on. Think of this as %height

static pointToBounds(x=0, y=0, bounds=None, N=10, color=700, power=1, coverage=0.9, angleOffset=0)[source]

Creates rays starting from a particular point to the bounds.

See also: parallelToBounds()

Parameters
  • coverage – instead of %height like parallelToBounds(), this is the anglular coverage, 1 for covering the entire object.

  • angleOffset – offset to the angle from the starting point to the center of the bounds

bounds()[source]

Should return (x1, y1, x2, y2). Base classes should always implement this method

hitbox(spread=2, maxWH=400, mode='simple', intersectOnly=True) PIL.Image.Image[source]

Visualizes the Ray’s starting point, with colors and whatnot. Example:

r = Rays.parallel(theta=pi/8)
r.hitbox()
Parameters
  • spread – will color this much nearby pixels

  • maxWH – size of the returned image. Either width or height will be around this big, whichever is bigger

  • mode – several plotting modes, each with its own performance and accuracy characteristics

  • intersectOnly – if True (default), will only plot the hitbox of the rays that actually hits the Surface that generates the ray and not some previous surface

focus() RaysFocus[source]
class k1lib.kop.Wavelengths(wavs: List[int])[source]

Bases: object

__init__(wavs: List[int])[source]

Container for wavelengths in nm. Pretty much only used to convert to rgb in vectorized way

k1lib.kop.sellmeier(wavelengths: ndarray, glassParam)[source]

Runs the sellmeier equation to get the index of refraction for multiple wavelengths for a particular glass material

class k1lib.kop.Surface(gp1=None, gp2=None, mode='glass', capture=False, angleStd=0, opticElement=None)[source]

Bases: Drawable

__init__(gp1=None, gp2=None, mode='glass', capture=False, angleStd=0, opticElement=None)[source]

Represents a geometry, like line, arc, or parabola. Rays can interact with a surface, resulting in new Rays. An optic element can have multiple of these surfaces and react differently to incoming rays.

The convension is that gp1 is the environment’s glassParam, while gp2 is the Surface’s glassParam. A surface needs both in order to calculate the deflection angles.

These modes are available:

  • glass: transparent (or semi-transparent) surface, bends incoming rays

  • mirror: perfectly reflective (or allow some percentage of light to pass through)

  • sensor: absorbes all incoming rays, can retrieve the image that’s absorbed

  • opaque: reflects incoming rays in a random direction outward

Parameters
  • gp1 – glass params 1, usually this is of the environment (air)

  • gp2 – glass param 2, usually this is of the object (borosilicate glass, sapphire, water)

  • mode – explained above

  • capture – if True, captures all rays that comes in through .cast() function. Captures will stay the same across copy.copy() instances to save perf! The captures will be available at .captures

  • angleStd – if specified, when casting outgoing rays, will add this much standard deviation in the rays’ angle, useful to quickly simulate a bumpy piece of glass

  • opticElement – the OpticElement that this Surface is a part of

castS(rays) Rays[source]

This will truncate the lengths of the incoming Rays object, and return a new rays, as well as the lengths of the old rays, for easy comparison. If the rays don’t interact with the lens at all, then just copy the old rays over, and the length is the same.

cast(rays, **kwargs) RaysPath[source]
class k1lib.kop.LineSurface(x1, y1, x2, y2, **kwargs)[source]

Bases: Surface

__init__(x1, y1, x2, y2, **kwargs)[source]

Surface defined using a straight line.

If x1,y1 is on the left, x2,y2 on the right, then gp1 (glass param) is on top, gp2 is at the bottom.

bounds()[source]

Should return (x1, y1, x2, y2). Base classes should always implement this method

class k1lib.kop.ArcSurface(x, y, r, startAngle, endAngle, **kwargs)[source]

Bases: Surface

__init__(x, y, r, startAngle, endAngle, **kwargs)[source]

Surface defined using a circular arc

static from2Points(x1, y1, x2, y2, r, **kwargs) ArcSurface[source]

Creates an ArcSurface from 2 points and a radius. Example:

kop.ArcSurface.from2Points(1, 2, 3, 4, 10)
bounds()[source]

Should return (x1, y1, x2, y2). Base classes should always implement this method

k1lib.kop.polySolver(polys)[source]

Expected to take in a (N,F) array of N different polynomials with order F-1 and return 2 arrays:

  • intersected (N,) bool array: whether there’re any roots

  • beta (N,) float array: the root of each polynomial that’s >0 and is the minimum out of all roots

Right now, only supports vectorized solvers with order 2 polynomials. Any higher and it falls back to using un-vectorized np.roots(), or other methods like it. This is separated out so that it can be swapped out for a better implementation

Potential candidates for >2 order polynomials: - scipy: fsolve, root, newton, brentq

class k1lib.kop.PolySurface(x=0, y=0, angle=0, scale=1, xmin=-10, xmax=10, coeff=(0, 0, 0.1), solver=<function polySolver>, **kwargs)[source]

Bases: Surface

__init__(x=0, y=0, angle=0, scale=1, xmin=-10, xmax=10, coeff=(0, 0, 0.1), solver=<function polySolver>, **kwargs)[source]

Surface defined using a polynomial.

Let’s say there’s a parabola that you’d like to input into the simulation, say x^2/10. There are several dials and knobs that you can change to position your Surface right where you want it.

There are 2 frame of reference I’ll be using. First is world frame, where all your elements sits in, and second is poly frame, where your polynomial coefficients will define (x, y) in.

In the poly frame, the surface is defined as all x points in (xmin, xmax), together with all y points calculated straight from your coefficients. So if coeff = [1, 2, 3], then the equation will be py = f(px) = 3px^2 + 2px + 1. Now pairs of (px, py) points in poly frame is obtained.

Then (px, py) will go through several transformations to get to the world frame: - Rotated by angle radians counterclockwise - Scaled by scale - Translated by (x, y)

Then to calculate reflections and whatnot, internally, all rays will be translated from world to poly frame, then the intersection points are solved and then results will be translated from poly back to world frame.

Parameters
  • x – translation applied to surface

  • angle – angle to rotate the surface by

  • scale – scaling factor to scale the surface by

  • xmin – to define the domain of the given polynomial in poly frame

  • coeff – polynomial coefficients

static parabola(x, y, r=5, width=16, angle=0, **kwargs)[source]
static parabolaFrom2Points(x1, y1, x2, y2, r=5, **kwargs)[source]
world2poly(wxs, wys)[source]
poly2world(pxs, pys)[source]
bounds()[source]

Should return (x1, y1, x2, y2). Base classes should always implement this method

class k1lib.kop.RaysPath(rayss: List[Rays])[source]

Bases: Drawable

__init__(rayss: List[Rays])[source]

Container for a list of Rays, and have mechanism to draw them out with a slight path correction. Expected to contain output of a ray tracing session.

bounds()[source]

Should return (x1, y1, x2, y2). Base classes should always implement this method

class k1lib.kop.OpticElement[source]

Bases: Drawable

surfaces: List[Surface]
__init__()[source]

Represents a solid object with several Surfaces

cast(rays, *args, **kwargs)[source]
class k1lib.kop.Polygon(points=None, **kw)[source]

Bases: OpticElement

__init__(points=None, **kw)[source]

Creates an OpticElement that has the shape of any polygon you want. Example:

p = Polygon([[1, 2], [3, 4], [5, 6]])

This will create a prism with verticies at the specified points

surfaces: List[Surface]
bounds()[source]

Should return (x1, y1, x2, y2). Base classes should always implement this method

class k1lib.kop.Lens(x=50, y=0, R1=300, R2=300, thickness=3, height=20, angle=0, surface='arc', **kw)[source]

Bases: OpticElement

__init__(x=50, y=0, R1=300, R2=300, thickness=3, height=20, angle=0, surface='arc', **kw)[source]

Represents a Lens with center at (x, y), and with 2 radiuses, R1 for left and R2 for right.

Parameters
  • R1 – radius of the left side, works for both arc and parabola mode

  • angle – angle offset of the Lens

  • surface – either “arc” or “parabola”, which will make a lens of that geometry

surfaces: List[Surface]
bounds()[source]

Should return (x1, y1, x2, y2). Base classes should always implement this method

class k1lib.kop.OpticSystem(elements: List[OpticElement | Surface] = None, envGlassParam=(0, 0, 0, 0, 0, 0))[source]

Bases: Drawable

add(*elements)[source]
property surfaces

Grabs all surfaces from all optic elements and raw surfaces

cast(rays: Rays, passes=20, verbose=False) RaysPath[source]
bounds()[source]

Should return (x1, y1, x2, y2). Base classes should always implement this method