Open source automated measurement of M², also known as the beam propagation ratio or beam quality factor, using the WinCamD / Nanoscan beam profilers. WinCamD and NanoScan code may be used independently.
Table of Content
- Important Note
- Supported Models
- Version Information
- Installation
- Independent Modules
- Extending this code
- Usage
- How it works
- Demo
- Troubleshooting
- Code Linting in VS Code
- References
Table of contents generated with markdown-toc
This repository also contains code to interact with the WinCamD and Nanoscan Beam Profiler. These functions may be used independent of the M² Measurement code.
See below for more information on how to use it.
-
The WinCamD Camera used is the DataRay WinCamD-IR-BB.
-
The NanoScan Camera used is the Ophir NanoScan 2s Pyro/9/5. You will need the PRO version instead of the STD Version. The activation code is device-specific and written into the EEPROM of the Scanhead. The NanoScan vendor software then checks the activation status of the Scanhead before marshalling the function calls to it.
-
The stage controller used for this project is the SIGMAKOKI/OptoSigma Controller
GSC-01
with the accompanying stageSGSP26-200
.
Due to software compability issues, device-interfacing code for the beam profilers in this repository can only run on Windows.
For Python packages used, refer to conda-environment.yml
.
The DataRay Software versions used for the development of this code are as follows:
- 32-bit: iDataRay80D63
- 64-bit: iDataRay80D62_x64
The NanoScan Software version is: v2.9.1.28
.
- Install the necessary vendor software for NanoScan and WinCamD.
- Clone the repository
- Using Git Bash:
git clone https://github.com/sunjerry019/nanosquared/
- Git GUI
- Right click on parent folder > Git GUI Here
- Clone a repository > Source Location: https://github.com/sunjerry019/nanosquared/
- Using Git Bash:
- To prepare the Python environment, you may choose to use Anaconda:
conda env create -f conda-environment.yml
conda activate nanosquared
- Thereafter, install the
nanosquared
package by navigating to the repository and doing:
pip install .
- Verify that you have installed it properly by doing:
import nanosquared
Step 4 makes it easier to use the package but is not essential.
Step 3 and 4 may be automated using anaconda-install.bat
Should there be new versions of the nanosquared software, you can simply run update-and-install.bat
, which will get the newest update from Github and then install it as a local package (not strictly required for use). [The environment nanosquared needs to be set up with Anaconda]
Alternatively, the updates may be obtained using the following method:
- Run
update.bat
- Using Git Bash in the folder:
git pull origin master
- Git GUI
- Right click on parent folder > Git GUI Here
- Remote > Fetch from > origin
- Merge > Local Merge > Tracking Branch > origin/master
Then you may run
conda activate nanosquared
pip install .
to install the latest version of the software as a local python package.
Should you need to directly debug the NanoScan interfacing C# code, prepare a 32-bit environment:
conda env create -f src\nanosquared\cameras\archive\nanoscan\nanoscan_32.yml
conda activate nanoscan-32bit
This might be necessary as not all function calls exposed by the NanoScan ActiveX Endpoint has been implemented into NanoScanLibrary.dll
. Consult the C# directory for more information.
To use the NanoScan
Python Interface, you first need to install the NanoScan
software. A copy of which lives in this repository for archival purposes.
Due to some security policy, loading a DLL from a network location may be disabled on certain computers. In this case, copy NanoScanLibrary.dll
and NS2_Interop.dll
to C:\nanosquared_include\
and it should load fine. The scripts are written in such a way as to fall back to that location (This behaviour may change in the future).
More to be added
To use the WinCamD
Python Interface, you first need to the install DataRay software.
Please install the version that corresponds to your Python installation (i.e. 64-bit DataRay for 64-bit Python). As DataRay is regularly putting out updates for their devices, we have decided not to include the installer in this repository. Please visit their website for more information, or see above for the links to the versions used during the development of this code.
More to be added
For every one of these modules described below, the LoggerMixIn
class has been used. While technically not necessary, it has been included to aid debugging.
Include helpers.py
for the LoggerMixIn
class. See below for more information.
We recommend initializing all modules with the with
keyword in Python to ensure that the __exit__()
method is called when the program ends.
You may use the NanoScan
and WinCamD
modules independent of the rest of the code in the repository to manage and use the respective beam-profilers (here loosely referred to as cameras). For that, follow the instructions detailed in the above sections (NanoScan and WinCamD) to install the necessary support software. Then follow the instructions under Usage to import the necessary packages:
# For NanoScan
from nanosquared.cameras.nanoscan import NanoScan
cam = NanoScan(devMode = False)
# For WinCamD
from nanosquared.cameras.wincamd import WinCamD
cam = WinCamD(devMode = False)
These 2 modules may be packaged into seperate packages should someone find the time to do it. It would be a meaningful endeavour in providing Python support for these 2 devices.
Both of these modules inherit the Camera
class, which guarantees the existence of the following:
x, y, both = cam.AXES.X, cam.AXES.Y, cam.AXES.BOTH # Enum for the different axes
cam.wait_stable() # Function returns when beam-profiler is stable
cam.log()
# To obtain an average D4Sigma diameter
cam.getAxis_avg_D4Sigma(axis = x, numsamples = 20, returnRaw = False)
# If returnRaw is set to True, function returns: (data, rawdata)
See the specific source code for more detailed documentation of the functions.
If you do not wish to download the entire repository to interface with these beam profilers, a list of necessary files may be found under cameras/README.
In addition to the above functions, NanoScan
also provides the cam.rotationFrequency
property to set the rotation speed of the scan head.
Most other functions exposed by the ActiveX Endpoint may be accessed using:
cam.NS.<func>
where <func>
is the function you want to call. Refer to the C# source code for a list of implemented function calls. The documentation for each of the ActiveX function can be (normally) found under:
C:\Program Files (x86)\Photon\NanoScan v2\Documentation\50318-001 NanoScan v2 Automation Developer Guide.pdf
where usually the NsAs
part of the function name has been removed in the C# function.
See csharp/README.md for more information on why such an implementation was used.
In addition to the above functions, WinCamD
also provides:
cam.startDevice(); cam.stopDevice()
cam.setClipMode(mode = CLIP_MODES.CLIP_LEVEL_METHOD, clip = 50) # Sets the clipping mode for the width data
cam.getAxis_D4Sigma(axis = cam.AXES.X) # Gets a single reading of the D4Sigma beam width
cam.getWinCamData(axis = cam.AXES.X) # Gets an array of pixel data
cam.getCameraIndex() # Gets the index of the camera, esp if more than one camera is attached
The GSC01
stage controller module from OptoSigma may also be used independently. If you only want to interact with the stage, then you only need the files from src/nanosquared/stage. Currently, only the accompanying stage SGSP26-200
has been implemented. To use another stage, make a copy of that class and modify the parameters accordingly. Theoretically, any stage that works with the GSC01
controller should work with the code. See the manual for more information.
To use the SGSP26-200
stage:
from nanosquared.stage.controller import GSC01
with GSC01(devMode = False) as control:
control.homeStage()
# move 10 um
pulses = control.um_to_pulse(um = 10, asint = True)
# we use asint because GSC01 can only use integer pulses
control.setSpeed(minSpeed = 500, maxSpeed = 6000)
control.rmove(delta = pulses)
# move to specific position
control.move(pos = 500)
See src/nanosquared/stage/controller.py for more available functions.
If a stage other than the SGSP26-200
is to be used, then implement a class as such:
import nanosquared.stage._stage as Stg
class NewStageName(Stg.GSC01_Stage):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.travel = 200 # mm # REQUIRED: Obtain from stage
self.pulseRange = 100557 # REQUIRED: Obtain from stage
self.recalculateUmPerPulse()
def resetStage(self):
upper = (self.pulseRange - 1) / 2
lower = - upper
return self.setLimits(lower = lower, upper = upper)
and then pass it into the controller:
st = GSC01(stage = NewStageName(), devMode = False)
See src/nanosquared/stage/_stage.py for more information.
The code responsible for communicating with each component are separated into different modules, which can be imported into a combination script. As OOP concepts have always been the core to the design of this software, any new stage/beam profiler can easily be integrated into the project by extending the base classes.
Refer to fitting for documentation on the fitting module, and how you might can extend it to suit your purposes.
All logging is provided by the LoggerMixIn
class under src/nanosquared/common/helpers.py
. All component classes inherit LoggerMixIn
, which provides the method self.log()
. This allows easy control of the log level and the way logging is handled in the entire project.
If you are adding modules to the codebase, it is recommended to inherit the LoggerMixIn
class.
To start the quick-and-dirty CLI Application, simply double click on launch_m2.bat
.
The environment nanosquared
needs to be set up with Anaconda.
If you are using a pip
-installed version, simply do:
import nanosquared
If you are planning to use the python files directly, add the following to the top of your script:
import os,sys
src_dir = os.path.join("full\path\to\repo","src")
sys.path.insert(0, src_dir)
You can import and use the modules, for example:
from nanosquared.cameras.nanoscan import NanoScan
n = NanoScan(devMode = False)
print(n.getAxis_avg_D4Sigma(axis = n.AXES.X, numsamples = 100))
Example on how to measure the M² of a beam:
from nanosquared.measurement.measure import Measurement
from nanosquared.cameras.nanoscan import NanoScan
from nanosquared.stage.controller import GSC01
from nanosquared.fitting.fitter import MsqFitter
cfg = { "port" : "COM13" } # Check with "Device Manager" for port number
with NanoScan(devMode = False) as n:
with GSC01(devMode = False, devConfig = cfg) as s:
with Measurement(devMode = False, camera = n, controller = s) as M:
meta = {
"Wavelength": "2300 nm",
"Lens": "f = 250mm CaF2 lens"
}
M.take_measurements(precision = 10, metadata = meta, removeOutliers = 0)
# incl. auto find beam waist and rayleigh length
# Measurement data will be saved under
# ``repo/data/M2/<datetime>_<random string>.dat``
# together with the metadata
# removeOutliers:
# 0 = Do not remove outliers, calculate as is
# 1 = Remove highest 10% of results
# 2 = Remove positive peaks from result based on a threshold of 20% * Mean.
# To save to a specific file, use:
# `M.take_measurements(precision = 10, metadata = meta, writeToFile = "path/to/file")`
# or run
# `M.write_to_file("path/to/file", metadata = meta)
# Explicit options
res = M.fit_data(axis = M.camera.AXES.X, wavelength = 2300, \
mode = MsqFitter.M2_MODE, useODR = False, xerror = None)
print(f"X-Axis")
print(f"Fit Result:\t{res}")
print(f"M-squared:\t{M.fitter.m_squared}")
fig, ax = M.fitter.getPlotOfFit()
fig.show()
res = M.fit_data(axis = M.camera.AXES.Y, wavelength = 2300) # Use defaults (same as above)
print(f"Y-Axis")
print(f"Fit Result:\t{res}")
print(f"M-squared:\t{M.fitter.m_squared}")
fig, ax = M.fitter.getPlotOfFit()
fig.show()
If the measurement has already been taken and only a fit is required, then you can run:
from nanosquared.measurement.measure import Measurement
with Measurement(devMode = True) as M:
M.read_from_file("path/to/file")
res = M.fit_data(axis = M.camera.AXES.X, wavelength = 2300)
print(f"X-Axis")
print(f"Fit Result:\t{res}")
print(f"M-squared:\t{M.fitter.m_squared}")
fig, ax = M.fitter.getPlotOfFit()
fig.show()
res = M.fit_data(axis = M.camera.AXES.Y, wavelength = 2300)
print(f"Y-Axis")
print(f"Fit Result:\t{res}")
print(f"M-squared:\t{M.fitter.m_squared}")
fig, ax = M.fitter.getPlotOfFit()
fig.show()
More to be added, or even separate README.
The measurement of the M² data is carried out by the Measurement module.
If no center is given, the code uses the ternary search algorithm will be used on each axis to find the center.
From the center, 10 equidistant points will be symmetrically chosen around the center within the Rayleigh Length and 10 between 2 and 3 times the Rayleigh Length. In total, 21 points will be taken (10 + 10 + 1), including the center.
If no Rayleigh Length is given, the code uses the ITP Method to find an approximation for it. This works by shifting all beam width data downwards by √2*w0
, where w0
is measured the beam width as measured at the center found by the ternary search algorithm. The default parameters used for the ITP method are as follows:
kappa_1 = 0
kappa_2 = golden ratio = 1.618
n_0 = 0
kappa_1 = 0
is not technically allowed, but experimentally it helps the algorithm to converge faster in certain cases.
NOTE: z_R searching working in both directions. However, the direction of beam propagation in which the beam is coming in from the dial side of the stage is preferred over the other and will be searched first.
This way all parameters of the beam may be determined experimentally.
The code will measure 10 points with +/- z_R around the center, and then based on the situation, try to measure:
- [symmetrical case] 5 points from +2z_R to +3z_R and 5 points from -3z_R to -2z_R
- [asymmetrical case] 10 points from +2z_R to +3z_R
- [inverted asymmetrical case] 10 points from -3z_R to -2z_R
in that order.
If using all 3 methods does not allow all points to fit within the travel range of the stage, then the function ends and refuses to continue. The setup is therefore not suitable for this stage. Change the setup so that the full measurement process may fit within the travel range of the stage.
The Fitter
module under fitting is used to fit the data obtained.
Fitter ┬─> ODRFitter (scipy.odr): optimizes x and y-error
├─> OCFFitter (scipy.optimize.curve_fit): optimizes y-error
│
┌──┴───> MsqODRFitter / MsqOCFFitter: Provides all functionalities
│ and fitting using the selected
│ fitting method
│
MsqFitter: Provides functionalities common to all fitting methods
There are 2 different fitters available (see above). See fitting for more details on the fitting modes available to obtain M² (M²λ, M² and ISO modes).
The default fitting method is scipy.optimize.curve_fit
.
By default, we do not use the fit equation described in ISO 11146-1:2021 Section 9 due its large errors. Instead, we use the M² Mode, which fits the obtained caustic to the guassian beam equation:
This obtains the M² parameter as one of the fit parameters.This could be because there are some limitation on the number of devices that can be plugged into one set of USB ports on the computer. Try plugging the stage and WinCamD to separate sides of the computer/laptop.
Go into the Task Manager (Ctrl + Shift + Esc), then click on the "Processes" Tab, find NanoScanII.exe and end the process.This is likely caused by an improper shutdown, in which the NanoScan Program was not closed properly.
Ensure that no instances of NanoScanII.exe are running before restarting the program.
Note: This could also manifest as nothing happening after requesting a beam width reading. This is because the software is waiting for sensible data as part of the wait_stable
subroutine.
There could be many reasons this could be happening. Troubleshoot by opening the NanoScan software provided by Ophir Optics to determine if the NanoScan is even providing any form of data.
If there is no data despite starting data acquisition, then perhaps you need to plug NanoScan into another USB Port.
Another reason could be that the NS software did not close properly. Try running nanoscan.py
directly and then:
n.NS.ShutdownNS()
Then try to restart your original application that makes use of NanoScan
.
In this case, you can try to use an attenuator, or adjust the scan head rotation frequency:
n = NanoScan()
n.rotationFrequency = 2.5
You can get the allowed rotation frequencies through n.allowedRots
or n.NS.GetHeadScanRates()
. For the NanoScan 2s Pyro/9/5 used in the development of this script, the allowed rotations (in Hz) are:
[1.25, 2.5, 5.0, 10.0, 20.0]
Sometimes this problem could be caused by the system taking very long to read from a network drive. In this case, try to connect the laptop to a Ethernet/LAN connection and try again.
Refer to https://stackoverflow.com/a/54488818 for taming PyLint. In particular, you can do:
"python.linting.pylintArgs": [
"--max-line-length=80",
"--disable=all",
"--enable=F,E,unreachable,duplicate-key,unnecessary-semicolon,global-variable-not-assigned,unused-variable,binary-op-exception,bad-format-string,anomalous-backslash-in-string,bad-open-mode",
"--disable=W0142,W0403,W0613,W0232,R0903,R0913,C0103,R0914,C0304,F0401,W0402,E1101,W0614,C0111,C0301"
]