From 817ca193ada70c2abbf184b8c67b8ae5f078ca4e Mon Sep 17 00:00:00 2001 From: TheoQM Date: Wed, 23 Oct 2024 10:57:38 +0200 Subject: [PATCH 01/10] video mode -> live mode --- qualang_tools/video_mode/README.md | 86 +++++++++---------- .../video_mode/{videomode.py => livemode.py} | 24 +++--- 2 files changed, 55 insertions(+), 55 deletions(-) rename qualang_tools/video_mode/{videomode.py => livemode.py} (96%) diff --git a/qualang_tools/video_mode/README.md b/qualang_tools/video_mode/README.md index f33e40bf..081dc7b5 100644 --- a/qualang_tools/video_mode/README.md +++ b/qualang_tools/video_mode/README.md @@ -1,12 +1,12 @@ -# Video Mode +# Live Mode ## Introduction In some cases, one might want to update parameters of the program while it is running and without knowing in advance when and how to update them. -We present here the ```VideoMode``` which enables such dynamic parameter tuning. +We present here the ```LiveMode``` which enables such dynamic parameter tuning. ## How to use it -The user can declare a ``VideoMode`` instance by passing in a ```QuantumMachine``` instance and a dictionary of +The user can declare a ``LiveMode`` instance by passing in a ```QuantumMachine``` instance and a dictionary of parameters intended to be updated dynamically. The dictionary should be of the form: @@ -23,11 +23,11 @@ where ```qua_type``` can be ```bool```, ```ìnt``` or ```fixed```. If a list of values is provided, the subsequent QUA variable will be a QUA array of specified type. -The simplest declaration of the VideoMode can be done as follows: +The simplest declaration of the LiveMode can be done as follows: ``` qmm = QuantumMachinesManager() qm = qmm.open_qm(config) -video_mode = VideoMode(qm, parameters_dict) +live_mode = LiveMode(qm, parameters_dict) ``` Once this declaration is done, some interesting thing will happen behind the scenes. @@ -40,20 +40,20 @@ It contains two methods that should be used as QUA macros to enable the dynamic This method should ideally be called at the beginning of the QUA program scope. - ```load_parameters```: handles the dynamic assignment of the parameters declared in the QUA program scope through the IO variables. -For convenience, those macros can be accessed directly through the ```VideoMode``` object. The QUA program should therefore look like this: +For convenience, those macros can be accessed directly through the ```LiveMode``` object. The QUA program should therefore look like this: ``` -with program() as video_mode_prog: +with program() as live_mode_prog: # Declare the QUA variables associated to the parameters - video_mode.declare_variables() + live_mode.declare_variables() with infinite_loop(): # Load the parameters from the Python environment - video_mode.load_parameters() + live_mode.load_parameters() # Use the parameters as QUA variables - play("my_pulse"*amp(video_mode["parameter_name_1"]), "qe1") - frame_rotation(video_mode["parameter_name_2"], "qe1") + play("my_pulse"*amp(live_mode["parameter_name_1"]), "qe1") + frame_rotation(live_mode["parameter_name_2"], "qe1") ``` @@ -65,13 +65,13 @@ The first line within the program is essentially a macro that has declared all t original parameter list by operating an appropriate casting for all parameters that were provided by the user. To be able to access those newly created QUA variables later in the design of the program, we can use -```video_mode["param_name"]``` for each parameter loaded initially in the dictionary. We can then use those variables +```live_mode["param_name"]``` for each parameter loaded initially in the dictionary. We can then use those variables as we please within the program, as shown above with the custom parameters used for the QUA statements. ### Updating variables dynamically -The ```VideoMode```, once launched in Python, will query continuously the user to input (through the keyboard) +The ```LiveMode```, once launched in Python, will query continuously the user to input (through the keyboard) the name of parameter, and the associated new value it should take. The format should be written as follows: ``` @@ -82,7 +82,7 @@ The user can also update a parameter that is a QUA array by providing a list of param_name= new_value_1 new_value_2 new_value_3 ... ``` -Once the user has typed an appropriate update, the ```VideoMode``` will use the IO variables of the ``QuantumMachine`` +Once the user has typed an appropriate update, the ```LiveMode``` will use the IO variables of the ``QuantumMachine`` to pass the following information: - which parameter of the initial dictionary should be updated next (IO1) - what new value should be set for the parameter (IO2) @@ -93,83 +93,83 @@ with a successive back and forth interaction between the program and the Python As everything happens behind the scenes, the user does not need to worry about the details of the implementation. The only thing that the user should implement in the QUA program on top of the declaration of variables shown above is the following macro: ``` -with program() as video_mode_prog: +with program() as live_mode_prog: ... with infinite_loop(): - video_mode.load_parameters() + live_mode.load_parameters() ... ``` -### Accessing QUA variables from ```VideoMode``` within the program -The QUA variables declared in the program through the ```VideoMode.declare_variables()``` can be accessed from the -through the ```video_mode["param_name"]``` syntax. This enables you to choose where your QUA variable should land in +### Accessing QUA variables from ```LiveMode``` within the program +The QUA variables declared in the program through the ```LiveMode.declare_variables()``` can be accessed from the +through the ```live_mode["param_name"]``` syntax. This enables you to choose where your QUA variable should land in your program. For instance, if you want to use the same QUA variable for two different pulse parameters, you can do the following: ``` -with program() as video_mode_prog: - video_mode.declare_variables() +with program() as live_mode_prog: + live_mode.declare_variables() with infinite_loop(): - video_mode.load_parameters() + live_mode.load_parameters() - play("my_pulse"*amp(video_mode["amp_param"]), "qe1") - play("my_other_pulse"*amp(video_mode["amp_param"]), "qe2") + play("my_pulse"*amp(live_mode["amp_param"]), "qe1") + play("my_other_pulse"*amp(live_mode["amp_param"]), "qe2") ``` -Note that you can also access to the full list of QUA variables declared in the program through the ```video_mode.variables``` attribute. +Note that you can also access to the full list of QUA variables declared in the program through the ```live_mode.variables``` attribute. Alternatively, you can also access the QUA variables later in the code as follows (note that the order of the variables is the same as the order of the parameters in the `parameters_dict` as of Python 3.7): ``` -with program() as video_mode_prog: - amp_param1, amp_param2, ... = video_mode.declare_variables() +with program() as live_mode_prog: + amp_param1, amp_param2, ... = live_mode.declare_variables() with infinite_loop(): - video_mode.load_parameters() + live_mode.load_parameters() play("my_pulse"*amp(amp_param1), "qe1") play("my_other_pulse"*amp(amp_param2), "qe2") ``` ## Outside the QUA program -The ```VideoMode``` class has only one ```execute```method to be called outside the QUA program scope, which is a simple wrapper of ```qm.execute()``` method. -It will start the video mode execution by creating a new thread that takes control of the console by doing a continuous query of input parameters from the user, while the QUA program is running in the background and possible live data plotting is done in the main script. +The ```LiveMode``` class has only one ```execute```method to be called outside the QUA program scope, which is a simple wrapper of ```qm.execute()``` method. +It will start the live mode execution by creating a new thread that takes control of the console by doing a continuous query of input parameters from the user, while the QUA program is running in the background and possible live data plotting is done in the main script. This new thread releases access of the console to the user once ```stop``` is typed in the console. ## Example Let us consider the following example where we want to update dynamically the amplitude and phase of a pulse. -We first declare the ```VideoMode``` instance and the parameters dictionary: +We first declare the ```LiveMode``` instance and the parameters dictionary: ``` parameters_dict = { 'amp': 0.5, 'phase': 0.0 } -video_mode = VideoMode(qm, parameters_dict) +live_mode = LiveMode(qm, parameters_dict) ``` We then declare the QUA program as follows: ``` -with program() as video_mode_prog: - video_mode.declare_variables() +with program() as live_mode_prog: + live_mode.declare_variables() with infinite_loop(): - video_mode.load_parameters() + live_mode.load_parameters() - play("my_pulse"*amp(video_mode["amp"]), "qe1") - frame_rotation(video_mode["phase"], "qe1") + play("my_pulse"*amp(live_mode["amp"]), "qe1") + frame_rotation(live_mode["phase"], "qe1") ``` Or an equivalent version: ``` -with program() as video_mode_prog: - amp_, phase = video_mode.declare_variables() +with program() as live_mode_prog: + amp_, phase = live_mode.declare_variables() with infinite_loop(): - video_mode.load_parameters() + live_mode.load_parameters() play("my_pulse"*amp(amp_), "qe1") frame_rotation(phase, "qe1") ``` -Finally, we start the video mode execution: +Finally, we start the live mode execution: ``` -video_mode.execute(video_mode_prog) +live_mode.execute(live_mode_prog) ``` At this point, the python console accepts specific inputs, type `help` to see the full list. For example, to change the variables, type: @@ -180,6 +180,6 @@ phase=0.5 ``` The program will then update the amplitude and phase of the pulse accordingly. -More detailed examples can be found under in [examples](https://github.com/qua-platform/py-qua-tools/tree/main/examples/Qcodes_drivers/video_mode) section. +More detailed examples can be found under in [examples](https://github.com/qua-platform/py-qua-tools/tree/main/examples/Qcodes_drivers/live_mode) section. \ No newline at end of file diff --git a/qualang_tools/video_mode/videomode.py b/qualang_tools/video_mode/livemode.py similarity index 96% rename from qualang_tools/video_mode/videomode.py rename to qualang_tools/video_mode/livemode.py index 562956df..08a861b6 100644 --- a/qualang_tools/video_mode/videomode.py +++ b/qualang_tools/video_mode/livemode.py @@ -1,4 +1,4 @@ -# VideoMode script enabling the update of parameters in a QUA program through user input while the program is running. +# LiveMode script enabling the update of parameters in a QUA program through user input while the program is running. # Authors: Arthur Strauss, Théo Laudat # Date: 19/12/2023 @@ -204,7 +204,7 @@ def __getitem__(self, item): else: raise ValueError( f"No QUA variable found for parameter {item}. Please use " - f"VideoMode.declare_variables() within QUA program first." + f"LiveMode.declare_variables() within QUA program first." ) @property @@ -215,7 +215,7 @@ def variables(self): except KeyError: raise KeyError( "No QUA variables found for parameters. Please use " - "VideoMode.declare_variables() within QUA program first." + "LiveMode.declare_variables() within QUA program first." ) def __repr__(self): @@ -225,7 +225,7 @@ def __repr__(self): return text -class VideoMode: +class LiveMode: """ This class aims to provide an easy way to update parameters in a QUA program through user input while the program is running. It is particularly useful for calibrating parameters dynamically and without deterministic @@ -241,7 +241,7 @@ def __init__(self, qm: QuantumMachine, parameters: Union[Dict, ParameterTable]): This class aims to provide an easy way to update parameters in a QUA program through user input while the program is running. It is particularly useful for calibrating parameters in real time. The user can specify the parameters to be updated and their initial values in the parameter dictionary called ```param_dict```. - The video mode will then automatically create the corresponding QUA variables and update them through user + The live mode will then automatically create the corresponding QUA variables and update them through user input. Parameters dictionary should be of the form: @@ -253,9 +253,9 @@ def __init__(self, qm: QuantumMachine, parameters: Union[Dict, ParameterTable]): - ```declare_variables```: This method will create the QUA variables corresponding to the parameters to be updated. - ```load_parameters```: This method will load the updated values for the parameters through IO variables 1 and 2. - The user can then start the video mode outside the QUA program by calling the ```execute``` method of this class. + The user can then start the live mode outside the QUA program by calling the ```execute``` method of this class. This will start a new parallel thread in charge of updating the - parameters in the parameter table through user input. The user can stop the video mode by entering 'stop' in + parameters in the parameter table through user input. The user can stop the live mode by entering 'stop' in the terminal. The user can also resume a paused program by entering 'done' again in the terminal. Args: @@ -271,7 +271,7 @@ def __init__(self, qm: QuantumMachine, parameters: Union[Dict, ParameterTable]): self.implemented_commands = ( "List of implemented commands: \n " "get: return the current value of the parameters. \n " - "stop: quit VideoMode. \n " + "stop: quit LiveMode. \n " "done: resume program (if pause_program==True). \n " "help: display the list of available commands. \n " "'param_name'='param_value': set the parameter to the specified value (ex: V1=0.152).\n " @@ -283,7 +283,7 @@ def __init__(self, qm: QuantumMachine, parameters: Union[Dict, ParameterTable]): def signal_handler(self, signum, frame): """Signal handler for SIGTERM and SIGINT signals.""" - print(f"Received signal {signum}, stopping VideoMode...") + print(f"Received signal {signum}, stopping LiveMode...") self.qm.set_io1_value(self._default_io1) self.qm.set_io2_value(self._default_io2) self.job.halt() @@ -414,12 +414,12 @@ def update_parameters(self): else: print(f"Invalid input. {param_name} is not a valid parameter name.") - print("VideoMode stopped.") + print("LiveMode stopped.") def execute(self, prog: Optional[Program] = None, *execute_args) -> QmJob: - """Start the video mode. + """Start the live mode. Args: - prog: QUA program to be executed. If None, the video mode will be started without executing a QUA program + prog: QUA program to be executed. If None, the live mode will be started without executing a QUA program (program assumed to be running) execute_args: Arguments to be passed to the execute method of the QuantumMachine for executing the program. Returns: From dd9db751c544466d32dff09a883d431e04dca2da Mon Sep 17 00:00:00 2001 From: TheoQM Date: Wed, 23 Oct 2024 10:59:58 +0200 Subject: [PATCH 02/10] rename folder from video_mode to live_mode --- qualang_tools/__init__.py | 1 + qualang_tools/video_mode/README.md | 185 ---------- qualang_tools/video_mode/livemode.py | 487 --------------------------- 3 files changed, 1 insertion(+), 672 deletions(-) delete mode 100644 qualang_tools/video_mode/README.md delete mode 100644 qualang_tools/video_mode/livemode.py diff --git a/qualang_tools/__init__.py b/qualang_tools/__init__.py index 428bc074..24c5ac69 100644 --- a/qualang_tools/__init__.py +++ b/qualang_tools/__init__.py @@ -3,6 +3,7 @@ "bakery", "config", "control_panel", + "live_mode" "loops", "results", "plot", diff --git a/qualang_tools/video_mode/README.md b/qualang_tools/video_mode/README.md deleted file mode 100644 index 081dc7b5..00000000 --- a/qualang_tools/video_mode/README.md +++ /dev/null @@ -1,185 +0,0 @@ -# Live Mode -## Introduction - -In some cases, one might want to update parameters of the program while it is running and without knowing in advance when and how to update them. -We present here the ```LiveMode``` which enables such dynamic parameter tuning. - -## How to use it - -The user can declare a ``LiveMode`` instance by passing in a ```QuantumMachine``` instance and a dictionary of -parameters intended to be updated dynamically. - -The dictionary should be of the form: - - -``` -parameters_dict = { - 'parameter_name_1': (parameter_value_1, qua_type_1), - 'parameter_name_2': (parameter_value_2, qua_type_2), - ... -} -``` -where ```qua_type``` can be ```bool```, ```ìnt``` or ```fixed```. -If a list of values is provided, the subsequent QUA variable will be a QUA array of specified type. - - -The simplest declaration of the LiveMode can be done as follows: -``` -qmm = QuantumMachinesManager() -qm = qmm.open_qm(config) -live_mode = LiveMode(qm, parameters_dict) - ``` - -Once this declaration is done, some interesting thing will happen behind the scenes. - -First, the provided dictionary of parameters will be converted to a ```ParameterTable``` instance. -The ```ParameterTable``` class serves as an interface between the QUA program and the Python environment. -It is used to facilitate the declaration of the QUA variables that are suitable for the parameters to be updated. -It contains two methods that should be used as QUA macros to enable the dynamic parameter update: - - ```declare_variables```: declares all the necessary QUA variables based on the parameters signature of the ```ParameterTable```object. - This method should ideally be called at the beginning of the QUA program scope. - - ```load_parameters```: handles the dynamic assignment of the parameters declared in the QUA program scope through the IO variables. - -For convenience, those macros can be accessed directly through the ```LiveMode``` object. The QUA program should therefore look like this: - -``` -with program() as live_mode_prog: - # Declare the QUA variables associated to the parameters - live_mode.declare_variables() - - with infinite_loop(): - # Load the parameters from the Python environment - live_mode.load_parameters() - - # Use the parameters as QUA variables - play("my_pulse"*amp(live_mode["parameter_name_1"]), "qe1") - frame_rotation(live_mode["parameter_name_2"], "qe1") - - -``` - -Let us explain what we did here. - -### Declaring variables -The first line within the program is essentially a macro that has declared all the QUA variables dynamically from the -original parameter list by operating an appropriate casting for all parameters that were provided by the user. - -To be able to access those newly created QUA variables later in the design of the program, we can use -```live_mode["param_name"]``` for each parameter loaded initially in the dictionary. We can then use those variables -as we please within the program, as shown above with the custom parameters used for the QUA statements. - -### Updating variables dynamically - - -The ```LiveMode```, once launched in Python, will query continuously the user to input (through the keyboard) -the name of parameter, and the associated new value it should take. -The format should be written as follows: -``` -param_name=new_value -``` -The user can also update a parameter that is a QUA array by providing a list of values separated by a space: -``` -param_name= new_value_1 new_value_2 new_value_3 ... -``` - -Once the user has typed an appropriate update, the ```LiveMode``` will use the IO variables of the ``QuantumMachine`` -to pass the following information: -- which parameter of the initial dictionary should be updated next (IO1) -- what new value should be set for the parameter (IO2) - -For updating a QUA array, a loop over the elements of the array is performed to update dynamically each element -with a successive back and forth interaction between the program and the Python (through interleaved ```pause()``` within QUA and ```job.resume()``` in Python). - -As everything happens behind the scenes, the user does not need to worry about the details of the implementation. -The only thing that the user should implement in the QUA program on top of the declaration of variables shown above is the following macro: -``` -with program() as live_mode_prog: - ... - with infinite_loop(): - live_mode.load_parameters() - ... -``` - -### Accessing QUA variables from ```LiveMode``` within the program -The QUA variables declared in the program through the ```LiveMode.declare_variables()``` can be accessed from the -through the ```live_mode["param_name"]``` syntax. This enables you to choose where your QUA variable should land in -your program. For instance, if you want to use the same QUA variable for two different pulse parameters, you can do the following: -``` -with program() as live_mode_prog: - live_mode.declare_variables() - - with infinite_loop(): - live_mode.load_parameters() - - play("my_pulse"*amp(live_mode["amp_param"]), "qe1") - play("my_other_pulse"*amp(live_mode["amp_param"]), "qe2") -``` - -Note that you can also access to the full list of QUA variables declared in the program through the ```live_mode.variables``` attribute. - -Alternatively, you can also access the QUA variables later in the code as follows -(note that the order of the variables is the same as the order of the parameters in the `parameters_dict` as of Python 3.7): -``` -with program() as live_mode_prog: - amp_param1, amp_param2, ... = live_mode.declare_variables() - - with infinite_loop(): - live_mode.load_parameters() - - play("my_pulse"*amp(amp_param1), "qe1") - play("my_other_pulse"*amp(amp_param2), "qe2") -``` -## Outside the QUA program -The ```LiveMode``` class has only one ```execute```method to be called outside the QUA program scope, which is a simple wrapper of ```qm.execute()``` method. -It will start the live mode execution by creating a new thread that takes control of the console by doing a continuous query of input parameters from the user, while the QUA program is running in the background and possible live data plotting is done in the main script. -This new thread releases access of the console to the user once ```stop``` is typed in the console. - -## Example -Let us consider the following example where we want to update dynamically the amplitude and phase of a pulse. -We first declare the ```LiveMode``` instance and the parameters dictionary: -``` -parameters_dict = { - 'amp': 0.5, - 'phase': 0.0 -} -live_mode = LiveMode(qm, parameters_dict) -``` -We then declare the QUA program as follows: -``` -with program() as live_mode_prog: - live_mode.declare_variables() - - with infinite_loop(): - live_mode.load_parameters() - - play("my_pulse"*amp(live_mode["amp"]), "qe1") - frame_rotation(live_mode["phase"], "qe1") -``` - -Or an equivalent version: -``` -with program() as live_mode_prog: - amp_, phase = live_mode.declare_variables() - - with infinite_loop(): - live_mode.load_parameters() - - play("my_pulse"*amp(amp_), "qe1") - frame_rotation(phase, "qe1") -``` -Finally, we start the live mode execution: -``` -live_mode.execute(live_mode_prog) -``` -At this point, the python console accepts specific inputs, type `help` to see the full list. -For example, to change the variables, type: - -``` -amp=0.8 -phase=0.5 -``` -The program will then update the amplitude and phase of the pulse accordingly. - -More detailed examples can be found under in [examples](https://github.com/qua-platform/py-qua-tools/tree/main/examples/Qcodes_drivers/live_mode) section. - - \ No newline at end of file diff --git a/qualang_tools/video_mode/livemode.py b/qualang_tools/video_mode/livemode.py deleted file mode 100644 index 08a861b6..00000000 --- a/qualang_tools/video_mode/livemode.py +++ /dev/null @@ -1,487 +0,0 @@ -# LiveMode script enabling the update of parameters in a QUA program through user input while the program is running. -# Authors: Arthur Strauss, Théo Laudat -# Date: 19/12/2023 - -import threading -from qm.qua import * -from qm import QuantumMachine, QmJob, Program -import time -import signal -from typing import Optional, List, Dict, Union, Tuple -import numpy as np - - -def set_type(qua_type: Union[str, type]): - """ - Set the type of the QUA variable to be declared. - Args: qua_type: Type of the QUA variable to be declared (int, fixed, bool). - """ - - if qua_type == "fixed" or qua_type == fixed: - return fixed - elif qua_type == "bool" or qua_type == bool: - return bool - elif qua_type == "int" or qua_type == int: - return int - else: - raise ValueError("Invalid QUA type. Please use 'fixed', 'int' or 'bool'.") - - -def infer_type(value: Union[int, float, List, np.ndarray] = None): - """ - Infer automatically the type of the QUA variable to be declared from the type of the initial parameter value. - """ - if isinstance(value, float): - if value.is_integer() and value > 8: - return int - else: - return fixed - - elif isinstance(value, bool): - return bool - - elif isinstance(value, int): - return int - - elif isinstance(value, (List, np.ndarray)): - if isinstance(value, np.ndarray): - assert value.ndim == 1, "Invalid parameter type, array must be 1D." - value = value.tolist() - assert all( - isinstance(x, type(value[0])) for x in value - ), "Invalid parameter type, all elements must be of same type." - if isinstance(value[0], bool): - return bool - elif isinstance(value[0], int): - return int - elif isinstance(value[0], float): - return fixed - else: - raise ValueError("Invalid parameter type. Please use float, int or bool or list.") - - -class ParameterValue: - def __init__( - self, - name: str, - value: Union[int, float, List, np.ndarray], - index: int, - qua_type: Optional[Union[str, type]] = None, - ): - """ """ - self.name = name - self.value = value - self.index = index - self.var = None - self.type = set_type(qua_type) if qua_type is not None else infer_type(value) - self.length = 0 if not isinstance(value, (List, np.ndarray)) else len(value) - - def __repr__(self): - return f"{self.name}: ({self.value}, {self.type}) \n" - - -class ParameterTable: - """ - Class enabling the mapping of parameters to be updated to their corresponding "to-be-declared" QUA variables. The - type of the QUA variable to be adjusted is automatically inferred from the type of the initial_parameter_value. - Each parameter in the dictionary should be given a name that the user can then easily access through the table - with table[parameter_name]. Calling this will return the QUA variable built within the QUA program corresponding - to the parameter name and its associated Python initial value. Args: parameters_dict: Dictionary of the form { - "parameter_name": initial_parameter_value }. the QUA program. - """ - - def __init__( - self, - parameters_dict: Dict[ - str, - Union[ - Tuple[Union[float, int, bool, List, np.ndarray], Optional[str]], - Union[float, int, bool, List, np.ndarray], - ], - ], - ): - """ - Class enabling the mapping of parameters to be updated to their corresponding "to-be-declared" QUA variables. - The type of the QUA variable to be adjusted can be specified or either be automatically inferred from the - type of the initial_parameter_value. Each parameter in the dictionary should be given a name that the user - can then easily access through the table with table[parameter_name]. Calling this will return the QUA - variable built within the QUA program corresponding to the parameter name and its associated Python initial - value. Args: parameters_dict: Dictionary should be of the form { "parameter_name": (initial_value, - qua_type) } where qua_type is the type of the QUA variable to be declared (int, fixed, bool). - - - """ - self.parameters_dict = parameters_dict - self.table = {} - for index, (parameter_name, parameter_value) in enumerate(self.parameters_dict.items()): - if isinstance(parameter_value, Tuple): - if len(parameter_value) != 2: - raise ValueError( - "Invalid format for parameter value. Please use (initial_value, qua_type) or initial_value." - ) - self.table[parameter_name] = ParameterValue( - parameter_name, parameter_value[0], index, parameter_value[1] - ) - else: - self.table[parameter_name] = ParameterValue(parameter_name, parameter_value, index) - - def declare_variables(self): - """ - QUA Macro to create the QUA variables associated with the parameter table. - """ - for parameter_name, parameter in self.table.items(): - parameter.var = declare(t=parameter.type, value=parameter.value) - pause() - if len(self.variables) == 1: - return self.variables[0] - else: - return self.variables - - def load_parameters(self, pause_program=False): - """QUA Macro to be called within QUA program to retrieve updated values for the parameters through IO 1 and IO2. - Args: - pause_program: Boolean indicating whether the program should be paused while waiting for user input. - """ - - if pause_program: - pause() - - param_index_var = declare(int) - assign(param_index_var, IO1) - - with switch_(param_index_var): - for parameter in self.table.values(): - with case_(parameter.index): - if parameter.length == 0: - assign(parameter.var, IO2) - else: - looping_var = declare(int) - with for_( - looping_var, - 0, - looping_var < parameter.var.length(), - looping_var + 1, - ): - pause() - assign(parameter.var[looping_var], IO2) - - with default_(): - pass - - def print_parameters(self): - text = "" - for parameter_name, parameter in self.table.items(): - text += f"{parameter_name}: {parameter.value}, \n" - print(text) - - def get_parameters(self): - """ - Get dictionary of all parameters with latest updated values - Args: parameter_name: Name of the parameter to be returned. If None, all parameters are printed. - Returns: if parameter_name is None, Dictionary of the form { "parameter_name": parameter_value }, - else parameter_value associated to parameter_name. - """ - return {parameter_name: parameter.value for parameter_name, parameter in self.table.items()} - - def get_parameter(self, param_name: str): - """ - Get the value of a specific arameter with latest updated value - Args: parameter_name: Name of the parameter to be returned. If None, all parameters are printed. - Returns: if parameter_name is None, Dictionary of the form { "parameter_name": parameter_value }, - else parameter_value associated to parameter_name. - """ - if param_name not in self.table.keys(): - raise KeyError(f"No parameter named {param_name} in the parameter table.") - return self.table[param_name].value - - def __getitem__(self, item): - """ - Returns the QUA variable corresponding to the parameter name. - """ - - if self.table[item].var is not None: - return self.table[item].var - else: - raise ValueError( - f"No QUA variable found for parameter {item}. Please use " - f"LiveMode.declare_variables() within QUA program first." - ) - - @property - def variables(self): - """List of the QUA variables corresponding to the parameters in the parameter table.""" - try: - return [self[item] for item in self.table.keys()] - except KeyError: - raise KeyError( - "No QUA variables found for parameters. Please use " - "LiveMode.declare_variables() within QUA program first." - ) - - def __repr__(self): - text = "" - for parameter in self.table.values(): - text += parameter.__repr__() - return text - - -class LiveMode: - """ - This class aims to provide an easy way to update parameters in a QUA program through user input while the - program is running. It is particularly useful for calibrating parameters dynamically and without deterministic - of the parameters values. The user can specify - the parameters to be updated and their initial values in the parameter dictionary called ```param_dict```. - """ - - _default_io1 = 2**31 - 1 - _default_io2 = 0.0 - - def __init__(self, qm: QuantumMachine, parameters: Union[Dict, ParameterTable]): - """ - This class aims to provide an easy way to update parameters in a QUA program through user input while the - program is running. It is particularly useful for calibrating parameters in real time. The user can specify - the parameters to be updated and their initial values in the parameter dictionary called ```param_dict```. - The live mode will then automatically create the corresponding QUA variables and update them through user - input. - - Parameters dictionary should be of the form: - ```{"parameter_name": (initial_parameter_value, qua_type) }``` - where ```qua_type``` is the type of the QUA variable to be declared (int, fixed, bool). - - The way this is done is by adding two methods of this class at the beginning of the QUA program declaration: - - - ```declare_variables```: This method will create the QUA variables corresponding to the parameters to be updated. - - ```load_parameters```: This method will load the updated values for the parameters through IO variables 1 and 2. - - The user can then start the live mode outside the QUA program by calling the ```execute``` method of this class. - This will start a new parallel thread in charge of updating the - parameters in the parameter table through user input. The user can stop the live mode by entering 'stop' in - the terminal. The user can also resume a paused program by entering 'done' again in the terminal. - - Args: - qm: Quantum Machine object. - parameters: Dictionary or ParameterTable containing the parameters to be updated and their initial values and types. - """ - self.qm = qm - self.job = None - self._parameter_table = parameters if isinstance(parameters, ParameterTable) else ParameterTable(parameters) - self.active = True - self.thread = threading.Thread(target=self.update_parameters) - self.stop_event = threading.Event() - self.implemented_commands = ( - "List of implemented commands: \n " - "get: return the current value of the parameters. \n " - "stop: quit LiveMode. \n " - "done: resume program (if pause_program==True). \n " - "help: display the list of available commands. \n " - "'param_name'='param_value': set the parameter to the specified value (ex: V1=0.152).\n " - "'param_name': return the value of the parameter.\n" - ) - - signal.signal(signal.SIGINT, self.signal_handler) - signal.signal(signal.SIGTERM, self.signal_handler) - - def signal_handler(self, signum, frame): - """Signal handler for SIGTERM and SIGINT signals.""" - print(f"Received signal {signum}, stopping LiveMode...") - self.qm.set_io1_value(self._default_io1) - self.qm.set_io2_value(self._default_io2) - self.job.halt() - self.stop_event.set() - # For shutting down the entire program - # sys.exit(0) - - def update_parameters(self): - """Update parameters in the parameter table through user input.""" - while not self.stop_event.is_set() and self.active: - param_name = input("Enter a command (type help for getting the list of available commands): ") - messages = param_name.split("=") - if len(messages) == 1: - if messages[0] == "stop": - self.active = False - self.job.halt() - break - - elif messages[0] == "done" and self.job is not None: - if self.job.is_paused(): - self.job.resume() - - elif messages[0] == "get": - self.parameter_table.print_parameters() - - elif messages[0] == "help": - print(self.implemented_commands) - - elif messages[0] in self.parameter_table.table.keys(): - print(self.parameter_table.table[messages[0]].value) - - else: - print(f"Invalid input. {messages[0]} is not a valid command.") - - elif len(messages) == 2: - param_name = messages[0] - param_value = messages[1] - if param_name in self.parameter_table.table.keys(): - self.qm.set_io1_value(self.parameter_table.table[param_name].index) - - if isinstance(self.parameter_table.table[param_name].value, list): - param_value = param_value.split() - if len(param_value) != self.parameter_table.table[param_name].length: - print( - f"Invalid input. {self.parameter_table[param_name]} should be a list of length " - f"{self.parameter_table.table[param_name].length}." - ) - elif param_value[0].isnumeric(): - try: - param_value = list(map(int, param_value)) - except ValueError: - print( - "One of the values could not be cast to int (" - "casting done according to type of first value): " - ) - elif param_value[0].replace(".", "", 1).isdigit(): - try: - param_value = list(map(float, param_value)) - except ValueError: - print( - "One of the values could not be cast to float (" - "casting done based on type of first value): " - ) - elif param_value[0] in ["True", "False"]: - try: - param_value = list(map(bool, param_value)) - except ValueError: - print( - "One of the values could not be cast to bool (casting done based on type of " - "first value): " - ) - - assert all(isinstance(x, type(param_value[0])) for x in param_value), ( - f"Invalid input. {self.parameter_table[param_name]} should be a list of elements of the " - f"same type." - ) - - if self.job.is_paused(): - # If the program is paused before param loading, - # update the parameters directly (no wait through done) - self.job.resume() - for value in param_value: - while not (self.job.is_paused()): - time.sleep(0.001) - self.qm.set_io2_value(value) - self.parameter_table.table[param_name].value = value - self.job.resume() - - else: - if self.parameter_table.table[param_name].type == int: - if not param_value.isnumeric(): - print(f"Invalid input. {self.parameter_table[param_name]} should be an integer.") - else: - try: - param_value = int(param_value) - except ValueError: - print( - f"Invalid input. {self.parameter_table[param_name]} could not be " - f"converted to int." - ) - continue - - elif self.parameter_table.table[param_name].type == fixed: - try: - param_value = float(param_value) - except ValueError: - print(f"Invalid input. {self.parameter_table[param_name]} should be a float.") - continue - - elif self.parameter_table.table[param_name].type == bool: - if param_value not in ["True", "False", "0", "1"]: - print(f"Invalid input. {self.parameter_table[param_name]} should be a boolean.") - elif param_value in ["0", "1"]: - param_value = bool(int(param_value)) - else: - try: - param_value = bool(param_value) - except ValueError: - print( - f"Invalid input. {self.parameter_table[param_name]} could not be cast to bool" - ) - else: - print(f"Invalid input. {param_value} is not a valid parameter value.") - - self.qm.set_io2_value(param_value) - self.parameter_table.table[param_name].value = param_value - - else: - print(f"Invalid input. {param_name} is not a valid parameter name.") - - print("LiveMode stopped.") - - def execute(self, prog: Optional[Program] = None, *execute_args) -> QmJob: - """Start the live mode. - Args: - prog: QUA program to be executed. If None, the live mode will be started without executing a QUA program - (program assumed to be running) - execute_args: Arguments to be passed to the execute method of the QuantumMachine for executing the program. - Returns: - the QM job - - """ - if self.job is None: - self.job = self.qm.execute(prog, *execute_args) - # Reinitialize the IO values - self.qm.set_io1_value(666) - self.qm.set_io2_value(0.0) - time.sleep(1) - self.job.resume() - print("start") - print(self.implemented_commands) - self.parameter_table.get_parameters() - self.thread.start() - - return self.job - - @property - def parameter_table(self): - """ - ParameterTable containing the parameters to be updated and their initial values. - """ - return self._parameter_table - - @property - def variables(self): - """List of the QUA variables corresponding to the parameters in the parameter table.""" - return self.parameter_table.variables - - def get_parameters(self): - """ - Get dictionary of parameters with latest updated values - Returns: Dictionary of the form { "parameter_name": parameter_value }. - """ - return self.parameter_table.get_parameters() - - def get_parameter(self, param_name: Optional[str] = None): - """ - Get the value of a parameter with latest updated values - """ - return self.parameter_table.get_parameter(param_name) - - def load_parameters(self, pause_program=False): - """ - QUA Macro to be called within QUA program to retrieve updated values for the parameters through IO 1 and IO2. - Args: - pause_program: Boolean indicating whether the program should be paused while waiting for user input. - """ - self.parameter_table.load_parameters(pause_program) - - def declare_variables(self): - """ - QUA Macro to create the QUA variables associated with the parameter table. - :returns : The list of declared QUA variables or the declared variable if there is only one item. - """ - return self.parameter_table.declare_variables() - - def __getitem__(self, item): - """ - Returns the QUA variable corresponding to the parameter name. - """ - return self._parameter_table[item] From 259ffba8b8a71905cc9dbb761c1c7b4fc92eb16c Mon Sep 17 00:00:00 2001 From: TheoQM Date: Wed, 23 Oct 2024 11:00:57 +0200 Subject: [PATCH 03/10] updated files --- qualang_tools/live_mode/README.md | 185 +++++++++++ qualang_tools/live_mode/__init__.py | 3 + qualang_tools/live_mode/livemode.py | 487 ++++++++++++++++++++++++++++ 3 files changed, 675 insertions(+) create mode 100644 qualang_tools/live_mode/README.md create mode 100644 qualang_tools/live_mode/__init__.py create mode 100644 qualang_tools/live_mode/livemode.py diff --git a/qualang_tools/live_mode/README.md b/qualang_tools/live_mode/README.md new file mode 100644 index 00000000..081dc7b5 --- /dev/null +++ b/qualang_tools/live_mode/README.md @@ -0,0 +1,185 @@ +# Live Mode +## Introduction + +In some cases, one might want to update parameters of the program while it is running and without knowing in advance when and how to update them. +We present here the ```LiveMode``` which enables such dynamic parameter tuning. + +## How to use it + +The user can declare a ``LiveMode`` instance by passing in a ```QuantumMachine``` instance and a dictionary of +parameters intended to be updated dynamically. + +The dictionary should be of the form: + + +``` +parameters_dict = { + 'parameter_name_1': (parameter_value_1, qua_type_1), + 'parameter_name_2': (parameter_value_2, qua_type_2), + ... +} +``` +where ```qua_type``` can be ```bool```, ```ìnt``` or ```fixed```. +If a list of values is provided, the subsequent QUA variable will be a QUA array of specified type. + + +The simplest declaration of the LiveMode can be done as follows: +``` +qmm = QuantumMachinesManager() +qm = qmm.open_qm(config) +live_mode = LiveMode(qm, parameters_dict) + ``` + +Once this declaration is done, some interesting thing will happen behind the scenes. + +First, the provided dictionary of parameters will be converted to a ```ParameterTable``` instance. +The ```ParameterTable``` class serves as an interface between the QUA program and the Python environment. +It is used to facilitate the declaration of the QUA variables that are suitable for the parameters to be updated. +It contains two methods that should be used as QUA macros to enable the dynamic parameter update: + - ```declare_variables```: declares all the necessary QUA variables based on the parameters signature of the ```ParameterTable```object. + This method should ideally be called at the beginning of the QUA program scope. + - ```load_parameters```: handles the dynamic assignment of the parameters declared in the QUA program scope through the IO variables. + +For convenience, those macros can be accessed directly through the ```LiveMode``` object. The QUA program should therefore look like this: + +``` +with program() as live_mode_prog: + # Declare the QUA variables associated to the parameters + live_mode.declare_variables() + + with infinite_loop(): + # Load the parameters from the Python environment + live_mode.load_parameters() + + # Use the parameters as QUA variables + play("my_pulse"*amp(live_mode["parameter_name_1"]), "qe1") + frame_rotation(live_mode["parameter_name_2"], "qe1") + + +``` + +Let us explain what we did here. + +### Declaring variables +The first line within the program is essentially a macro that has declared all the QUA variables dynamically from the +original parameter list by operating an appropriate casting for all parameters that were provided by the user. + +To be able to access those newly created QUA variables later in the design of the program, we can use +```live_mode["param_name"]``` for each parameter loaded initially in the dictionary. We can then use those variables +as we please within the program, as shown above with the custom parameters used for the QUA statements. + +### Updating variables dynamically + + +The ```LiveMode```, once launched in Python, will query continuously the user to input (through the keyboard) +the name of parameter, and the associated new value it should take. +The format should be written as follows: +``` +param_name=new_value +``` +The user can also update a parameter that is a QUA array by providing a list of values separated by a space: +``` +param_name= new_value_1 new_value_2 new_value_3 ... +``` + +Once the user has typed an appropriate update, the ```LiveMode``` will use the IO variables of the ``QuantumMachine`` +to pass the following information: +- which parameter of the initial dictionary should be updated next (IO1) +- what new value should be set for the parameter (IO2) + +For updating a QUA array, a loop over the elements of the array is performed to update dynamically each element +with a successive back and forth interaction between the program and the Python (through interleaved ```pause()``` within QUA and ```job.resume()``` in Python). + +As everything happens behind the scenes, the user does not need to worry about the details of the implementation. +The only thing that the user should implement in the QUA program on top of the declaration of variables shown above is the following macro: +``` +with program() as live_mode_prog: + ... + with infinite_loop(): + live_mode.load_parameters() + ... +``` + +### Accessing QUA variables from ```LiveMode``` within the program +The QUA variables declared in the program through the ```LiveMode.declare_variables()``` can be accessed from the +through the ```live_mode["param_name"]``` syntax. This enables you to choose where your QUA variable should land in +your program. For instance, if you want to use the same QUA variable for two different pulse parameters, you can do the following: +``` +with program() as live_mode_prog: + live_mode.declare_variables() + + with infinite_loop(): + live_mode.load_parameters() + + play("my_pulse"*amp(live_mode["amp_param"]), "qe1") + play("my_other_pulse"*amp(live_mode["amp_param"]), "qe2") +``` + +Note that you can also access to the full list of QUA variables declared in the program through the ```live_mode.variables``` attribute. + +Alternatively, you can also access the QUA variables later in the code as follows +(note that the order of the variables is the same as the order of the parameters in the `parameters_dict` as of Python 3.7): +``` +with program() as live_mode_prog: + amp_param1, amp_param2, ... = live_mode.declare_variables() + + with infinite_loop(): + live_mode.load_parameters() + + play("my_pulse"*amp(amp_param1), "qe1") + play("my_other_pulse"*amp(amp_param2), "qe2") +``` +## Outside the QUA program +The ```LiveMode``` class has only one ```execute```method to be called outside the QUA program scope, which is a simple wrapper of ```qm.execute()``` method. +It will start the live mode execution by creating a new thread that takes control of the console by doing a continuous query of input parameters from the user, while the QUA program is running in the background and possible live data plotting is done in the main script. +This new thread releases access of the console to the user once ```stop``` is typed in the console. + +## Example +Let us consider the following example where we want to update dynamically the amplitude and phase of a pulse. +We first declare the ```LiveMode``` instance and the parameters dictionary: +``` +parameters_dict = { + 'amp': 0.5, + 'phase': 0.0 +} +live_mode = LiveMode(qm, parameters_dict) +``` +We then declare the QUA program as follows: +``` +with program() as live_mode_prog: + live_mode.declare_variables() + + with infinite_loop(): + live_mode.load_parameters() + + play("my_pulse"*amp(live_mode["amp"]), "qe1") + frame_rotation(live_mode["phase"], "qe1") +``` + +Or an equivalent version: +``` +with program() as live_mode_prog: + amp_, phase = live_mode.declare_variables() + + with infinite_loop(): + live_mode.load_parameters() + + play("my_pulse"*amp(amp_), "qe1") + frame_rotation(phase, "qe1") +``` +Finally, we start the live mode execution: +``` +live_mode.execute(live_mode_prog) +``` +At this point, the python console accepts specific inputs, type `help` to see the full list. +For example, to change the variables, type: + +``` +amp=0.8 +phase=0.5 +``` +The program will then update the amplitude and phase of the pulse accordingly. + +More detailed examples can be found under in [examples](https://github.com/qua-platform/py-qua-tools/tree/main/examples/Qcodes_drivers/live_mode) section. + + \ No newline at end of file diff --git a/qualang_tools/live_mode/__init__.py b/qualang_tools/live_mode/__init__.py new file mode 100644 index 00000000..193cf771 --- /dev/null +++ b/qualang_tools/live_mode/__init__.py @@ -0,0 +1,3 @@ +from .livemode import LiveMode, ParameterTable + +__all__ = ["LiveMode", "ParameterTable"] diff --git a/qualang_tools/live_mode/livemode.py b/qualang_tools/live_mode/livemode.py new file mode 100644 index 00000000..08a861b6 --- /dev/null +++ b/qualang_tools/live_mode/livemode.py @@ -0,0 +1,487 @@ +# LiveMode script enabling the update of parameters in a QUA program through user input while the program is running. +# Authors: Arthur Strauss, Théo Laudat +# Date: 19/12/2023 + +import threading +from qm.qua import * +from qm import QuantumMachine, QmJob, Program +import time +import signal +from typing import Optional, List, Dict, Union, Tuple +import numpy as np + + +def set_type(qua_type: Union[str, type]): + """ + Set the type of the QUA variable to be declared. + Args: qua_type: Type of the QUA variable to be declared (int, fixed, bool). + """ + + if qua_type == "fixed" or qua_type == fixed: + return fixed + elif qua_type == "bool" or qua_type == bool: + return bool + elif qua_type == "int" or qua_type == int: + return int + else: + raise ValueError("Invalid QUA type. Please use 'fixed', 'int' or 'bool'.") + + +def infer_type(value: Union[int, float, List, np.ndarray] = None): + """ + Infer automatically the type of the QUA variable to be declared from the type of the initial parameter value. + """ + if isinstance(value, float): + if value.is_integer() and value > 8: + return int + else: + return fixed + + elif isinstance(value, bool): + return bool + + elif isinstance(value, int): + return int + + elif isinstance(value, (List, np.ndarray)): + if isinstance(value, np.ndarray): + assert value.ndim == 1, "Invalid parameter type, array must be 1D." + value = value.tolist() + assert all( + isinstance(x, type(value[0])) for x in value + ), "Invalid parameter type, all elements must be of same type." + if isinstance(value[0], bool): + return bool + elif isinstance(value[0], int): + return int + elif isinstance(value[0], float): + return fixed + else: + raise ValueError("Invalid parameter type. Please use float, int or bool or list.") + + +class ParameterValue: + def __init__( + self, + name: str, + value: Union[int, float, List, np.ndarray], + index: int, + qua_type: Optional[Union[str, type]] = None, + ): + """ """ + self.name = name + self.value = value + self.index = index + self.var = None + self.type = set_type(qua_type) if qua_type is not None else infer_type(value) + self.length = 0 if not isinstance(value, (List, np.ndarray)) else len(value) + + def __repr__(self): + return f"{self.name}: ({self.value}, {self.type}) \n" + + +class ParameterTable: + """ + Class enabling the mapping of parameters to be updated to their corresponding "to-be-declared" QUA variables. The + type of the QUA variable to be adjusted is automatically inferred from the type of the initial_parameter_value. + Each parameter in the dictionary should be given a name that the user can then easily access through the table + with table[parameter_name]. Calling this will return the QUA variable built within the QUA program corresponding + to the parameter name and its associated Python initial value. Args: parameters_dict: Dictionary of the form { + "parameter_name": initial_parameter_value }. the QUA program. + """ + + def __init__( + self, + parameters_dict: Dict[ + str, + Union[ + Tuple[Union[float, int, bool, List, np.ndarray], Optional[str]], + Union[float, int, bool, List, np.ndarray], + ], + ], + ): + """ + Class enabling the mapping of parameters to be updated to their corresponding "to-be-declared" QUA variables. + The type of the QUA variable to be adjusted can be specified or either be automatically inferred from the + type of the initial_parameter_value. Each parameter in the dictionary should be given a name that the user + can then easily access through the table with table[parameter_name]. Calling this will return the QUA + variable built within the QUA program corresponding to the parameter name and its associated Python initial + value. Args: parameters_dict: Dictionary should be of the form { "parameter_name": (initial_value, + qua_type) } where qua_type is the type of the QUA variable to be declared (int, fixed, bool). + + + """ + self.parameters_dict = parameters_dict + self.table = {} + for index, (parameter_name, parameter_value) in enumerate(self.parameters_dict.items()): + if isinstance(parameter_value, Tuple): + if len(parameter_value) != 2: + raise ValueError( + "Invalid format for parameter value. Please use (initial_value, qua_type) or initial_value." + ) + self.table[parameter_name] = ParameterValue( + parameter_name, parameter_value[0], index, parameter_value[1] + ) + else: + self.table[parameter_name] = ParameterValue(parameter_name, parameter_value, index) + + def declare_variables(self): + """ + QUA Macro to create the QUA variables associated with the parameter table. + """ + for parameter_name, parameter in self.table.items(): + parameter.var = declare(t=parameter.type, value=parameter.value) + pause() + if len(self.variables) == 1: + return self.variables[0] + else: + return self.variables + + def load_parameters(self, pause_program=False): + """QUA Macro to be called within QUA program to retrieve updated values for the parameters through IO 1 and IO2. + Args: + pause_program: Boolean indicating whether the program should be paused while waiting for user input. + """ + + if pause_program: + pause() + + param_index_var = declare(int) + assign(param_index_var, IO1) + + with switch_(param_index_var): + for parameter in self.table.values(): + with case_(parameter.index): + if parameter.length == 0: + assign(parameter.var, IO2) + else: + looping_var = declare(int) + with for_( + looping_var, + 0, + looping_var < parameter.var.length(), + looping_var + 1, + ): + pause() + assign(parameter.var[looping_var], IO2) + + with default_(): + pass + + def print_parameters(self): + text = "" + for parameter_name, parameter in self.table.items(): + text += f"{parameter_name}: {parameter.value}, \n" + print(text) + + def get_parameters(self): + """ + Get dictionary of all parameters with latest updated values + Args: parameter_name: Name of the parameter to be returned. If None, all parameters are printed. + Returns: if parameter_name is None, Dictionary of the form { "parameter_name": parameter_value }, + else parameter_value associated to parameter_name. + """ + return {parameter_name: parameter.value for parameter_name, parameter in self.table.items()} + + def get_parameter(self, param_name: str): + """ + Get the value of a specific arameter with latest updated value + Args: parameter_name: Name of the parameter to be returned. If None, all parameters are printed. + Returns: if parameter_name is None, Dictionary of the form { "parameter_name": parameter_value }, + else parameter_value associated to parameter_name. + """ + if param_name not in self.table.keys(): + raise KeyError(f"No parameter named {param_name} in the parameter table.") + return self.table[param_name].value + + def __getitem__(self, item): + """ + Returns the QUA variable corresponding to the parameter name. + """ + + if self.table[item].var is not None: + return self.table[item].var + else: + raise ValueError( + f"No QUA variable found for parameter {item}. Please use " + f"LiveMode.declare_variables() within QUA program first." + ) + + @property + def variables(self): + """List of the QUA variables corresponding to the parameters in the parameter table.""" + try: + return [self[item] for item in self.table.keys()] + except KeyError: + raise KeyError( + "No QUA variables found for parameters. Please use " + "LiveMode.declare_variables() within QUA program first." + ) + + def __repr__(self): + text = "" + for parameter in self.table.values(): + text += parameter.__repr__() + return text + + +class LiveMode: + """ + This class aims to provide an easy way to update parameters in a QUA program through user input while the + program is running. It is particularly useful for calibrating parameters dynamically and without deterministic + of the parameters values. The user can specify + the parameters to be updated and their initial values in the parameter dictionary called ```param_dict```. + """ + + _default_io1 = 2**31 - 1 + _default_io2 = 0.0 + + def __init__(self, qm: QuantumMachine, parameters: Union[Dict, ParameterTable]): + """ + This class aims to provide an easy way to update parameters in a QUA program through user input while the + program is running. It is particularly useful for calibrating parameters in real time. The user can specify + the parameters to be updated and their initial values in the parameter dictionary called ```param_dict```. + The live mode will then automatically create the corresponding QUA variables and update them through user + input. + + Parameters dictionary should be of the form: + ```{"parameter_name": (initial_parameter_value, qua_type) }``` + where ```qua_type``` is the type of the QUA variable to be declared (int, fixed, bool). + + The way this is done is by adding two methods of this class at the beginning of the QUA program declaration: + + - ```declare_variables```: This method will create the QUA variables corresponding to the parameters to be updated. + - ```load_parameters```: This method will load the updated values for the parameters through IO variables 1 and 2. + + The user can then start the live mode outside the QUA program by calling the ```execute``` method of this class. + This will start a new parallel thread in charge of updating the + parameters in the parameter table through user input. The user can stop the live mode by entering 'stop' in + the terminal. The user can also resume a paused program by entering 'done' again in the terminal. + + Args: + qm: Quantum Machine object. + parameters: Dictionary or ParameterTable containing the parameters to be updated and their initial values and types. + """ + self.qm = qm + self.job = None + self._parameter_table = parameters if isinstance(parameters, ParameterTable) else ParameterTable(parameters) + self.active = True + self.thread = threading.Thread(target=self.update_parameters) + self.stop_event = threading.Event() + self.implemented_commands = ( + "List of implemented commands: \n " + "get: return the current value of the parameters. \n " + "stop: quit LiveMode. \n " + "done: resume program (if pause_program==True). \n " + "help: display the list of available commands. \n " + "'param_name'='param_value': set the parameter to the specified value (ex: V1=0.152).\n " + "'param_name': return the value of the parameter.\n" + ) + + signal.signal(signal.SIGINT, self.signal_handler) + signal.signal(signal.SIGTERM, self.signal_handler) + + def signal_handler(self, signum, frame): + """Signal handler for SIGTERM and SIGINT signals.""" + print(f"Received signal {signum}, stopping LiveMode...") + self.qm.set_io1_value(self._default_io1) + self.qm.set_io2_value(self._default_io2) + self.job.halt() + self.stop_event.set() + # For shutting down the entire program + # sys.exit(0) + + def update_parameters(self): + """Update parameters in the parameter table through user input.""" + while not self.stop_event.is_set() and self.active: + param_name = input("Enter a command (type help for getting the list of available commands): ") + messages = param_name.split("=") + if len(messages) == 1: + if messages[0] == "stop": + self.active = False + self.job.halt() + break + + elif messages[0] == "done" and self.job is not None: + if self.job.is_paused(): + self.job.resume() + + elif messages[0] == "get": + self.parameter_table.print_parameters() + + elif messages[0] == "help": + print(self.implemented_commands) + + elif messages[0] in self.parameter_table.table.keys(): + print(self.parameter_table.table[messages[0]].value) + + else: + print(f"Invalid input. {messages[0]} is not a valid command.") + + elif len(messages) == 2: + param_name = messages[0] + param_value = messages[1] + if param_name in self.parameter_table.table.keys(): + self.qm.set_io1_value(self.parameter_table.table[param_name].index) + + if isinstance(self.parameter_table.table[param_name].value, list): + param_value = param_value.split() + if len(param_value) != self.parameter_table.table[param_name].length: + print( + f"Invalid input. {self.parameter_table[param_name]} should be a list of length " + f"{self.parameter_table.table[param_name].length}." + ) + elif param_value[0].isnumeric(): + try: + param_value = list(map(int, param_value)) + except ValueError: + print( + "One of the values could not be cast to int (" + "casting done according to type of first value): " + ) + elif param_value[0].replace(".", "", 1).isdigit(): + try: + param_value = list(map(float, param_value)) + except ValueError: + print( + "One of the values could not be cast to float (" + "casting done based on type of first value): " + ) + elif param_value[0] in ["True", "False"]: + try: + param_value = list(map(bool, param_value)) + except ValueError: + print( + "One of the values could not be cast to bool (casting done based on type of " + "first value): " + ) + + assert all(isinstance(x, type(param_value[0])) for x in param_value), ( + f"Invalid input. {self.parameter_table[param_name]} should be a list of elements of the " + f"same type." + ) + + if self.job.is_paused(): + # If the program is paused before param loading, + # update the parameters directly (no wait through done) + self.job.resume() + for value in param_value: + while not (self.job.is_paused()): + time.sleep(0.001) + self.qm.set_io2_value(value) + self.parameter_table.table[param_name].value = value + self.job.resume() + + else: + if self.parameter_table.table[param_name].type == int: + if not param_value.isnumeric(): + print(f"Invalid input. {self.parameter_table[param_name]} should be an integer.") + else: + try: + param_value = int(param_value) + except ValueError: + print( + f"Invalid input. {self.parameter_table[param_name]} could not be " + f"converted to int." + ) + continue + + elif self.parameter_table.table[param_name].type == fixed: + try: + param_value = float(param_value) + except ValueError: + print(f"Invalid input. {self.parameter_table[param_name]} should be a float.") + continue + + elif self.parameter_table.table[param_name].type == bool: + if param_value not in ["True", "False", "0", "1"]: + print(f"Invalid input. {self.parameter_table[param_name]} should be a boolean.") + elif param_value in ["0", "1"]: + param_value = bool(int(param_value)) + else: + try: + param_value = bool(param_value) + except ValueError: + print( + f"Invalid input. {self.parameter_table[param_name]} could not be cast to bool" + ) + else: + print(f"Invalid input. {param_value} is not a valid parameter value.") + + self.qm.set_io2_value(param_value) + self.parameter_table.table[param_name].value = param_value + + else: + print(f"Invalid input. {param_name} is not a valid parameter name.") + + print("LiveMode stopped.") + + def execute(self, prog: Optional[Program] = None, *execute_args) -> QmJob: + """Start the live mode. + Args: + prog: QUA program to be executed. If None, the live mode will be started without executing a QUA program + (program assumed to be running) + execute_args: Arguments to be passed to the execute method of the QuantumMachine for executing the program. + Returns: + the QM job + + """ + if self.job is None: + self.job = self.qm.execute(prog, *execute_args) + # Reinitialize the IO values + self.qm.set_io1_value(666) + self.qm.set_io2_value(0.0) + time.sleep(1) + self.job.resume() + print("start") + print(self.implemented_commands) + self.parameter_table.get_parameters() + self.thread.start() + + return self.job + + @property + def parameter_table(self): + """ + ParameterTable containing the parameters to be updated and their initial values. + """ + return self._parameter_table + + @property + def variables(self): + """List of the QUA variables corresponding to the parameters in the parameter table.""" + return self.parameter_table.variables + + def get_parameters(self): + """ + Get dictionary of parameters with latest updated values + Returns: Dictionary of the form { "parameter_name": parameter_value }. + """ + return self.parameter_table.get_parameters() + + def get_parameter(self, param_name: Optional[str] = None): + """ + Get the value of a parameter with latest updated values + """ + return self.parameter_table.get_parameter(param_name) + + def load_parameters(self, pause_program=False): + """ + QUA Macro to be called within QUA program to retrieve updated values for the parameters through IO 1 and IO2. + Args: + pause_program: Boolean indicating whether the program should be paused while waiting for user input. + """ + self.parameter_table.load_parameters(pause_program) + + def declare_variables(self): + """ + QUA Macro to create the QUA variables associated with the parameter table. + :returns : The list of declared QUA variables or the declared variable if there is only one item. + """ + return self.parameter_table.declare_variables() + + def __getitem__(self, item): + """ + Returns the QUA variable corresponding to the parameter name. + """ + return self._parameter_table[item] From f4d91966a32f2611600b74a5296dd1d3758e4e90 Mon Sep 17 00:00:00 2001 From: TheoQM Date: Wed, 23 Oct 2024 11:04:30 +0200 Subject: [PATCH 04/10] updated examples folder --- examples/live_mode/PID_example.py | 182 +++++++++++++++++++ examples/live_mode/README.md | 10 ++ examples/live_mode/basic_example.py | 80 +++++++++ examples/live_mode/configuration.py | 172 ++++++++++++++++++ examples/live_mode/notebook_example.ipynb | 205 ++++++++++++++++++++++ qualang_tools/video_mode/__init__.py | 3 - 6 files changed, 649 insertions(+), 3 deletions(-) create mode 100644 examples/live_mode/PID_example.py create mode 100644 examples/live_mode/README.md create mode 100644 examples/live_mode/basic_example.py create mode 100644 examples/live_mode/configuration.py create mode 100644 examples/live_mode/notebook_example.ipynb delete mode 100644 qualang_tools/video_mode/__init__.py diff --git a/examples/live_mode/PID_example.py b/examples/live_mode/PID_example.py new file mode 100644 index 00000000..ff91393d --- /dev/null +++ b/examples/live_mode/PID_example.py @@ -0,0 +1,182 @@ +from matplotlib import pyplot as plt +from qm.qua import * +from qm import QuantumMachinesManager +from qualang_tools.addons.variables import assign_variables_to_element +from qualang_tools.results import fetching_tool +from configuration import * +from qualang_tools.video_mode.livemode import LiveMode + + +#################### +# Helper functions # +#################### +def PID_derivation(input_signal, bitshift_scale_factor, gain_P, gain_I, gain_D, alpha, target): + """ + + :param input_signal: Result of the demodulation + :param bitshift_scale_factor: Scale factor as 2**bitshift_scale_factor that multiplies the error signal to avoid resolution issues with QUA fixed (3.28) + :param gain_P: Proportional gain. + :param gain_I: Integral gain. + :param gain_D: Derivative gain. + :param alpha: Ratio between proportional and integral error: integrator_error = (1.0 - alpha) * integrator_error + alpha * error + :param target: Setpoint to which the input signal should stabilize. + :return: The total, proportional, integral and derivative errors. + """ + error = declare(fixed) + integrator_error = declare(fixed) + derivative_error = declare(fixed) + old_error = declare(fixed) + + # calculate the error + assign(error, (target - input_signal) << bitshift_scale_factor) + # calculate the integrator error with exponentially decreasing weights with coefficient alpha + assign(integrator_error, (1.0 - alpha) * integrator_error + alpha * error) + # calculate the derivative error + assign(derivative_error, old_error - error) + # save old error to be error + assign(old_error, error) + + return ( + gain_P * error + gain_I * integrator_error + gain_D * derivative_error, + error, + integrator_error, + derivative_error, + ) + + +################### +# The QUA program # +################### +def PID_prog(vm: LiveMode, PDH_angle: float = 0.0): + with program() as prog: + # Results variables + I = declare(fixed) + Q = declare(fixed) + single_shot_DC = declare(fixed) + single_shot_AC = declare(fixed) + dc_offset_1 = declare(fixed) + # PID variables + vm.declare_variables() + # Streams + single_shot_st = declare_stream() + error_st = declare_stream() + integrator_error_st = declare_stream() + derivative_error_st = declare_stream() + offset_st = declare_stream() + + # Ensure that the results variables are assigned to the measurement elements + assign_variables_to_element("detector_DC", single_shot_DC) + assign_variables_to_element("detector_AC", I, Q, single_shot_AC) + + with infinite_loop_(): + # with for_(n, 0, n < N_shots, n + 1): + # Update the PID parameters based on the user input. + vm.load_parameters() + # Ensure that the two digital oscillators will start with the same phase + reset_phase("phase_modulator") + reset_phase("detector_AC") + # Phase angle between the sideband and demodulation in units of 2pi + frame_rotation_2pi(PDH_angle, "phase_modulator") + # Sync all the elements + align() + # Play the PDH sideband + play("cw", "phase_modulator") + # Measure and integrate the signal received by the detector --> DC measurement + measure( + "readout", + "detector_DC", + None, + integration.full("constant", single_shot_DC, "out1"), + ) + # Measure and demodulate the signal received by the detector --> AC measurement sqrt(I**2 + Q**2) + measure( + "readout", + "detector_AC", + None, + demod.full("constant", I, "out1"), + demod.full("constant", Q, "out1"), + ) + assign(single_shot_AC, I) + # PID correction signal + correction, error, int_error, der_error = PID_derivation(single_shot_DC, *vm.variables) + # Update the DC offset + assign(dc_offset_1, dc_offset_1 + correction) + # Handle saturation - Make sure that the DAC won't be asked to output more than 0.5V + with if_(dc_offset_1 > 0.5 - phase_mod_amplitude): + assign(dc_offset_1, 0.5 - phase_mod_amplitude) + with if_(dc_offset_1 < -0.5 + phase_mod_amplitude): + assign(dc_offset_1, -0.5 + phase_mod_amplitude) + # Apply the correction + set_dc_offset("filter_cavity_1", "single", dc_offset_1) + + # Save the desired variables + save(single_shot_DC, single_shot_st) + save(dc_offset_1, offset_st) + save(error, error_st) + save(der_error, derivative_error_st) + save(int_error, integrator_error_st) + + # Wait between each iteration + wait(1000) + + with stream_processing(): + single_shot_st.buffer(1000).save("single_shot") + offset_st.buffer(1000).save("offset") + error_st.buffer(1000).save("error") + integrator_error_st.buffer(1000).save("int_err") + derivative_error_st.buffer(1000).save("der_err") + return prog + + +if __name__ == "__main__": + # Open the Quantum Machine Manager + qmm = QuantumMachinesManager(qop_ip, cluster_name=cluster_name) + # Open the Quantum Machine + qm = qmm.open_qm(config) + # Define the parameters to be updated in video mode with their initial value and QUA type + param_dict = { + "bitshift_scale_factor": (3, int), + "gain_P": (-1e-4, fixed), # The proportional gain + "gain_I": (0.0, fixed), # The integration gain + "gain_D": (0.0, fixed), # The derivative gain + "alpha": (0.0, fixed), # The ratio between integration and proportional error + "target": (0.0, fixed), # The target value + } + # Initialize the video mode + video_mode = LiveMode(qm, param_dict) + # Get the QUA program + qua_prog = PID_prog(video_mode) + job = video_mode.execute(qua_prog) + # Get the results from the OPX in live mode + data_list = ["error", "int_err", "der_err", "single_shot", "offset"] + results = fetching_tool(job, data_list, mode="live") + # Live plotting + fig = plt.figure() + while results.is_processing(): + error, int_err, der_err, single_shot, offset = results.fetch_all() + plt.subplot(231) + plt.cla() + plt.plot(error, "-") + plt.title("Error signal [a.u.]") + plt.xlabel("Time [μs]") + plt.ylabel("Amplitude Error [arb. units]") + plt.subplot(232) + plt.cla() + plt.plot(int_err, "-") + plt.title("integration_error signal [a.u.]") + plt.xlabel("Time [μs]") + plt.subplot(233) + plt.cla() + plt.plot(der_err, "-") + plt.title("derivative_error signal [a.u.]") + plt.xlabel("Time [μs]") + plt.subplot(234) + plt.cla() + plt.plot(single_shot) + plt.title("Single shot measurement") + plt.subplot(235) + plt.cla() + plt.plot(offset) + plt.title("Applied offset [V]") + plt.tight_layout() + plt.pause(0.1) diff --git a/examples/live_mode/README.md b/examples/live_mode/README.md new file mode 100644 index 00000000..361df5f1 --- /dev/null +++ b/examples/live_mode/README.md @@ -0,0 +1,10 @@ +# Video Mode usage examples + +__Package__: https://github.com/qua-platform/py-qua-tools/tree/main/qualang_tools/video_mode + +This package contains modules to set the OPX in a video-mode, where pre-defined parameters of the QUA program can be +updated while looking at data extracted and plotted in real-time. + +In this example folder, you will find two files: +* [simple test](basic_example.py): demonstrating the basic functionalities by updating two OPX output offsets. +* [PID loop](PID_example.py): showing how to use this framework to calibrate and optimize the parameters of a PID loop. diff --git a/examples/live_mode/basic_example.py b/examples/live_mode/basic_example.py new file mode 100644 index 00000000..fa61e63c --- /dev/null +++ b/examples/live_mode/basic_example.py @@ -0,0 +1,80 @@ +from matplotlib import pyplot as plt +from qm.qua import * +from qm import QuantumMachinesManager +from qualang_tools.results import fetching_tool +from configuration import * +from qualang_tools.video_mode.livemode import LiveMode + + +def qua_prog(vm: LiveMode): + with program() as prog: + # Results variables + single_shot_1 = declare(fixed) + single_shot_2 = declare(fixed) + # Get the parameters from the video mode + dc_offset_1, dc_offset_2 = vm.declare_variables() + # Streams + signal1_st = declare_stream() + signal2_st = declare_stream() + + with infinite_loop_(): + # Update the parameters + vm.load_parameters() + # Update the dc_offset of the channel connected to the OPX analog input 1 + set_dc_offset("filter_cavity_1", "single", dc_offset_1) + set_dc_offset("filter_cavity_2", "single", dc_offset_2) + # Measure and integrate the signal received by the OPX + measure( + "readout", + "detector_DC", + None, + integration.full("constant", single_shot_1, "out1"), + integration.full("constant", single_shot_2, "out2"), + ) + # Save the measured value to its stream + save(single_shot_1, signal1_st) + save(single_shot_2, signal2_st) + # Wait between each iteration + wait(1000) + + with stream_processing(): + signal1_st.buffer(1000).save("signal1") + signal2_st.buffer(1000).save("signal2") + return prog + + +if __name__ == "__main__": + # Open the Quantum Machine Manager + qmm = QuantumMachinesManager(qop_ip, cluster_name=cluster_name) + # Open the Quantum Machine + qm = qmm.open_qm(config) + # Define the parameters to be updated in video mode with their initial value and QUA type + param_dict = { + "dc_offset_1": (0.0, fixed), + "dc_offset_2": (0.0, fixed), + } + # Initialize the video mode + video_mode = LiveMode(qm, param_dict) + # Get the QUA program + qua_prog = qua_prog(video_mode) + # Execute the QUA program in video mode + job = video_mode.execute(qua_prog) + # Get the results from the OPX in live mode + results = fetching_tool(job, ["signal1", "signal2"], mode="live") + # Live plotting + fig = plt.figure() + while results.is_processing(): + # Fetch data from the OPX + signal1, signal2 = results.fetch_all() + # Convert the data into Volt + signal1 = -u.demod2volts(signal1, readout_len) + signal2 = -u.demod2volts(signal2, readout_len) + # Plot the data + plt.cla() + plt.plot(signal1, "b-") + plt.plot(signal2, "r-") + plt.title("Error signal [a.u.]") + plt.xlabel("Time [μs]") + plt.ylabel("Amplitude Error [arb. units]") + plt.ylim((-0.5, 0.5)) + plt.pause(0.1) diff --git a/examples/live_mode/configuration.py b/examples/live_mode/configuration.py new file mode 100644 index 00000000..143209dd --- /dev/null +++ b/examples/live_mode/configuration.py @@ -0,0 +1,172 @@ +from qualang_tools.units import unit + + +###################### +# AUXILIARY FUNCTIONS: +###################### +u = unit() + +###################### +# Network parameters # +###################### +qop_ip = "127.0.0.1" # Write the QM router IP address +cluster_name = "my_cluster" # Write your cluster_name if version >= QOP220 +qop_port = None # Write the QOP port if version < QOP220 + + +################ +# CONFIGURATION: +################ +# Phase modulation +phase_modulation_IF = 5 * u.MHz +phase_mod_amplitude = 0.1 +phase_mod_len = 10 * u.us + +# AOM +AOM_IF = 0 +const_amplitude = 0.1 +const_len = 100 + +# Filter cavity +offset_amplitude = 0.25 # Fixed do not change +offset_len = 16 # Fixed do not change + +# Photo-diode +readout_len = phase_mod_len +time_of_flight = 192 + + +config = { + "version": 1, + "controllers": { + "con1": { + "analog_outputs": { + 8: {"offset": 0.0}, + 9: {"offset": 0.0}, + 10: {"offset": 0.0}, + }, + "digital_outputs": { + 1: {}, + }, + "analog_inputs": { + 1: {"offset": 0.0}, + 2: {"offset": 0.0}, + }, + } + }, + "elements": { + "phase_modulator": { + "singleInput": { + "port": ("con1", 8), + }, + "intermediate_frequency": phase_modulation_IF, + "operations": { + "cw": "phase_mod_pulse", + }, + }, + "filter_cavity_1": { + "singleInput": { + "port": ("con1", 9), + }, + "operations": { + "offset": "offset_pulse", + }, + "sticky": {"analog": True, "duration": 60}, + }, + "filter_cavity_2": { + "singleInput": { + "port": ("con1", 10), + }, + "operations": { + "offset": "offset_pulse", + }, + "sticky": {"analog": True, "duration": 60}, + }, + "detector_DC": { + "singleInput": { + "port": ("con1", 9), + }, + "operations": { + "readout": "DC_readout_pulse", + }, + "outputs": { + "out1": ("con1", 1), + "out2": ("con1", 2), + }, + "time_of_flight": time_of_flight, + "smearing": 0, + }, + "detector_AC": { + "singleInput": { + "port": ("con1", 9), + }, + "intermediate_frequency": phase_modulation_IF, + "operations": { + "readout": "AC_readout_pulse", + }, + "outputs": { + "out1": ("con1", 1), + }, + "time_of_flight": time_of_flight, + "smearing": 0, + }, + }, + "pulses": { + "offset_pulse": { + "operation": "control", + "length": offset_len, + "waveforms": { + "single": "offset_wf", + }, + }, + "phase_mod_pulse": { + "operation": "control", + "length": phase_mod_len, + "waveforms": { + "single": "phase_mod_wf", + }, + }, + "cw_pulse": { + "operation": "control", + "length": const_len, + "waveforms": { + "single": "const_wf", + }, + }, + "DC_readout_pulse": { + "operation": "measurement", + "length": readout_len, + "waveforms": { + "single": "zero_wf", + }, + "integration_weights": { + "constant": "constant_weights", + }, + "digital_marker": "ON", + }, + "AC_readout_pulse": { + "operation": "measurement", + "length": readout_len, + "waveforms": { + "single": "zero_wf", + }, + "integration_weights": { + "constant": "constant_weights", + }, + "digital_marker": "ON", + }, + }, + "waveforms": { + "phase_mod_wf": {"type": "constant", "sample": phase_mod_amplitude}, + "const_wf": {"type": "constant", "sample": const_amplitude}, + "offset_wf": {"type": "constant", "sample": offset_amplitude}, + "zero_wf": {"type": "constant", "sample": 0.0}, + }, + "digital_waveforms": {"ON": {"samples": [(1, 0)]}}, + "integration_weights": { + "constant_weights": { + "cosine": [(1.0, readout_len)], + "sine": [(0.0, readout_len)], + }, + }, +} diff --git a/examples/live_mode/notebook_example.ipynb b/examples/live_mode/notebook_example.ipynb new file mode 100644 index 00000000..244cb741 --- /dev/null +++ b/examples/live_mode/notebook_example.ipynb @@ -0,0 +1,205 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2023-12-11 18:41:51,803 - qm - INFO - Starting session: b1513fbb-fb34-4a3b-b991-d84bd3983780\n" + ] + } + ], + "source": [ + "from matplotlib import pyplot as plt\n", + "from qm.qua import *\n", + "from qm import QuantumMachinesManager\n", + "from qualang_tools.results import fetching_tool\n", + "from configuration import *\n", + "from qualang_tools.video_mode import VideoMode" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Using matplotlib backend: TkAgg\n" + ] + } + ], + "source": [ + "import matplotlib\n", + "%matplotlib" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2023-12-11 18:41:54,116 - qm - INFO - Performing health check\n", + "2023-12-11 18:41:54,358 - qm - INFO - Health check passed\n", + "2023-12-11 18:41:55,047 - qm - INFO - Sending program to QOP for compilation\n", + "2023-12-11 18:41:55,496 - qm - INFO - Executing program\n", + "start\n", + "List of implemented commands: \n", + " get: returns the current value of the parameters. \n", + " stop: quit VideoMode. \n", + " done: resume program (if pause_program==True). \n", + " help: displays the list of available commands. \n", + " 'param_name'='param_value': sets the parameter to the specified value (ex: V1=0.152).\n", + " 'param_name': returns the value of the parameter.\n", + "\n", + "dc_offset_1: 0.0, dc_offset_2: 0.0, \n" + ] + }, + { + "name": "stdin", + "output_type": "stream", + "text": [ + "Enter a command (type help for getting the list of available commands): get\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "dc_offset_1: 0.0, dc_offset_2: 0.0, \n" + ] + }, + { + "name": "stdin", + "output_type": "stream", + "text": [ + "Enter a command (type help for getting the list of available commands): dc_offset_1=0.2\n", + "Enter a command (type help for getting the list of available commands): dc_offset_2=0.2\n", + "Enter a command (type help for getting the list of available commands): dc_offset_2=-0.2\n", + "Enter a command (type help for getting the list of available commands): dc_offset_2\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "-0.2\n" + ] + } + ], + "source": [ + "\n", + "def qua_prog(video_mode: VideoMode):\n", + " with program() as prog:\n", + " # Results variables\n", + " single_shot_1 = declare(fixed)\n", + " single_shot_2 = declare(fixed)\n", + " # Get the parameters from the video mode\n", + " dc_offset_1, dc_offset_2 = video_mode.declare_variables()\n", + " # Streams\n", + " signal1_st = declare_stream()\n", + " signal2_st = declare_stream()\n", + "\n", + " with infinite_loop_():\n", + " # Update the parameters\n", + " video_mode.load_parameters()\n", + " # Update the dc_offset of the channel connected to the OPX analog input 1\n", + " set_dc_offset(\"filter_cavity_1\", \"single\", dc_offset_1)\n", + " set_dc_offset(\"filter_cavity_2\", \"single\", dc_offset_2)\n", + " # Measure and integrate the signal received by the OPX\n", + " measure(\n", + " \"readout\",\n", + " \"detector_DC\",\n", + " None,\n", + " integration.full(\"constant\", single_shot_1, \"out1\"),\n", + " integration.full(\"constant\", single_shot_2, \"out2\"),\n", + " )\n", + " # Save the measured value to its stream\n", + " save(single_shot_1, signal1_st)\n", + " save(single_shot_2, signal2_st)\n", + " # Wait between each iteration\n", + " wait(1000)\n", + "\n", + " with stream_processing():\n", + " signal1_st.buffer(1000).save(\"signal1\")\n", + " signal2_st.buffer(1000).save(\"signal2\")\n", + " return prog\n", + "\n", + "\n", + "if __name__ == \"__main__\":\n", + " # Open the Quantum Machine Manager\n", + " qmm = QuantumMachinesManager(qop_ip, cluster_name=cluster_name)\n", + " # Open the Quantum Machine\n", + " qm = qmm.open_qm(config)\n", + " # Define the parameters to be updated in video mode with their initial value\n", + " param_dict = {\n", + " \"dc_offset_1\": (0.0, fixed),\n", + " \"dc_offset_2\": (0.0, fixed)\n", + " }\n", + " # Initialize the video mode\n", + " video_mode = VideoMode(qm, param_dict)\n", + " # Get the QUA program\n", + " prog = qua_prog(video_mode)\n", + " # Execute the QUA program in video mode\n", + " job = video_mode.execute(prog)\n", + " # Get the results from the OPX in live mode\n", + " results = fetching_tool(job, [\"signal1\", \"signal2\"], mode=\"live\")\n", + " # Live plotting\n", + " fig = plt.figure()\n", + " while results.is_processing():\n", + " # Fetch data from the OPX\n", + " signal1, signal2 = results.fetch_all()\n", + " # Convert the data into Volt\n", + " signal1 = -signal1 * 2**12 / readout_len\n", + " signal2 = -signal2 * 2**12 / readout_len\n", + " # Plot the data\n", + " plt.cla()\n", + " plt.plot(signal1, \"b-\")\n", + " plt.plot(signal2, \"r-\")\n", + " plt.title(\"Error signal [a.u.]\")\n", + " plt.xlabel(\"Time [μs]\")\n", + " plt.ylabel(\"Amplitude Error [arb. units]\")\n", + " plt.ylim((-0.5, 0.5))\n", + " plt.pause(0.1)\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.0" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/qualang_tools/video_mode/__init__.py b/qualang_tools/video_mode/__init__.py deleted file mode 100644 index 7ca803f3..00000000 --- a/qualang_tools/video_mode/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .videomode import VideoMode, ParameterTable - -__all__ = ["VideoMode", "ParameterTable"] From 811f68c42b60787b6c06e03df69d6e38400648f0 Mon Sep 17 00:00:00 2001 From: TheoQM Date: Wed, 23 Oct 2024 11:04:53 +0200 Subject: [PATCH 05/10] remove previous files --- examples/video_mode/PID_example.py | 182 ------------------ examples/video_mode/README.md | 10 - examples/video_mode/basic_example.py | 80 -------- examples/video_mode/configuration.py | 172 ----------------- examples/video_mode/notebook_example.ipynb | 205 --------------------- qualang_tools/live_mode/__init__.py | 2 +- 6 files changed, 1 insertion(+), 650 deletions(-) delete mode 100644 examples/video_mode/PID_example.py delete mode 100644 examples/video_mode/README.md delete mode 100644 examples/video_mode/basic_example.py delete mode 100644 examples/video_mode/configuration.py delete mode 100644 examples/video_mode/notebook_example.ipynb diff --git a/examples/video_mode/PID_example.py b/examples/video_mode/PID_example.py deleted file mode 100644 index e67dbfc5..00000000 --- a/examples/video_mode/PID_example.py +++ /dev/null @@ -1,182 +0,0 @@ -from matplotlib import pyplot as plt -from qm.qua import * -from qm import QuantumMachinesManager -from qualang_tools.addons.variables import assign_variables_to_element -from qualang_tools.results import fetching_tool -from configuration import * -from qualang_tools.video_mode.videomode import VideoMode - - -#################### -# Helper functions # -#################### -def PID_derivation(input_signal, bitshift_scale_factor, gain_P, gain_I, gain_D, alpha, target): - """ - - :param input_signal: Result of the demodulation - :param bitshift_scale_factor: Scale factor as 2**bitshift_scale_factor that multiplies the error signal to avoid resolution issues with QUA fixed (3.28) - :param gain_P: Proportional gain. - :param gain_I: Integral gain. - :param gain_D: Derivative gain. - :param alpha: Ratio between proportional and integral error: integrator_error = (1.0 - alpha) * integrator_error + alpha * error - :param target: Setpoint to which the input signal should stabilize. - :return: The total, proportional, integral and derivative errors. - """ - error = declare(fixed) - integrator_error = declare(fixed) - derivative_error = declare(fixed) - old_error = declare(fixed) - - # calculate the error - assign(error, (target - input_signal) << bitshift_scale_factor) - # calculate the integrator error with exponentially decreasing weights with coefficient alpha - assign(integrator_error, (1.0 - alpha) * integrator_error + alpha * error) - # calculate the derivative error - assign(derivative_error, old_error - error) - # save old error to be error - assign(old_error, error) - - return ( - gain_P * error + gain_I * integrator_error + gain_D * derivative_error, - error, - integrator_error, - derivative_error, - ) - - -################### -# The QUA program # -################### -def PID_prog(vm: VideoMode, PDH_angle: float = 0.0): - with program() as prog: - # Results variables - I = declare(fixed) - Q = declare(fixed) - single_shot_DC = declare(fixed) - single_shot_AC = declare(fixed) - dc_offset_1 = declare(fixed) - # PID variables - vm.declare_variables() - # Streams - single_shot_st = declare_stream() - error_st = declare_stream() - integrator_error_st = declare_stream() - derivative_error_st = declare_stream() - offset_st = declare_stream() - - # Ensure that the results variables are assigned to the measurement elements - assign_variables_to_element("detector_DC", single_shot_DC) - assign_variables_to_element("detector_AC", I, Q, single_shot_AC) - - with infinite_loop_(): - # with for_(n, 0, n < N_shots, n + 1): - # Update the PID parameters based on the user input. - vm.load_parameters() - # Ensure that the two digital oscillators will start with the same phase - reset_phase("phase_modulator") - reset_phase("detector_AC") - # Phase angle between the sideband and demodulation in units of 2pi - frame_rotation_2pi(PDH_angle, "phase_modulator") - # Sync all the elements - align() - # Play the PDH sideband - play("cw", "phase_modulator") - # Measure and integrate the signal received by the detector --> DC measurement - measure( - "readout", - "detector_DC", - None, - integration.full("constant", single_shot_DC, "out1"), - ) - # Measure and demodulate the signal received by the detector --> AC measurement sqrt(I**2 + Q**2) - measure( - "readout", - "detector_AC", - None, - demod.full("constant", I, "out1"), - demod.full("constant", Q, "out1"), - ) - assign(single_shot_AC, I) - # PID correction signal - correction, error, int_error, der_error = PID_derivation(single_shot_DC, *vm.variables) - # Update the DC offset - assign(dc_offset_1, dc_offset_1 + correction) - # Handle saturation - Make sure that the DAC won't be asked to output more than 0.5V - with if_(dc_offset_1 > 0.5 - phase_mod_amplitude): - assign(dc_offset_1, 0.5 - phase_mod_amplitude) - with if_(dc_offset_1 < -0.5 + phase_mod_amplitude): - assign(dc_offset_1, -0.5 + phase_mod_amplitude) - # Apply the correction - set_dc_offset("filter_cavity_1", "single", dc_offset_1) - - # Save the desired variables - save(single_shot_DC, single_shot_st) - save(dc_offset_1, offset_st) - save(error, error_st) - save(der_error, derivative_error_st) - save(int_error, integrator_error_st) - - # Wait between each iteration - wait(1000) - - with stream_processing(): - single_shot_st.buffer(1000).save("single_shot") - offset_st.buffer(1000).save("offset") - error_st.buffer(1000).save("error") - integrator_error_st.buffer(1000).save("int_err") - derivative_error_st.buffer(1000).save("der_err") - return prog - - -if __name__ == "__main__": - # Open the Quantum Machine Manager - qmm = QuantumMachinesManager(qop_ip, cluster_name=cluster_name) - # Open the Quantum Machine - qm = qmm.open_qm(config) - # Define the parameters to be updated in video mode with their initial value and QUA type - param_dict = { - "bitshift_scale_factor": (3, int), - "gain_P": (-1e-4, fixed), # The proportional gain - "gain_I": (0.0, fixed), # The integration gain - "gain_D": (0.0, fixed), # The derivative gain - "alpha": (0.0, fixed), # The ratio between integration and proportional error - "target": (0.0, fixed), # The target value - } - # Initialize the video mode - video_mode = VideoMode(qm, param_dict) - # Get the QUA program - qua_prog = PID_prog(video_mode) - job = video_mode.execute(qua_prog) - # Get the results from the OPX in live mode - data_list = ["error", "int_err", "der_err", "single_shot", "offset"] - results = fetching_tool(job, data_list, mode="live") - # Live plotting - fig = plt.figure() - while results.is_processing(): - error, int_err, der_err, single_shot, offset = results.fetch_all() - plt.subplot(231) - plt.cla() - plt.plot(error, "-") - plt.title("Error signal [a.u.]") - plt.xlabel("Time [μs]") - plt.ylabel("Amplitude Error [arb. units]") - plt.subplot(232) - plt.cla() - plt.plot(int_err, "-") - plt.title("integration_error signal [a.u.]") - plt.xlabel("Time [μs]") - plt.subplot(233) - plt.cla() - plt.plot(der_err, "-") - plt.title("derivative_error signal [a.u.]") - plt.xlabel("Time [μs]") - plt.subplot(234) - plt.cla() - plt.plot(single_shot) - plt.title("Single shot measurement") - plt.subplot(235) - plt.cla() - plt.plot(offset) - plt.title("Applied offset [V]") - plt.tight_layout() - plt.pause(0.1) diff --git a/examples/video_mode/README.md b/examples/video_mode/README.md deleted file mode 100644 index 361df5f1..00000000 --- a/examples/video_mode/README.md +++ /dev/null @@ -1,10 +0,0 @@ -# Video Mode usage examples - -__Package__: https://github.com/qua-platform/py-qua-tools/tree/main/qualang_tools/video_mode - -This package contains modules to set the OPX in a video-mode, where pre-defined parameters of the QUA program can be -updated while looking at data extracted and plotted in real-time. - -In this example folder, you will find two files: -* [simple test](basic_example.py): demonstrating the basic functionalities by updating two OPX output offsets. -* [PID loop](PID_example.py): showing how to use this framework to calibrate and optimize the parameters of a PID loop. diff --git a/examples/video_mode/basic_example.py b/examples/video_mode/basic_example.py deleted file mode 100644 index ed655453..00000000 --- a/examples/video_mode/basic_example.py +++ /dev/null @@ -1,80 +0,0 @@ -from matplotlib import pyplot as plt -from qm.qua import * -from qm import QuantumMachinesManager -from qualang_tools.results import fetching_tool -from configuration import * -from qualang_tools.video_mode.videomode import VideoMode - - -def qua_prog(vm: VideoMode): - with program() as prog: - # Results variables - single_shot_1 = declare(fixed) - single_shot_2 = declare(fixed) - # Get the parameters from the video mode - dc_offset_1, dc_offset_2 = vm.declare_variables() - # Streams - signal1_st = declare_stream() - signal2_st = declare_stream() - - with infinite_loop_(): - # Update the parameters - vm.load_parameters() - # Update the dc_offset of the channel connected to the OPX analog input 1 - set_dc_offset("filter_cavity_1", "single", dc_offset_1) - set_dc_offset("filter_cavity_2", "single", dc_offset_2) - # Measure and integrate the signal received by the OPX - measure( - "readout", - "detector_DC", - None, - integration.full("constant", single_shot_1, "out1"), - integration.full("constant", single_shot_2, "out2"), - ) - # Save the measured value to its stream - save(single_shot_1, signal1_st) - save(single_shot_2, signal2_st) - # Wait between each iteration - wait(1000) - - with stream_processing(): - signal1_st.buffer(1000).save("signal1") - signal2_st.buffer(1000).save("signal2") - return prog - - -if __name__ == "__main__": - # Open the Quantum Machine Manager - qmm = QuantumMachinesManager(qop_ip, cluster_name=cluster_name) - # Open the Quantum Machine - qm = qmm.open_qm(config) - # Define the parameters to be updated in video mode with their initial value and QUA type - param_dict = { - "dc_offset_1": (0.0, fixed), - "dc_offset_2": (0.0, fixed), - } - # Initialize the video mode - video_mode = VideoMode(qm, param_dict) - # Get the QUA program - qua_prog = qua_prog(video_mode) - # Execute the QUA program in video mode - job = video_mode.execute(qua_prog) - # Get the results from the OPX in live mode - results = fetching_tool(job, ["signal1", "signal2"], mode="live") - # Live plotting - fig = plt.figure() - while results.is_processing(): - # Fetch data from the OPX - signal1, signal2 = results.fetch_all() - # Convert the data into Volt - signal1 = -u.demod2volts(signal1, readout_len) - signal2 = -u.demod2volts(signal2, readout_len) - # Plot the data - plt.cla() - plt.plot(signal1, "b-") - plt.plot(signal2, "r-") - plt.title("Error signal [a.u.]") - plt.xlabel("Time [μs]") - plt.ylabel("Amplitude Error [arb. units]") - plt.ylim((-0.5, 0.5)) - plt.pause(0.1) diff --git a/examples/video_mode/configuration.py b/examples/video_mode/configuration.py deleted file mode 100644 index 143209dd..00000000 --- a/examples/video_mode/configuration.py +++ /dev/null @@ -1,172 +0,0 @@ -from qualang_tools.units import unit - - -###################### -# AUXILIARY FUNCTIONS: -###################### -u = unit() - -###################### -# Network parameters # -###################### -qop_ip = "127.0.0.1" # Write the QM router IP address -cluster_name = "my_cluster" # Write your cluster_name if version >= QOP220 -qop_port = None # Write the QOP port if version < QOP220 - - -################ -# CONFIGURATION: -################ -# Phase modulation -phase_modulation_IF = 5 * u.MHz -phase_mod_amplitude = 0.1 -phase_mod_len = 10 * u.us - -# AOM -AOM_IF = 0 -const_amplitude = 0.1 -const_len = 100 - -# Filter cavity -offset_amplitude = 0.25 # Fixed do not change -offset_len = 16 # Fixed do not change - -# Photo-diode -readout_len = phase_mod_len -time_of_flight = 192 - - -config = { - "version": 1, - "controllers": { - "con1": { - "analog_outputs": { - 8: {"offset": 0.0}, - 9: {"offset": 0.0}, - 10: {"offset": 0.0}, - }, - "digital_outputs": { - 1: {}, - }, - "analog_inputs": { - 1: {"offset": 0.0}, - 2: {"offset": 0.0}, - }, - } - }, - "elements": { - "phase_modulator": { - "singleInput": { - "port": ("con1", 8), - }, - "intermediate_frequency": phase_modulation_IF, - "operations": { - "cw": "phase_mod_pulse", - }, - }, - "filter_cavity_1": { - "singleInput": { - "port": ("con1", 9), - }, - "operations": { - "offset": "offset_pulse", - }, - "sticky": {"analog": True, "duration": 60}, - }, - "filter_cavity_2": { - "singleInput": { - "port": ("con1", 10), - }, - "operations": { - "offset": "offset_pulse", - }, - "sticky": {"analog": True, "duration": 60}, - }, - "detector_DC": { - "singleInput": { - "port": ("con1", 9), - }, - "operations": { - "readout": "DC_readout_pulse", - }, - "outputs": { - "out1": ("con1", 1), - "out2": ("con1", 2), - }, - "time_of_flight": time_of_flight, - "smearing": 0, - }, - "detector_AC": { - "singleInput": { - "port": ("con1", 9), - }, - "intermediate_frequency": phase_modulation_IF, - "operations": { - "readout": "AC_readout_pulse", - }, - "outputs": { - "out1": ("con1", 1), - }, - "time_of_flight": time_of_flight, - "smearing": 0, - }, - }, - "pulses": { - "offset_pulse": { - "operation": "control", - "length": offset_len, - "waveforms": { - "single": "offset_wf", - }, - }, - "phase_mod_pulse": { - "operation": "control", - "length": phase_mod_len, - "waveforms": { - "single": "phase_mod_wf", - }, - }, - "cw_pulse": { - "operation": "control", - "length": const_len, - "waveforms": { - "single": "const_wf", - }, - }, - "DC_readout_pulse": { - "operation": "measurement", - "length": readout_len, - "waveforms": { - "single": "zero_wf", - }, - "integration_weights": { - "constant": "constant_weights", - }, - "digital_marker": "ON", - }, - "AC_readout_pulse": { - "operation": "measurement", - "length": readout_len, - "waveforms": { - "single": "zero_wf", - }, - "integration_weights": { - "constant": "constant_weights", - }, - "digital_marker": "ON", - }, - }, - "waveforms": { - "phase_mod_wf": {"type": "constant", "sample": phase_mod_amplitude}, - "const_wf": {"type": "constant", "sample": const_amplitude}, - "offset_wf": {"type": "constant", "sample": offset_amplitude}, - "zero_wf": {"type": "constant", "sample": 0.0}, - }, - "digital_waveforms": {"ON": {"samples": [(1, 0)]}}, - "integration_weights": { - "constant_weights": { - "cosine": [(1.0, readout_len)], - "sine": [(0.0, readout_len)], - }, - }, -} diff --git a/examples/video_mode/notebook_example.ipynb b/examples/video_mode/notebook_example.ipynb deleted file mode 100644 index 244cb741..00000000 --- a/examples/video_mode/notebook_example.ipynb +++ /dev/null @@ -1,205 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-12-11 18:41:51,803 - qm - INFO - Starting session: b1513fbb-fb34-4a3b-b991-d84bd3983780\n" - ] - } - ], - "source": [ - "from matplotlib import pyplot as plt\n", - "from qm.qua import *\n", - "from qm import QuantumMachinesManager\n", - "from qualang_tools.results import fetching_tool\n", - "from configuration import *\n", - "from qualang_tools.video_mode import VideoMode" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Using matplotlib backend: TkAgg\n" - ] - } - ], - "source": [ - "import matplotlib\n", - "%matplotlib" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-12-11 18:41:54,116 - qm - INFO - Performing health check\n", - "2023-12-11 18:41:54,358 - qm - INFO - Health check passed\n", - "2023-12-11 18:41:55,047 - qm - INFO - Sending program to QOP for compilation\n", - "2023-12-11 18:41:55,496 - qm - INFO - Executing program\n", - "start\n", - "List of implemented commands: \n", - " get: returns the current value of the parameters. \n", - " stop: quit VideoMode. \n", - " done: resume program (if pause_program==True). \n", - " help: displays the list of available commands. \n", - " 'param_name'='param_value': sets the parameter to the specified value (ex: V1=0.152).\n", - " 'param_name': returns the value of the parameter.\n", - "\n", - "dc_offset_1: 0.0, dc_offset_2: 0.0, \n" - ] - }, - { - "name": "stdin", - "output_type": "stream", - "text": [ - "Enter a command (type help for getting the list of available commands): get\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "dc_offset_1: 0.0, dc_offset_2: 0.0, \n" - ] - }, - { - "name": "stdin", - "output_type": "stream", - "text": [ - "Enter a command (type help for getting the list of available commands): dc_offset_1=0.2\n", - "Enter a command (type help for getting the list of available commands): dc_offset_2=0.2\n", - "Enter a command (type help for getting the list of available commands): dc_offset_2=-0.2\n", - "Enter a command (type help for getting the list of available commands): dc_offset_2\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "-0.2\n" - ] - } - ], - "source": [ - "\n", - "def qua_prog(video_mode: VideoMode):\n", - " with program() as prog:\n", - " # Results variables\n", - " single_shot_1 = declare(fixed)\n", - " single_shot_2 = declare(fixed)\n", - " # Get the parameters from the video mode\n", - " dc_offset_1, dc_offset_2 = video_mode.declare_variables()\n", - " # Streams\n", - " signal1_st = declare_stream()\n", - " signal2_st = declare_stream()\n", - "\n", - " with infinite_loop_():\n", - " # Update the parameters\n", - " video_mode.load_parameters()\n", - " # Update the dc_offset of the channel connected to the OPX analog input 1\n", - " set_dc_offset(\"filter_cavity_1\", \"single\", dc_offset_1)\n", - " set_dc_offset(\"filter_cavity_2\", \"single\", dc_offset_2)\n", - " # Measure and integrate the signal received by the OPX\n", - " measure(\n", - " \"readout\",\n", - " \"detector_DC\",\n", - " None,\n", - " integration.full(\"constant\", single_shot_1, \"out1\"),\n", - " integration.full(\"constant\", single_shot_2, \"out2\"),\n", - " )\n", - " # Save the measured value to its stream\n", - " save(single_shot_1, signal1_st)\n", - " save(single_shot_2, signal2_st)\n", - " # Wait between each iteration\n", - " wait(1000)\n", - "\n", - " with stream_processing():\n", - " signal1_st.buffer(1000).save(\"signal1\")\n", - " signal2_st.buffer(1000).save(\"signal2\")\n", - " return prog\n", - "\n", - "\n", - "if __name__ == \"__main__\":\n", - " # Open the Quantum Machine Manager\n", - " qmm = QuantumMachinesManager(qop_ip, cluster_name=cluster_name)\n", - " # Open the Quantum Machine\n", - " qm = qmm.open_qm(config)\n", - " # Define the parameters to be updated in video mode with their initial value\n", - " param_dict = {\n", - " \"dc_offset_1\": (0.0, fixed),\n", - " \"dc_offset_2\": (0.0, fixed)\n", - " }\n", - " # Initialize the video mode\n", - " video_mode = VideoMode(qm, param_dict)\n", - " # Get the QUA program\n", - " prog = qua_prog(video_mode)\n", - " # Execute the QUA program in video mode\n", - " job = video_mode.execute(prog)\n", - " # Get the results from the OPX in live mode\n", - " results = fetching_tool(job, [\"signal1\", \"signal2\"], mode=\"live\")\n", - " # Live plotting\n", - " fig = plt.figure()\n", - " while results.is_processing():\n", - " # Fetch data from the OPX\n", - " signal1, signal2 = results.fetch_all()\n", - " # Convert the data into Volt\n", - " signal1 = -signal1 * 2**12 / readout_len\n", - " signal2 = -signal2 * 2**12 / readout_len\n", - " # Plot the data\n", - " plt.cla()\n", - " plt.plot(signal1, \"b-\")\n", - " plt.plot(signal2, \"r-\")\n", - " plt.title(\"Error signal [a.u.]\")\n", - " plt.xlabel(\"Time [μs]\")\n", - " plt.ylabel(\"Amplitude Error [arb. units]\")\n", - " plt.ylim((-0.5, 0.5))\n", - " plt.pause(0.1)\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.10.0" - } - }, - "nbformat": 4, - "nbformat_minor": 4 -} diff --git a/qualang_tools/live_mode/__init__.py b/qualang_tools/live_mode/__init__.py index 193cf771..b8f51924 100644 --- a/qualang_tools/live_mode/__init__.py +++ b/qualang_tools/live_mode/__init__.py @@ -1,3 +1,3 @@ -from .livemode import LiveMode, ParameterTable +from qualang_tools.live_mode.livemode import LiveMode, ParameterTable __all__ = ["LiveMode", "ParameterTable"] From 9e1e304d34dc7e56ed2df19481efcef3cb97c4c8 Mon Sep 17 00:00:00 2001 From: TheoQM Date: Wed, 23 Oct 2024 11:08:18 +0200 Subject: [PATCH 06/10] Updated video mode -> live mode --- examples/live_mode/PID_example.py | 20 +++++++++---------- examples/live_mode/README.md | 6 +++--- examples/live_mode/basic_example.py | 22 ++++++++++----------- examples/live_mode/notebook_example.ipynb | 24 +++++++++++------------ 4 files changed, 36 insertions(+), 36 deletions(-) diff --git a/examples/live_mode/PID_example.py b/examples/live_mode/PID_example.py index ff91393d..5cf4574f 100644 --- a/examples/live_mode/PID_example.py +++ b/examples/live_mode/PID_example.py @@ -4,7 +4,7 @@ from qualang_tools.addons.variables import assign_variables_to_element from qualang_tools.results import fetching_tool from configuration import * -from qualang_tools.video_mode.livemode import LiveMode +from qualang_tools.live_mode.livemode import LiveMode #################### @@ -47,7 +47,7 @@ def PID_derivation(input_signal, bitshift_scale_factor, gain_P, gain_I, gain_D, ################### # The QUA program # ################### -def PID_prog(vm: LiveMode, PDH_angle: float = 0.0): +def PID_prog(lm: LiveMode, PDH_angle: float = 0.0): with program() as prog: # Results variables I = declare(fixed) @@ -56,7 +56,7 @@ def PID_prog(vm: LiveMode, PDH_angle: float = 0.0): single_shot_AC = declare(fixed) dc_offset_1 = declare(fixed) # PID variables - vm.declare_variables() + lm.declare_variables() # Streams single_shot_st = declare_stream() error_st = declare_stream() @@ -71,7 +71,7 @@ def PID_prog(vm: LiveMode, PDH_angle: float = 0.0): with infinite_loop_(): # with for_(n, 0, n < N_shots, n + 1): # Update the PID parameters based on the user input. - vm.load_parameters() + lm.load_parameters() # Ensure that the two digital oscillators will start with the same phase reset_phase("phase_modulator") reset_phase("detector_AC") @@ -98,7 +98,7 @@ def PID_prog(vm: LiveMode, PDH_angle: float = 0.0): ) assign(single_shot_AC, I) # PID correction signal - correction, error, int_error, der_error = PID_derivation(single_shot_DC, *vm.variables) + correction, error, int_error, der_error = PID_derivation(single_shot_DC, *lm.variables) # Update the DC offset assign(dc_offset_1, dc_offset_1 + correction) # Handle saturation - Make sure that the DAC won't be asked to output more than 0.5V @@ -133,7 +133,7 @@ def PID_prog(vm: LiveMode, PDH_angle: float = 0.0): qmm = QuantumMachinesManager(qop_ip, cluster_name=cluster_name) # Open the Quantum Machine qm = qmm.open_qm(config) - # Define the parameters to be updated in video mode with their initial value and QUA type + # Define the parameters to be updated in live mode with their initial value and QUA type param_dict = { "bitshift_scale_factor": (3, int), "gain_P": (-1e-4, fixed), # The proportional gain @@ -142,11 +142,11 @@ def PID_prog(vm: LiveMode, PDH_angle: float = 0.0): "alpha": (0.0, fixed), # The ratio between integration and proportional error "target": (0.0, fixed), # The target value } - # Initialize the video mode - video_mode = LiveMode(qm, param_dict) + # Initialize the live mode + live_mode = LiveMode(qm, param_dict) # Get the QUA program - qua_prog = PID_prog(video_mode) - job = video_mode.execute(qua_prog) + qua_prog = PID_prog(live_mode) + job = live_mode.execute(qua_prog) # Get the results from the OPX in live mode data_list = ["error", "int_err", "der_err", "single_shot", "offset"] results = fetching_tool(job, data_list, mode="live") diff --git a/examples/live_mode/README.md b/examples/live_mode/README.md index 361df5f1..fa4b973d 100644 --- a/examples/live_mode/README.md +++ b/examples/live_mode/README.md @@ -1,8 +1,8 @@ -# Video Mode usage examples +# Live Mode usage examples -__Package__: https://github.com/qua-platform/py-qua-tools/tree/main/qualang_tools/video_mode +__Package__: https://github.com/qua-platform/py-qua-tools/tree/main/qualang_tools/live_mode -This package contains modules to set the OPX in a video-mode, where pre-defined parameters of the QUA program can be +This package contains modules to set the OPX in a live-mode, where pre-defined parameters of the QUA program can be updated while looking at data extracted and plotted in real-time. In this example folder, you will find two files: diff --git a/examples/live_mode/basic_example.py b/examples/live_mode/basic_example.py index fa61e63c..28adbdae 100644 --- a/examples/live_mode/basic_example.py +++ b/examples/live_mode/basic_example.py @@ -3,23 +3,23 @@ from qm import QuantumMachinesManager from qualang_tools.results import fetching_tool from configuration import * -from qualang_tools.video_mode.livemode import LiveMode +from qualang_tools.live_mode.livemode import LiveMode -def qua_prog(vm: LiveMode): +def qua_prog(lm: LiveMode): with program() as prog: # Results variables single_shot_1 = declare(fixed) single_shot_2 = declare(fixed) - # Get the parameters from the video mode - dc_offset_1, dc_offset_2 = vm.declare_variables() + # Get the parameters from the live mode + dc_offset_1, dc_offset_2 = lm.declare_variables() # Streams signal1_st = declare_stream() signal2_st = declare_stream() with infinite_loop_(): # Update the parameters - vm.load_parameters() + lm.load_parameters() # Update the dc_offset of the channel connected to the OPX analog input 1 set_dc_offset("filter_cavity_1", "single", dc_offset_1) set_dc_offset("filter_cavity_2", "single", dc_offset_2) @@ -48,17 +48,17 @@ def qua_prog(vm: LiveMode): qmm = QuantumMachinesManager(qop_ip, cluster_name=cluster_name) # Open the Quantum Machine qm = qmm.open_qm(config) - # Define the parameters to be updated in video mode with their initial value and QUA type + # Define the parameters to be updated in live mode with their initial value and QUA type param_dict = { "dc_offset_1": (0.0, fixed), "dc_offset_2": (0.0, fixed), } - # Initialize the video mode - video_mode = LiveMode(qm, param_dict) + # Initialize the live mode + live_mode = LiveMode(qm, param_dict) # Get the QUA program - qua_prog = qua_prog(video_mode) - # Execute the QUA program in video mode - job = video_mode.execute(qua_prog) + qua_prog = qua_prog(live_mode) + # Execute the QUA program in live mode + job = live_mode.execute(qua_prog) # Get the results from the OPX in live mode results = fetching_tool(job, ["signal1", "signal2"], mode="live") # Live plotting diff --git a/examples/live_mode/notebook_example.ipynb b/examples/live_mode/notebook_example.ipynb index 244cb741..345db631 100644 --- a/examples/live_mode/notebook_example.ipynb +++ b/examples/live_mode/notebook_example.ipynb @@ -19,7 +19,7 @@ "from qm import QuantumMachinesManager\n", "from qualang_tools.results import fetching_tool\n", "from configuration import *\n", - "from qualang_tools.video_mode import VideoMode" + "from qualang_tools.live_mode import LiveMode" ] }, { @@ -56,7 +56,7 @@ "start\n", "List of implemented commands: \n", " get: returns the current value of the parameters. \n", - " stop: quit VideoMode. \n", + " stop: quit LiveMode. \n", " done: resume program (if pause_program==True). \n", " help: displays the list of available commands. \n", " 'param_name'='param_value': sets the parameter to the specified value (ex: V1=0.152).\n", @@ -99,20 +99,20 @@ ], "source": [ "\n", - "def qua_prog(video_mode: VideoMode):\n", + "def qua_prog(live_mode: LiveMode):\n", " with program() as prog:\n", " # Results variables\n", " single_shot_1 = declare(fixed)\n", " single_shot_2 = declare(fixed)\n", - " # Get the parameters from the video mode\n", - " dc_offset_1, dc_offset_2 = video_mode.declare_variables()\n", + " # Get the parameters from the live mode\n", + " dc_offset_1, dc_offset_2 = live_mode.declare_variables()\n", " # Streams\n", " signal1_st = declare_stream()\n", " signal2_st = declare_stream()\n", "\n", " with infinite_loop_():\n", " # Update the parameters\n", - " video_mode.load_parameters()\n", + " live_mode.load_parameters()\n", " # Update the dc_offset of the channel connected to the OPX analog input 1\n", " set_dc_offset(\"filter_cavity_1\", \"single\", dc_offset_1)\n", " set_dc_offset(\"filter_cavity_2\", \"single\", dc_offset_2)\n", @@ -141,17 +141,17 @@ " qmm = QuantumMachinesManager(qop_ip, cluster_name=cluster_name)\n", " # Open the Quantum Machine\n", " qm = qmm.open_qm(config)\n", - " # Define the parameters to be updated in video mode with their initial value\n", + " # Define the parameters to be updated in live mode with their initial value\n", " param_dict = {\n", " \"dc_offset_1\": (0.0, fixed),\n", " \"dc_offset_2\": (0.0, fixed)\n", " }\n", - " # Initialize the video mode\n", - " video_mode = VideoMode(qm, param_dict)\n", + " # Initialize the live mode\n", + " live_mode = LiveMode(qm, param_dict)\n", " # Get the QUA program\n", - " prog = qua_prog(video_mode)\n", - " # Execute the QUA program in video mode\n", - " job = video_mode.execute(prog)\n", + " prog = qua_prog(live_mode)\n", + " # Execute the QUA program in live mode\n", + " job = live_mode.execute(prog)\n", " # Get the results from the OPX in live mode\n", " results = fetching_tool(job, [\"signal1\", \"signal2\"], mode=\"live\")\n", " # Live plotting\n", From 1ef688982f09c5ee3231e7a15f578401070d3da1 Mon Sep 17 00:00:00 2001 From: TheoQM Date: Wed, 23 Oct 2024 11:11:20 +0200 Subject: [PATCH 07/10] Updated import --- examples/live_mode/PID_example.py | 2 +- examples/live_mode/basic_example.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/live_mode/PID_example.py b/examples/live_mode/PID_example.py index 5cf4574f..9cd18f7b 100644 --- a/examples/live_mode/PID_example.py +++ b/examples/live_mode/PID_example.py @@ -4,7 +4,7 @@ from qualang_tools.addons.variables import assign_variables_to_element from qualang_tools.results import fetching_tool from configuration import * -from qualang_tools.live_mode.livemode import LiveMode +from qualang_tools.live_mode import LiveMode #################### diff --git a/examples/live_mode/basic_example.py b/examples/live_mode/basic_example.py index 28adbdae..13f8a2bc 100644 --- a/examples/live_mode/basic_example.py +++ b/examples/live_mode/basic_example.py @@ -3,7 +3,7 @@ from qm import QuantumMachinesManager from qualang_tools.results import fetching_tool from configuration import * -from qualang_tools.live_mode.livemode import LiveMode +from qualang_tools.live_mode import LiveMode def qua_prog(lm: LiveMode): From b45973582aabaa5161fcd77ff417316c5fd6f3c8 Mon Sep 17 00:00:00 2001 From: TheoQM Date: Wed, 23 Oct 2024 11:19:59 +0200 Subject: [PATCH 08/10] fix init --- qualang_tools/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qualang_tools/__init__.py b/qualang_tools/__init__.py index 24c5ac69..31ac6d2e 100644 --- a/qualang_tools/__init__.py +++ b/qualang_tools/__init__.py @@ -3,7 +3,7 @@ "bakery", "config", "control_panel", - "live_mode" + "live_mode", "loops", "results", "plot", From 2f677f72206f8f0bba373be5f297f93cd1351b3a Mon Sep 17 00:00:00 2001 From: TheoQM Date: Wed, 23 Oct 2024 11:20:18 +0200 Subject: [PATCH 09/10] changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ddcc9b04..305d5f7f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) ## [Unreleased] +### Changed +- **Breaking change!** - `video_mode` became `live_mode` to avoid confusion with the video mode tool used to tune up quantum dots devices. ## [0.18.0] - 2024-10-23 ### Added From fe97a66a26e3391c04e086f7b3abdac91b286658 Mon Sep 17 00:00:00 2001 From: TheoQM Date: Wed, 23 Oct 2024 11:36:48 +0200 Subject: [PATCH 10/10] update test --- tests/{test_videomode.py => test_livemode.py} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename tests/{test_videomode.py => test_livemode.py} (98%) diff --git a/tests/test_videomode.py b/tests/test_livemode.py similarity index 98% rename from tests/test_videomode.py rename to tests/test_livemode.py index 3857e3b4..d4c4d2d3 100644 --- a/tests/test_videomode.py +++ b/tests/test_livemode.py @@ -1,8 +1,8 @@ -# Tests for the video mode functionality +# Tests for the live mode functionality import pytest from qm.qua import * import numpy as np -from qualang_tools.video_mode import ParameterTable +from qualang_tools.live_mode import ParameterTable def gauss(amplitude, mu, sigma, length):