Skip to content

Knife-edge experiment

Image title

History

The Foucault knife-edge test was described in 1858 by French physicist Léon Foucault as a way to measure conic shapes of optical mirrors. It measures mirror surface dimensions by reflecting light into a knife edge at or near the mirror's center of curvature. (Image on the left was created with chatGPT.)

The Foucault knife-edge test, originally designed for testing mirror curvature, can be adapted to measure the diameter of a laser beam.

Measuring the diameter of a laser beam.

Image title

https://www.cnilaser.com/Laser-Gaussian-Beam-Measurement.htm

A simple method to determine the diameter of a laser beam is to gradually eclipse the laser beam using a sharp knife-edge. As the knife-edge intersects the beam in a direction perpendicular to the propagation axis of the beam, a photodetector measures the intensity of the unmasked portion of the beam. If the spatial profile of the laser beam is described by a Gaussian line shape, the signal measured by the detector is represented by an integrated Gaussian function. In principle, the beam diameter can be determined from such photodetector data. One approach involves electronic or numerical differentiation of the experimental data, thus reconstructing the original Gaussian profile. See this for more information. See this on for non-Gaussian beams.

Here, we are going to perform a knife-edge scan, using the modules we have written so far, and a linear stage and a power meter devices. Below you can see all the modules we discussed previously.

Python Module for the linear stage

The complete module discussed in Linear Stage

import elliptec
import sys, os
import time
import save_data

class LinearStage():
    """ Linear stage interface"""

    def __init__(self):
        self.ports = None
        self.devices = None
        self._controller = None
        self.device = None
        self.current_position = None


    def find_devices(self):
        """ Lists serial port names """

        ports = elliptec.scan.find_ports()
        self.ports = ports[:]    
        return ports

    def connect(self, idn):
        self._controller = elliptec.Controller(idn, debug=False)
        self._controller.port = idn
        self.devices = elliptec.scan_for_devices(controller=self._controller)
        self.device = elliptec.Linear(self._controller)
        self.current_position = self.check_position()
        return self.device.info


    def home(self):
        sys.stdout = open(os.devnull, 'w')
        self.device.home()
        sys.stdout = sys.__stdout__


    def move(self, pos):
        sys.stdout = open(os.devnull, 'w')
        position = self.device.set_distance(pos)
        sys.stdout = sys.__stdout__
        self.current_position = position
        return position


    def check_position(self):
        return self.device.get_distance()


    def jog_and_measure(self, start=0, end=60, step=1, function=None, time_wait=1):

        if not function:
            print("Need funcion")
            return 1

        position, measure = [], []
        for pos in range(start, end+1, step):
            position.append(self.move(pos))
            measure.append(function())
            time.sleep(time_wait)

        save_data.with_print(position=position, measure=measure, parameters={"step":step})
        return 0


if __name__ == "__main__":

    ls = LinearStage()
    lls = ls.find_devices()
    info = ls.connect(lls[0])
    print(info)

    ls.home()
    ls.jog_and_measure(step = 5, function=time.time, time_wait=0)
    ls.move(14)

Python Module for the power meter

The complete module discussed in power meter:

import pyvisa
from ThorlabsPM100 import ThorlabsPM100
import numpy as np

# import warnings
# warnings.filterwarnings('ignore')
# pip install zeroconf psutil

