# Sweeping ## Basic Example A Sweep is created out of two main components, an iterable **pointer** and a variable number of **actions**. Both **pointer** and **actions** may generate **records**. The most bare example would look like this: ```python >>> for data in Sweep(range(3)): >>> print(data) {} {} {} ``` In this example, the ``range(3)`` iterable object is our **pointer**. This Sweep does not contain any **actions** or generate any **records**, but instead simply loops over the iterable **pointer**. ## Recording Data ### Concepts Even though the **pointer** in the previous example does generate data, we cannot see it when we iterate through the sweep. To have **pointers** and **actions** generate visible data, we need to annotate them so that they generate **records**. For a Sweep to know that either a **pointer** (an iterable object), or an **action** (a callable object) they need to be wrapped by an instance of {py:class}`DataSpec `. {py:class}`DataSpec ` is a [data class](https://docs.python.org/3/library/dataclasses.html) that holds information about the variable itself. Understanding the inner workings are not necessary to fully utilize Sweeps, however, it is good to know they exists and what information they hold. Annotating a function or generator does not change what they do, it gets the values generated by them and places them inside a dictionary with the key for those values being the name of the variable we assign. The important fields of a {py:class}`DataSpec ` are its `name` and `depends_on` fields. `name` simply indicates the name of the variable, e.i. the key that the sweep will have for the value of this variable. `depends_on` indicates whether the variable is an independent variable (we control) or a dependent variable (the things we are trying to measure). If ``depends_on=None`` it means this variable is an independent variable. If ``depends_on=['x']``, this variable is dependent on a separate variable with name ``x``. If ``depends_on=[]``, the variable will be automatically assigned as a dependent of all other independents in the same Sweep. :::{note} {py:class}`DataSpec ` also contains two more fields: `unit` and `types`, these however, have no impact in the way the code behaves and are for adding extra metadata for the user. ::: While this might seem like a lot of information, its use is very intuitive and easy to use once you get used to it. ### Implementation To wrap functions we use the {py:func}`recording ` decorator on the function we want to annotate: ```python >>> @recording(DataSpec('x'), DataSpec('y', depends_on=['x'], type='array')) >>> def measure_stuff(n, *args, **kwargs): >>> return n, np.random.normal(size=n) >>> >>> measure_stuff(1) {'x': 1, 'y': array([0.70663348])} ``` In the example above we annotate the function ``measure_stuff()`` indicating that the first item it returns is ``x``, an independent variable since it does not have a ``depends_on`` field, and the second item is ``y``, a variable that depends on ``x``. We can annotate generators in the same way: ```python >>> @recording(DataSpec('a')) >>> def make_sequence(n): >>> for i in range(n): >>> yield i >>> >>> for data in make_sequence(3): >>> print(data) {'a': 0} {'a': 1} {'a': 2} ``` A nicer way of creating {py:class}`DataSpec ` instances is to use the functions {py:func}`independent ` and {py:func}`dependent `. This function just makes the recording of data easier to read. {py:func}`independent ` does not let you indicate the ``depends_on`` field while {py:func}`dependent `, has an empty list (indicating that it depends an all other independents) as a default. ```python >>> @recording(independent('x'), dependent('y', type='array')) >>> def measure_stuff(n, *args, **kwargs): >>> return n, np.random.normal(size=n) >>> >>> measure_stuff(1) {'x': 1, 'y': array([1.60113794])} ``` :::{note} You can also use the shorter versions of these functions: * {py:func}`labcore.measurement.record.ds` for shorter {py:class}`DataSpec ` * {py:func}`labcore.measurement.record.indep` for shorter {py:func}`independent ` * {py:func}`labcore.measurement.record.dep` for shorter {py:func}`dependent ` ::: Sometimes we don't want to annotate a function or generator itself, but instead we want to annotate at the moment of execution. For this we can use the function {py:func}`record_as ` to annotate any function or generator on the fly: ```python >>> def get_some_data(n): >>> return np.random.normal(size=n) >>> >>> record_as(get_some_data, independent('random_var'))(3) {'random_var': array([0.16099358, 0.74873271, 0.01160423])} ``` You can add multiple {py:class}`DataSpec ` with in a single {py:func}`record_as `: ```python >>> for data in record_as(zip(np.linspace(0,1,3), np.arange(3)), indep('x'), dep('y')): >>> print(data) {'x': 0.0, 'y': 0} {'x': 0.2, 'y': 1} {'x': 0.4, 'y': 2} ``` It will also make sure to add items for annotated **records** (by adding ``None`` items to any empty **record**) that do not have any values assigned to them: ```python >>> for data in record_as(np.linspace(0,1,3), indep('x'), dep('y')): >>> print(data) {'x': 0.0, 'y': None} {'x': 0.5, 'y': None} {'x': 1.0, 'y': None} ``` And it will ignore any extra values that are not annotated: ```python >>> for data in record_as(zip(np.linspace(0,1,3), np.arange(3)), indep('x')): >>> print(data) {'x': 0.0} {'x': 0.5} {'x': 1.0} ``` ## Construction of Sweeps Now that we know how to annotate data so that it generates records, we can finally start creating Sweeps that creates some data. A Sweep is composed of two main parts: **pointers** and **actions**: * **Pointers** are iterables that the Sweep iterates through, these usually represent the independent variables of our experiments. * **Actions** are callables that get called after each iteration of our **pointer** and usually are in charge of performing anything that needs to happen at every iteration of the experiment. This can be either set up a instruments and usually includes measuring a dependent variable too. Both **pointers** and **actions** can generate **records** if annotated correctly, but it is not a requirement. ### Basic Sweeps A basic annotated Sweep looks something like this: ```python def my_func(): return 0 sweep = Sweep( record_as(range(3), independent('x')), # This is the pointer. We specify 'x' as an independent (we control it). record_as(my_func, dependent('y'))) # my_func is an action. We specify 'y' as a dependent. ``` Once the Sweep is created we can see the **records** it will produce by using the function method {py:meth}`get_data_specs `: ```python >>> sweep.get_data_specs() (x, y(x)) ``` Printing a Sweep will also display more information about, specifying the pointers, the actions taken afterwards and the **records** it will produce: ```python >>> print(sweep) range(0, 3) as {x} >> my_func() as {y} ==> {x, y(x)} ``` Now to run the Sweep we just have to iterate through it: ```python >>> for data in sweep: >>> print(data) {'x': 0, 'y': 0} {'x': 1, 'y': 0} {'x': 2, 'y': 0} ``` If you are trying to sweep over a single parameter, a more convenient syntax for creating Sweep is to utilize the {py:func}`sweep_parameter ` function: ```python >>> sweep = sweep_parameter('x', range(3), record_as(my_func, 'y')) >>> for data in sweep: >>> print(data) {'x': 0, 'y': 0} {'x': 1, 'y': 0} {'x': 2, 'y': 0} ``` There is no restriction on how many parameters a **pointer** or an **action** can generate as long as each parameter is properly annotated. ```python >>> def my_func(): >>> return 1, 2 >>> >>> sweep = Sweep( >>> record_as(zip(range(3), ['a', 'b', 'c']), independent('number'), independent('string')), # a pointer with two parameters >>> record_as(my_func, 'one', 'two')) # an action with two parameters >>> >>> print(sweep.get_data_specs()) >>> >>> for data in sweep: >>> print(data) (number, string, one(number, string), two(number, string)) {'number': 0, 'string': 'a', 'one': 1, 'two': 2} {'number': 1, 'string': 'b', 'one': 1, 'two': 2} {'number': 2, 'string': 'c', 'one': 1, 'two': 2} ``` ### Specifying Options Before Executing a Sweep Many **actions** we are using take optional parameters that we only want to specify just before executing the Sweep (but are constant throughout the Sweep). If we don't want to resort to global variables we can do so by using the method {py:meth}`set_options `. It accepts the names of any **action** function in that Sweep as keywords, and dictionaries containing keyword arguments to pass to those functions as values. Keywords specified in this way always override keywords that are passed around internally in the sweep (for more information see the [Passing Parameters in a Sweep](#passing-parameters-in-a-sweep) section): ```python >>> def test_fun(a_property=False, *args, **kwargs): >>> print('inside test_fun:') >>> print(f"a_property: {a_property}") >>> print(f"other stuff:", args, kwargs) >>> print('----') >>> return 0 >>> >>> sweep = sweep_parameter('value', range(3), record_as(test_fun, dependent('data'))) >>> sweep.set_options(test_fun=dict(a_property=True, another_property='Hello')) >>> >>> for data in sweep: >>> print("Data:", data) >>> print('----') inside test_fun: property: True other stuff: () {'value': 0, 'another_property': 'Hello'} ---- Data: {'value': 0, 'data': 0} ---- inside test_fun: property: True other stuff: () {'value': 1, 'data': 0, 'another_property': 'Hello'} ---- Data: {'value': 1, 'data': 0} ---- inside test_fun: property: True other stuff: () {'value': 2, 'data': 0, 'another_property': 'Hello'} ---- Data: {'value': 2, 'data': 0} ``` ### A QCoDeS Parameter Sweep If you are using QCoDeS to interact with hardware, it is very common to want to do a sweep over a QCoDeS parameter. In this minimal example we set a parameter (``x``) to a range of values, and get data from another parameter for each set value. ```python >>> def measure_stuff(): >>> return np.random.normal() >>> >>> x = Parameter('x', set_cmd=lambda x: print(f'setting x to {x}'), initial_value=0) # QCoDeS Parameter >>> data = Parameter('data', get_cmd=lambda: np.random.normal()) # QCoDeS Parameter >>> >>> for record in sweep_parameter(x, range(3), get_parameter(data)): >>> print(record) setting x to 0 setting x to 0 {'x': 0, 'data': -0.4990053668503893} setting x to 1 {'x': 1, 'data': -0.5132204673887943} setting x to 2 {'x': 2, 'data': 1.8634243556469932} ``` ### Sweep Combinations One of the most valuable features of Sweeps is their ability to be combined through the use of operators. This allows us to mix and match different aspects of an experiment without having to rewrite code. We can combine different Sweeps with each other or different annotated **actions** ### Appending The most basic combination of Sweeps is appending them. When appending two Sweeps, the resulting sweep will execute the first Sweep to completion followed by the second Sweep to completion. To append two Sweeps or actions we use the ``+`` symbol: ```python >>> def get_random_number(): >>> return np.random.rand() >>> >>> Sweep.record_none = False # See note on what this does. >>> >>> sweep_1 = sweep_parameter('x', range(3), record_as(get_random_number, dependent('y'))) >>> sweep_2 = sweep_parameter('a', range(4), record_as(get_random_number, dependent('b'))) >>> my_sweep = sweep_1 + sweep_2 >>> >>> for data in my_sweep: >>> print(data) {'x': 0, 'y': 0.34404570192577155} {'x': 1, 'y': 0.02104831292457654} {'x': 2, 'y': 0.9006367857458307} {'a': 0, 'b': 0.10539935409724577} {'a': 1, 'b': 0.9368463758729733} {'a': 2, 'b': 0.9550070757291859} {'a': 3, 'b': 0.9812445448108895} ``` :::{note} {py:attr}`record_none ` controls whether we include data fields that have returned nothing during setting a pointer or executing an action. Setting it to true (the default) guarantees that each data spec of the sweep has an entry per sweep point, even if it is ``None``. ::: For more information see: [Passing Parameters in a Sweep](#passing-parameters-in-a-sweep) section. ### Multiplying By multiplying we refer to an inner product, i.e. the result is what you'd expect from [`zip`](https://docs.python.org/3.3/library/functions.html#zip]) two iterables. To multiply two Sweeps or actions we use the ``*`` symbol. A basic example is if we have a sweep and want to attach another action to each sweep point: ```python >>> my_sweep = ( >>> sweep_parameter('x', range(3), record_as(get_random_number, dependent('data_1'))) >>> * record_as(get_random_number, dependent('data_2')) >>> ) >>> >>> print(sweep.get_data_specs()) >>> print('----') >>> >>> for data in my_sweep: >>> print(data) (x, data_1(x), data_2(x)) ---- {'x': 0, 'data_1': 0.12599818360565485, 'data_2': 0.09261266841087679} {'x': 1, 'data_1': 0.5665798938860637, 'data_2': 0.7493750740615404} {'x': 2, 'data_1': 0.9035085438172156, 'data_2': 0.5419023528195611} ``` If you are combining two different Sweeps, then we get zip-like behavior while maintain the dependency structure separate: ```python >>> my_sweep = ( >>> sweep_parameter('x', range(3), record_as(get_random_number, dependent('data_1'))) >>> * sweep_parameter('y', range(5), record_as(get_random_number, dependent('data_2'))) >>> ) >>> >>> print(sweep.get_data_specs()) >>> print('----') >>> >>> for data in my_sweep: >>> print(data) (x, data_1(x), y, data_2(y)) ---- {'x': 0, 'data_1': 0.3808452915069015, 'y': 0, 'data_2': 0.14309246334791337} {'x': 1, 'data_1': 0.6094608905204076, 'y': 1, 'data_2': 0.3560530722571186} {'x': 2, 'data_1': 0.15950240245080072, 'y': 2, 'data_2': 0.2477391943438858} ``` ### Nesting Nesting two Sweeps runs the entire second Sweep for each point of the first Sweep. A basic example is if we have multiple Sweep parameters against each other and we want to perform a measurement at each point. To nest two Sweeps we use the ``@`` symbol: ```python >>> def measure_something(): >>> return np.random.rand() >>> >>> my_sweep = ( >>> sweep_parameter('x', range(3)) >>> @ sweep_parameter('y', np.linspace(0,1,3)) >>> @ record_as(measure_something, 'my_data') >>> ) >>> >>> for data in my_sweep: >>> print(data) {'x': 0, 'y': 0.0, 'my_data': 0.727404046865409} {'x': 0, 'y': 0.5, 'my_data': 0.11112429412122715} {'x': 0, 'y': 1.0, 'my_data': 0.09081900115421426} {'x': 1, 'y': 0.0, 'my_data': 0.8160224024098803} {'x': 1, 'y': 0.5, 'my_data': 0.1517092154216605} {'x': 1, 'y': 1.0, 'my_data': 0.9253018251769569} {'x': 2, 'y': 0.0, 'my_data': 0.881089486629102} {'x': 2, 'y': 0.5, 'my_data': 0.3897577898200387} {'x': 2, 'y': 1.0, 'my_data': 0.6895312744116066} ``` Nested sweeps can be as complex as needed, with as many actions as they need. An example of this can be executing measurements on each nested level: ```python >>> def measure_something(): >>> return np.random.rand() >>> >>> sweep_1 = sweep_parameter('x', range(3), record_as(measure_something, 'a')) >>> sweep_2 = sweep_parameter('y', range(2), record_as(measure_something, 'b')) >>> my_sweep = sweep_1 @ sweep_2 @ record_as(get_random_number, 'more_data') >>> >>> for data in my_sweep: >>> print(data) {'x': 0, 'a': 0.09522178419462424, 'y': 0, 'b': 0.1821505218348034, 'more_data': 0.13257002268089835} {'x': 0, 'a': 0.09522178419462424, 'y': 1, 'b': 0.014940266372080457, 'more_data': 0.9460879863404558} {'x': 1, 'a': 0.13994892182170526, 'y': 0, 'b': 0.4708657480125388, 'more_data': 0.12792337523097086} {'x': 1, 'a': 0.13994892182170526, 'y': 1, 'b': 0.8209492135277935, 'more_data': 0.23270477191895111} {'x': 2, 'a': 0.06159208933324678, 'y': 0, 'b': 0.651545802505077, 'more_data': 0.8944257582518365} {'x': 2, 'a': 0.06159208933324678, 'y': 1, 'b': 0.9064557565446919, 'more_data': 0.8258102740474211} ``` :::{note} All operators symbols are just there for syntactic brevity. All three of them have corresponding functions attached to them: * Appending: {py:func}`append_sweeps ` * Multiplying: {py:func}`zip_sweeps ` * Nesting: {py:func}`nest_sweeps ` ::: ## Passing Parameters in a Sweep Often times our measurement actions depend on the states of previous steps. Because of that, everything that is generated by **pointers**, **actions** or other Sweeps can be passed on subsequently executed elements. :::{note} There are two different Sweep configuration related to passing arguments in Sweeps. For more information on them see the [Configuring Sweeps](#configuring-sweeps). ::: ### Positional Arguments When there are no record annotations, the values generated `only` by **pointers** are passed as positional arguments to all actions, but values generated by **actions** are not passed to other **actions**: ```python >>> def test(*args, **kwargs): >>> print('test:', args, kwargs) >>> return 101 >>> >>> def test_2(*args, **kwargs): >>> print('test_2:', args, kwargs) >>> return 102 >>> >>> for data in Sweep(range(3), test, test_2): >>> print(data) test: (0,) {} test_2: (0,) {} {} test: (1,) {} test_2: (1,) {} {} test: (2,) {} test_2: (2,) {} {} ``` Because it would get too confusing otherwise, positional arguments only get passed when originating from a **pointer** to all **actions** in a single sweep. Meaning that if we combine two or more sweeps, positional arguments would only get to the **actions** of their respective Sweeps: ```python >>> for data in Sweep(range(3), test) * Sweep(zip(['x', 'y'], [True, False]), test): >>> print(data) (0,) {} ('x', True) {} {} (1,) {} ('y', False) {} {} (2,) {} ``` As we can see the ``test`` function in the second sweep is only getting (``x``, ``True``) or (``y``, ``False``) but not any arguments from the first Sweep. It is also important to note that hte values generated by either ``test`` function are not being passed to any other object. In previous examples, the functions we used were accepting the arguments because their signature included variation positional arguments (``*args``). The situation changes when this is not the case. **Actions** only receive arguments that they can accept: ```python >>> def test_3(x=10): >>> print(x) >>> return True >>> >>> for data in Sweep(zip([1,2], [3,4]), test_3): >>> pass 1 2 ``` As we can see, ``test_3`` only accepted the first argument. ### Keyword Arguments Passing keyword arguments is more flexible. Any **record** that gets produced is passed to all subsequent **pointers** or **actions** in the sweep that accept that keyword. This is true even across different sub-sweeps. If a **pointer** yields non-annotated values, these are still used as positional arguments, but only when accepted, and with higher priority given to keywords. In the following example we can see this in action: ```python >>> def test(x, y, z=5): >>> print(f'my three arguments, x: {x}, y: {y}, z: {z}') >>> return x, y, z >>> >>> def print_all_args(*args, **kwargs): >>> print(f'arguments at the end of the line, args: {args}, kwargs: {kwargs}') >>> >>> sweep = sweep_parameter('x', range(3), record_as(test, dep('xx'), dep('yy'), dep('zz'))) * \ >>> Sweep(range(3), print_all_args) >>> for data in sweep: >>> pass my three arguments, x: 0, y: None, z: 5 arguments at the end of the line, args:(0,), kwargs:{'x': 0, 'xx': 0, 'zz': 5} my three arguments, x: 1, y: None, z: 5 arguments at the end of the line, args:(1,), kwargs:{'x': 1, 'xx': 1, 'zz': 5} my three arguments, x: 2, y: None, z: 5 arguments at the end of the line, args:(2,), kwargs:{'x': 2, 'xx': 2, 'zz': 5} ``` In the example above we have two different sweeps. The **pointer** of the first one is producing **records** which is why we see its value in the test function for ``x``. Since the first sweep is being multiplied to the second sweep we can see how all the **records** (both produced by the **pointer** and **action**) of the first sweep reach the second sweep as keyword arguments, and the non-annotated value of its own **pointer** reaches the action of the second sweep as a positional argument. :::{warning} When creating **records**, it is very important that each **record** has a *unique* name. Having multiple variables create **records** with the same names, will make the passing of arguments behave in unpredictable ways. ::: A simple way of renaming conflicting arguments and **records** is to use the combination of ``lambda`` and {py:func}`record_as `: ```python >>> sweep = ( >>> Sweep(record_as(zip(range(3), range(10,13)), independent('x'), independent('y')), record_as(test, dependent('xx'), dependent('yy'), dependent('zz'))) >>> @ record_as(lambda xx, yy, zz: test(xx, yy, zz), dependent('some'), dependent('different'), dependent('names')) >>> @ print_all_args >>> + print_all_args) >>> >>> print(sweep.get_data_specs()) >>> >>> for data in sweep: >>> print("data:", data) (x, y, xx(x, y), yy(x, y), zz(x, y), some(x, y), different(x, y), names(x, y)) my three arguments: 0 10 5 my three arguments: 0 10 5 arguments at the end of the line: () {'x': 0, 'y': 10, 'xx': 0, 'yy': 10, 'zz': 5, 'some': 0, 'different': 10, 'names': 5} data: {'x': 0, 'y': 10, 'xx': 0, 'yy': 10, 'zz': 5, 'some': 0, 'different': 10, 'names': 5} my three arguments: 1 11 5 my three arguments: 1 11 5 arguments at the end of the line: () {'x': 1, 'y': 11, 'xx': 1, 'yy': 11, 'zz': 5, 'some': 1, 'different': 11, 'names': 5} data: {'x': 1, 'y': 11, 'xx': 1, 'yy': 11, 'zz': 5, 'some': 1, 'different': 11, 'names': 5} my three arguments: 2 12 5 my three arguments: 2 12 5 arguments at the end of the line: () {'x': 2, 'y': 12, 'xx': 2, 'yy': 12, 'zz': 5, 'some': 2, 'different': 12, 'names': 5} data: {'x': 2, 'y': 12, 'xx': 2, 'yy': 12, 'zz': 5, 'some': 2, 'different': 12, 'names': 5} arguments at the end of the line: () {'x': 2, 'y': 12, 'xx': 2, 'yy': 12, 'zz': 5, 'some': 2, 'different': 12, 'names': 5} data: {} ``` ## Configuring Sweeps The class {py:class}`Sweep ` has three global parameters that are used to configure the behavior of it. ### record_none {py:attr}`record_none `, ``True`` by default, adds ``None`` to any **action** or **pointer** that didn't generate any **record** that iteration. This is useful if we want every variable we are storing to be composed of arrays of the same number of items: ```python >>> def get_random_number(): >>> return np.random.rand() >>> >>> sweep_1 = sweep_parameter('x', range(3), record_as(get_random_number, dependent('y'))) >>> sweep_2 = sweep_parameter('a', range(4), record_as(get_random_number, dependent('b'))) >>> my_sweep = sweep_1 + sweep_2 >>> >>> Sweep.record_none = False >>> print(f'----record_none=False----') >>> for data in my_sweep: >>> print(data) >>> >>> Sweep.record_none = True >>> print(f'----record_none=True----') >>> for data in my_sweep: >>> print(data) ----record_none=False---- {'x': 0, 'y': 0.804635124804199} {'x': 1, 'y': 0.24410055642545125} {'x': 2, 'y': 0.10828652013926787} {'a': 0, 'b': 0.4303128288315823} {'a': 1, 'b': 0.9498154942316515} {'a': 2, 'b': 0.7150406031589893} {'a': 3, 'b': 0.2012281139956017} ----record_none=True---- {'x': 0, 'y': 0.22753548379033073, 'a': None, 'b': None} {'x': 1, 'y': 0.9024597689210428, 'a': None, 'b': None} {'x': 2, 'y': 0.11393941613249503, 'a': None, 'b': None} {'x': None, 'y': None, 'a': 0, 'b': 0.8678669225696442} {'x': None, 'y': None, 'a': 1, 'b': 0.3537275760737344} {'x': None, 'y': None, 'a': 2, 'b': 0.23555393946522196} {'x': None, 'y': None, 'a': 3, 'b': 0.19388827122308672} ``` ### pass_on_returns {py:attr}`pass_on_returns `, ``True`` by default, specifies if we want arguments to be passed between sweeps. When it is set to ``False`` no **record** will be passed either as positional arguments or as keyword arguments: ```python >>> sweep = ( >>> sweep_parameter('y', range(3), record_as(test, dependent('xx'), dependent('yy'), dependent('zz'))) >>> @ print_all_args) >>> >>> Sweep.pass_on_returns = False >>> print(f'----pass_on_returns=False----') >>> for data in sweep: >>> print("data:", data) >>> >>> Sweep.pass_on_returns = True >>> print(f'----pass_on_returns=True----') >>> for data in sweep: >>> print("data:", data) ----pass_on_returns=False---- my three arguments: None None 5 arguments at the end of the line: () {} data: {'y': 0, 'xx': None, 'yy': None, 'zz': 5} my three arguments: None None 5 arguments at the end of the line: () {} data: {'y': 1, 'xx': None, 'yy': None, 'zz': 5} my three arguments: None None 5 arguments at the end of the line: () {} data: {'y': 2, 'xx': None, 'yy': None, 'zz': 5} ----pass_on_returns=True---- my three arguments: None 0 5 arguments at the end of the line: () {'zz': 5, 'y': 0, 'yy': 0} data: {'y': 0, 'xx': None, 'yy': 0, 'zz': 5} my three arguments: None 1 5 arguments at the end of the line: () {'zz': 5, 'y': 1, 'yy': 1} data: {'y': 1, 'xx': None, 'yy': 1, 'zz': 5} my three arguments: None 2 5 arguments at the end of the line: () {'zz': 5, 'y': 2, 'yy': 2} data: {'y': 2, 'xx': None, 'yy': 2, 'zz': 5} ``` ### pass_on_none {py:attr}`pass_on_none `, ``False`` by default, specifies if variables that return ``None`` should be passed as arguments to other **actions** or Sweeps (Because ``None`` is typically indicating that function did not return anything as data even though a record was declared using {py:func}`recording ` or {py:func}`record_as `): ```python >>> sweep = ( >>> sweep_parameter('y', range(3), record_as(test, dependent('xx'), dependent('yy'), dependent('zz'))) >>> @ print_all_args) >>> >>> Sweep.pass_on_none = False >>> print(f'----pass_on_none=False----') >>> for data in sweep: >>> print("data:", data) >>> >>> Sweep.pass_on_none = True >>> print(f'----pass_on_returns=True----') >>> for data in sweep: >>> print("data:", data) ----pass_on_none=False---- my three arguments: None 0 5 arguments at the end of the line: () {'y': 0, 'yy': 0, 'zz': 5} data: {'y': 0, 'xx': None, 'yy': 0, 'zz': 5} my three arguments: None 1 5 arguments at the end of the line: () {'y': 1, 'yy': 1, 'zz': 5} data: {'y': 1, 'xx': None, 'yy': 1, 'zz': 5} my three arguments: None 2 5 arguments at the end of the line: () {'y': 2, 'yy': 2, 'zz': 5} data: {'y': 2, 'xx': None, 'yy': 2, 'zz': 5} ----pass_on_returns=True---- my three arguments: None 0 5 arguments at the end of the line: (None,) {'y': 0, 'yy': 0, 'zz': 5, 'xx': None} data: {'y': 0, 'xx': None, 'yy': 0, 'zz': 5} my three arguments: None 1 5 arguments at the end of the line: (None,) {'y': 1, 'yy': 1, 'zz': 5, 'xx': None} data: {'y': 1, 'xx': None, 'yy': 1, 'zz': 5} my three arguments: None 2 5 arguments at the end of the line: (None,) {'y': 2, 'yy': 2, 'zz': 5, 'xx': None} data: {'y': 2, 'xx': None, 'yy': 2, 'zz': 5} ``` ## Running Sweeps As seen in previous examples, the most basic way of running a Sweep is to just iterate through it. This is simple but does not do much else. If we only want to store the data generated by a Sweep in disk for later analysis we can use the function {py:func}`run_and_save_sweep `: ```python >>> sweep = sweep_parameter('x', range(3), record_as(my_func, 'y')) >>> run_and_save_sweep(sweep, './data', 'my_data') Data location: data/2022-12-05/2022-12-05T142539_fbfce3e4-my_data/data.ddh5 The measurement has finished successfully and all of the data has been saved. ``` :::{note} Because this guide has been ported from an older page plottr is not being used anymore, a fix will come. ::: {py:func}`run_and_save_sweep ` automatically runs the Sweep indicated, stores the **records** generated by it in a :class:`DataDict ` in a ddh5 file with time tag followed by a random sequence followed by the third argument, in the directory passed by the second argument. Internally the function utilizes the :class:`DDH5Writer ` from `plottr`. For more information on how `plottr` handles data please see: :doc:`../plottr/data`. :::{note} {py:func}`run_and_save_sweep ` can save multiple objects to disk by accepting them as extra arguments. It is a good idea to read over its documentation if you want to be able to save things with it. ::: Sometimes we have an **action** that we want to run a single time, some kind of setup function or maybe a closing function (or any single **action** in between sweeps). If we also need this **action** to be a Sweep, the function {py:func}`once ` will create a Sweep with no **pointer** that runs an **action** a single time: ```python >>> def startup_function(): >>> print(f'starting an instrument') >>> >>> def closing_function(): >>> print(f'closing an instrument') >>> >>> sweep = sweep_parameter('x', range(3), record_as(my_func, 'y')) >>> starting_sweep = once(startup_function) >>> closing_sweep = once(closing_function) >>> >>> for data in starting_sweep + sweep + closing_sweep: >>> print(data) starting an instrument {} {'x': 0, 'y': 1} {'x': 1, 'y': 1} {'x': 2, 'y': 1} closing an instrument {} ```