Skip to content

Tutorial

Robert Niffenegger edited this page Jun 9, 2026 · 1 revision

Tutorial

This guide will run through how to create an example baseplate using PyOpticL.

PyOpticL designs are created as FreeCAD Macros. These are essentially just python scripts that can be run within FreeCAD. By default, FreeCAD macros are generally stored in <FreeCAD-User-Directory>/Macro. However, you can also run macros from any location on your computer.

To start, create an empty python file (e.g., example_baseplate.py) and open it in your code editor of choice.

Imports

from PyOpticL import (
    Component, # Base class for all physical components
    BeamPath, # Class for defining a simulated beam path
    dim, # Unit handling system for distance quantities
    cardinal_angle, # Dictionary of angles for standard cardinal directions
    turn_angle, # Dictionary of angles for standard "turns" using mirrors
)
from PyOpticL.library import baseplate, optics, thorlabs # Library of general-purpose component definition classes

Global parameters

Before definiting the layout, we can set any global PyOpticL parameters we need. For example, if we want to switch all applicable hardware to metric versions as well as set the "grid" units of the Dimension class to a 25mm grid, we can do so as follows:

from PyOpticL import settings

settings.set_measurement_system("metric")

For a full list of global parameters and how to set them, see the Global Parameters documentation.

Instantiate component definitions

Here you define the component definitions for any parts you intend to use in the design. These are just definitions, and as such can be used to create multiple instances of the same component.

baseplate_def = baseplate(
    dimensions=(dim(4, "in"), dim(4, "in"), dim(1, "in")),
    optical_height=dim(0.5, "in")
)

mirror_def = optics.circular_mirror(
    diameter=dim(0.5, "in"),
    mount_definition=thorlabs.k05s1(),
)

waveplate_def = optics.wave_plate(
    diameter=dim(0.5, "in"),
    mount_definition=thorlabs.rp05(),
)

beamsplitter_def = optics.beamsplitter_cube_on_surface_adapter(
    side_length=dim(0.5, "in"),
)

Build the layout

By creating an instance of the Component class using one of our definitions, we can place on object in our layout. We'll start by placing the baseplate.

baseplate = Component(label="Example Baseplate", definition=baseplate_def)

Now that we have our parent object, we can add aditional objects using the add function. In this case, we will add the beam.

beam = baseplate.add(
    BeamPath(label="422nm Laser", wavelength=422),
    position=(0, dim(1, "in"), 0),
    rotation=cardinal_angle["right"],
    )

The rotation parameter here can be given either as a single angle (which will be applied as a rotation about the z-axis), or as a tuple of three angles (which will be applied as rotations about the x, y, and z axes, in that order). The starting direction for all rotations is the positive x direction.

As cardinal_angle or turn_angle are dictionaries of convenient angles for layout creation. Cardinal angle contains standard top-down cardinal directions (left, right, up, down, ne, se, nw, sw) where "right" is 0 degrees, "up" is 90 degrees, etc. Turn angle is quite verbose. It represents the angle of a mirror needed to redirect a beam in the format "<incoming direction>-<outgoing direction>. For example if a beam is coming from the right and you would like it to go up, you would use turn_angle["right-up"].

While it is possible to define all components using absolute positions as we have done for the beam object, it is much simpler to instead place component along the beam path. To facilitate this, the add function of the beam path object behaves slightly differently.

beam.add(
    Component(
        label="Beamsplitter", definition=beamsplitter_def
    ),
    beam_index=0b1,
    distance=dim(30, "mm"),
    rotation=cardinal_angle["right"],
)

There are 3 necessary arguments for placing a component along the beam:

  • The beam index
  • One position constraint, this can be the distance from the last component, an x coordinate, or a y coordinate.
  • The component rotation

The beam index is what allows for placement along branching beam paths.
You should think of the beam index as a binary string, with each 1 and 0 representing what path the beam took at each junction.
Every beam starts at beam index 0b1 (while you could write this as an integer, taking advantage of python's binary literals makes for much better readability).
Whenever a beam splits, a zero is added to the index of the transmitted beam and a 1 is added to the index of the reflected/diffracted beam.
This image should help demostrate the structure:
image

Now using this technique we can place the rest of our components.

beam.add(
    Component(
        label="Waveplate", definition=waveplate_def
    ),
    beam_index=0b10,
    distance=dim(30, "mm"),
    rotation=cardinal_angle["right"],
)

beam.add(
    Component(
        label="Mirror", definition=mirror_def
    ),
    beam_index=0b11,
    distance=dim(30, "mm"),
    rotation=turn_angle["up-right"],
)

Finalize the macro

Lastly, in order for the layout to be generated we need to call baseplate.recompute(). However, in order to allow us to import this design for use in larger subsystems, we will make sure to only call this function when running the macro directly.

if __name__ == "__main__":
    baseplate.recompute()

This leaves us with the final macro for our example baseplate design:

from PyOpticL import (
    Component,
    BeamPath,
    dim,
    cardinal_angle,
    turn_angle,
)
from PyOpticL.library import baseplate, optics, thorlabs

baseplate_def = baseplate(
    dimensions=(dim(4, "in"), dim(4, "in"), dim(1, "in")),
    optical_height=dim(0.5, "in")
)

mirror_def = optics.circular_mirror(
    diameter=dim(0.5, "in"),
    mount_definition=thorlabs.k05s1(),
)

waveplate_def = optics.wave_plate(
    diameter=dim(0.5, "in"),
    mount_definition=thorlabs.rp05(),
)

beamsplitter_def = optics.beamsplitter_cube_on_surface_adapter(
    side_length=dim(0.5, "in"),
)

baseplate = Component(label="Example Baseplate", definition=baseplate_def)

beam = baseplate.add(
    BeamPath(label="422nm Laser", wavelength=422),
    position=(0, dim(1, "in"), 0),
    rotation=cardinal_angle["right"],
)

beam.add(
    Component(
        label="Beamsplitter", definition=beamsplitter_def
    ),
    beam_index=0b1,
    distance=dim(30, "mm"),
    rotation=cardinal_angle["right"],
)

beam.add(
    Component(
        label="Waveplate", definition=waveplate_def
    ),
    beam_index=0b10,
    distance=dim(30, "mm"),
    rotation=cardinal_angle["right"],
)

beam.add(
    Component(
        label="Mirror", definition=mirror_def
    ),
    beam_index=0b11,
    distance=dim(30, "mm"),
    rotation=turn_angle["up-right"],
)

if __name__ == "__main__":
    baseplate.recompute()

Running the macro

  1. Open FreeCAD, and navigate to "Macro" > "Macros ..." in the top bar
    image

  2. Select the location of your script in the "User macros location:" bar
    image

  3. Now select your script in the list and hit "Execute"!
    image

Clone this wiki locally