4.14. GUI Automation

Some exemplary automation tasks include:

  • changing the geometry

  • changing camera views accurately

  • displaying specific rays

  • picking specific rays

  • debugging

  • taking automated screenshots of the scene

  • calling functions with settings not available through the GUI

4.14.1. The Control Method

To do automated tasks, the TraceGUI.control method needs to be used instead of TraceGUI.run. It requires an automation function as well as an argument tuple.

def automated(GUI):

    # do some automation things
    ...

# create the GUI and provide the automation function to TraceGUI.control()
sim = TraceGUI(RT)
sim.control(func=automated, args=(sim,))

After the control function is executed the program is kept running. This makes it possible to interact with the GUI. Closing the application automatically is described in Section 4.14.10

To avoid race conditions, all actions inside the provided automation function are run sequentially. During this time, the GUI is unresponsive, as the automation function is run in the main thread (as user input would also be) so the interaction with the scene in this time will be limited.

4.14.2. Applying Properties

Available TraceGUI properties are discussed in Section 4.13.3. All these can be set programmatically.

While the variables are set, the TraceGUI does not execute the functions or actions that react to these changes automatically. This is in contrast to the standard TraceGUI.run method, where a setting is applied subsequently (for instance, changing the ray color setting updates the color in the scene automatically).

To process all pending events TraceGUI.process must be called. This is not needed after every property change, but only if the changes should be visible/executed at this point.

def automated(GUI):

    # change properties
    GUI.minimalistic_view = True
    GUI.hide_labels = True
    GUI.ray_count = 1000

    # GUI properties were set, but the changes need to be processed
    GUI.process()

    # dome some other things
    ...

# create the GUI and provide the automation function to TraceGUI.control()
sim = TraceGUI(RT)
sim.control(func=automated, args=(sim,))

Note that some functions, like TraceGUI.replot also call TraceGUI.process internally.

4.14.3. Replotting

While TraceGUI.process reacts to changes in the TraceGUI itself, it does not handle changes of the raytracer or tracing geometry.

When changing the geometry, the changes are not automatically applied to the scene. The geometry is also not automatically raytraced.

To force the redrawing and retracing of the full scene, you can call TraceGUI.replot.

With the context manager TraceGUI.smart_replot it is possible to only update changed objects. For instance, if a detector is moved, there is no need for updating the lenses inside the geometry or retracing the scene. TraceGUI.smart_replot handles the detection of changes and updating automatically.

Here is an example:

def automated(GUI):

    # replot everything
    GUI.replot()

    # do some actions and at the end replot only changed objects
    # and/or retrace the geometry if needed.
    with GUI.smart_replot():
        some_action_1()

# create the GUI and provide the automation function to TraceGUI.control()
sim = TraceGUI(RT)
sim.control(func=automated, args=(sim,))

When controlling the TraceGUI through the CommandWindow of the GUI, there is also the option to replot all objects automatically. The implementation is done internally in the same way by using TraceGUI.smart_replot.

4.14.4. Controlling the Camera

Controlling the camera is done with the functions TraceGUI.set_camera and TraceGUI.get_camera. The former sets the properties, while the latter one returns a dictionary of the current settings.

The following settings are available:

Property

Description

center

3D coordinates of center of view in mm

height

half of vertical visible scene height in mm

direction

camera view direction vector (direction of vector perpendicular to your monitor and in your viewing direction)

roll

absolute camera roll angle in degrees

You can find example code below:

def automated(sim):

    # store initial camera properties
    cam_props = sim.get_camera()

    # change the center of the view as well as the scaling
    sim.set_camera(center=[1, -0.5, 2], height=2.5)

    # reset to initial view
    sim.set_camera(**cam_props)

# create the GUI and provide the automation function to TraceGUI.control()
sim = TraceGUI(RT)
sim.control(func=automated, args=(sim,))

Applying camera properties at startup is possible using the initial_camera parameter of the TraceGUI class. This parameter is a dictionary that can include all possible parameters of function TraceGUI.set_camera.

sim = TraceGUI(RT, initial_camera=dict(direction=[0, 1, 0], roll=45))

4.14.5. Taking Screenshots

The TraceGUI.screenshot function make it possible to capture screenshots of the scene. A path string is required for this function. The file type is determined automatically from the file name.

Internally, the mayavi.mlab.savefig function from mayavi is utilized, therefore supporting this function’s additional parameters.

def automated(sim):

    # default call
    sim.screenshot("image.png")

    # call with additional parameters
    sim.screenshot("image2.png", magnification=2)

# create the GUI and provide the automation function to TraceGUI.control()
sim = TraceGUI(RT)
sim.control(func=automated, args=(sim,))

Note that the magnification parameter leads to a rescaled scene, where some elements change their relative size.

