gtrace.layout

gtrace.layout

A module providing the OpticalLayout class: a container representing a whole optical system (optics, source beams and tracing rules).

An OpticalLayout is the model object that GUI front ends interact with. The intended workflow is:

  1. Construct and align the optics with ordinary Python code (using sequential tracing, cavity eigenmode solving, etc.).

  2. Register the finished optics, the source beam(s) and the tracing rules into an OpticalLayout.

  3. Use layout.trace() to run the non-sequential trace and layout.draw() / layout.scene_dict() to visualize the result.

The layout holds the registered optics and sources BY REFERENCE (no copies). Changing an attribute of a registered mirror (e.g. M.HRcenter) and calling layout.trace() again gives the updated result. This is the basis of the future bidirectional GUI: an edit made in the GUI is applied to the corresponding object in the layout, which is the same object the user has in their code.

Attributes

__author__

__copyright__

__credits__

__license__

__version__

__maintainer__

__email__

__status__

EDITABLE_OPTIC_ATTRS

ATTR_CHOICES

DRAW_OPTIONS

EDITABLE_DRAW_OPTIONS

EDITABLE_RULE_ATTRS

CREATABLE_OPTIC_PARAMS

_INHERITED_PARAMS

CREATABLE_LENS_PARAMS

DEFAULT_LENS_F

CREATABLE_OPTIC_TYPES

_LENS_TYPES

EDITABLE_SOURCE_ATTRS

_SOURCE_WAIST_ATTRS

_SOURCE_ATTR_ORDER

_SOURCE_ATTR_RANK

CREATABLE_SOURCE_PARAMS

SOURCE_TYPE

DEFAULT_SOURCE_WL

DEFAULT_SOURCE_WAIST

EDITABLE_DIMENSION_ATTRS

_DIMENSION_POINTS

_DXF_TICK

_DXF_TEXT

DIMENSION_TYPE

MECHANICS_TYPE

DUMP_TYPE

ASSEMBLY_TYPE

CREATABLE_DUMP_PARAMS

CREATABLE_ASSEMBLY_PARAMS

_ASSEMBLY_ATTRS

_OPTIC_POSE_ATTRS

EDITABLE_MECHANICS_ATTRS

_MECHANICS_POSE_ATTRS

CREATABLE_MECHANICS_PARAMS

UNDO_DEPTH

_NOT_UNDOABLE

EXPORT_FORMATS

_ATTR_ORDER

_ATTR_RANK

MAX_RULE_ORDER

SHARED_POINT_TOL

DUMP_REFLECTIVITY

_SNAP_FACE_POINTS

DEFAULT_PEDESTAL

DEFAULT_FORK

ASSEMBLY_KINDS

Exceptions

EditError

Raised when an edit message cannot be applied: unknown operation,

Classes

TraceRules

Rules controlling the non-sequential trace of an OpticalLayout.

Dimension

A distance measured between two points of a layout.

OpticalLayout

A container representing a whole optical system: optics,

Functions

_fmt_mm(metres)

_ordered(attrs)

The items of an attribute dict, in the order they must be applied.

_check_choice(key, value)

Reject a value outside the set an attribute allows.

_as_point(value, key)

A pair of finite coordinates from an edit message, or an EditError.

_as_distance(value, key)

A finite distance in metres from an edit message, or an EditError.

_as_rule_value(key, value)

A tracing rule value from an edit message, or an EditError.

_set_optic_attr(optics, key, value)

Set one whitelisted attribute of an optics on behalf of a front end.

_in_notebook()

Whether we are running inside a Jupyter kernel.

optic_to_dict(m)

Convert a Mirror, CyMirror, Lens or CyLens to a JSON-compatible

optic_from_dict(d)

Construct a Mirror, CyMirror, Lens or CyLens from a dict produced

source_to_dict(b)

Convert a source GaussianBeam to a JSON-compatible dict of its

dimension_to_dict(d)

Convert a Dimension to a JSON-compatible dict.

dimension_from_dict(d)

Construct a Dimension from a dict produced by dimension_to_dict().

_update_dimension(dim, d)

Apply a serialized dimension to an existing one, in place.

mechanics_to_dict(m)

Convert a Mechanics to a JSON-compatible dict.

mechanics_from_dict(d)

Construct a Mechanics from a dict produced by mechanics_to_dict().

_update_mechanics(m, d)

Apply a serialized Mechanics to an existing one, in place.

_shared_point(m, host)

The point of m that already coincides with a point of the host,

mechanics_scene_dict(m)

Convert a Mechanics into the dict a viewer addresses it by: its

_turn(p, angle)

A point turned about the origin. The joint arithmetic of an

_pose_host(obj)

What an element or a body takes its pose from, or None.

beam_dump([name, center, angle, reflectivity, apex, ...])

A V beam dump: two absorbing faces and the housing they sit in.

_dump_shapes(apex, length, thick, half, radius, ...)

The housing of a beam dump, in its own frame: the post hole at the

mechanics_snap_points(m)

The points of a Mechanics worth snapping to: the four corners of

optic_snap_points(o)

The points of an optics worth snapping a measurement to: the four

_merge_by_name(registered, specs, build, update)

Rebuild a list of registered objects from their serialized form,

_update_optic(m, d)

Apply a serialized optics to an existing one, in place.

_update_source(b, d)

Apply a serialized source beam to an existing one, in place.

source_from_dict(d)

Construct a source GaussianBeam from a dict produced by

rayleigh_range(w0, wl[, n])

The Rayleigh range of a beam of waist size w0.

q_from_waist(w0, d, wl[, n])

The q-parameter, at the origin of a beam, of light whose waist is of

source_waist(b)

The waist of a source beam, as the pair a front end shows: sizes and

source_scene_dict(b)

Convert a source beam into the dict a viewer draws and addresses it

_ordered_source(attrs)

The items of a source attribute dict, in the order they must be

_as_positive(value, key[, what])

A finite positive number from an edit message, or an EditError.

_as_complex(value, key)

A q-parameter from an edit message, or an EditError.

_set_source_attr(b, key, value)

Set one whitelisted attribute of a source beam on behalf of a front

_stand(optic[, mount, pedestal, fork, mount_name, ...])

The parts that hold one element, jointed to it and to each other.

mirror_assembly([name, center, angle, HRcenter, ...])

A mirror and what holds it: the optic, its mount, the pedestal and

lens_assembly([name, center, angle, f, diameter, ...])

A lens and what holds it: the optic, its holder, the pedestal and

assembly_kinds()

What assembly() can build: kind, label and description, in

_assembly_kind(kind)

assembly(kind, **kwargs)

Build one of the assemblies assembly_kinds() lists.

Module Contents

gtrace.layout.__author__ = 'Yoichi Aso'
gtrace.layout.__credits__ = ['Yoichi Aso']
gtrace.layout.__license__ = 'BSD'
gtrace.layout.__version__ = '0.8.0'
gtrace.layout.__maintainer__ = 'Yoichi Aso'
gtrace.layout.__email__ = 'asoy01@gmail.com'
gtrace.layout.__status__ = 'Beta'
class gtrace.layout.TraceRules(order=10, power_threshold=0.1, open_beam_length=1.0)

Bases: object

Rules controlling the non-sequential trace of an OpticalLayout.