class PowerMeter():

    def __init__(self):
        """
        A general powermeter interface.
        """

        # self.rm = pyvisa.ResourceManager('@py')
        self.rm = pyvisa.ResourceManager()
        self.powermeters = {}
        self.inst = None
        self.powermeter = None
        self.lastAcq = None
        self.trace = np.zeros(100)
        self.parameters = {"wavelength": None,
                           "background": 0,
                           "unit": None}

        self._str_wavelength = 'sense:corr:wav'
        self._str_power = 'power:dc:unit '

    def find_powermeter(self):
        """Find all available ports with Power Meter: PM100 attached."""
        for addr in self.rm.list_resources():
            try:
                name = self.rm.open_resource(addr).query('*IDN?')
                if 'PM100' in name:
                    self.powermeters[name] = addr
            except:
                pass
        return list(self.powermeters.keys())

    def connect(self, idn):
        """ Connects to the power meter attached to port idn 
            idn example: list(self.powermeters.keys())[0] 
        """
        self.inst = self.rm.open_resource(self.powermeters[idn])
        self.inst.read_termination = '\n'
        self.inst.write_termination = '\r\n'
        self.powermeter = ThorlabsPM100(inst=self.inst)
        self.powermeter.configure.scalar.power()
        self.lastAcq = self.powermeter.read

        self.get_wavelength()
        self.get_unit()

        print('Connected to powermeter: {}'.format(idn))

    def read(self, pure=False, printval=False):
        """ Make measurement: in this case, read power, if 
            pure is False, subtracts the background to the measurement

        """
        if pure:
            val = self.powermeter.read
        else:
            val = self.powermeter.read - self.parameters["background"]
        self.lastAcq = val
        self.trace = np.append(self.trace[1:], val)
        if printval:
            print('Measure: {} {}'.format(val, self.parameters["unit"]))
        return val

    def get_background(self):
        """ Make a background measurement """
        self.parameters["background"] = self.read(pure=True)
        self.read()
        print('Background: {} {}'.format(self.parameters["background"], self.parameters["unit"]))

    def get_wavelength(self):
        """ Get current wavelength used by the power meter""" 
        wl = self.inst.query(self._str_wavelength+'?')
        self.parameters["wavelength"] = wl
        print('Wavelength: {} {}'.format(wl, "nm"))
        return wl

    def set_wavelength(self, wl):
        """ Change wavelength used by the powermeter to calculate the power"""
        self.inst.write(self._str_wavelength+' '+str(int(wl)))
        return self.get_wavelength()

    def get_unit(self):
        """ Get current unit used by the power meter""" 
        u = self.powermeter.sense.power.dc.unit
        self.parameters["unit"] = u     
        return u 

    def set_unit(self, unit):
        """ Set units of the power meter, allowed units: W or dBm"""
        if unit not in ['W', 'dBm']:
            print("Unit must be W or dBm")
        else:
            self.inst.write(self._str_power+unit)
        return self.get_unit()

    def switch_unit(self):
        """ Switch units of powermeter between dBm and W"""
        if self.parameters["unit"] == 'W':
            self.inst.write(self._str_power+'dBm')
        else:
            self.inst.write(self._str_power+'W')
        return self.get_unit()   

if __name__ == "__main__":    
    #initialize class             
    pm = PowerMeter()

    #Find powermeters and connect to first one
    lpm = pm.find_powermeter()

    print(lpm)
    pm.connect(lpm[0])


    #Measure, get background and measure again
    pm.read(printval=True)
    pm.get_background()
    pm.read(printval=True)

    pm.get_wavelength()

    pm.switch_unit()
    pm.read(printval=True)
    pm.switch_unit()
    pm.read(printval=True)

Save_data module

The complete module discussed in save data

def with_print(parameters={}, **kwargs):
    """ Print parameters and measurements into a file, slightly organized"""
    for key, val in parameters.items():
        print("#", "%s: %s" % (key, val))
    print("#", *kwargs.keys())
    for row in zip(*kwargs.values()):
        print(*row)

def with_print_dump(**kwargs):
    """ Print data, not organized """
    print(kwargs)

def with_print_fancy(**kwargs):
    """ print data after separating lists of measurements from parameters"""

    print_as_list = []
    for key, val in kwargs.items():
        if not isinstance(val, list):
            print("#", "%s: %s" % (key, val))
        else:
            print_as_list.append(key)

    print("#", *print_as_list)
    for row in zip(*(kwargs[i] for i in print_as_list)):
        print(*row) 


def to_file(filename = "out", parameters={}, **kwargs):
    """ save data into a .txt file, it is basically function:with_print but we redirect the output to the file""" 
    filename = filename + ".txt"
    with open(filename, "w") as f:
        for key, val in parameters.items():
            print("#", "%s: %s" % (key, val), file=f)
        print("#", *kwargs.keys(), file=f)
        for row in zip(*kwargs.values()):
            print(*row, file=f)

def with_json(filename = "out", **kwargs):
    """ write data into a json file"""
    import json

    filename = filename + ".json"

    with open(filename, "w") as f:
        json.dump(kwargs, f)