4.14.6. Selecting Rays

By default, a random selection of rays is displayed inside the scene where the number is specified by TraceGUI.rays_visible. A custom selection can be set using the function TraceGUI.select_rays. It takes a mask parameter, which is a one-dimensional boolean numpy.array, and an optional max_show parameter, that specified the maximum amount of rays to display. Parameter mask must have the same length as there are rays simulated, which is set by TraceGUI.ray_count Note that there is a maximum amount of rays that can be displayed (specified by the maximum value of TraceGUI.rays_visible, by default 50000). If the mask includes more values, a random subset is selected. Accessing TraceGUI.ray_selection returns the boolean array for the currently displayed rays.

Typical useful scenarios are debugging or ray analysis. For instance, only rays from a specific source, region or wavelength range can be selected and displayed. See Accessing Ray Properties to learn how to access ray properties. You can find examples for ray selections below.

def automated(GUI):

    # display rays with wavelengths between 400 and 450nm
    mask = (GUI.raytracer.rays.wl_list >= 400) & (GUI.raytracer.rays.wl_list <= 450)
    GUI.select_rays(mask) # no max_show provided, but might be limited by this function

    # display 2000 rays that start at x > 0
    mask = GUI.raytracer.rays.p_list[:, :, 0] > 0
    GUI.select_rays(mask[:, 0], 2000)  # slicing with 0 so mask is 1D

    # get mask for actually displayed selection
    selection = GUI.ray_selection

# create the GUI and provide the automation function to TraceGUI.control()
sim = TraceGUI(RT)
sim.control(func=automated, args=(sim,))

4.14.7. Picking Manually

The function TraceGUI.pick_ray highlights a full ray. An integer index is required as to select a given ray. Only currently displayed rays are pickable, which are defined by TraceGUI.ray_selection, see Selecting Rays. So an index=50 means that the 50th True value of TraceGUI.ray_selection is picked.

Function TraceGUI.pick_ray_section highlights a ray at a given intersection. The ray is highlighted, a crosshair is shown at the intersection position and a ray information text is shown inside the scene. Compared to the previous function, an additional integer section parameter is needed. An optional parameter detailed defines if more detailed information should be shown. This would be equivalent to picking a section manually in the scene with the Shift key held.

To deactivate the ray highlighting, information text and cross hair, TraceGUI.reset_picking needs to be called.

Here is an example:

def automated(sim):

    # pick the ray with index 100
    sim.pick_ray(index=100)

    # pick ray section 2 of ray 50 with default view
    sim.pick_ray_section(index=50, section=2)

    # pick ray section with detailed view
    sim.pick_ray_section(index=50, section=2, detailed=True)

    # reset (=hide) the picking view
    sim.reset_picking()

# create the GUI and provide the automation function to TraceGUI.control()
sim = TraceGUI(RT)
sim.control(func=automated, args=(sim,))

4.14.8. Showing Plots

Available plotting functions include TraceGUI.source_image , TraceGUI.source_profile, TraceGUI.detector_image, TraceGUI.detector_profile, TraceGUI.detector_spectrum, TraceGUI.source_spectrum, TraceGUI.move_to_focus.

There are more settings available than through the GUI. For example, it is possible to save a image to the disk. Additionally, a custom detector/source extent can be specified, a setting not available through the GUI.

def automated(sim):

    # change plot settings
    sim.image_pixels = 315
    sim.image_mode = "Lightness (CIELUV)"

    # show a source profile with a user-defined extent
    sim.source_profile(extent=[0, 0.1, 0.2, 0.25])

    # save a detector image with higher dpi
    sim.detector_image(path="detector.png", sargs=(dpi=600))

    # example for an automated focus plots
    sim.detector_index = 1
    sim.source_index = 0
    sim.cost_function_plot = True
    sim.move_to_focus()

# create the GUI and provide the automation function to TraceGUI.control()
sim = TraceGUI(RT)
sim.control(func=automated, args=(sim,))

4.14.9. Accessing custom UI elements

Custom elements are accessible through a name, consisting of their type and a chronological number. The number corresponds to the order that the element has been created. Assigning values works analogously to all other parameters. To button action is called with a special function.

You can find examples below.

sim.custom_value_2 = 4.5
sim.custom_checkbox_1 = False
sim.custom_selection_3 = "Case 2"

sim.custom_button_action_1()

4.14.10. Closing Down

To close the GUI down programmatically, the function TraceGUI.close can be called:

def automated(sim):

    # do some things
    ...

    # close everything down
    sim.close()

# create the GUI and provide the automation function to TraceGUI.control()
sim = TraceGUI(RT)
sim.control(func=automated, args=(sim,))

This will close all GUI and plotting windows and exit all background tasks.