Source code for pylablib.devices.Sirah.tuner

from ...core.dataproc import filters
from ...core.fileio import loadfile
from ...core.utils import general, funcargparse, py3
from .base import GenericSirahError

import numpy as np
import pandas as pd
import time


[docs] class FrequencyReadSirahError(GenericSirahError): """Sirah error indicating an error while trying to read frequency value""" def __init__(self, timeout=None): msg="could not read frequency in {} seconds".format(timeout) if timeout is not None else "could not read frequency" super().__init__(msg)
[docs] class MatisseTuner: """ Matisse tuner. Helps to coordinate with an external wavemeter to perform more complicated tasks: motors calibration, fine frequency tuning, and stitching scans. Args: laser: opened Matisse laser object wavemeter: opened wavemeter object (currently only HighFinesse wavemeters are supported) calibration: either a calibration dictionary, or a path to the calibration dictionary file """ def __init__(self, laser, wavemeter, calibration=None, ref_cell=True): self.laser=laser self.wavemeter=wavemeter self.apply_calibration(calibration) self._tune_units="int" self._fine_scan_start=None self._freq_avg_time=0 self._last_read_frequency=None,0 self._use_ref_cell=ref_cell
[docs] def set_tune_units(self, units="int"): """ Set default units for fine tuning and sweeping (fine sweep or stitched scan). Can be either ``"int"`` (internal units between 0 and 1) or ``"freq"`` (frequency units; requires calibration). """ funcargparse.check_parameter_range(units,"units",["int","freq"]) if (self._slow_piezo_cal is None or (self._use_ref_cell and self._ref_cell_cal is None)) and units=="freq": raise ValueError("frequency tuning units require calibration") self._tune_units=units
def _check_device(self, device): if device=="ref_cell" and not self._use_ref_cell: raise ValueError("reference cell is disabled") def _to_int_units(self, value, device): if self._tune_units=="freq": self._check_device(device) if device=="slow_piezo": value=value/self._slow_piezo_cal elif device=="ref_cell": value=value/self._ref_cell_cal return value
[docs] def apply_calibration(self, calibration): """ Apply the given calibration. `calibration` is either a calibration dictionary, or a path to the calibration dictionary file. Contains information about the relation between bifi motor and wavelength, thin etalon motor span, slow piezo tuning rate (frequency to internal units) and its maximal sweep rate, ref cell tuning rate (frequency to internal units) and its maximal sweep rate. """ if isinstance(calibration,py3.textstring): try: calibration=loadfile.load_dict(calibration) except (IOError,RuntimeError): raise GenericSirahError("could not load calibration file {}".format(calibration)) if calibration is None: return if "te_search_rng" in calibration: self._te_search_rng=calibration["te_search_rng"] if "te_full_rng" in calibration: self._te_full_rng=calibration["te_full_rng"] if "te_plateau_scan_steps" in calibration: self._te_plateau_scan_steps=calibration["te_plateau_scan_steps"] if "te_plateau_step" in calibration: self._te_plateau_step=calibration["te_plateau_step"] self._te_center_step=max(1,self._te_plateau_step//3) if "bifi_full_rng" in calibration: self._bifi_full_rng=calibration["bifi_full_rng"] if "bifi_freqs" in calibration: self._bifi_freqs=calibration["bifi_freqs"] if "bifi_step_precision" in calibration: prec=calibration["bifi_step_precision"] self._bifi_search_step=(prec*8,2,prec) if "slow_piezo_cal" in calibration: self._slow_piezo_cal=calibration["slow_piezo_cal"] if "slow_piezo_max_speed" in calibration: self._slow_piezo_max_speed=calibration["slow_piezo_max_speed"] if "ref_cell_cal" in calibration: self._ref_cell_cal=calibration["ref_cell_cal"] if "ref_cell_max_speed" in calibration: self._ref_cell_max_speed=calibration["ref_cell_max_speed"]
[docs] def get_frequency(self, timeout=1.): """ Get current frequency reading. The only method relying on the wavemeter. Can be extended or overloaded to support different wavemeters. """ avg_countdown=general.Countdown(self._freq_avg_time) countdown=general.Countdown(timeout) acc=[] while True: f=self.wavemeter.get_frequency(error_on_invalid=False) if isinstance(f,float): acc.append(f) if avg_countdown.passed(): self._last_read_frequency=np.mean(acc),time.time() return self._last_read_frequency[0] time.sleep(1E-3) countdown.reset() elif countdown.passed(): raise FrequencyReadSirahError(timeout) else: time.sleep(5E-3)
[docs] def get_last_read_frequency(self, max_delay=1.): """Get the last valid read frequency, or ``None`` if none has been acquired yet""" if time.time()>self._last_read_frequency[1]+max_delay: return self.get_frequency() return self._last_read_frequency[0]
[docs] def set_frequency_average_time(self, avg_time=0): """Set averaging time for frequency measurements (reduces measured frequency jitter)""" self._freq_avg_time=avg_time
def _get_motor_position(self, motor): funcargparse.check_parameter_range(motor,"motor",["bifi","thinet"]) return self.laser.bifi_get_position() if motor=="bifi" else self.laser.thinet_get_position() def _move_motor(self, motor, position, wait=True): funcargparse.check_parameter_range(motor,"motor",["bifi","thinet"]) position=max(position,0) return self.laser.bifi_move_to(position,wait=wait) if motor=="bifi" else self.laser.thinet_move_to(position,wait=wait) def _move_motor_by(self, motor, steps, wait=True, rng=None): funcargparse.check_parameter_range(motor,"motor",["bifi","thinet"]) newpos=self._get_motor_position(motor)+steps if rng is not None and (newpos<rng[0] or newpos>=rng[1]): return False self._move_motor(motor,newpos,wait=wait) return True def _is_motor_moving(self, motor): funcargparse.check_parameter_range(motor,"motor",["bifi","thinet"]) return self.laser.bifi_is_moving() if motor=="bifi" else self.laser.thinet_is_moving()
[docs] def scan_steps(self, motor, start, stop, step): """ Scan the given motor (``"bifi"`` or ``"thinet"``) in discrete steps within the given range with a given step. Return a 4-column numpy array containing motor position, internal diode power, thin etalon reflection power, and wavemeter frequency. """ self.unlock_all() diode_power=[] thinet_power=[] frequency=[] start,stop=sorted([start,stop]) position=np.arange(start,stop,step) for p in position: self._move_motor(motor,p) diode_power.append(self.laser.get_diode_power()) thinet_power.append(self.laser.get_thinet_power()) frequency.append(self.get_frequency()) return np.column_stack([position,diode_power,thinet_power,frequency])
[docs] def scan_centered(self, motor, span, step): """ Scan the given motor (``"bifi"`` or ``"thinet"``) in discrete steps in a given span around the current position. After the scan, return the motor to the original position. Return a 4-column numpy array containing motor position, internal diode power, thin etalon reflection power, and wavemeter frequency. """ self.unlock_all() pos=self._get_motor_position(motor) scan=self.scan_steps(motor,pos-span/2,pos+span/2,step) self._move_motor(motor,pos) return scan
[docs] def scan_quick(self, motor, start, stop, autodir=True): """ Do a quick continuous scan of the given motor (``"bifi"`` or ``"thinet"``) within the given range. Compared to :meth:`scan_steps`, which does a series of discrete defined moves, this method does a single continuous move and records values in its progress. This is quicker, but does not allow for the step size control, and results in non-uniform recorded positions. If ``autodir==False``, first initialize the motor to `start` and then move to `stop`; otherwise, initialize to whichever border is closer. Return a 4-column numpy array containing motor position, internal diode power, thin etalon reflection power, and wavemeter frequency. """ self.unlock_all() position=[] diode_power=[] thinet_power=[] frequency=[] if autodir: p=self._get_motor_position(motor) if abs(p-start)>abs(p-stop): start,stop=stop,start self._move_motor(motor,start) self._move_motor(motor,stop,wait=False) while self._is_motor_moving(motor): position.append(self._get_motor_position(motor)) diode_power.append(self.laser.get_diode_power()) thinet_power.append(self.laser.get_thinet_power()) frequency.append(self.get_frequency()) return np.column_stack([position,diode_power,thinet_power,frequency])
[docs] def scan_quick_centered(self, motor, span): """ Do a quick continuous scan of the given motor (``"bifi"`` or ``"thinet"``) in a given span around the current position. After the scan, return the motor to the original position. Return a 4-column numpy array containing motor position, internal diode power, thin etalon reflection power, and wavemeter frequency. """ self.unlock_all() pos=self._get_motor_position(motor) scan=self.scan_quick(motor,pos-span/2,pos+span/2) self._move_motor(motor,pos) return scan
[docs] def scan_both_motors(self, bifi_rng, te_rng, verbose=False): """ Perform a 2D grid scan changing positions of both birefringent filter and thin etalon motors. `bifi_rng` and `te_rng` are both 3-tuples ``(start, stop, step)`` specifying the scan ranges. If ``verbose==True``, print a message per every birefringent filter position indicating the scan progress. Return a 5-column numpy array containing birefringent filter motor position, thin etalon motor position, internal diode power, thin etalon reflection power, and wavemeter frequency. """ self.unlock_all() diode_power=[] thinet_power=[] frequency=[] bifi_position=np.arange(*bifi_rng) te_position=np.arange(*te_rng) t0=time.time() for i,bfp in enumerate(bifi_position): self._move_motor("bifi",bfp) for tep in te_position: self._move_motor("thinet",tep) diode_power.append(self.laser.get_diode_power()) thinet_power.append(self.laser.get_thinet_power()) frequency.append(self.get_frequency()) if verbose: dt=time.time()-t0 tleft=(dt)/(i+1)*(len(bifi_position)-i-1) print("{:3d} / {:3d} {:5.1f}mins left".format(i+1,len(bifi_position),tleft/60)) bffpos=bifi_position[None,:].repeat(len(te_position),axis=0).flatten() tefpos=te_position[:,None].repeat(len(bifi_position),axis=1).flatten() return np.column_stack([bffpos,tefpos,diode_power,thinet_power,frequency])
[docs] def scan_both_motors_quick(self, bifi_rng, te_rng, verbose=False): """ Perform a quick 2D grid scan changing positions of both birefringent filter and thin etalon motors. For each discrete position of a birefringent filter motor perform a quick scan of the thin etalon motor. `bifi_rng` is a 3-tuple ``(start, stop, step)``, while ``te_rng`` is a 2-tuple ``(start, stop)`` specifying the scan ranges. If ``verbose==True``, print a message per every birefringent filter position indicating the scan progress. Return a 5-column numpy array containing birefringent filter motor position, thin etalon motor position, internal diode power, thin etalon reflection power, and wavemeter frequency. """ self.unlock_all() bffpos=[] tefpos=[] diode_power=[] thinet_power=[] frequency=[] bifi_position=np.arange(*bifi_rng) t0=time.time() for i,bfp in enumerate(bifi_position): self._move_motor("bifi",bfp) tescan=self.scan_quick("thinet",*te_rng[:2]) bffpos+=[bfp]*len(tescan) tefpos+=list(tescan[:,0]) diode_power+=list(tescan[:,1]) thinet_power+=list(tescan[:,2]) frequency+=list(tescan[:,3]) if verbose: dt=time.time()-t0 tleft=(dt)/(i+1)*(len(bifi_position)-i-1) print("{:3d} / {:3d} {:5.1f}mins left".format(i+1,len(bifi_position),tleft/60)) return np.column_stack([bffpos,tefpos,diode_power,thinet_power,frequency])
def _get_thinet_range(self, t): prng=np.percentile(t.EtPower,10),np.percentile(t.EtPower,90) cutoff=prng[0]+(prng[1]-prng[0])*0.2 inrng=t.ThinEt[t.EtPower>=cutoff] return np.percentile(inrng,10),np.percentile(inrng,90) def _get_motor_calibration(self, scan, cal=None): cal=cal or {} thinet_rngs=np.array([self._get_thinet_range(t) for _,t in scan.groupby("BiFi")]) te_rng=np.median(thinet_rngs,axis=0) cal["te_search_rng"]=tuple(te_rng) cal["te_full_rng"]=tuple(te_rng+np.array([-1,1])*1000) fscan=scan[(scan.ThinEt>=te_rng[0])&(scan.ThinEt<te_rng[1])] freq_rngs=np.array([(bf,np.percentile(t.Freq,10),np.percentile(t.Freq,90)) for bf,t in fscan.groupby("BiFi")]) valid_spans=freq_rngs[:,2]-freq_rngs[:,1]<1E12 freq_rngs=freq_rngs[valid_spans] cal["bifi_full_rng"]=freq_rngs[:,0].min(),freq_rngs[:,0].max() bifi_freqs=np.column_stack((freq_rngs[:,0],(freq_rngs[:,1]+freq_rngs[:,2])/2)) maxfreq=np.maximum.accumulate(bifi_freqs[:,1]) bifi_freqs=bifi_freqs[bifi_freqs[:,1]==maxfreq] cal["bifi_freqs"]=bifi_freqs return cal _bifi_cal_rng=(100000,400000,400) _te_cal_rng=(2000,22000)
[docs] def calibrate(self, motors=True, slow_piezo=True, slow_piezo_speeds=None, ref_cell=True, ref_cell_speeds=None, verbose=True, bifi_range=None, thinet_range=None, return_scans=True): """ Calibrate the laser and return the calibration results. If ``motors==True``, perform motors calibration (bifi range and wavelengths, thin etalon range). If ``slow_piezo==True``, perform slow piezo calibration (ratio between internal tuning units and frequency shift). If ``slow_piezo_speeds`` is not ``None``, it defines a list of slow piezo tuning speeds to use for the calibration (in case it depends on the speed). If ``ref_cell==True``, perform ref cell calibration (ratio between internal tuning units and frequency shift). If ``ref_cell_speeds`` is not ``None``, it defines a list of ref cell tuning speeds to use for the calibration (in case it depends on the speed). If `bifi_range` is specified, it is a tuple ``(start, stop, step)`` defining the tested bifi positions (default is between 100000 and 400000 with a step of 400). If `thinet_range` is specified, it is a tuple ``(start, stop)`` defining the tested thin etalon position range. IF ``verbose==True``, print the progress updates during scan. If ``return_scans==True``, return a tuple ``(calibration, scans)``, where ``scans`` is a tuple ``(motor_scan, slow_piezo_scan, ref_cell)`` containing detail scan result tables; otherwise, return just the calibration dictionary. """ cal={} if motors: if verbose: print("Calibrating motors") bifi_range=self._bifi_cal_rng if bifi_range is None else bifi_range thinet_range=self._te_cal_rng if thinet_range is None else thinet_range mot_scan=self.scan_both_motors_quick(bifi_range,thinet_range,verbose=verbose) mot_scan=pd.DataFrame(mot_scan,columns=["BiFi","ThinEt","Power","EtPower","Freq"]) cal=self._get_motor_calibration(mot_scan,cal=cal) self.apply_calibration(cal) else: mot_scan=None slow_piezo_scan=None if slow_piezo and len(self._bifi_freqs): if verbose: print("Calibrating slow piezo response") slow_piezo_cal_freqs=np.linspace(self._bifi_freqs[:,1].min(),self._bifi_freqs[:,1].max(),12)[1:-1] slow_piezo_scan=[] t0=time.time() if slow_piezo_speeds is None: slow_piezo_speeds=[self._slow_piezo_max_speed] slow_piezo_speeds=sorted(slow_piezo_speeds) for i,f in enumerate(slow_piezo_cal_freqs): self.tune_to(f,level="thinet") for s in slow_piezo_speeds: slope,_=self._estimate_slow_piezo_slope(speed=s) slow_piezo_scan.append((f,s,slope)) if verbose: dt=time.time()-t0 tleft=(dt)/(i+1)*(len(slow_piezo_cal_freqs)-i-1) print("{:3d} / {:3d} {:5.1f}mins left".format(i+1,len(slow_piezo_cal_freqs),tleft/60)) slow_piezo_scan=pd.DataFrame(slow_piezo_scan,columns=["Freq","Speed","Slope"]) split_slow_piezo_scans={s:t for s,t in slow_piezo_scan.groupby("Speed")} self._slow_piezo_cal=cal["slow_piezo_cal"]=np.median(split_slow_piezo_scans[min(slow_piezo_speeds)].Slope) cal["slow_piezo_cal_all"]=split_slow_piezo_scans ref_cell_scan=None if ref_cell and len(self._bifi_freqs): self._check_device("ref_cell") try: if verbose: print("Calibrating ref cell response") ref_cell_cal_freqs=np.linspace(self._bifi_freqs[:,1].min(),self._bifi_freqs[:,1].max(),12)[1:-1] ref_cell_scan=[] t0=time.time() if ref_cell_speeds is None: ref_cell_speeds=[self._ref_cell_max_speed] ref_cell_speeds=sorted(ref_cell_speeds) for i,f in enumerate(ref_cell_speeds): for s in slow_piezo_speeds: self.tune_to(f,level="thinet") self._lock_ref_cell() slope,_=self._estimate_ref_cell_slope(speed=s) ref_cell_scan.append((f,s,slope)) if verbose: dt=time.time()-t0 tleft=(dt)/(i+1)*(len(ref_cell_cal_freqs)-i-1) print("{:3d} / {:3d} {:5.1f}mins left".format(i+1,len(ref_cell_cal_freqs),tleft/60)) ref_cell_scan=pd.DataFrame(ref_cell_scan,columns=["Freq","Speed","Slope"]) split_ref_cell_scans={s:t for s,t in ref_cell_scan.groupby("Speed")} self._ref_cell_cal=cal["ref_cell_cal"]=np.median(split_ref_cell_scans[min(ref_cell_speeds)].Slope) cal["ref_cell_cal_all"]=split_ref_cell_scans except self.laser.Error: pass if return_scans: return cal,(mot_scan,slow_piezo_scan,ref_cell_scan) return cal
def _dfcmp(self, f1, f2, app): if app=="below": if f1>0 and f2<0: return True if f1<0 and f2>0: return False if app=="above": if f1<0 and f2>0: return True if f1>0 and f2<0: return False return abs(f1)>abs(f2) def _pass_plateau(self, motor, step, prec, target=None, fltw=3, approach="both", rng=None, after_steps=0): if target is None: target=self.get_frequency() cldf=0 else: cldf=self.get_frequency()-target pdf=cldf flt=filters.RunningDecimationFilter(fltw,mode="median") while True: if not self._move_motor_by(motor,step,rng=rng): return df=self.get_frequency()-target fdf=flt.add(df) if fdf is not None: if abs(fdf-pdf)>prec: if self._dfcmp(cldf,fdf,app=approach): cldf=fdf else: if after_steps: self._move_motor_by(motor,step*after_steps,rng=rng) return pdf=fdf def _center_plateau(self, motor, step, prec, target=None, approach="both", rng=None): if target is None: target=self.get_frequency() # self._pass_plateau(motor,-step*5,prec,target=target,approach=approach,rng=rng) self._pass_plateau(motor,-step,prec,target=target,approach=approach,rng=rng) self._pass_plateau(motor,step,prec,target=target,approach=approach,rng=rng) p0=self._get_motor_position(motor) self._pass_plateau(motor,-step,prec,target=target,approach=approach,rng=rng) p1=self._get_motor_position(motor) self._move_motor(motor,(p0+p1)/2) return p0,p1 def _split_plateaus(self, scan, prec, minw=2): p0,v0=0,scan[0] plats=[] for p,v in enumerate(scan): if abs(v-v0)>prec: if p-p0>=minw: plats.append((p0,p)) p0,v0=p,v if len(scan)-p0>=minw: plats.append((p0,len(scan))) return plats def _get_closest_plateau(self, scan, plats, target): dfs=[abs(np.mean(scan[s:e,3])-target) for s,e in plats] p=plats[np.argmin(dfs)] return scan[p[0]:p[1],:] _bifi_full_rng=(1E5,4E5) # maximal bifi search range _bifi_search_step=(800,2,100) # bifi "zoom-in" parameters ``(init, factor, final)`` # start with ``init`` step size, and every time the desired frequency is reached, reduce it by ``factor``, until ``final`` (or smaller) step size is reached _bifi_pos_dir=1 # direction of bifi tuning corresponding to the increasing frequencies _bifi_plateau_freq_span=30E9 # maximal estimate of the frequency variation withing one bifi 'plateau' (frequency changes smaller than that are ignored during tuning) _bifi_freqs=np.zeros((0,2)) def _align_bifi(self, target, approach="both"): if len(self._bifi_freqs): bifi_pos=self._bifi_freqs[abs(self._bifi_freqs[:,1]-target).argmin(),0]-500 self._move_motor("bifi",bifi_pos) s0,sr,sm=self._bifi_search_step s=s0*(1 if target>self.get_frequency() else -1)*self._bifi_pos_dir while abs(s)>=sm: df=target-self.get_frequency() if np.sign(df)*self._bifi_pos_dir==np.sign(s): if not self._move_motor_by("bifi",s,rng=self._bifi_full_rng): s=-s else: s=-s/sr s*=sr self._center_plateau("bifi",s,self._bifi_plateau_freq_span,target=target,approach=approach,rng=self._bifi_full_rng) def _refine_bifi(self, target): min_err=None,None for t in range(4): scan=self.scan_quick("thinet",*self._te_search_rng) plat_span=np.percentile(scan[:,3],5),np.percentile(scan[:,3],95) target_frac=(target-plat_span[0])/(plat_span[1]-plat_span[0]) if abs(target_frac-0.5)>0.4: if min_err[0] is None or abs(target_frac-0.5)<min_err[0]: min_err=abs(target_frac-0.5),self._get_motor_position("bifi") center_te_pos=scan[abs(scan[:,3]-np.median(scan[:,3])).argmin(),0] self._move_motor("thinet",center_te_pos) time.sleep(0.1) plat_dir=1 if target_frac>0.5 else -1 self._pass_plateau("bifi",self._bifi_search_step[2]*plat_dir//2,self._bifi_plateau_freq_span,after_steps=2) if t>1: self._center_plateau("bifi",self._bifi_search_step[2]//2,self._bifi_plateau_freq_span) else: return scan self._move_motor("bifi",min_err[1]) return scan _te_search_rng=(2000,12000) # initial thin etalon search range _te_full_rng=(1000,13000) # maximal thin etalon tune range (used when zooming in on the target 'plateau' center) _te_plateau_freq_span=5E9 # maximal estimate of the frequency variation withing thin etalon 'plateau' (frequency changes smaller than that are ignored during tuning) _te_plateau_scan_steps=None # number of steps over the full te range to search for thin etalon plateau (``None`` means do a quick scan) _te_plateau_step=50 # thin etalon motor step used to find the desired 'plateau' _te_center_step=20 # thin etalon motor step used for the final probing of the desired 'plateau' boundaries _te_lock_frac=0.3 # relative position within the 'plateau' to center the thin etalon lock (0.5 would be in the center, 0 is on the left edge, etc.) def _scan_te_plateaus(self): if self._te_plateau_scan_steps is None: return self.scan_quick("thinet",*self._te_search_rng) return self.scan_steps("thinet",self._te_search_rng[0],self._te_search_rng[1],abs(self._te_search_rng[1]-self._te_search_rng[0])//self._te_plateau_scan_steps) def _align_te(self, target, prescan=None): scan=self._scan_te_plateaus() if (prescan is None or self._te_plateau_scan_steps is not None) else prescan plats=self._split_plateaus(scan[:,3],self._te_plateau_freq_span,minw=3) if not plats: self._move_motor("thinet",np.mean(self._te_search_rng)) return p=self._get_closest_plateau(scan,plats,target) self._move_motor("thinet",(p[0,0]+p[-1,0])/2) p0,p1=self._center_plateau("thinet",self._te_plateau_step,self._te_plateau_freq_span,target=target,rng=self._te_full_rng) scan=self.scan_steps("thinet",p0-self._te_center_step*3,p1+self._te_center_step*3,self._te_center_step) plats=self._split_plateaus(scan[:,3],self._te_plateau_freq_span,minw=3) if not plats: self._move_motor("thinet",np.mean([p0,p1])) return p=self._get_closest_plateau(scan,plats,target) minpos=p[:,2].argmin() side=-1 if minpos>len(p)//2 else 1 etval=p[:,2].min()+(p[:,2].max()-p[:,2].min())*self._te_lock_frac pos=(p[0,0]+p[-1,0])/2 self._move_motor("thinet",pos) ctl_par=self.laser.get_thinet_ctl_params() self.laser.set_thinet_ctl_params(setpoint=etval/np.median(p[:,1]),P=abs(ctl_par.P)*side,I=abs(ctl_par.I)*side) return scan def _setup_fine_tune_lock(self, device): if device=="slow_piezo": self._unlock_ref_cell() else: self._lock_ref_cell() def _lock_ref_cell(self): self._check_device("ref_cell") self.laser.set_scan_status("stop") self.laser.set_scan_params(device="none") self.laser.set_slowpiezo_ctl_status("run") self.laser.set_fastpiezo_ctl_status("run") def _unlock_ref_cell(self): self.laser.set_scan_status("stop") self.laser.set_scan_params(device="none") try: self.laser.set_fastpiezo_ctl_status("stop") except self.laser.Error: pass self.laser.set_slowpiezo_ctl_status("stop") try: self.laser.set_refcell_position(np.mean(self._ref_cell_rng)) except self.laser.Error: pass def _get_fine_position(self, device): self._check_device(device) if device=="slow_piezo": return self.laser.get_slowpiezo_position() return self.laser.get_refcell_position() def _set_fine_position(self, device, position): self._check_device(device) if device=="slow_piezo": return self.laser.set_slowpiezo_position(position) return self.laser.set_refcell_position(position) def _move_cont_gen(self, device, position, speed): speed=abs(speed) scanpar=self.laser.get_scan_params() if scanpar[0]!=device: scanpar=("none",)+scanpar[1:] self._setup_fine_tune_lock(device) self.laser.set_scan_params(device="none") _,tune_rng,_=self._get_fine_tune_params(device) position=max(tune_rng[0],min(position,tune_rng[1])) start=self._get_fine_position(device) try: if abs(position-start)<1E-3: self.laser.set_scan_params(device="none") self._set_fine_position(device,position) return if position>start: self.laser.set_scan_params(device=device,mode=(False,False,True),lower_limit=tune_rng[0],upper_limit=position,rise_speed=speed,fall_speed=speed) else: self.laser.set_scan_params(device=device,mode=(True,True,False),lower_limit=position,upper_limit=tune_rng[1],rise_speed=speed,fall_speed=speed) self.laser.set_scan_status("run") while self.laser.get_scan_status()=="run": yield finally: self.laser.set_scan_status("stop") self.laser.set_scan_params(*scanpar) self._get_fine_position(device) def _move_cont(self, device, position, speed): for _ in self._move_cont_gen(device,position,speed): time.sleep(1E-3) _slow_piezo_rng=(0,0.7) # total accessible slow piezo range _slow_piezo_max_speed=0.05 # maximal speed speed of slow piezo fine tuning (slow enough to avoid mode hopping, fast enough to be quick) _slow_piezo_cal=None # slow piezo sensitivity (Hz/tuning value) _slow_piezo_cal_est=(0.2,7,0.2,10.) # parameters for slow piezo sensitivity estimation ``(span, segments, delay, tseg_max)``; # to estimate the sensitivity, range of ``span`` size around tuning center is split into ``segments`` chunks, slow is estimated over each of them, and then median is returned def _estimate_slow_piezo_slope(self, speed=None): if speed is None: speed=self._slow_piezo_max_speed center=np.mean(self._slow_piezo_rng) span=min(self._slow_piezo_cal_est[0],speed*self._slow_piezo_cal_est[3]) poss=center+np.linspace(-span/2,span/2,self._slow_piezo_cal_est[1]+1) freqs=[] self._move_cont("slow_piezo",poss[0],max(self._slow_piezo_max_speed,speed,0.2)) for p in poss: self._move_cont("slow_piezo",p,speed) time.sleep(self._slow_piezo_cal_est[2]) freqs.append(self.get_frequency()) df=np.median(np.diff(freqs)) return (df*self._slow_piezo_cal_est[1])/span,np.column_stack((poss,freqs)) _ref_cell_rng=(0,0.7) # total accessible reference cell range _ref_cell_max_speed=0.05 # maximal speed speed of ref cell fine tuning (slow enough to avoid mode hopping, fast enough to be quick) _ref_cell_cal=None # reference cell sensitivity (Hz/tuning value) _ref_cell_cal_est=(0.2,7,0.2) # parameters for reference cell sensitivity estimation ``(span, segments, delay)``; # to estimate the sensitivity, range of ``span`` size around tuning center is split into ``segments`` chunks, slow is estimated over each of them, and then median is returned def _estimate_ref_cell_slope(self, speed=None): if speed is None: speed=self._ref_cell_max_speed center=np.mean(self._ref_cell_rng) poss=center+np.linspace(-self._ref_cell_cal_est[0]/2,self._ref_cell_cal_est[0]/2,self._ref_cell_cal_est[1]+1) freqs=[] for p in poss: self._move_cont("ref_cell",p,speed) time.sleep(self._ref_cell_cal_est[2]) freqs.append(self.get_frequency()) df=np.median(np.diff(freqs)) return (df*self._ref_cell_cal_est[1])/self._ref_cell_cal_est[0],freqs
[docs] def unlock_all(self): """Unlock all relevant locks (slow piezo, fast piezo, piezo etalon, thin etalon)""" self._unlock_ref_cell() self.laser.set_slowpiezo_position(np.mean(self._slow_piezo_rng)) self.laser.set_piezoet_ctl_status("stop") self.laser.set_piezoet_position(0) self.laser.set_thinet_ctl_status("stop") self.laser.thinet_clear_errors() self.laser.bifi_clear_errors()
[docs] def set_fine_lock(self, device="slow_piezo"): """Set fine lock (slow and fast piezo) parameters for the given device (``"low_piezo"`` or ``"ref_cell"``)""" funcargparse.check_parameter_range(device,"device",["slow_piezo","ref_cell"]) self._setup_fine_tune_lock(device)
def _get_fine_tune_params(self, device): max_speed=self._slow_piezo_max_speed if device=="slow_piezo" else self._ref_cell_max_speed tune_rng=self._slow_piezo_rng if device=="slow_piezo" else self._ref_cell_rng cal=self._slow_piezo_cal if device=="slow_piezo" else self._ref_cell_cal return max_speed,tune_rng,cal _fine_tune_step_par=(0.01,2,0.001,0.1) # slow piezo "zoom-in" parameters ``(init, factor, final, delay)`` # start with ``init`` position step, and every time the desired frequency is reached, reduce it by ``factor`` and flip the direction, # until ``final`` (or smaller) position step is reached; when the step is within a factor of 10 from ``final``, start applying ``delay`` after every step _fine_tune_slope_par=(10,0.05E9,0.2) _fine_tune_dir=1 # direction of slow piezo tuning corresponding to the increasing frequencies def _fine_tune_step(self, target, device="slow_piezo"): max_speed,tune_rng,_=self._get_fine_tune_params(device) pos=self._get_fine_position(device) df=self.get_frequency()-target step=self._fine_tune_step_par[0]*(1 if df<0 else -1)*self._fine_tune_dir while abs(step)>self._fine_tune_step_par[2]: if pos+step<tune_rng[0]+0.05 or pos+step>tune_rng[1]-0.05: break for _ in self._move_cont_gen(device,pos+step,max_speed): yield pos+=step ndf=self.get_frequency()-target if abs(ndf)>abs(df): step=-step/self._fine_tune_step_par[1] df=ndf if abs(step)<self._fine_tune_step_par[2]*10: time.sleep(self._fine_tune_step_par[3]) yield def _fine_tune_slope(self, target, device="slow_piezo", tolerance=None): max_speed,tune_rng,cal=self._get_fine_tune_params(device) if cal is None: raise ValueError("{} calibration is not specified".format(device)) bound_hit=[False,False] dfs=[] tolerance=self._fine_tune_slope_par[1] if tolerance is None else tolerance for i in range(self._fine_tune_slope_par[0]): pos=self._get_fine_position(device) if pos<tune_rng[0]+0.07: bound_hit[0]=True if pos>tune_rng[1]-0.07: bound_hit[1]=True if all(bound_hit): return df=self.get_frequency()-target dfs.append(abs(df)) if len(dfs)>=4 and np.min(dfs[-2:])>np.min(dfs[-4:-2])*0.8: return if abs(df)<tolerance and i>2: return step=-df/cal newpos=max(tune_rng[0]+0.05,min(pos+step,tune_rng[1]-0.05)) for _ in self._move_cont_gen(device,newpos,max_speed): yield time.sleep(self._fine_tune_slope_par[2]) yield
[docs] def fine_tune_to_gen(self, target, device="slow_piezo", method="auto", tolerance=None): """ Same as :meth:`fine_tune_to`, but made as a generator which yields occasionally. Can be used to run this scan in parallel with some other task, or to be able to interrupt it in the middle. """ funcargparse.check_parameter_range(method,"method",["auto","step","cal"]) funcargparse.check_parameter_range(device,"device",["slow_piezo","ref_cell"]) _,_,cal=self._get_fine_tune_params(device) if method=="auto": method="cal" if cal is not None else "step" tune_gen=self._fine_tune_step(target,device=device) if method=="step" else self._fine_tune_slope(target,device=device,tolerance=tolerance) for _ in tune_gen: yield
[docs] def fine_tune_to(self, target, device="slow_piezo", method="auto", tolerance=None): """ Fine tune the laser to the given target frequency using only fine tuning. `device` specifies the device used for fine tuning: either ``"slow_piezo"``, or ``"ref_cell"``. `method` can be ``"step"`` for step-based binary search method, or ``"cal"`` for slope-based method using the fine tuning calibration (frequency detuning per element position shift). (generally faster, but requires a known calibration). If ``method=="auto"``, use ``"cal"`` when the calibration is available and ``"step"`` otherwise. `tolerance` gives the final frequency tolerance for the ``"cal"`` tuning method; if ``None``, use the standard value (50MHz by default). """ for _ in self.fine_tune_to_gen(target,device=device,method=method,tolerance=tolerance): time.sleep(1E-3)
_tune_local_thinet_range=100E9 _tune_local_fine_range=25E9 _tune_local_final_tolerance=1E9
[docs] def tune_to_gen(self, target, level="full", fine_device="slow_piezo", tolerance=None, local_level="none"): """ Same as :meth:`tune_to`, but made as a generator which yields occasionally. Can be used to run this scan in parallel with some other task, or to be able to interrupt it in the middle. """ funcargparse.check_parameter_range(level,"level",["bifi","thinet","slow_piezo","ref_cell","full"]) funcargparse.check_parameter_range(fine_device,"fine_device",["slow_piezo","ref_cell"]) funcargparse.check_parameter_range(local_level,"local_level",["none","thinet","fine"]) if level=="full": level=fine_device while True: self.unlock_all() te_prescan=None if local_level=="none" or abs(self.get_frequency()-target)>self._tune_local_thinet_range: local_level="none" self._move_motor("thinet",np.mean(self._te_search_rng)) yield self._align_bifi(target) yield te_prescan=self._refine_bifi(target) yield if level=="bifi": self._move_motor("thinet",np.mean(self._te_search_rng)) return if local_level in ["none","thinet"] or abs(self.get_frequency()-target)>self._tune_local_fine_range: local_level="thinet" self._align_te(target,prescan=te_prescan) yield self.laser.set_thinet_ctl_status("run") self.laser.set_piezoet_ctl_status("run") oversamp=self.laser.get_piezoet_drive_params().oversamp self.laser.set_piezoet_drive_params(oversamp=oversamp+1 if oversamp<32 else oversamp-1) self.laser.set_piezoet_drive_params(oversamp=oversamp) # cycling oversampling to prevent a certain rare bug where the piezo etalon lock turns off if level=="thinet": return if level=="ref_cell": self._lock_ref_cell() for _ in self.fine_tune_to_gen(target,device=level,tolerance=tolerance): yield if abs(self.get_frequency()-target)<max(self._tune_local_final_tolerance,tolerance*2 if tolerance is not None else 0): return local_level={"none":"none","thinet":"none","fine":"thinet"}[local_level]
[docs] def tune_to(self, target, level="full", fine_device="slow_piezo", tolerance=None, local_level="none"): """ Tune the laser to the given frequency (in Hz) using multiple elements (bifi, thin etalon, piezo etalon, slow piezo / ref cell). `level` can be ``"bifi"`` (only tune the bifi motor), ``"thinet"`` (tune bifi motor and thin etalon), or ``"full"`` (full tuning using all elements). `fine_device` specifies the device used for fine tuning: either ``"slow_piezo"``, or ``"ref_cell"``. `tolerance` gives the final fine tuning frequency tolerance; if ``None``, use the standard value (50MHz by default). `local_level` defines the level on which to start adjustment; can be ``"fine"`` (start with the slow piezo or the ref cell, if the laser is within their tuning range), ``"thinet"`` (start with the thin etalon), or ``"none"`` (start with the bifi; default). If using just the finer control does not work, progressively move to the coarser ones. """ for _ in self.tune_to_gen(target,level=level,fine_device=fine_device,tolerance=tolerance,local_level=local_level): time.sleep(1E-3)
[docs] def fine_sweep_start(self, span, up_speed, down_speed=None, device="slow_piezo", kind="cont_up", current_pos=0.5): """ Start a fine sweep using the slow piezo or the ref cell. `span` is a sweep span, `up_speed` and `down_speed` are the corresponding speeds (if `down_speed` is ``None``, use the same as `up_speed`), `device` is the scan device (``"slow_piezo"`` or ``"ref_cell"``), `kind` is the sweep kind (``"cont_up"``, ``"cont_down"``, ``"single_up"``, or ``"single_down"``), and `current_pos` is the relative position of the current position withing the sweep range (0 means that it's the lowest position of the sweep, 1 means it's the highest, 0.5 means that it's in the center). """ funcargparse.check_parameter_range(kind,"kind",["cont_up","cont_down","single_up","single_down"]) funcargparse.check_parameter_range(device,"device",["slow_piezo","ref_cell"]) span=self._to_int_units(span,device) max_speed,tune_rng,_=self._get_fine_tune_params(device) up_speed=min(self._to_int_units(abs(up_speed),device),max_speed) down_speed=up_speed if down_speed is None else min(self._to_int_units(abs(down_speed),device),max_speed) self._setup_fine_tune_lock(device) p=self._get_fine_position(device) self._fine_scan_start=p scan_rng=max(tune_rng[0],p-span*current_pos),min(tune_rng[1],p-span*current_pos+span) if kind in ["cont_up","single_up"]: start=scan_rng[0] mode=(False,False,kind=="single_up") else: start=scan_rng[1] mode=(True,kind=="single_down",False) self._move_cont(device,start,max_speed) self.laser.set_scan_params(device=device,mode=mode,lower_limit=scan_rng[0],upper_limit=scan_rng[1],rise_speed=up_speed,fall_speed=down_speed) self.laser.set_scan_status("run") return scan_rng
[docs] def fine_sweep_stop(self, return_to_start=True, start_point=None): """ Stop currently running fast sweep. If ``return_to_start==True``, return to the original start tuning position after the sweeps is stopped; otherwise, stay at the current position. """ self.laser.set_scan_status("stop") if start_point is None: start_point=self._fine_scan_start if return_to_start and start_point is not None: device=self.laser.get_scan_params()[0] self._move_cont(device,start_point,self._get_fine_tune_params(device)[0]) self._fine_scan_start=None
[docs] def scan_coarse_gen(self, bifi_rng, te_rng): """ Perform a 2D grid scan changing positions of both birefringent filter and thin etalon motors. `bifi_rng` and `te_rng` are both 3-tuples ``(start, stop, step)`` specifying the scan ranges. Yields a tuple ``((bifi_idx, bifi_npos), (te_idx, te_npos))``, where ``bifi_idx`` and ``te_idx`` are the indices of the current birefringent filter and thin etalon motor positions, and ``bifi_npos`` and ``te_npos`` are the corresponding total numbers of positions. """ self.unlock_all() if (bifi_rng[1]-bifi_rng[0])*bifi_rng[2]<0: bifi_rng=bifi_rng[0],bifi_rng[1],-bifi_rng[2] if (te_rng[1]-te_rng[0])*te_rng[2]<0: te_rng=te_rng[0],te_rng[1],-te_rng[2] bifi_position=np.arange(*bifi_rng) te_position=np.arange(*te_rng) for i,bfp in enumerate(bifi_position): self._move_motor("bifi",bfp) for j,tep in enumerate(te_position): self._move_motor("thinet",tep) yield (i,len(bifi_position)),(j,len(te_position))
_default_stitch_tune_precision=5E9 _default_stitch_tune_maxreps=2
[docs] def stitched_scan_gen(self, full_rng, single_span, speed, device="slow_piezo", overlap=0.1, freq_step=None): """ Same as :meth:`stitched_scan`, but made as a generator which yields occasionally. Can be used to run this scan in parallel with some other task, or to be able to interrupt it in the middle. Yields ``True`` whenever the main scanning region is passing, and ``False`` during the stitching intervals. """ f=full_rng[0] scandir=1 if full_rng[1]>full_rng[0] else -1 single_span=abs(single_span) freq_step=abs(freq_step) if freq_step is not None else None stitch_tune_precision=single_span*.2 if self._tune_units=="freq" else self._default_stitch_tune_precision fail_step=freq_step*0.5 if freq_step is not None else (single_span*0.5 if self._tune_units=="freq" else self._default_stitch_tune_precision*2) expected_span=single_span if self._tune_units=="freq" else None single_span=self._to_int_units(single_span,device) speed=self._to_int_units(speed,device) local_level="none" rep=0 while (full_rng[1]-f)*scandir>0: for _ in self.tune_to_gen(f,fine_device=device,local_level=local_level): yield False f0=self.get_frequency() scan_freqs=[] if abs(f0-f)<stitch_tune_precision: p=self._get_fine_position(device) yield False for _ in self._move_cont_gen(device,p+single_span*scandir,speed): scan_freqs.append(self.get_frequency()) yield True yield False f1=self.get_frequency() else: f1=None span_success=f1 is not None and (f1-f0)*scandir>0 and (expected_span is None or ((f1-f0)*scandir>expected_span/4 and (f1-f0)*scandir<expected_span*2)) local_level="fine" if span_success and len(scan_freqs)>5: df=np.diff(scan_freqs[:-1]) med_step=np.abs(np.median(df)) valid_step=np.abs(df)<max(np.abs(med_step)*3,1E9) if not valid_step.all(): f1=scan_freqs[valid_step.argmin()] if abs(f1-f0)<fail_step/4: span_success=False if valid_step.argmin()<len(valid_step)/2: local_level="none" if not span_success: local_level="none" if rep+1<self._default_stitch_tune_maxreps: rep+=1 continue if freq_step is None: if span_success: f=f1-(f1-f0)*overlap else: f=f+fail_step*scandir else: f+=(freq_step if span_success else fail_step)*scandir rep=0
[docs] def stitched_scan(self, full_rng, single_span, speed, device="slow_piezo", overlap=0.1, freq_step=None): """ Perform a stitched laser scan. Args: full_rng: 2-tuple ``(start, stop)`` with the full frequency scan range. single_span: magnitude of a single continuous scan segment given in the slow piezo scan units (between 0 and 1) speed: single segment scan speed device: the scan device (``"slow_piezo"`` or ``"ref_cell"``) overlap: overlap of consecutive segments, as a fraction of `single_span` freq_step: if ``None``, the start of the next segment is calculated based on the end of the previous segment and `overlap`; otherwise, it specifies a fixed frequency step between segments. """ for _ in self.stitched_scan_gen(full_rng,single_span,speed,device=device,overlap=overlap,freq_step=freq_step): time.sleep(1E-3)