Variables:
  • order (int) – Number of ghost reflections a beam may go through before it stops being followed. Every beam carries the count as its stray_order, and the count is not reset when the beam leaves one element for the next, so this limits the whole trace rather than one element. Defaults to 10.

  • power_threshold (float) – Beams with power below this threshold are not propagated further. Defaults to 0.1.

  • open_beam_length (float) – Drawn length of beams that do not hit anything. Defaults to 1.0.

  • element (Note that the order can be overridden for an individual)

  • the (through its max_stray_order attribute. That setting lives on)

  • an (optics rather than here, next to term_on_HR, because how deep)

  • element. (element's ghosts are worth chasing is a property of the)

order = 10
power_threshold = 0.1
open_beam_length = 1.0
to_dict()

Convert to a JSON-compatible dict.

classmethod from_dict(d)

Construct a TraceRules from a dict produced by to_dict().

class gtrace.layout.Dimension(p1, p2, name='D', offset=0.0)

Bases: object

A distance measured between two points of a layout.

A dimension is a note about the system rather than a part of it: it takes part in no trace and changes no beam. It is registered in the layout all the same, because a measurement worth taking is worth keeping - it is saved with the layout, comes back with it, and can be taken back with undo, like anything else the layout holds.

The two points are plain coordinates. A dimension does not hold on to the element a point was taken from: an element that then moves leaves the measurement where it was made, which is what a note should do. Measuring the same thing again after a change is a matter of drawing it again.

Variables:
  • name (str) – Name of the dimension, unique within the layout.

  • p2 (p1,) – The two ends, of shape (2,), in global coordinates.

  • offset (float) –

    How far to one side of the two ends the dimension line is drawn, in metres, positive to the left of the direction from p1 to p2. Zero puts the line straight between them.

    This is where a drawing puts the line, not what is being measured: what a bench wants measured usually runs along a beam or through an element, which is exactly where a line drawn on top of it cannot be read. Extension lines carry the ends out to wherever there is room, as they do on any engineering drawing.

name = 'D'
p1
p2
offset
property length

The distance between the two ends.

The offset does not come into it: the dimension line is moved aside to be read, not to measure something else.

property normal

to the left of the direction from p1 to p2. Zero for a dimension of no length.

Type:

The unit vector the offset is measured along

line_ends()

The two ends of the dimension line itself - the ends carried out by the offset. Equal to p1 and p2 when the offset is zero.

Return type:

(numpy.ndarray, numpy.ndarray)

measure(optics=())

What this dimension comes to, against a list of optics.

The optical distance is reported only when the whole span runs inside one substrate, where it is the physical distance times the refractive index. A span that crosses in and out of glass has an optical length too, but it is not a dimension of anything - it depends on where the ends happen to fall - so it is left out rather than written next to a number it would be mistaken for.

Parameters:

optics (sequence, optional) – The optics to test the span against.

Returns:

{'length': float, 'optical': float or None, 'inside': str or None, 'n': float or None}

Return type:

dict

copy()

A new Dimension with the same ends, offset and name.

gtrace.layout.EDITABLE_OPTIC_ATTRS
gtrace.layout.ATTR_CHOICES
gtrace.layout.DRAW_OPTIONS
gtrace.layout.EDITABLE_DRAW_OPTIONS
gtrace.layout.EDITABLE_RULE_ATTRS
gtrace.layout.CREATABLE_OPTIC_PARAMS
gtrace.layout._INHERITED_PARAMS = ['diameter', 'thickness', 'wedgeAngle', 'n', 'Refl_HR', 'Trans_HR', 'Refl_AR', 'Trans_AR']
gtrace.layout.CREATABLE_LENS_PARAMS
gtrace.layout.DEFAULT_LENS_F = 0.5
gtrace.layout.CREATABLE_OPTIC_TYPES
gtrace.layout._LENS_TYPES
gtrace.layout.EDITABLE_SOURCE_ATTRS
gtrace.layout._SOURCE_WAIST_ATTRS
gtrace.layout._SOURCE_ATTR_ORDER = ['wl', 'n', 'P', 'qx', 'qy', 'waist_size_x', 'waist_size_y', 'waist_pos_x', 'waist_pos_y',...
gtrace.layout._SOURCE_ATTR_RANK
gtrace.layout.CREATABLE_SOURCE_PARAMS
gtrace.layout.SOURCE_TYPE = 'Source'
gtrace.layout.DEFAULT_SOURCE_WL = 1.064e-06
gtrace.layout.DEFAULT_SOURCE_WAIST = 0.0002
gtrace.layout.EDITABLE_DIMENSION_ATTRS
gtrace.layout._DIMENSION_POINTS
gtrace.layout._DXF_TICK = 0.02
gtrace.layout._DXF_TEXT = 0.08
gtrace.layout._fmt_mm(metres)
gtrace.layout.DIMENSION_TYPE = 'Dimension'
gtrace.layout.MECHANICS_TYPE = 'Mechanics'
gtrace.layout.DUMP_TYPE = 'BeamDump'
gtrace.layout.ASSEMBLY_TYPE = 'Assembly'
gtrace.layout.CREATABLE_DUMP_PARAMS
gtrace.layout.CREATABLE_ASSEMBLY_PARAMS
gtrace.layout._ASSEMBLY_ATTRS
gtrace.layout._OPTIC_POSE_ATTRS
gtrace.layout.EDITABLE_MECHANICS_ATTRS
gtrace.layout._MECHANICS_POSE_ATTRS
gtrace.layout.CREATABLE_MECHANICS_PARAMS
gtrace.layout.UNDO_DEPTH = 50
gtrace.layout._NOT_UNDOABLE
gtrace.layout.EXPORT_FORMATS
exception gtrace.layout.EditError

Bases: ValueError

Raised when an edit message cannot be applied: unknown operation, unknown target or an attribute that is not editable.

gtrace.layout._ATTR_ORDER = ['anchor_point', 'diameter', 'thickness', 'wedgeAngle', 'n', 'Refl_HR', 'Trans_HR', 'Refl_AR',...
gtrace.layout._ATTR_RANK
gtrace.layout._ordered(attrs)

The items of an attribute dict, in the order they must be applied.

Anything not in _ATTR_ORDER sorts last, by name, so that an attribute added to the whitelist without a thought about ordering still lands somewhere reproducible.

gtrace.layout._check_choice(key, value)

Reject a value outside the set an attribute allows.

gtrace.layout._as_point(value, key)

A pair of finite coordinates from an edit message, or an EditError.

Points arrive from a browser as JSON arrays, where a missing number is null and a runaway one is a string; neither is a place on a bench, and both would otherwise settle quietly into a numpy array as nan.

gtrace.layout._as_distance(value, key)

A finite distance in metres from an edit message, or an EditError.

gtrace.layout.MAX_RULE_ORDER = 200
gtrace.layout._as_rule_value(key, value)

A tracing rule value from an edit message, or an EditError.

gtrace.layout._set_optic_attr(optics, key, value)

Set one whitelisted attribute of an optics on behalf of a front end.

Two things the whitelist cannot express happen here. An attribute may exist on only some classes, and a model that accepts the name may still refuse the value - assigning a focal length re-solves both curvatures, and there are focal lengths a given blank cannot have. Both come back as EditError, so that a front end sees one kind of refusal however the model spelt it, and in both cases the optics is left exactly as it was.

gtrace.layout._in_notebook()

Whether we are running inside a Jupyter kernel.

Looks the module up in sys.modules rather than importing it, so that gtrace does not pull IPython in when it is not already there.

gtrace.layout.optic_to_dict(m)

Convert a Mirror, CyMirror, Lens or CyLens to a JSON-compatible dict of its construction parameters.

A Lens is written out by its curvatures, like any other optics, and not by its focal length: the focal length is derived from them, and a lens whose radii were edited after it was built is that lens, not the one originally ordered.

gtrace.layout.optic_from_dict(d)

Construct a Mirror, CyMirror, Lens or CyLens from a dict produced by optic_to_dict().

gtrace.layout.source_to_dict(b)

Convert a source GaussianBeam to a JSON-compatible dict of its construction parameters.

gtrace.layout.dimension_to_dict(d)

Convert a Dimension to a JSON-compatible dict.

Only the ends are written. What the dimension comes to - the distance, and whether it runs inside a substrate - is derived from them and from the optics around it, and is recomputed on the way into a scene rather than stored, so that it cannot go stale.

gtrace.layout.dimension_from_dict(d)

Construct a Dimension from a dict produced by dimension_to_dict().

gtrace.layout._update_dimension(dim, d)

Apply a serialized dimension to an existing one, in place.

gtrace.layout.mechanics_to_dict(m)

Convert a Mechanics to a JSON-compatible dict.

The shapes are written out by value, not by model name: a saved layout is complete in itself, the way a written HTML page is, and a library that has moved on since cannot silently redraw it. The model name travels alongside as a label.

An attached body writes its host’s name and its offset, and no pose: the pose is derived, and a stored copy of a derived value is exactly the second description this design exists to avoid. The name is read from the host object at save time, so a host renamed since the attachment saves under its current name.

gtrace.layout.mechanics_from_dict(d)

Construct a Mechanics from a dict produced by mechanics_to_dict().

An attached body comes back attached by name: the host is another entry of the same file, so only the layout that is loading both can join them up - which OpticalLayout does when the body is registered or merged. Until then the pose refuses to be read.

gtrace.layout._update_mechanics(m, d)

Apply a serialized Mechanics to an existing one, in place.

The attachment is settled first, because it decides what the pose keys then mean: a body the file says is attached takes an offset and no pose, and one the file says is free takes a pose. The host name is left pending for the layout to resolve against its own optics - see OpticalLayout._link_mechanics.

gtrace.layout.SHARED_POINT_TOL = 0.002
gtrace.layout._shared_point(m, host)

The point of m that already coincides with a point of the host, in m’s local coordinates, or None.

This is what makes “snap it into place, then attach it” mean what it looks like: the pedestal was dropped onto a hole, so the hole is what it is pinned by, and a fork clamped on a post turns about the post rather than about its own origin. Nothing near enough leaves the attach point alone, which is the local origin unless something else said otherwise.

gtrace.layout.mechanics_scene_dict(m)

Convert a Mechanics into the dict a viewer addresses it by: its pose, and the polygon it is picked with.

The shapes are not here - they are drawn through the canvas, on the body’s own layer, like the substrates of the optics. What a front end needs on top of the drawing is the identity (“the user grabbed the breadboard”) and the outline, which is what a click is tested against and what a drag previews.

gtrace.layout._turn(p, angle)

A point turned about the origin. The joint arithmetic of an assembly, which is the same turn a body’s does - written here rather than reached for across modules.

gtrace.layout._pose_host(obj)

What an element or a body takes its pose from, or None.

A body follows what it is attached to and an element follows what it is assembled to. One question with one answer, so that walking a chain - to refuse a circle, or to settle in order - does not have to know which kind of thing it is holding.

gtrace.layout.DUMP_REFLECTIVITY = 0.04
gtrace.layout.beam_dump(name='BD1', center=(0.0, 0.0), angle=0.0, reflectivity=DUMP_REFLECTIVITY, apex=0.025, face_length=0.05, face_thickness=0.003, opening=None, body_diameter=0.05, top_width=0.027327, width=0.047848, post_diameter=0.0045, boss_diameter=0.0075, **kwargs)

A V beam dump: two absorbing faces and the housing they sit in.

The point of the V is that a black face is not perfectly black. What it sends back is caught by the other face and sent back again, so two bounces leave a hundredth of what one leaves - and the residue goes out of the mouth rather than back up the beam. That is worth tracing, which is why the faces are elements rather than a shape drawn on the housing.

The three come back jointed: the second face follows the first and the housing is attached to it, so the dump is one thing to move. They come back split - the elements, then the bodies - since that is the division everything downstream makes: a layout registers the two by different doors, the trace sees only the first, and the order within each list is hosts first, which is the order they have to be registered in.

The dimensions default to the drawing in local/BeamDump.dxf - a 50 mm face 3 mm thick, a 28 degree opening, and a 50 mm body with its top corners cut off. Three of those numbers settle the whole V: the faces run from the apex, and their centres come out where the drawing dimensions them. The reflectivity is not from that drawing; see DUMP_REFLECTIVITY.

Aim it into one side of the V rather than at the apex. A ray sent exactly at the apex hits neither face - that is the point where the two of them end - which is a knife edge on a bench as well as here.

Parameters:
  • name (str, optional) – What the dump is called. The three pieces are named from it: BD1a, BD1b and BD1box for the default ‘BD1’. A dump is numbered and its pieces lettered, so that a second dump is BD2 and ‘BD2b’ reads as the far face of it.

  • center (sequence of 2 floats, optional) – Where the post hole stands, in metres.

  • angle (float, optional) – The direction the light it is to catch travels, in radians, so that a dump is aimed the way the beam runs rather than by where its mouth points. 0 - the default - catches a beam travelling along +x.

  • reflectivity (float, optional) – What one face sends back, as a power fraction.

  • apex (float, optional) – How far above the post hole the two faces meet, in metres. The post hole is the origin the whole dump is laid out from - the drawing is dimensioned from it, and it is the point the dump is bolted down by - so it is where the housing’s local origin sits and where center puts it.

  • face_length (float, optional) – Each face, in metres. Defaults to 50 mm.

  • face_thickness (float, optional) – The substrate behind each face. Defaults to 3 mm.

  • opening (float or None, optional) – The angle between the faces, in radians. None is 28 degrees.

  • body_diameter (float, optional) – The housing: a disc of that diameter with its top corners cut off by two chords, from top_width across at the top to width at its widest.

  • top_width (float, optional) – The housing: a disc of that diameter with its top corners cut off by two chords, from top_width across at the top to width at its widest.

  • width (float, optional) – The housing: a disc of that diameter with its top corners cut off by two chords, from top_width across at the top to width at its widest.

  • post_diameter (float, optional) – The hole it is bolted down through, and the boss around it.

  • boss_diameter (float, optional) – The hole it is bolted down through, and the boss around it.

  • **kwargs – Passed to the two faces, so that anything an Optics takes - a layer, a wedge - can be set here.

Returns:

([face1, face2], [housing]): the elements and the bodies, already jointed.

Return type:

tuple

gtrace.layout._dump_shapes(apex, length, thick, half, radius, top_half, wide_half, post_r, boss_r)

The housing of a beam dump, in its own frame: the post hole at the origin, the mouth facing -y.

A drawing of what the part occupies and of the features a layout is read for - the outline, the slots the two faces sit in, and the hole it is bolted down through - rather than a reproduction of every line of the machine drawing.

gtrace.layout.mechanics_snap_points(m)

The points of a Mechanics worth snapping to: the four corners of its outline, the middle of each of its edges, its center, and the centre of every circle it draws.

The circles are the screw holes of a breadboard - which is where a bench actually puts things, so they are what a measurement wants to run between and what a dragged mirror wants to land on. They are marked ‘hole’, which is the kind the drag snap listens for; knobs and other decorative circles ride along, which costs a snap point that is still a real feature of the drawing.

The points the part names for itself come before them, because two marks at one place are the same point as far as snapping goes and the first of them wins: a mount’s post hole is both a circle in the drawing and the point it is stood on its pedestal by, and ‘MT post’ says more than ‘MT hole’. The same rule, for the same reason, as an optics winning over the beam that ends on it.

gtrace.layout._SNAP_FACE_POINTS = [('HRcenter', 'HR'), ('ARcenter', 'AR'), ('center', 'centre')]
gtrace.layout.optic_snap_points(o)

The points of an optics worth snapping a measurement to: the four corners of the substrate, the apex of each face, the middle, and the middle of each side.

These come from Python rather than being worked out by a front end because they are geometry: a corner is where the wedge and the sagitta of a curved face put it, and there is no reason for a second description of that to exist in a browser. Beam ends are a different matter - they are already carried literally in the scene - so they are not here.

The sides get a middle and the faces do not, for the reason the shape editor gives: the middle of an arc is not a place anything is lined up on. A side is a straight line, so its middle is a point on the drawing. A curved face is an arc, and the middle of its chord is inside the glass with nothing drawn there - while the middle of the arc itself is the apex, which is already offered above. A flat face has its apex and its chord centre at the same place, so that one is covered either way.

Returns:

{'point': [x, y], 'optic': name, 'kind': str, 'label': str}

Return type:

list of dict

gtrace.layout._merge_by_name(registered, specs, build, update)

Rebuild a list of registered objects from their serialized form, reusing an object whenever the name and the class still match.

This is what lets a load keep the identity of the elements that survived it, which matters because the whole design rests on the layout holding the user’s own objects by reference.

gtrace.layout._update_optic(m, d)

Apply a serialized optics to an existing one, in place.

The joint comes back as a pending name, as it does on a fresh one: the host is another element of the same file and only the layout holding both can join them up.

gtrace.layout._update_source(b, d)

Apply a serialized source beam to an existing one, in place.

gtrace.layout.source_from_dict(d)

Construct a source GaussianBeam from a dict produced by source_to_dict().

gtrace.layout.rayleigh_range(w0, wl, n=1.0)

The Rayleigh range of a beam of waist size w0.

Parameters:
  • w0 (float) – Waist size: the radius at which the power falls to 1/e^2 there, which is the convention GaussianBeam.width uses throughout.

  • wl (float) – Vacuum wavelength.

  • n (float, optional) – Index of refraction of the medium. It is folded into the q-parameter the same way GaussianBeam does it, so that the two agree. Defaults 1.0.

Return type:

float

gtrace.layout.q_from_waist(w0, d, wl, n=1.0)

The q-parameter, at the origin of a beam, of light whose waist is of size w0 and lies a distance d further along.

This is the inverse of what GaussianBeam.waist() reports, and exists so that a front end can offer the pair a laser is actually specified by without having to hold a second description of what a waist is. The distance runs the same way waist() reports it: positive means the waist is downstream of the origin.

Return type:

complex

gtrace.layout.source_waist(b)

The waist of a source beam, as the pair a front end shows: sizes and distances, each in x and y.

Returns:

{'waist_size': (wx, wy), 'waist_pos': (dx, dy)}

Return type:

dict

gtrace.layout.source_scene_dict(b)

Convert a source beam into the dict a viewer draws and addresses it by: where the laser stands, which way it points, and what light it puts out.

This is not beam_to_dict. That describes a beam of the trace - a segment with two ends, whose q is a result - and every beam in the scene is one of those, including the copy of this source that the trace began from. What is missing there is the source itself: which of the beams the user put in the layout, and which are consequences. A front end that cannot tell them apart cannot offer to edit the one and not the others, and cannot draw the laser at all.

The waist travels alongside the q-parameters rather than instead of them. The q is what the model holds and what a saved layout carries; the waist is what the panel shows, and working it out here keeps the formula on the side that owns the beam.

gtrace.layout._ordered_source(attrs)

The items of a source attribute dict, in the order they must be applied. See _SOURCE_ATTR_ORDER for why the order matters.

gtrace.layout._as_positive(value, key, what='a positive number')

A finite positive number from an edit message, or an EditError.

gtrace.layout._as_complex(value, key)

A q-parameter from an edit message, or an EditError.

JSON has no complex numbers, so one arrives as a pair. A q whose imaginary part is not positive is not a beam - the Rayleigh range is what gives it a width - so it is refused here rather than left to surface as a nan the first time the width is asked for.

gtrace.layout._set_source_attr(b, key, value)

Set one whitelisted attribute of a source beam on behalf of a front end.

The four waist names are not attributes of the beam at all: each stands for one half of one q-parameter, and is converted here. The other half is left as it was, so that setting a waist size does not also move the waist, and moving it does not resize it.

gtrace.layout.DEFAULT_PEDESTAL = 'PEDESTAL-25'
gtrace.layout.DEFAULT_FORK = 'FORK-125'
gtrace.layout._stand(optic, mount=None, pedestal=DEFAULT_PEDESTAL, fork=DEFAULT_FORK, mount_name=None, pedestal_name=None, fork_name=None, mount_offset=None)

The parts that hold one element, jointed to it and to each other.

The mount is built around its optic, so it seats at the model’s designed position unless mount_offset says where else, in the optic’s own frame - x along the face normal, so a negative x sets the mount further back than the model would. The pedestal stands in the hole the mount is bolted down through - the point the model names ‘post’ - and the fork closes round the pedestal, with its turn left free, since a fork swings about the post it clamps and which way its handle points is settled on the bench.

Every piece derives its pose from the one below it, so the whole stack follows the element: move the mirror and the mount, the pedestal and the fork come along, with nothing to keep in step.

A piece may be left out by passing None for its model: a mirror in a mount and nothing else is a real thing to draw.

Returns:

Hosts before what stands on them, which is the order they have to be registered in.

Return type:

list of Mechanics

gtrace.layout.mirror_assembly(name='M1', center=None, angle=np.pi, HRcenter=None, diameter=1 * unit.inch, thickness=6 * unit.mm, mount='MOUNT-25', pedestal=DEFAULT_PEDESTAL, fork=DEFAULT_FORK, mount_name=None, pedestal_name=None, fork_name=None, mount_offset=None, **kwargs)

A mirror and what holds it: the optic, its mount, the pedestal and the clamping fork, jointed so the four move as one.

This is what goes onto a bench. A mirror on its own is an element to trace with; a mirror in a mount on a pedestal is the thing that is bolted down, takes up room and has to clear its neighbours - and drawing it is most of what a layout is for.

The parts are attached to each other, so the mirror is the thing to move: everything else derives its pose from it. Register them in the order they come back, hosts first.

Parameters:
  • name (str, optional) – What the mirror is called. The parts are named from their own models - MT1, P1, FK1 - rather than from this: a part is known by what it is, and a bench numbers its mounts as mounts.

  • center (sequence of 2 floats, optional) – Where the substrate centre stands, in metres. The mount is drawn around that point, so it is what lines the stack up. Defaults to the origin.

  • angle (float, optional) – Which way the front face looks, in radians. Defaults to pi, facing back down -x, which is what a new mirror does in the viewer.

  • HRcenter (sequence of 2 floats, optional) – Where the centre of the front face stands, in metres. The other way of saying where the mirror goes, and the one a layout is built from: light reflects off that face, and the distances between elements are measured to it. Give this or center, not both.

  • diameter (float, optional) – The optic. 1 inch by 6 mm, which is what the one-inch mount is drawn to seat.

  • thickness (float, optional) – The optic. 1 inch by 6 mm, which is what the one-inch mount is drawn to seat.

  • mount (str or None, optional) – Model names for the three parts. None leaves that piece out.

  • pedestal (str or None, optional) – Model names for the three parts. None leaves that piece out.

  • fork (str or None, optional) – Model names for the three parts. None leaves that piece out.

  • mount_name (str or None, optional) – What to call them. None takes the model’s own prefix and the first number, which is what a layout then makes unique.

  • pedestal_name (str or None, optional) – What to call them. None takes the model’s own prefix and the first number, which is what a layout then makes unique.

  • fork_name (str or None, optional) – What to call them. None takes the model’s own prefix and the first number, which is what a layout then makes unique.

  • mount_offset (sequence of 2 floats or None, optional) – Where the mount sits on the optic, in the optic’s own frame - x along the face normal. None is the model’s designed position, which is where a mount drawn around its optic belongs; a number here is how far off that it is really bolted. What the pedestal stands in moves with it, since the post hole is a point of the mount.

  • **kwargs – Passed to the Mirror: a curvature, a reflectivity, a layer.

Returns:

([mirror], [mount, pedestal, fork]): the elements and the bodies, minus whatever was left out. Split because that is the division everything downstream makes - a layout registers the two by different doors, and the trace sees only the first - and hosts first within each, which is the order they have to be registered in.

Return type:

tuple

gtrace.layout.lens_assembly(name='L1', center=(0.0, 0.0), angle=np.pi, f=0.2, diameter=1 * unit.inch, thickness=6 * unit.mm, holder='HOLDER-25', pedestal=DEFAULT_PEDESTAL, fork=DEFAULT_FORK, holder_name=None, pedestal_name=None, fork_name=None, holder_offset=None, **kwargs)

A lens and what holds it: the optic, its holder, the pedestal and the clamping fork, jointed so the four move as one.

The same as mirror_assembly() over a Lens, which is ordered by its focal length.

Parameters:
  • name (str, optional) – What the lens is called.

  • center (sequence of 2 floats, optional) – Where the substrate centre stands, in metres.

  • angle (float, optional) – Which way the front face looks, in radians.

  • f (float, optional) – Focal length, in metres. Positive converges.

  • diameter (float, optional) – The optic.

  • thickness (float, optional) – The optic.

  • holder (str or None, optional) – Model names for the three parts. None leaves that piece out.

  • pedestal (str or None, optional) – Model names for the three parts. None leaves that piece out.

  • fork (str or None, optional) – Model names for the three parts. None leaves that piece out.

  • holder_name (str or None, optional) – What to call them.

  • pedestal_name (str or None, optional) – What to call them.

  • fork_name (str or None, optional) – What to call them.

  • holder_offset (sequence of 2 floats or None, optional) – Where the holder sits on the lens, in the lens’s own frame. None is the model’s designed position. See mirror_assembly().

  • **kwargs – Passed to the Lens.

Returns:

([lens], [holder, pedestal, fork]): the elements and the bodies, minus whatever was left out, hosts first within each.

Return type:

tuple

gtrace.layout.ASSEMBLY_KINDS
gtrace.layout.assembly_kinds()

What assembly() can build: kind, label and description, in the order a menu should show them.

place names the parameter that says where the assembly goes, so that a front end with one point to give knows what to call it: 'HRcenter' for a mirror, 'center' for a lens.

Return type:

list of dict

gtrace.layout._assembly_kind(kind)
gtrace.layout.assembly(kind, **kwargs)

Build one of the assemblies assembly_kinds() lists.

assembly('MIRROR-2IN', center=[0.3, 0.1]) is mirror_assembly() with the two-inch numbers filled in. What the kind settles can still be overridden - a thicker substrate, a different fork - since it is only a set of defaults.

Returns:

(optics, bodies): the element and the parts that hold it, hosts first within each.

Return type:

tuple

class gtrace.layout.OpticalLayout(optics=None, sources=None, rules=None, name='Layout', dimensions=None, mechanics=None)

Bases: object

A container representing a whole optical system: optics, source beams and tracing rules.

Variables:
  • name (str) – Name of the layout.

  • optics (list of gtrace.optcomp.Optics) – Registered optics (held by reference). All optics must have unique names.

  • sources (list of gtrace.beam.GaussianBeam) – Registered source beams (held by reference). All sources must have unique names.

  • rules (TraceRules) – Rules for the non-sequential trace.

  • beams (list of gtrace.beam.GaussianBeam or None) – The result of the last trace(). None if trace() has not been run yet.

  • beams_by_source (dict or None) – Mapping from a source name to the list of beams originating from that source, from the last trace().

name = 'Layout'
optics = []
sources = []
dimensions = []
mechanics = []
rules
draw_options
beams = None
beams_by_source = None
_history = []
_future = []
add_optics(m)

Register an optics. The optics is held by reference. Its name must be unique within the layout.

add_beam_dump(**kwargs)

Build a V beam dump and register it: two absorbing faces and the housing they sit in, jointed so the three move as one.

Takes what beam_dump() takes. Without a name it is given the first free one, and the pieces are registered elements first and then bodies, hosts before what stands on them, which is the order they have to go in.

Returns:

([face1, face2], [housing]), as beam_dump() returns them.

Return type:

tuple

add_assembly(kind, name=None, **params)

Build one of the assemblies assembly_kinds() lists and register the lot: the element, its mount, the pedestal and the fork, jointed so the four move as one.

The names are filled in from the layout - the element takes the first free one of its kind, and each part the first free one of its own - so adding a second two-inch mirror gives M2 held by MT2 on P2 in FK2, without asking.

Takes what the builder takes, on top of what the kind settles.

Parameters:
  • kind (str) – One of assembly_kinds(), e.g. 'MIRROR-2IN'.

  • name (str or None, optional) – What to call the element. None takes the first free name.

  • **params – Passed to the builder: center, angle, and whatever else that assembly takes.

Returns:

(optics, bodies), as the builder returned them.

Return type:

tuple

assemble(name, host, offset=None, offset_angle=None, fix_rotation=True)

Make one element follow another: an assembly.

Two absorbing faces in a V are one beam dump, a pair of steering mirrors is one periscope, and a bench is built out of such assemblies rather than out of loose elements. This says so: the follower keeps its place relative to the host, and moving or turning the host carries it along.

Where a body attached to an optics derives its pose on every read, an element cannot: an Optics holds its pose in traits whose derived geometry - the face centres, the normals - is what the trace reads, and a pose computed on demand would mean rewriting that. So the joint is stored here and settled just before the layout is read, which comes to the same thing: see _settle_assemblies.

The follower keeps where it stands unless an offset says otherwise. There is no designed position to fall back on the way a mount has one - where the second face of a dump sits relative to the first is the drawing of that dump, not a fact about mirrors - so what is on the bench is what is meant.

Parameters:
  • name (str or Optics) – The element that is to follow.

  • host (str, Optics or Mechanics) – What it follows.

  • offset (sequence of 2 floats or None, optional) – Where its anchor point sits in the host’s frame, in metres. None keeps where it stands now.

  • offset_angle (float or None, optional) – How far it is turned relative to the host, in radians. None keeps the angle it stands at now.

  • fix_rotation (bool, optional) – Whether the relative angle is frozen. True - the default - is a face of a dump, which is built at its angle. False lets the angle be edited, and the element still turns with its host.

Returns:

self

Return type:

OpticalLayout

disassemble(name)

Free an element from what it was following, leaving it exactly where it stands.

Nothing has to be baked in: the pose is the element’s own and always was. This only stops it being written to again.

assemblies()

The elements that follow something, hosts before followers.

The order is what settling needs: an element whose host is itself a follower has to be written after it.

_settle_assemblies()

Write every follower’s pose from the host it follows.

Called just before the layout is read - by trace(), draw() and snap_points() - rather than kept up to date as things move. That is what makes it as reliable as a pose derived on every read: there is no notification to miss, because nothing is listening. Assigning M1.HRcenter in a cell and then tracing carries the assembly along.

The one thing it cannot cover is reading a follower’s pose in a cell without tracing or drawing first. That value is the one from before the host moved.

Join up followers that name their host by name, as a loaded file does. A name nothing answers to is refused.

add_source(b)

Register a source beam. The beam is held by reference. Its name must be unique within the layout.

add_dimension(d)

Register a Dimension. It is held by reference, and its name must be unique within the layout.

add_mechanics(m)

Register a Mechanics. It is held by reference, and its name must be unique within the layout.

A body attached by name (built as Mechanics(attached_to='M1') or loaded from a file) is joined here to the optics or body of that name; a name nothing answers to is refused, since a body with an unreadable pose is not something to hold. A host must therefore be registered before what stands on it - which is what the constructor and the loader arrange for a list that arrives in any order.

_add_mechanics_in_order(bodies)

Register a list of bodies, hosts before what stands on them.

A body may stand on another body, and a list - a constructor argument, a file - carries them in whatever order it happens to. Rather than make the caller sort them, each pass registers everything whose host is already in; a pass that registers nothing means what is left names a host that does not exist or stands in a circle, and add_mechanics says which.

Redraw registered mechanics from the current library definitions, matched by their model labels.

The saved values are always the truth: a layout opens with the shapes it was saved with, however far the library has moved on since. This is the one deliberate exception, and it runs only when called - “bring these up to the library” is a thing to ask for, not something a load does behind your back.

Only the shapes are replaced. The pose, the attachment, the layer and the name all stay: a relink redraws the body, it does not re-place it.

Parameters:

names (sequence of str or None, optional) – Which mechanics to relink. None - the default - offers them all. A body with no model label, or whose model the library does not know, is left as it is either way: the saved shapes are all anyone knows about it.

Returns:

The names of the mechanics that were relinked.

Return type:

list of str

Resolve every attachment left pending by name, against the optics and the other bodies of this layout.

Loading and restoring build the bodies before the joins can be made - the host is another entry of the same file or snapshot - so the names are kept until everything is in place and then resolved here, after both lists are settled. A host may itself be a body waiting for its own host, so this resolves what it can and goes round again; what is left after a pass that changed nothing is a cycle, which is refused rather than left to be discovered by a pose deriving from itself.

_attachment_host(name)

The optics or body of that name, or None. A body may stand on either, and the four kinds share one namespace, so this is one lookup rather than two tried in an order that would have to be remembered.

_check_name_free(name)

Refuse a name already taken by an optics, a source, a dimension or a mechanics.

The four share a namespace because a front end points at all of them the same way - an edit message names its target and nothing else - and a name that meant one thing in one message and another in the next would be a trap. Sources joined that namespace when they became editable; before that they were only ever addressed as a list. Mechanics joined it on arrival.

remove_optics(name)

Remove the optics with the given name, and everything that stands on it.

A mount on the mirror, a pedestal under the mount, the second face of a beam dump: those are not things that happen to be near the element, they are things whose place is its place. Leaving them behind would leave each one deriving a pose from a ghost, and asking the user to take them away one at a time would be asking them to say twice what they meant once.

Detach or disassemble first to keep something that is standing on it - that is what says “this one is its own now”.

Returns:

Everything that was removed, the target first.

Return type:

list

_remove_with_what_it_carries(target)

Take something out of the layout along with everything whose pose comes from it, however deep that goes.

copy_optics(name, newname=None, offset=None)

Add a second one of an element, with everything standing on it.

A mount is bolted to its mirror, a pedestal to the mount and a fork to the pedestal, and none of that is worth building twice for the second mirror of a pair. So the whole stack comes along: every body attached to the element, and every body attached to those, each pinned to the copy exactly as its original is pinned to the original.

The copies are made through the same dicts a saved layout is written with, so what is copied is what would have been saved - by value, with nothing shared with the original. Only the poses of the bodies are not copied, because they were never stored: each one derives its own from the copy it now stands on.

Parameters:
  • name (str) – The element to copy.

  • newname (str or None, optional) – What to call the copy. None takes the original’s name without its trailing number and finds the first free one: copying ‘M1’ offers ‘M2’.

  • offset (sequence of 2 floats or None, optional) – How far to put the copy from the original, in metres. None is the element’s own diameter, along both axes - far enough that the copy does not sit inside what it was made from, near enough that it is plainly the same thing moved.

Returns:

The copy, already registered.

Return type:

gtrace.optcomp.Optics

_carried_by(host)

Everything whose pose comes from this one, hosts first.

Bodies attached to it, elements assembled to it, and whatever stands on those in turn. One walk rather than two, because a pedestal under the second face of a dump is as much a part of the dump as the face is.

_standing_on(host)

Every body attached to this, and to those, hosts first.

_copy_name(name)

A free name for a copy of this one: the name without its trailing number, and the first number that name leaves free. So a copy of ‘M1’ is ‘M2’, and a copy of ‘Post’ is ‘Post1’.

remove_source(name)

Remove the source with the given name from the layout.

remove_dimension(name)

Remove the dimension with the given name from the layout.

remove_mechanics(name)

Remove the mechanics with the given name, and everything that stands on it.

The same rule an optics is removed by, for the same reason: what stands on it would be left deriving a pose from a ghost.

Returns:

Everything that was removed, the target first.

Return type:

list

get_optics(name)

Return the registered optics with the given name.

get_source(name)

Return the registered source with the given name.

get_dimension(name)

Return the registered dimension with the given name.

get_mechanics(name)

Return the registered mechanics with the given name.

unique_optics_name(prefix='M')

Return a name of the form prefix + number that nothing in the layout uses. Front ends need a name before they can talk about the element they are asking for.

unique_dump_name(prefix='BD')

Return a name for a beam dump that leaves all three of its pieces free to be called what they are called.

A dump has no object of its own - it is two elements and a body - so what has to be free is not one name but the three made from it.

unique_dimension_name(prefix='D')

Return a name of the form prefix + number that nothing in the layout uses. The same namespace as unique_optics_name, since edit messages resolve a target across both.

unique_source_name(prefix='S')

Return a name of the form prefix + number that nothing in the layout uses. The same namespace again.

unique_mechanics_name(prefix='P')

Return a name of the form prefix + number that nothing in the layout uses. The same namespace again; P for part, since M already means a mirror.

trace()

Run the non-sequential trace from all the registered sources through the registered optics, according to the rules.

The registered source beams are not modified: a copy of each source is used for tracing.

The result is stored in self.beams (a flat list) and self.beams_by_source (a dict keyed by source name), and self.beams is also returned.

stretch_beam(index, length)

Draw one open beam at a length of your own choosing.

A beam that reaches nothing is drawn at the length the rules give every such beam - open_beam_length. Sometimes one of them is worth following further: to see where it would land, or to find room for the element that is to catch it.

This changes the drawing and nothing else. A beam that reaches nothing reaches nothing at any length, so no beam is added, removed or recomputed. The next trace throws it away, since the length belongs to the beam that trace made. traced_length remembers what the trace gave it, so the beam can be put back without tracing again.

Only an open beam can be drawn longer. A beam that ends on a surface is as long as the distance to that surface, and drawing it longer would draw it through the glass.

Parameters:
  • index (int) – Which beam, as an index into beams - the order the scene carries them in.

  • length (float) – How far to draw it, in metres. Above zero.

Returns:

beam – The beam that was lengthened.

Return type:

gtrace.beam.GaussianBeam

apply_edit(msg)

Apply an edit message coming from a front end.

The message is a plain dict, so the same protocol travels over the notebook widget’s comm and, later, over a websocket:

{'op': 'move',   'target': 'M1', 'HRcenter': [0.52, 0.0]}
{'op': 'rotate', 'target': 'M1', 'normAngleHR': 2.3}
{'op': 'set',    'target': 'M1', 'attrs': {'diameter': 0.15}}
{'op': 'draw',   'params': {'sigma_main': 1.0,
                            'width_mode': 'y'}}
{'op': 'save',   'path': 'layout.json'}
{'op': 'load',   'path': 'layout.json'}
{'op': 'export', 'format': 'dxf', 'path': 'layout.dxf'}
{'op': 'rename', 'target': 'M1', 'name': 'PRM'}
{'op': 'add',    'type': 'Mirror', 'name': 'M4',
                 'params': {'HRcenter': [0.3, 0.2]}}
{'op': 'add',    'type': 'Lens', 'name': 'L1',
                 'params': {'f': 0.3, 'shape': 'plano-convex'}}
{'op': 'add',    'type': 'Source', 'name': 'S1',
                 'params': {'pos': [0.0, 0.0],
                            'waist_size': 0.0002}}
{'op': 'move',   'target': 'S1', 'pos': [0.1, 0.0]}
{'op': 'rotate', 'target': 'S1', 'dirAngle': 0.0}
{'op': 'set',    'target': 'S1',
                 'attrs': {'waist_size_x': 0.0003,
                           'waist_pos_x': 0.05}}
{'op': 'remove', 'target': 'M4'}
{'op': 'copy',   'target': 'M1', 'name': 'M2',
                 'offset': [0.05, 0.05]}
{'op': 'align',  'target': 'L1', 'beam': 'b0',
                 'beam_index': 0, 'point': [0.4, 0.02]}
{'op': 'slide',  'target': 'L1', 'beam': 'b0',
                 'beam_index': 0, 'distance': 0.05}
{'op': 'rules',  'rules': {'power_threshold': 1e-6}}
{'op': 'add',    'type': 'Dimension', 'name': 'D1',
                 'params': {'p1': [0.0, 0.0],
                            'p2': [0.5, 0.0],
                            'offset': 0.05}}
{'op': 'set',    'target': 'D1', 'attrs': {'p2': [0.6, 0.0]}}
{'op': 'add',    'type': 'Mechanics', 'name': 'BB1',
                 'params': {'center': [0.0, -0.1],
                            'shapes': [{'type': 'rectangle',
                                        'point': [-0.15, -0.15],
                                        'width': 0.3,
                                        'height': 0.3,
                                        'thickness': 0}]}}
{'op': 'move',   'target': 'BB1', 'center': [0.2, 0.1]}
{'op': 'rotate', 'target': 'BB1', 'rotationAngle': 0.1}
{'op': 'add',    'type': 'Mechanics', 'name': 'MT1',
                 'params': {'attached_to': 'M1',
                            'offset': [0.0, 0.0],
                            'shapes': [...]}}
{'op': 'set',    'target': 'MT1', 'attrs': {'attached_to': None}}
{'op': 'undo'}
{'op': 'redo'}

A source, a dimension and a mechanics are named and addressed exactly as an optics is: ‘remove’, ‘rename’ and ‘set’ resolve their target across all four, which share one namespace. What the operations mean differs where the things differ - ‘move’ on a source names where the laser stands rather than the middle of a substrate, and a source takes a waist where an optics takes a curvature - so the attribute whitelists are separate.

The edit is applied to the registered object itself, which is the same object the user holds in their own code. The trace result is invalidated so that the next draw() or scene_dict() re-traces.

The state before the edit is kept, so that undo() can put it back; see there for what that costs and what it restores. An edit that is refused changes nothing and is not recorded. An edit that goes through discards whatever undo() had put aside for redo(): the layout has taken a different turn.

Parameters:

msg (dict) – The edit message.

Returns:

self

Return type:

OpticalLayout

Raises:

EditError – If the operation, the target or an attribute is not allowed.

_snapshot()

Capture the current state, for undo() or redo() to put back.

The objects are kept alongside their serialized form. Restoring is then a matter of putting the values back onto the very objects that held them, rather than matching them up by name the way loading a file has to - so a rename comes back without swapping the object it named, and an element that was removed comes back as itself rather than as a copy.

_restore(snapshot)

Put back a state captured by _snapshot().

undo()

Put the layout back as it was before the last edit.

A step of the history holds the elements themselves as well as their values, so undoing restores those values onto those same objects. The M1 of the user’s own code, and the selection of a front end, go on naming the right thing - through a rename, and even through a removal, since an element taken out of the layout is put back as itself rather than as a copy. This is stronger than what loading a file can offer, which has only names to match objects up by.

The state being left is put aside for redo(), and stays there until an edit goes through.

Only edits applied through apply_edit are recorded. Assigning to an optics directly in Python is not an edit the layout ever sees, and is not undone - though it is captured by the snapshot of the next edit that does go through, and so is restored by undoing that one.

Returns:

self

Return type:

OpticalLayout

Raises:

EditError – If there is nothing left to undo.

redo()

Put back the state that the last undo() stepped out of.

Redo undoes an undo, and restores as exactly as undo() does: the same elements, so an element that undoing a removal brought back goes away again as itself rather than as a copy.

Only undoing fills the redo stack, and the next edit that goes through empties it - once the layout has taken a different turn there is no branch left to return to.

Returns:

self

Return type:

OpticalLayout

Raises:

EditError – If there is nothing left to redo.

property can_undo

Whether there is an edit left to undo.

property can_redo

Whether there is an undo left to take back.

_apply_edit(msg)

Apply one edit message. See apply_edit, which wraps this with the history that makes an edit undoable.

_is_dimension(name)

Whether a target name belongs to a registered dimension.

_is_source(name)

Whether a target name belongs to a registered source.

_is_mechanics(name)

Whether a target name belongs to a registered mechanics.

_is_optics(name)

Whether a target name belongs to a registered optics.

_resolve_target(name)

The optics, source, dimension or mechanics a message names.

The four share a namespace, so a message can say ‘remove D1’, ‘remove S1’ or ‘remove M1’ without also having to say which kind of thing it is: a front end has a name under the cursor, not a class.

_edit_mechanics(m, op, msg)

Apply a ‘move’, ‘rotate’ or ‘set’ to a mechanics.

As everywhere else, ‘move’ and ‘rotate’ are spellings of a one-attribute ‘set’. A free body takes a pose; an attached one has no pose of its own - it takes an offset instead, and a message trying to move it is refused with the reason. The attachment itself is edited through ‘attached_to’: an optics name seats the body on it at the model’s designed position (see Mechanics.attach - where a mount belongs on its host is the model’s to say, not the drop point’s), and null detaches it, baking the derived pose in.

Everything is checked before anything is set - the host looked up, the numbers converted - so a message half right leaves the body exactly as it was.

_reseat_free_turn(optics, attrs)

Read a turn of a free-turning follower back into its joint.

The angle a follower stands at is written from the joint at every settle, so turning one by writing the angle alone would last until the next trace and no longer. What the turn means is the relative angle, and this is where the one becomes the other - the same translation a body’s rotationAngle setter makes for a clamping fork.

_refuse_assembled_pose(optics, attrs, joint)

Refuse to place an element that follows another.

A follower goes where its host goes, and a pose typed into it would be written over at the next trace - which is worse than a refusal, because it would look as though it had worked. The exception is the turn of an element whose relative angle is free, which is the one thing that is genuinely still its own.

Nothing is refused when the same message is letting it go: setting a pose and detaching in one breath is how an element is freed to somewhere.

_set_assembly(optics, joint)

Apply the joint part of a ‘set’ message to an element.

_mechanics_from_message(msg)

Build a Mechanics from an ‘add’ message.

The shapes arrive serialized, exactly as a saved layout writes them; a shape gtrace cannot draw, or one missing a coordinate, comes back as a refusal rather than as a body that fails the first time the scene is built.

A message naming a ‘model’ and no shapes means “one of those”: the shapes come off the library shelf, along with the layer the model prefers and the builder parameters that keep a breadboard resizable. A message carrying both keeps its own shapes, and the model name is the label it always was.

_edit_source(b, op, msg)

Apply a ‘move’, ‘rotate’ or ‘set’ to a source beam.

As with an optics, ‘move’ and ‘rotate’ are spellings of a one-attribute ‘set’; they exist so that a front end says what it means. What they name is different, because a beam is a ray from a point rather than a body: its position is where it starts and its orientation is where it is aimed.

_source_from_message(msg)

Build a source beam from an ‘add’ message.

Unlike a new optics, a new source copies nothing from the sources already in the layout. A laser is not cut to match the one next to it, and inheriting a q-parameter would be worse than useless: it describes a waist measured from a point the new source does not stand at.

_set_dimension_attrs(dim, attrs)

Move the ends of a dimension, or the line drawn between them.

_dimension_from_message(msg)

Build a Dimension from an ‘add’ message.

_beam_from_message(msg)

The beam a message names, from the last trace.

Identified by its index, with the name as a check. The front end is looking at a scene built from that trace, so the index is exact; but two beams can share a name and a stale scene would make an index alone point quietly at the wrong one, so the name has to agree, and is what the search falls back to when the index does not fit.

_slide_along_beam(optics, msg)

Move an optics along a beam’s axis by a given distance, in metres, positive in the direction the beam travels.

Nothing else about the optics changes: not its orientation, not its offset across the beam. An element already square on the beam therefore stays square and simply slides along it, which is the one degree of freedom left after aligning, and the one that wants a number rather than a drag - a lens goes where the mode matching says, not where the mouse happens to land.

This is a translation of the whole substrate, so which point of it is nominally being moved makes no difference to the result.

_align_to_beam(optics, msg)

Turn an optics square to a beam and slide it onto that beam’s axis, leaving it at the point along the beam it was dropped.

Almost every element on a bench is meant to sit square across a beam with the beam through its middle. Dragging one gets it approximately there and no closer, and the two things a drag cannot say - the exact angle and the exact offset across the beam - are the two the bench does not leave to chance. The one the user does mean to choose, where along the beam it sits, is the one kept.

Which point of the element lands on the axis is what anchor_point already names: the apex of the front face for a mirror, since that is where the beam stops, and the middle of the substrate for a lens, since the beam goes through. At normal incidence the two are on the same line anyway, so the choice only shifts the element along the beam - which is the direction that was left approximate to begin with.

The beam is identified by its index in the last trace, with its name as a check: the viewer is looking at a scene built from that trace, and an index alone would quietly align to the wrong beam if the two had drifted apart.

_optics_from_message(msg)

Build the optics described by an ‘add’ message.

Parameters not given are taken from the optics already in the layout, so that an element added to a system of 10 cm mirrors comes out a 10 cm mirror instead of a 25 cm one. The surfaces are flat unless asked otherwise: a curvature copied from a neighbour would be a surprise rather than a convenience.

A lens is the exception, and inherits nothing. Its coatings, its aperture and its wedge are the lens’s own: a lens given a mirror’s 99% front face is one the main beam does not go through, and an aperture taken from a large mirror is a focal length the blank cannot be ground to. A lens comes off the catalogue shelf instead, at DEFAULT_LENS_F unless the message says otherwise.

_lens_from_params(params, name, kind='Lens')

Build the lens described by an ‘add’ message.

Raises:

EditError – If the lens asked for cannot be made. The solver says why - a blank too thin for the faces, a shape that contradicts the sign of f - and that reason is what the front end shows.

draw(canvas=None, **options)

Draw the optics and the result of the last trace into a canvas.

If trace() has not been run yet, it is run automatically.

Parameters:
  • canvas (draw.Canvas or None, optional) – The canvas to draw into. If None, a new canvas is created.

  • **options – Any of the keys of DRAW_OPTIONS, which documents what each one does and what it defaults to. Options not given here fall back to self.draw_options, then to those defaults, so a front end can change how the layout is drawn without every caller having to pass the choice along.

Returns:

canvas

Return type:

draw.Canvas

resolve_draw_options(**options)

The drawing options in force: the defaults, overridden by self.draw_options, overridden by the arguments given here.

Raises:

TypeError – If an option is not one of DRAW_OPTIONS. Accepting a misspelt key silently would mean the setting is ignored without anything saying so.

scene_dict(**kwargs)

Return the JSON-compatible scene dict {‘canvas’: …, ‘beams’: …} of this layout, for consumption by the GUI viewer.

If trace() has not been run yet, it is run automatically.

Parameters:

**kwargs – Passed to draw().

mechanics_dict()

The registered mechanics, as a front end addresses them. See mechanics_scene_dict for what each carries. The outline is worked out here rather than stored, like everything else that is derived: a stored copy could only go stale.

sources_dict()

The registered sources, as a front end draws and addresses them.

See source_scene_dict for what each carries. The waist is worked out here rather than stored, for the same reason a dimension’s length is: it is derived from the q-parameters, and a stored copy could only go stale.

dimensions_dict()

The registered dimensions, each with what it comes to.

The measurement is worked out here rather than stored on the dimension, so that moving the optics a span runs through cannot leave a stale number behind: the answer is recomputed every time the scene is built, which is after every edit.

Returns:

The keys of dimension_to_dict, plus line - the two ends of the dimension line itself, carried aside by the offset - and length, optical, inside and n. See Dimension.measure.

Return type:

list of dict

snap_points()

The points of the optics a front end may snap a measurement to.

Beam ends are not here: the scene already carries the ends of every beam literally, so a front end can offer those without anything being worked out twice. See optic_snap_points for what each optics contributes.

draw_dimensions(canvas, layername='dimensions')

Draw the registered dimensions into a canvas, on a layer of their own.

A layer, because a dimension is a note about the system rather than part of it: a layer is exactly the mechanism CAD offers for something you want to be able to switch off, so an exported drawing carries the measurements without imposing them.

This is deliberately not part of draw(). That method means “the physical picture”, and the viewer draws dimensions itself from the scene, so putting them in draw() would draw them twice there.

Parameters:
  • canvas (draw.Canvas) – The canvas to draw into.

  • layername (str, optional) – Layer the dimensions go on. Defaults to ‘dimensions’.

Returns:

canvas

Return type:

draw.Canvas

export_dxf(filename, dimensions=True, **kwargs)

Write the layout to a DXF file. The companion of render_html.

If trace() has not been run yet, it is run automatically.

Parameters:
  • filename (str) – Name of the DXF file to write.

  • dimensions (bool, optional) – Whether the dimensions noted on the layout are drawn, on a layer of their own. Defaults to True; see draw_dimensions.

  • **kwargs – Passed to draw(), e.g. sigma_main or drawMainWidth.

Returns:

filename

Return type:

str

render_html(filename, title=None, **kwargs)

Write the layout to a self-contained HTML file that can be opened in any browser, with zoom, pan and click readout of the beam parameters.

If trace() has not been run yet, it is run automatically.

Parameters:
  • filename (str) – Name of the HTML file to write.

  • title (str or None, optional) – Title shown in the browser tab and in the viewer. Defaults to the name of the layout.

  • **kwargs – Passed to draw(), e.g. sigma_main or drawMainWidth.

Returns:

filename

Return type:

str

widget(height=None, editable=True, path='layout.json', dxf_path=None, **kwargs)

Return a Jupyter widget showing this layout.

The widget carries the scene in a traitlet, so a re-trace can be pushed into a view that is already on screen:

w = layout.widget() w # displays the viewer M1.HRcenter = [0.6, 0] w.update() # re-traces and redraws in place

The loop also runs the other way: dragging an optics in the viewer sends an edit message, which this layout applies to the registered object before re-tracing and pushing the result back. Since the optics are held by reference, M1 in your own code is the object that moved.

Requires anywidget. Use render_html() or show(backend=’html’) if it is not installed.

Parameters:
  • height (int or None, optional) – Height of the viewer in pixels. Defaults to None, which lets the front end make it as tall as it is wide - the width of the output area, capped to the window, both of which only the browser knows. Dragging the viewer’s bottom edge settles it on a height of its own.

  • editable (bool, optional) – Whether the optics can be dragged in the viewer. Defaults to True.

  • path (str, optional) – File the Save and Load buttons start on, relative to where the kernel is running. Defaults to ‘layout.json’.

  • dxf_path (str or None, optional) – File the Export button starts on. Defaults to None, which names it after path with a .dxf extension - the two are usually wanted together, and it is still the user’s to change in the panel.

  • **kwargs – Passed to draw(), and remembered for the redraws that follow an edit.

Returns:

widget

Return type:

anywidget.AnyWidget

show(filename=None, browser=True, title=None, backend=None, **kwargs)

Show the layout in the viewer.

This is the front end entry point of the layout. In a notebook it returns a widget that renders in the output cell; anywhere else it writes a self-contained HTML file and opens it in the default browser. Both drive the same viewer core.

Parameters:
  • filename (str or None, optional) – Name of the HTML file to write. If None, a temporary file is created (and left behind for the browser to read). Ignored by the widget backend.

  • browser (bool, optional) – Whether to open the file in the default browser. Defaults to True. Ignored by the widget backend.

  • title (str or None, optional) – Title of the page, shown in the browser tab. Ignored by the widget backend, which has no tab to name.

  • backend ({'widget', 'html'} or None, optional) – Which front end to use. Defaults to None, which picks the widget inside a Jupyter kernel with anywidget installed and HTML otherwise.

  • **kwargs – Passed to draw(), and to widget() for the widget backend.

Returns:

The widget for the ‘widget’ backend, the name of the file that was written for the ‘html’ one.

Return type:

widget or filename

to_dict()

Convert the layout (optics, sources, dimensions, rules and drawing options) to a JSON-compatible dict. The trace result is not included; it can be regenerated with trace().

classmethod from_dict(d)

Construct an OpticalLayout from a dict produced by to_dict().

classmethod _from_dict_parts(d)
save(filename)

Save the layout to a JSON file.

classmethod load(filename)

Load a layout from a JSON file created by save().

Returns a new layout. Use update_from_file() to load into an existing one instead.

update_from_dict(d)

Replace the contents of this layout with the ones described by d, in place.

An optics of the file that matches a registered one by name and by class is updated rather than replaced, so a variable holding it - the M1 of the user’s own code, or the selection of a front end - keeps pointing at the right object. Anything else is built afresh, and registered elements the file does not mention are dropped.

Loading a genuinely different layout therefore leaves the variables that named the old elements pointing at objects no longer registered; get_optics() gives the current ones.

update_from_file(filename)

Load a JSON file created by save() into this layout, in place. See update_from_dict for what happens to the objects already registered.