def with_numpy(filename = "out", parameters={}, **kwargs):
    """ Write data to a .txt file with numpy""" 
    import numpy as np

    filename = filename + ".txt"
    header = ""
    for key, val in parameters.items():
        header += "{}: {} \n".format(key, val)
    header += " ".join(kwargs.keys())

    np.savetxt(filename, np.c_[ *kwargs.values() ], header=header)

Plot module

The complete program discussed in plot data:

import json
import glob
import numpy as np
import matplotlib.pyplot as plt

# find the file containig the data
filenames = glob.glob("out*json")
print(filenames)

# open the file and get the data
with open(filenames[-1], "r") as f:
    data = json.load(f)

# check what is in the file
print(data.keys())

# treat the data if necessary
y = np.array(data["measurements"])
y *= 1000 # scale to change units, plot looks better

# factor = y[0]           # remove background if necessary
# y = (y - factor)*1000

# make a nice plot for the data
plt.rcParams.update({'font.size': 15})
fig, ax = plt.subplots(figsize=(8,8))
ax.plot(data["positions"], y, label="Step = {} mm".format(data["parameters"]["step"]))
ax.legend()
ax.set_xlabel("Position (mm)")
ax.set_ylabel("Power (mW)")
ax.tick_params(axis='both', direction='in', top=True, right=True)

# show the plot or save it into a file
#plt.show()
plt.savefig("my_plot_{}.png".format(data["parameters"]["step"]))

Exercise

Use the modules or programs above and write a knife_edge experiment class with python.

Image title

Here is the solution:

import my_powermeter as mpm
import my_linear as mls
import time
import datetime
import save_data

class KnifeEdge:

    """ Class to perform knife-edge experiment """

    def __init__(self):
        self.powermeter = mpm.PowerMeter()
        self.linear_stage = mls.LinearStage()
        self.parameters = {            # parameters from the linear stage
            "start": 0,
            "end":   60,
            "step":  1, 
            "sleep": 0.5 
        }
        self.positions = []
        self.measurements = []

        # First we need to find the devices
        lpm = self.powermeter.find_powermeter()
        lls = self.linear_stage.find_devices()


        # Second, attempt to connect to the devices
        try:
            self.powermeter.connect(lpm[0])
        except:
            print("Could not connect to powermeter")
            print(lpm)
            return 

        try:
            self.linear_stage.connect(lls[0])
        except:
            print("Could not connect to linear stage")
            print(lls)
            return


    def initialize(self):
        """ Home the linear stage and measure the backgroung of the powermeter """
        self.linear_stage.home()
        self.powermeter.get_background()

    def get_parameters(self):
        """ Obtain parameters from the linear stage and from the powermeter """
        for key, val in self.parameters.items():
            print("%s: %s" % (key, val))
        for key, val in self.powermeter.parameters.items():
            print("%s: %s" % (key, val))
        return self.parameters, self.powermeter.parameters

    def set_parameters(self, start=0, end=60, step=1, sleep=0, 
                      pm_unit='W', pm_wl=400):
        """ Function to change the experiment parametes """
        self.parameters["start"] = start
        self.parameters["end"] = end
        self.parameters["step"] = step
        self.parameters["sleep"] = sleep

        self.powermeter.set_wavelength(pm_wl)
        self.powermeter.set_unit(pm_unit)
        return self.get_parameters()

    def run(self):
        """ Function to run the experiment itself, creates a file with parametes and the measured values """

        # first get a file name with the current date and time
        filename = "out_"+str(datetime.datetime.now()).replace('.','_').replace(' ', '_').replace(':', '_')

        # loop over the possible positions of the linear stage
        for pos in range(self.parameters["start"], 
                         self.parameters["end"], self.parameters["step"]):

            # move the stage and store position
            self.positions.append(self.linear_stage.move(pos))
            # measure the power
            self.measurements.append(self.powermeter.read()) 
            # wait for a bit, just in case
            time.sleep(self.parameters["sleep"])

            # save parametes and measurements to file at every iteration, in case there is a crash interruption
            save_data.with_json(filename=filename,
                                parameters=self.parameters, 
                                pm_parameters=self.powermeter.parameters, 
                                positions=self.positions, 
                                measurements=self.measurements)       


exp = KnifeEdge()
# exp.initialize()
# exp.set_parameters()

exp.get_parameters()
exp.run()
print(exp.__dict__)