# version: 1.0.0 2024/11/28 #Downloaded from https://www.novoptel.de/Home/Downloads_de.php
# version: 1.1.0 2025/02/07 modified by Maxim.Weizel for LU1000 CBand+OBand (partially tested)
from NovoptelUSB import NovoptelUSB
from NovoptelTCP import NovoptelTCP
from time import time, sleep
print(
'''
#####################################################################################
To use the LU1000 Laser you need to install the FTDI D2XX Driver e.g.
from https://www.novoptel.de/Home/Downloads_de.php - USB Driver for USB2.0
or https://ftdichip.com/drivers/d2xx-drivers/ - 2.12.36.4 (accessed on 08.02.2025)
Python Library needed: pip install ftd2xx
#####################################################################################
'''
)
##################################
# LU1000 Laser Base Class #
##################################
[docs]
class LU1000_Base:
def __init__(self, target='USB', port=5025):
if target == 'USB':
self.n = NovoptelUSB('LU1000')
if self.n.DEVNO < 0:
raise ConnectionError("Could not open USB connection")
else:
self.n = NovoptelTCP(target, port=port)
self._available_lasers = [1, 2]
[docs]
def Close(self):
self.n.close()
self.n = None
def _read(self, addr: int) -> int:
return self.n.read(addr)
def _write(self, addr: int, data: int) -> None:
self.n.write(addr, data)
def _validate_laser(self, laser: int) -> None:
if laser not in self._available_lasers:
raise ValueError(f"Laser {laser} is not in available lasers {self._available_lasers}")
def _calc_address(self, laser: int, offset: int) -> int:
if laser not in self._available_lasers:
raise ValueError("Invalid laser number. Must be one of: " + str(self._available_lasers))
return int(128 * laser + offset)
# =============================================================================
# Base Class - General instrument data
# =============================================================================
[docs]
def get_controller_temp(self) -> float:
'''Controller module temperature in Celsius
Parameters
----------
Raises
------
ValueError
Error message
Returns
-------
res : float
Controller module temperature in Celsius
'''
CONTROLLER_TEMP_ADDR = 51
raw = self._read(CONTROLLER_TEMP_ADDR) # Celsius * 16
return float(raw / 16.0)
[docs]
def get_firmware(self): # as string
return hex(self.n.read(64)) #4 Digit BCD
[docs]
def get_serial_number(self): # as integer
return self.n.read(65)
[docs]
def get_module_type(self) -> str:
module_type = []
for ii in range(16):
dummy = self._read(68 + ii)
module_type.append(chr(dummy >> 8))
module_type.append(chr(dummy & 0xFF))
return "".join(module_type).strip()
# =============================================================================
# Base Class - GET functions
# =============================================================================
[docs]
def get_laser_output(self, laser: int) -> int:
'''Returns the Laser output state. Enabled = 1 , Disabled = 0
Parameters
----------
laser : int
Laser output selected - 1 or 2
Raises
------
ValueError
Error message
Returns
-------
int
Laser enabled = 1 or laser disabled = 0
'''
addr = self._calc_address(laser, 50)
res = self._read(addr)
return 1 if int(res) == 8 else 0
# =============================================================================
# Base Class - SET functions
# =============================================================================
[docs]
def set_laser_output(self, laser: int, value: str|int) -> None:
'''Turn Laser N output ON/OFF
Parameters
----------
laser : int
Laser output selected - 1 or 2
value : int/str
value = 'ON'|'OFF'|1|0
Raises
------
ValueError
Error message
Returns
-------
None.
'''
state_mapping = { 'on': 8, 'off': 0, 1: 8, 0: 0}
state_normalized = state_mapping.get(value.lower() if isinstance(value, str) else int(value))
if state_normalized is None:
raise ValueError("Invalid state. Expected 'ON', 'OFF', 1, or 0.")
addr = self._calc_address(laser, 50)
self._write(addr, state_normalized)
####################################################################################
####################################################################################
##################################
# C-Band Tuable Laser Class #
##################################
[docs]
class LU1000_Cband(LU1000_Base):
def __init__(self, target='USB'):
super().__init__(target)
# implement LU1000_Cband specific initializations here
self._max_freq = {
1: self.get_max_freq(1),
2: self.get_max_freq(2)
}
self._min_freq = {
1: self.get_min_freq(1),
2: self.get_min_freq(2)
}
self._grid_spacing = {
1: self.get_grid_spacing(1),
2: self.get_grid_spacing(2)
}
self._max_channel_number = {
1: self._update_max_channel_number(1),
2: self._update_max_channel_number(2)
}
# =============================================================================
# C-Band Laser - General instrument data
# =============================================================================
# Inherit from parent class
# =============================================================================
# C-Band Laser - GET functions
# =============================================================================
def _update_max_channel_number(self, laser: int) -> int:
'''_internal function: Update max channel number
Parameters
----------
laser : int
Laser output selected - 1 or 2
Raises
------
ValueError
Error message
Returns
-------
int
max channel number
'''
self._validate_laser(laser)
max_f = self._max_freq[laser]
min_f = self._min_freq[laser]
sleep(0.1)
grid_spacing = self.get_grid_spacing(laser)
return int( (max_f-min_f)/(grid_spacing/1e4) + 1)
[docs]
def get_channel(self, laser: int) -> int:
'''Returns the Laser module's current channel.
Parameters
----------
laser : int
Laser output selected - 1 or 2
Raises
------
ValueError
Error message
Returns
-------
res : int
Laser module's current channel number
'''
addr = self._calc_address(laser, 48)
res = self._read(addr)
return int(res)
[docs]
def get_target_power(self, laser: int) -> float:
'''Returns the laser module's current Optical Power in dBm
Parameters
----------
laser : int
Laser output selected - 1 or 2
Raises
------
ValueError
Error message
Returns
-------
float
Returns the laser module's current optical power in dBm
'''
addr = self._calc_address(laser, 49)
res = self._read(addr)
return float(res/100)
[docs]
def get_grid_spacing(self, laser: int) -> int:
'''Grid spacing in GHz*10
Parameters
----------
laser : int
Laser output selected - 1 or 2
Raises
------
ValueError
Error message
Returns
-------
int
Grid spacing in GHz*10
'''
addr = self._calc_address(laser, 52)
res = self._read(addr)
return int(res)
def _get_first_chann_freq_THz(self, laser: int) -> float:
'''_internal function: First channel's frequency, THz
Bit unclear what this is. Is this the minimum Frequency?
Parameters
----------
laser : int
Laser output selected - 1 or 2
Raises
------
ValueError
Error message
Returns
-------
res : float
First channel's frequency, THz
'''
addr = self._calc_address(laser, 53)
res = self._read(addr)
return float(res)
def _get_first_chann_freq_GHz(self, laser: int) -> float:
'''_internal function: First channel's frequency, GHz*10
Bit unclear what this is. Is this the minimum Frequency?
Parameters
----------
laser : int
Laser output selected - 1 or 2
Raises
------
ValueError
Error message
Returns
-------
res : float
First channel's frequency, GHz*10
'''
addr = self._calc_address(laser, 54)
res = self._read(addr)
return float(res)
def _get_channel_freq_THz(self, laser: int) -> float:
'''Retrun channel Frequency in THz
Parameters
----------
laser : int
Laser output selected - 1 or 2
Raises
------
ValueError
Error message
Returns
-------
float
Retrun channel Frequency in THz
'''
addr = self._calc_address(laser, 64)
res = self._read(addr)
return float(res)
def _get_channel_freq_GHz(self, laser: int) -> float:
'''Returns channel's frequency as GHZ*10
Parameters
----------
laser : int
Laser output selected - 1 or 2
Raises
------
ValueError
Error message
Returns
-------
float
Returns channel's frequency as GHZ*10
'''
addr = self._calc_address(laser, 65)
res = self._read(addr)
return float(res)
[docs]
def get_measured_power(self, laser: int) -> float:
'''Returns the current optical power encoded as dBm
Parameters
----------
laser : int
Laser output selected - 1 or 2
Raises
------
ValueError
Error message
Returns
-------
res : float
Returns the current optical power encoded as dBm
'''
addr = self._calc_address(laser, 66)
res = self._read(addr) # dBm * 100
return float(res/100.0)
[docs]
def get_temperature(self, laser: int) -> float:
'''Returns the current temperature encoded as °C.
Parameters
----------
laser : int
Laser out selected - 1 or 2
Raises
------
ValueError
Error message
Returns
-------
res : float
Returns the current temperature encoded as °C.
'''
addr = self._calc_address(laser, 67)
res = self._read(addr) # Celsius * 100
return float(res/100)
[docs]
def get_min_optical_power(self, laser: int) -> float:
'''Get minimum possible optical power setting
Parameters
----------
laser : int
Laser out selected - 1 or 2
Raises
------
ValueError
Error message
Returns
-------
res : float
Get minimum possible optical power setting
'''
addr = self._calc_address(laser, 80)
res = self._read(addr)
return float(res)
[docs]
def get_max_optical_power(self, laser: int) -> float:
'''Get maximum possible optical power setting
Parameters
----------
laser : int
Laser out selected - 1 or 2
Raises
------
ValueError
Error message
Returns
-------
res : float
Get maximum possible optical power setting
'''
addr = self._calc_address(laser, 81)
res = self._read(addr)
return float(res)
def _get_min_freq_THz(self, laser: int) -> float:
'''_internal function: Laser's minimum (first) Frequency, THz
Parameters
----------
laser : int
Laser output selected - 1 or 2
Raises
------
ValueError
Error message
Returns
-------
res : float
Laser's minimum frequency, THz
'''
addr = self._calc_address(laser, 82)
res = self._read(addr)
return float(res)
def _get_min_freq_GHz(self, laser: int) -> float:
'''_internal function: Laser's minimum (first) Frequency, GHz*10
Parameters
----------
laser : int
Laser output selected - 1 or 2
Raises
------
ValueError
Error message
Returns
-------
res : float
Laser's minimum frequency, GHz*10
'''
addr = self._calc_address(laser, 83)
res = self._read(addr)
return float(res)
def _get_max_freq_THz(self, laser: int) -> float:
'''_internal function: Laser's maximum Frequency, THz
Parameters
----------
laser : int
Laser output selected - 1 or 2
Raises
------
ValueError
Error message
Returns
-------
res : float
Laser's maximum Frequency, THz
'''
addr = self._calc_address(laser, 84)
res = self._read(addr)
return float(res)
def _get_max_freq_GHz(self, laser: int) -> float:
'''_internal function: Laser's maximum Frequency, GHz*10
Parameters
----------
laser : int
Laser output selected - 1 or 2
Raises
------
ValueError
Error message
Returns
-------
res : float
Laser's maximum Frequency, GHz*10
'''
addr = self._calc_address(laser, 85)
res = self._read(addr)
return float(res)
[docs]
def get_min_grid_freq(self, laser: int) -> float:
'''Laser's minimum supported grid spacing, GHz*10
Parameters
----------
laser : int
Laser output selected - 1 or 2
Raises
------
ValueError
Error message
Returns
-------
float
Laser's minimum supported grid spacing, GHz*10
'''
addr = self._calc_address(laser, 86)
res = self._read(addr)
return float(res)
[docs]
def get_whispermode(self, laser: int) -> int:
'''Whispermode Status, ON = 2, OFF = 0
Parameters
----------
laser : int
Laser output selected - 1 or 2
Raises
------
ValueError
Error message
Returns
-------
res : int
Whispermode Status. ON = 2, OFF = 0
'''
addr = self._calc_address(laser, 108)
res = self._read(addr)
return int(res)
# =============================================================================
# GET Function Wrappers implemented by SCT-Group
# =============================================================================
[docs]
def get_min_freq(self, laser: int) -> float:
'''Laser's minimum possible frequency
Parameters
----------
laser : int
Laser out selected - 1 or 2
Raises
------
ValueError
Error message
Returns
-------
float
min possible frequency.
'''
self._validate_laser(laser)
THz = self._get_min_freq_THz(laser)
GHz = self._get_min_freq_GHz(laser)
Freq = THz + GHz*1e-4
return Freq
[docs]
def get_max_freq(self, laser: int) -> float:
'''Lasers's maximum possible Frequency.
Parameters
----------
laser : int
Laser output selected - 1 or 2
Raises
------
ValueError
Error message
Returns
-------
float
max possible frequency
'''
self._validate_laser(laser)
THz = self._get_max_freq_THz(laser)
GHz = self._get_max_freq_GHz(laser)
Freq = THz + GHz*1e-4
return float(Freq)
[docs]
def get_frequency(self, laser: int) -> float:
'''Calculate and return Frequency on the selected channel
Parameters
----------
laser : int
Laser output selected - 1 or 2
Raises
------
ValueError
Error message
Returns
-------
Freq : float
Calculate and return Frequency on the selected channel
'''
self._validate_laser(laser)
THz = float(self._get_channel_freq_THz(laser))
GHz = float(self._get_channel_freq_GHz(laser))
Freq = THz + GHz*1e-4
return Freq
# =============================================================================
# SET
# =============================================================================
[docs]
def set_target_power(self, laser: int, value: int|float, ignore_warning: bool = False) -> None:
'''Sets the laser module's optical power in dBm
Parameters
----------
laser : int
Laser output selected - 1 or 2
value : float
optical power in dBm
ignore_warning : bool
When True, no warning for power > 10dBm is displayed
Raises
------
ValueError
Error message
Returns
-------
None.
'''
self._validate_laser(laser)
if 10 < value <= 16 and not ignore_warning:
raise ValueError("Power value above 10dBm requires explicit confirmation.")
elif 6 <= value <= 16:
addr = self._calc_address(laser, 49)
self._write(addr, int(value * 100))
else:
raise ValueError(
'Unknown input! See function description for more info.')
[docs]
def set_channel(self, laser: int, value: int) -> None:
'''Sets the laser module's current channel
Parameters
----------
laser : int
Laser output selected - 1 or 2
value : int
Sets the laser module's current channel
value = select channel value
Raises
------
ValueError
Error message
Returns
-------
None.
'''
addr = self._calc_address(laser, 48)
if 1 <= value <= self._max_channel_number[laser]:
self._write(addr, int(value))
else:
raise ValueError(
f'Channel number: {value} out of range!')
[docs]
def set_grid_spacing(self, laser: int, value: int) -> None:
'''Set Grid spacing in GHz*10.
Parameters
----------
laser : int
Laser output selected - 1 or 2
value : int
Set Grid spacing. Smallest possible value = 1
Raises
------
ValueError
Error message
Returns
-------
None.
'''
addr = self._calc_address(laser, 52)
if value >= 1:
for ii in range(5): # try 5 times
self._write(addr, int(value))
self._max_channel_number[laser] = self._update_max_channel_number(laser)
sleep(0.1)
if self.get_grid_spacing(laser) == value:
break
else:
raise ValueError('Failed to set grid spacing.')
else:
raise ValueError(
'Unknown input! See function description for more info.')
def _set_first_chann_freq_THz(self, laser: int, value: int|float) -> None:
'''_internal function: Channel's frequency, THz
Parameters
----------
laser : int
Laser output selected - 1 or 2
value : int | float
Channel's frequency, THz
Raises
------
ValueError
Error message
Returns
-------
None.
'''
addr = self._calc_address(laser, 53)
self._write(addr, int(value))
def _set_first_chann_freq_GHz(self, laser: int, value: int|float) -> None:
'''_internal function:Channel's frequency, GHz*10
Parameters
----------
laser : int
Laser output selected - 1 or 2
value : int | float
Channel's frequency, GHz*10
Raises
------
ValueError
Error message
Returns
-------
None.
'''
addr = self._calc_address(laser, 54)
self._write(addr, int(value))
[docs]
def set_fine_tune(self, laser: int, value: int) -> None:
'''Fine-tuning set the frequency in MHz steps
Parameters
----------
laser : int
Laser output selected - 1 or 2
value : int
Fine-tuning set the frequency in MHz steps
Returns
-------
None.
'''
addr = self._calc_address(laser, 98)
self._write(addr, int(value))
[docs]
def set_whispermode(self, laser: int, state: str|int, timeout: int = 30) -> None:
'''Activates/Deactivates Whispermode
Parameters
----------
laser : int
Laser output selected - 1 or 2
state : str | int
['ON','OFF', 1, 0]
timeout : int
Timeout in seconds
Returns
-------
None.
'''
state_mapping = { 'on': 2, 'off': 0, 1: 2, 0: 0}
state_write = state_mapping.get(state.lower() if isinstance(state, str) else int(state))
if state_write is not None:
addr = self._calc_address(laser, 108)
self._write(addr, state_write)
timeout = int(timeout) if isinstance(timeout, (int, float)) else 30 # Timeout in seconds
start_time = time()
while time() - start_time < timeout:
temp_read = self.get_whispermode(laser)
if temp_read == state_write:
break
self._write(addr, state_write)
sleep(0.5)
else:
raise TimeoutError(f"Failed to set Whispermode for laser {laser} within {timeout} seconds.")
else:
raise ValueError('Unknown input! See function description for more info.')
# =============================================================================
# SET Wrapper Functions implemented by SCT-Group
# =============================================================================
[docs]
def set_frequency(self, laser: int, value: float) -> None:
'''Set Laser Frequency value in value
Parameters
----------
laser : int
Laser output selected - 1 or 2
value : float
Set Laser Frequency in THz
e.g value = 192.876
Returns
-------
None.
'''
self._validate_laser(laser)
GHz = int((value % 1)*1e4)
THz = int(value // 1)
self._set_first_chann_freq_THz(laser, THz)
sleep(0.1)
self._set_first_chann_freq_GHz(laser, GHz)
# =============================================================================
# Get/Save Data
# =============================================================================
[docs]
def get_data(self, laser: int) -> dict:
'''Return a dictionary with the measured power and set frequency.
Parameters
----------
laser : int
Laser output selected - 1 or 2
Raises
------
ValueError
DESCRIPTION.
Returns
-------
OutPut : dict
Return a dictionary with the measured power and set frequency.
'''
OutPut = {}
self._validate_laser(laser)
Power = self.get_measured_power(laser)
Freq = self.get_frequency(laser)
OutPut['Power/dBm'] = Power
OutPut['Set Frequency/THz'] = Freq
return OutPut
##################################
# O-Band DFB Laser Class #
##################################
[docs]
class LU1000_Oband(LU1000_Base):
def __init__(self, target='192.168.1.100'):
super().__init__(target)
# Implement O-Band Laser specific initializations here
# =============================================================================
# Basic communication
# =============================================================================
# Inherit from LU1000_Base
# =============================================================================
# General instrument data
# =============================================================================
# Inherit from LU1000_Base
# =============================================================================
# Get
# =============================================================================
[docs]
def get_temperature(self, laser: int) -> float:
'''Returns the laser module temperature in °C.
Parameters
----------
laser : int
Laser output selected - 1 or 2
Raises
------
ValueError
Error message
Returns
-------
Temperature in °C
'''
addr = self._calc_address(laser, 23)
return float(self._read(addr) / 1000.0)
[docs]
def get_target_current(self, laser: int) -> float:
'''Retruns the laser module's current in mA
Parameters
----------
laser : int
Laser output selected - 1 or 2
Raises
------
ValueError
Error message
Returns
-------
Current in mA.
'''
addr = self._calc_address(laser, 24)
return float(self._read(addr) / 100.0)
[docs]
def get_measured_power_dBm(self, laser: int) -> float:
'''Retruns the laser module's measured power in dBm
Parameters
----------
laser : int
Laser output selected - 1 or 2
Raises
------
ValueError
Error message
Returns
-------
Measured power in dBm or 'nan' if laser is off
'''
addr = self._calc_address(laser, 31) # Adjust the offset if needed
raw_value = self._read(addr)
# Check for the "laser off" magic value.
if raw_value == 45535:
return float('nan') #laser is off -inf dBm
# Convert raw_value to a signed 16-bit integer (two's complement)
if raw_value >= 32768:
raw_value -= 65536
# Scale the value to dBm.
return raw_value / 1000.0
[docs]
def get_measured_power_mW(self, laser: int) -> float:
'''Retruns the laser module's measured power in mW
Parameters
----------
laser : int
Laser output selected - 1 or 2
Raises
------
ValueError
Error message
Returns
-------
Measured power in mW or 10 if laser is off
'''
addr = self._calc_address(laser, 29) # Adjust the offset if needed
raw_value = self._read(addr)
# Scale the value to mW.
return raw_value / 1000.0
[docs]
def get_measured_current_1(self, laser: int) -> float:
'''Experimental!
Retruns the laser module's measured current in mA
Parameters
----------
laser : int
Laser output selected - 1 or 2
Raises
------
ValueError
Error message
Returns
-------
Measured current in mA
'''
addr = self._calc_address(laser, 28)
return self._read(addr) *6.35647/1000
[docs]
def get_measured_current_2(self, laser: int) -> float:
'''Experimental!
Retruns the laser module's measured current in mA
Parameters
----------
laser : int
Laser output selected - 1 or 2
Raises
------
ValueError
Error message
Returns
-------
Measured current in mA
'''
addr = self._calc_address(laser, 26)
return (self._read(addr) + 4957)/589.9
# =============================================================================
# Set
# =============================================================================
[docs]
def set_temperature(self, laser: int, temperature: float) -> None:
'''Sets the laser module's temperature in °C.
Parameters
----------
laser : int
Laser output selected - 1 or 2
temperature : float
9°C <= temperature <= 45°C
Raises
------
ValueError
Error message
Returns
-------
None.
'''
addr = self._calc_address(laser, 23)
if 9.0 <= temperature <= 45.0: #temperature in °C
self._write(addr, int(temperature * 1000))
else:
raise ValueError(
'Unknown input! See function description for more info.')
[docs]
def set_target_current(self, laser: int, current: float, ignore_warning: bool = False) -> None:
'''Sets the laser module's current in mA
Parameters
----------
laser : int
Laser output selected - 1 or 2
value : float
0mA <= value <= 100mA
Raises
------
ValueError
Error message
Returns
-------
None.
'''
addr = self._calc_address(laser, 24)
if 45 <= current <= 100.0 and not ignore_warning:
raise ValueError("Power value above 10dBm requires explicit confirmation.")
elif 0.0 <= current < 100.0:
addr = self._calc_address(laser, 24)
self._write(addr, int(current * 100))
else:
raise ValueError(
'Unknown input! See function description for more info.')