File: graphical.rst

package info (click to toggle)
python-pymeasure 0.14.0-2
  • links: PTS, VCS
  • area: main
  • in suites: sid, trixie
  • size: 8,788 kB
  • sloc: python: 47,201; makefile: 155
file content (807 lines) | stat: -rw-r--r-- 43,368 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
###########################
Using a graphical interface
###########################

In the previous tutorial we measured the IV characteristic of a sample to show how we can set up a simple experiment in PyMeasure. The real power of PyMeasure comes when we also use the graphical tools that are included to turn our simple example into a full-fledged user interface.

.. _tutorial-plotterwindow:

Using the Plotter
~~~~~~~~~~~~~~~~~

While it lacks the nice features of the ManagedWindow, the Plotter object is the simplest way of getting live-plotting. The Plotter takes a Results object and plots the data at a regular interval, grabbing the latest data each time from the file.

.. warning::
   The example in this section is known to raise issues when executed: a `QApplication was not created in the main thread` / `nextEventMatchingMask should only be called from the Main Thread` warning is raised.
   While the example works without issues on some operating systems and python configurations, users are advised not to rely on the plotter while this issue is unresolved.
   Users can hence skip this example and continue with the `Using the ManagedWindow`_ section.


Let's extend our SimpleProcedure with a RandomProcedure, which generates random numbers during our loop. This example does not include instruments to provide a simpler example. ::

    import logging
    log = logging.getLogger(__name__)
    log.addHandler(logging.NullHandler())

    import random
    from time import sleep
    from pymeasure.log import console_log
    from pymeasure.display import Plotter
    from pymeasure.experiment import Procedure, Results, Worker
    from pymeasure.experiment import IntegerParameter, FloatParameter, Parameter

    class RandomProcedure(Procedure):

        iterations = IntegerParameter('Loop Iterations')
        delay = FloatParameter('Delay Time', units='s', default=0.2)
        seed = Parameter('Random Seed', default='12345')

        DATA_COLUMNS = ['Iteration', 'Random Number']

        def startup(self):
            log.info("Setting the seed of the random number generator")
            random.seed(self.seed)

        def execute(self):
            log.info("Starting the loop of %d iterations" % self.iterations)
            for i in range(self.iterations):
                data = {
                    'Iteration': i,
                    'Random Number': random.random()
                }
                self.emit('results', data)
                log.debug("Emitting results: %s" % data)
                self.emit('progress', 100 * i / self.iterations)
                sleep(self.delay)
                if self.should_stop():
                    log.warning("Caught the stop flag in the procedure")
                    break


    if __name__ == "__main__":
        console_log(log)

        log.info("Constructing a RandomProcedure")
        procedure = RandomProcedure()
        procedure.iterations = 100

        data_filename = 'random.csv'
        log.info("Constructing the Results with a data file: %s" % data_filename)
        results = Results(procedure, data_filename)

        log.info("Constructing the Plotter")
        plotter = Plotter(results)
        plotter.start()
        log.info("Started the Plotter")

        log.info("Constructing the Worker")
        worker = Worker(results)
        worker.start()
        log.info("Started the Worker")

        log.info("Joining with the worker in at most 1 hr")
        worker.join(timeout=3600) # wait at most 1 hr (3600 sec)
        log.info("Finished the measurement")

The important addition is the construction of the Plotter from the Results object. ::

    plotter = Plotter(results)
    plotter.start()

The Plotter is started in a different process so that it can be run on a separate CPU for higher performance. The Plotter launches a Qt graphical interface using pyqtgraph which allows the Results data to be viewed based on the columns in the data.

.. image:: pymeasure-plotter.png
    :alt: Results Plotter Example

.. _tutorial-managedwindow:

Using the ManagedWindow
~~~~~~~~~~~~~~~~~~~~~~~

The ManagedWindow is the most convenient tool for running measurements with your Procedure. This has the major advantage of accepting the input parameters graphically. From the parameters, a graphical form is automatically generated that allows the inputs to be typed in. With this feature, measurements can be started dynamically, instead of defined in a script.

Another major feature of the ManagedWindow is its support for running measurements in a sequential queue. This allows you to set up a number of measurements with different input parameters, and watch them unfold on the live-plot. This is especially useful for long running measurements. The ManagedWindow achieves this through the Manager object, which coordinates which Procedure the Worker should run and keeps track of its status as the Worker progresses.

Below we adapt our previous example to use a ManagedWindow. ::

    import logging
    log = logging.getLogger(__name__)
    log.addHandler(logging.NullHandler())

    import sys
    import tempfile
    import random
    from time import sleep
    from pymeasure.log import console_log
    from pymeasure.display.Qt import QtWidgets
    from pymeasure.display.windows import ManagedWindow
    from pymeasure.experiment import Procedure, Results
    from pymeasure.experiment import IntegerParameter, FloatParameter, Parameter

    class RandomProcedure(Procedure):

        iterations = IntegerParameter('Loop Iterations', default=100)
        delay = FloatParameter('Delay Time', units='s', default=0.2)
        seed = Parameter('Random Seed', default='12345')

        DATA_COLUMNS = ['Iteration', 'Random Number']

        def startup(self):
            log.info("Setting the seed of the random number generator")
            random.seed(self.seed)

        def execute(self):
            log.info("Starting the loop of %d iterations" % self.iterations)
            for i in range(self.iterations):
                data = {
                    'Iteration': i,
                    'Random Number': random.random()
                }
                self.emit('results', data)
                log.debug("Emitting results: %s" % data)
                self.emit('progress', 100 * i / self.iterations)
                sleep(self.delay)
                if self.should_stop():
                    log.warning("Caught the stop flag in the procedure")
                    break


    class MainWindow(ManagedWindow):

        def __init__(self):
            super().__init__(
                procedure_class=RandomProcedure,
                inputs=['iterations', 'delay', 'seed'],
                displays=['iterations', 'delay', 'seed'],
                x_axis='Iteration',
                y_axis='Random Number'
            )
            self.setWindowTitle('GUI Example')


    if __name__ == "__main__":
        app = QtWidgets.QApplication(sys.argv)
        window = MainWindow()
        window.show()
        sys.exit(app.exec())



This results in the following graphical display.

.. image:: pymeasure-managedwindow.png
    :alt: ManagedWindow Example

In the code, the :class:`MainWindow` class is a sub-class of the :class:`~pymeasure.display.windows.managed_window.ManagedWindow` class. We override the constructor to provide information about the procedure class and its options. The :code:`inputs` are a list of :class:`Parameters` class-variable names, which the display will generate graphical fields for. When the list of inputs is long, a boolean key-word argument :code:`inputs_in_scrollarea` is provided that adds a scrollbar to the input area. The :code:`displays` is a list similar to the :code:`inputs` list, which instead defines the parameters to display in the browser window. This browser keeps track of the experiments being run in the sequential queue.

As a bit of background information (for basic usage this needs not be known): the :meth:`~pymeasure.display.windows.managed_window.ManagedWindowBase.queue` method establishes how the :class:`~pymeasure.experiment.procedure.Procedure` object is constructed.
The :meth:`~pymeasure.display.windows.managed_window.ManagedWindowBase.make_procedure` method is used to create a :class:`~pymeasure.experiment.procedure.Procedure` based on the graphical input fields.
Here we are free to modify the procedure before putting it on the queue.
In this context, the :class:`~pymeasure.display.manager.Manager` uses an :class:`~pymeasure.display.manager.Experiment` object to keep track of the :class:`~pymeasure.experiment.procedure.Procedure`, :class:`~pymeasure.experiment.results.Results`, and its associated graphical representations in the browser and live-graph.
This is then given to the :class:`~pymeasure.display.manager.Manager` to queue the experiment.

.. image:: pymeasure-managedwindow-queued.png
    :alt: ManagedWindow Queue Example

By default the Manager starts a measurement when its procedure is queued. The abort button can be pressed to stop an experiment. In the Procedure, the :code:`self.should_stop` call will catch the abort event and halt the measurement. It is important to check this value, or the Procedure will not be responsive to the abort event.

.. image:: pymeasure-managedwindow-resume.png
    :alt: ManagedWindow Resume Example

If you abort a measurement, the resume button must be pressed to continue the next measurement. This allows you to adjust anything, which is presumably why the abort was needed.

.. image:: pymeasure-managedwindow-running.png
    :alt: ManagedWindow Running Example

Now that you have learned about the ManagedWindow, you have all of the basics to get up and running quickly with a measurement and produce an easy to use graphical interface with PyMeasure.

.. note::
   For performance reasons, the default linewidth of all the graphs has been set to 1.
   If performance is not an issue, the linewidth can be changed to 2 (or any other value) for better visibility by using the `linewidth` keyword-argument in the `Plotter` or the `ManagedWindow`.
   Whenever a linewidth of 2 is preferred and a better performance is required, it is possible to enable using OpenGL in the import section of the file: ::

      import pyqtgraph as pg
      pg.setConfigOption("useOpenGL", True)

The filename and directory input
################################

By default, a ManagedWindow instance contains fields for the filename and the directory (as part of the :class:`~pymeasure.display.widgets.fileinput_widget.FileInputWidget`) to control where the results of an experiment are saved.

.. image:: pymeasure-fileinput.png
    :alt: The filename and directory input widget
    :width: 24%
.. image:: pymeasure-fileinput_disabled.png
    :alt: The filename and directory input widget, disabled to store the measurement to a temporary file
    :width: 24%
.. image:: pymeasure-fileinput_complete_filename.png
    :alt: The filename and directory input widget showing the auto-complete for the filename
    :width: 24%
.. image:: pymeasure-fileinput_complete_directory.png
    :alt: The filename and directory input widget showing the auto-complete for the directory
    :width: 24%

If the checkbox named :guilabel:`Save data` is enabled, the measurement is written to a file.
Otherwise, it is stored in a temporary file.

The filename in the designated field can be entered with or without extension.
If the entered extension is recognized (by default :code:`.csv` and :code:`.txt` are recognized), that extension is used.
If the extension is not recognized, the first of the available extensions will be used (default is :code:`.csv`).
Additionally, a sequence number is added just before the extension to ensure the uniqueness of the filename.

The filename can also contain placeholders, which are filled in using the standard python :code:`format` function (i.e., placeholders can be entered as :code:`'{placeholder name:formatspec}'`).
Valid placeholders are the names of the all the input parameters or metadata of the measurement procedure; the valid placeholders are listed in the tooltip of the input field.
As the standard :code:`format` functionality is used, the placeholders can be formatted as such; for example, the filename :code:`'DATA_delay{Delay Time:08.3f}s'` gets formatted into :code:`'DATA_delay0000.010s'`.

Both the filename and the directory field are provided with auto-completion to help with filling in these fields.
The directory field contains a button on the right side to open a folder-selection window.

The default values can be easily set after the :class:`~pymeasure.display.windows.managed_window.ManagedWindow` has been initialized; this allows setting a default location and a default filename, changing the default recognized extensions, control the default toggle-value for the :guilabel:`Save data` option, and control whether the filename input field is frozen.

.. code-block:: python
   :emphasize-lines: 13, 14, 15, 16, 17

    class MainWindow(ManagedWindow):

        def __init__(self):
            super().__init__(
                procedure_class=TestProcedure,
                inputs=['iterations', 'delay', 'seed'],
                displays=['iterations', 'delay', 'seed'],
                x_axis='Iteration',
                y_axis='Random Number',
            )
            self.setWindowTitle('GUI Example')

            self.filename = r'default_filename_delay{Delay Time:4f}s'   # Sets default filename
            self.directory = r'C:/Path/to/default/directory'            # Sets default directory
            self.store_measurement = False                              # Controls the 'Save data' toggle
            self.file_input.extensions = ["csv", "txt", "data"]         # Sets recognized extensions, first entry is the default extension
            self.file_input.filename_fixed = False                      # Controls whether the filename-field is frozen (but still displayed)


The presence of the widget is controlled by the boolean argument :code:`enabled_file_input` of the :class:`~pymeasure.display.windows.managed_window.ManagedWindow` init.
Note that when this is set to :code:`False`, the default :meth:`~pymeasure.display.windows.managed_window.ManagedWindowBase.queue` method of the :class:`~pymeasure.display.windows.managed_window.ManagedWindow` class will no longer work, and a new, custom, method needs to be implemented; a basic implementation is shown in the documentation of the :meth:`~pymeasure.display.windows.managed_window.ManagedWindowBase.queue` method.

Customising the plot options
~~~~~~~~~~~~~~~~~~~~~~~~~~~~

For both the PlotterWindow and ManagedWindow, plotting is provided by the pyqtgraph_ library. This library allows you to change various plot options, as you might expect: axis ranges (by default auto-ranging), logarithmic and semilogarithmic axes, downsampling, grid display, FFT display, etc. There are two main ways you can do this:

1. You can right click on the plot to manually change any available options. This is also a good way of getting an overview of what options are available in pyqtgraph. Option changes will, of course, not persist across a restart of your program.
2. You can programmatically set these options using pyqtgraph's PlotItem_ API, so that the window will open with these display options already set, as further explained below.

For :class:`~pymeasure.display.plotter.Plotter`, you can make a sub-class that overrides the :meth:`~pymeasure.display.plotter.Plotter.setup_plot` method. This method will be called when the Plotter constructs the window. As an example ::

    class LogPlotter(Plotter):
        def setup_plot(self, plot):
            # use logarithmic x-axis (e.g. for frequency sweeps)
            plot.setLogMode(x=True)

For :class:`~pymeasure.display.windows.managed_window.ManagedWindow`, the mechanism to customize plots is much more flexible by using specialization via inheritance. Indeed :class:`~pymeasure.display.windows.managed_window.ManagedWindowBase` is the base class for :class:`~pymeasure.display.windows.managed_window.ManagedWindow` and :class:`~pymeasure.display.windows.managed_image_window.ManagedImageWindow` which are subclasses ready to use for GUI.

Using tabular format
~~~~~~~~~~~~~~~~~~~~

In some experiments, data in tabular format may be useful in addition or in alternative to graphical plot.
:class:`~pymeasure.display.windows.managed_window.ManagedWindowBase` allows adding a :class:`~pymeasure.display.widgets.table_widget.TableWidget` to show
experiments data, the widget supports also exporting data in some popular format like CSV, HTML, etc.
Below an example on how to customize :class:`~pymeasure.display.windows.managed_window.ManagedWindowBase` to use tabular format,
it derived from example above and changed lines are marked.

.. code-block:: python
   :emphasize-lines: 11, 12, 18, 44, 47, 48, 49, 50, 51, 52, 57, 59, 60, 61

    import logging
    log = logging.getLogger(__name__)
    log.addHandler(logging.NullHandler())

    import sys
    import tempfile
    import random
    from time import sleep
    from pymeasure.log import console_log
    from pymeasure.display.Qt import QtWidgets
    from pymeasure.display.windows import ManagedWindowBase
    from pymeasure.display.widgets import TableWidget, LogWidget
    from pymeasure.experiment import Procedure, Results
    from pymeasure.experiment import IntegerParameter, FloatParameter, Parameter

    class RandomProcedure(Procedure):

        iterations = IntegerParameter('Loop Iterations', default=10)
        delay = FloatParameter('Delay Time', units='s', default=0.2)
        seed = Parameter('Random Seed', default='12345')

        DATA_COLUMNS = ['Iteration', 'Random Number']

        def startup(self):
            log.info("Setting the seed of the random number generator")
            random.seed(self.seed)

        def execute(self):
            log.info("Starting the loop of %d iterations" % self.iterations)
            for i in range(self.iterations):
                data = {
                    'Iteration': i,
                    'Random Number': random.random()
                }
                self.emit('results', data)
                log.debug("Emitting results: %s" % data)
                self.emit('progress', 100 * i / self.iterations)
                sleep(self.delay)
                if self.should_stop():
                    log.warning("Caught the stop flag in the procedure")
                    break


    class MainWindow(ManagedWindowBase):

        def __init__(self):
            widget_list = (TableWidget("Experiment Table",
                                       RandomProcedure.DATA_COLUMNS,
                                       by_column=True,
                                       ),
                           LogWidget("Experiment Log"),
                           )
            super().__init__(
                procedure_class=RandomProcedure,
                inputs=['iterations', 'delay', 'seed'],
                displays=['iterations', 'delay', 'seed'],
                widget_list=widget_list,
            )
            logging.getLogger().addHandler(widget_list[1].handler)
            log.setLevel(self.log_level)
            log.info("ManagedWindow connected to logging")
            self.setWindowTitle('GUI Example')

    if __name__ == "__main__":
        app = QtWidgets.QApplication(sys.argv)
        window = MainWindow()
        window.show()
        sys.exit(app.exec())


This results in the following graphical display.

.. image:: pymeasure-tablewidget.png
    :alt: TableWidget Example


Defining your own ManagedWindow's widgets
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

The parameter :code:`widget_list` in :class:`~pymeasure.display.windows.managed_window.ManagedWindowBase` constructor allow to introduce user's defined widget in the GUI results display area.
The user's widget should inherit from :class:`~pymeasure.display.widgets.tab_widget.TabWidget` and could reimplement any of the methods that needs customization.
In order to get familiar with the mechanism, users can check the following widgets already provided:

- :class:`~pymeasure.display.widgets.log_widget.LogWidget`
- :class:`~pymeasure.display.widgets.plot_widget.PlotWidget`
- :class:`~pymeasure.display.widgets.image_widget.ImageWidget`
- :class:`~pymeasure.display.widgets.image_widget.DockWidget`
- :class:`~pymeasure.display.widgets.table_widget.TableWidget`

Using the sequencer
~~~~~~~~~~~~~~~~~~~

As an extension to the way of graphically inputting parameters and executing multiple measurements using the :class:`~pymeasure.display.windows.managed_window.ManagedWindow`, :class:`~pymeasure.display.widgets.sequencer_widget.SequencerWidget` is provided which allows users to queue a series of measurements with varying one, or more, of the parameters. This sequencer thereby provides a convenient way to scan through the parameter space of the measurement procedure.

To activate the sequencer, two additional keyword arguments are added to :class:`~pymeasure.display.windows.managed_window.ManagedWindow`, namely :code:`sequencer` and :code:`sequencer_inputs`. :code:`sequencer` accepts a boolean stating whether or not the sequencer has to be included into the window and :code:`sequencer_inputs` accepts either :code:`None` or a list of the parameter names are to be scanned over. If no list of parameters is given, the parameters displayed in the manager queue are used.

In order to be able to use the sequencer, the :class:`~pymeasure.display.windows.managed_window.ManagedWindow` class is required to have a :code:`queue` method which takes a keyword (or better keyword-only for safety reasons) argument :code:`procedure`, where a procedure instance can be passed. The sequencer will use this method to queue the parameter scan.

In order to implement the sequencer into the previous example, only the :class:`~pymeasure.display.windows.managed_window.ManagedWindow` has to be modified slightly (where modified lines are marked):

.. code-block:: python
   :emphasize-lines: 10,11,12

    class MainWindow(ManagedWindow):

        def __init__(self):
            super().__init__(
                procedure_class=TestProcedure,
                inputs=['iterations', 'delay', 'seed'],
                displays=['iterations', 'delay', 'seed'],
                x_axis='Iteration',
                y_axis='Random Number',
                sequencer=True,                                      # Added line
                sequencer_inputs=['iterations', 'delay', 'seed'],    # Added line
                sequence_file="gui_sequencer_example_sequence.txt",  # Added line, optional
            )
            self.setWindowTitle('GUI Example')

This adds the sequencer underneath the input panel.

.. image:: pymeasure-sequencer.png
    :alt: Example of the sequencer widget

The widget contains a tree-view where you can build the sequence.
It has three columns: :code:`level` (indicated how deep an item is nested), :code:`parameter` (a drop-down menu to select which parameter is being sequenced by that item), and :code:`sequence` (the text-box where you can define the sequence).
While the two former columns are rather straightforward, filling in the later requires some explanation.

In order to maintain flexibility, the sequence is defined in a text-box, allowing the user to enter any list-generating single-line piece of code.
To assist in this, a number of functions is supported, either from the main python library (namely :code:`range`, :code:`sorted`, and :code:`list`) or the numpy library.
The supported numpy functions (prepending :code:`numpy.` or any abbreviation is not required) are: :code:`arange`, :code:`linspace`, :code:`arccos`, :code:`arcsin`, :code:`arctan`, :code:`arctan2`, :code:`ceil`, :code:`cos`, :code:`cosh`, :code:`degrees`, :code:`e`, :code:`exp`, :code:`fabs`, :code:`floor`, :code:`fmod`, :code:`frexp`, :code:`hypot`, :code:`ldexp`, :code:`log`, :code:`log10`, :code:`modf`, :code:`pi`, :code:`power`, :code:`radians`, :code:`sin`, :code:`sinh`, :code:`sqrt`, :code:`tan`, and :code:`tanh`.

As an example, :code:`arange(0, 10, 1)` generates a list increasing with steps of 1, while using :code:`exp(arange(0, 10, 1))` generates an exponentially increasing list.
This way complex sequences can be entered easily.

The sequences can be extended and shortened using the buttons :code:`Add root item`, :code:`Add item`, and :code:`Remove item`.
The latter two either add an item as a child of the currently selected item or remove the selected item, respectively.
To queue the entered sequence the button :code:`Queue` sequence can be used.
If an error occurs in evaluating the sequence text-boxes, this is mentioned in the logger, and nothing is queued.

Finally, it is possible to create a sequence file such that the user does not need to write the sequence again each time. The sequence file can be created by saving current sequence built within the GUI using the :code:`Save sequence` button or directly writing a simple text file.
Once created, the sequence can be loaded with the :code:`Load sequence` button.

In the sequence file each line adds one item to the sequence tree, starting with a number of dashes (:code:`-`) to indicate the level of the item (starting with 1 dash for top level), followed by the name of the parameter and the sequence string, both as a python string between parentheses.

An example of such a sequence file is given below, resulting in the sequence shown in the figure above.

.. literalinclude:: gui_sequencer_example_sequence.txt

This file can also be automatically loaded at the start of the program by adding the key-word argument :code:`sequence_file="filename.txt"` to the :code:`super().__init__` call, as was done in the example.

Using the estimator widget
~~~~~~~~~~~~~~~~~~~~~~~~~~

In order to provide estimates of the measurement procedure, an :class:`~pymeasure.display.widgets.estimator_widget.EstimatorWidget` is provided that allows the user to define and calculate estimates.
The widget is automatically activated when the :code:`get_estimates` method is added in the :code:`Procedure`.

The quickest and most simple implementation of the :code:`get_estimates` function simply returns the estimated duration of the measurement in seconds (as an :code:`int` or a :code:`float`).
As an example, in the example provided in the `Using the ManagedWindow`_ section, the :code:`Procedure` is changed to:

.. code-block:: python

   class RandomProcedure(Procedure):

       # ...

       def get_estimates(self, sequence_length=None, sequence=None):

           return self.iterations * self.delay

This will add the estimator widget at the dock on the left.
The duration and finishing-time of a single measurement is always displayed in this case.
Depending on whether the SequencerWidget is also used, the length, duration and finishing-time of the full sequence is also shown.

For maximum flexibility (e.g. for showing multiple and other types of estimates, such as the duration, filesize, finishing-time, etc.) it is also possible that the :code:`get_estimates` returns a list of tuples.
Each of these tuple consists of two strings: the first is the name (label) of the estimate, the second is the estimate itself.

As an example, in the example provided in the `Using the ManagedWindow`_ section, the :code:`Procedure` is changed to:

.. code-block:: python

   class RandomProcedure(Procedure):

       # ...

       def get_estimates(self, sequence_length=None, sequence=None):

           duration = self.iterations * self.delay

           estimates = [
               ("Duration", "%d s" % int(duration)),
               ("Number of lines", "%d" % int(self.iterations)),
               ("Sequence length", str(sequence_length)),
               ('Measurement finished at', str(datetime.now() + timedelta(seconds=duration))),
           ]

           return estimates


This will add the estimator widget at the dock on the left.

.. image:: pymeasure-estimator.png
    :alt: Example of the estimator widget

Note that after the initialisation of the widget both the label of the estimate as of course the estimate itself can be modified, but the amount of estimates is fixed.

The keyword arguments are not required in the implementation of the function, but are passed if asked for (i.e. :code:`def get_estimates(self)` does also works).
Keyword arguments that are accepted are :code:`sequence`, which contains the full sequence of the sequencer (if present), and :code:`sequence_length`, which gives the length of the sequence as integer (if present).
If the sequencer is not present or the sequence cannot be parsed, both :code:`sequence` and :code:`sequence_length` will contain :code:`None`.

The estimates are automatically updated every 2 seconds.
Changing this update interval is possible using the "Update continuously"-checkbox, which can be toggled between three states: off (i.e. no updating), auto-update every two seconds (default) or auto-update every 100 milliseconds.
Manually updating the estimates (useful whenever continuous updating is turned off) is also possible using the "update"-button.

Flexible hiding of inputs
~~~~~~~~~~~~~~~~~~~~~~~~~

There can be situations when it may be relevant to turn on or off a number of inputs (e.g. when a part of the measurement script is skipped upon turning of a single :code:`BooleanParameter`).
For these cases, it is possible to assign a :code:`Parameter` to a controlling :code:`Parameter`, which will hide or show the :code:`Input` of the :code:`Parameter` depending on the value of the :code:`Parameter`.
This is done with the :code:`group_by` key-word argument.

.. code-block:: python

    toggle = BooleanParameter("toggle", default=True)
    param = FloatParameter('some parameter', group_by='toggle')

When both the :code:`toggle` and :code:`param` are visible in the :code:`InputsWidget` (via :code:`inputs=['iterations', 'delay', 'seed']` as demonstrated above) one can control whether the input-field of :code:`param` is visible by checking and unchecking the checkbox of :code:`toggle`.
By default, the group will be visible if the value of the :code:`group_by` :code:`Parameter` is :code:`True` (which is only relevant for a :code:`BooleanParameter`), but it is possible to specify other value as conditions using the :code:`group_condition` keyword argument.

.. code-block:: python

    iterations = IntegerParameter('Loop Iterations', default=100)
    param = FloatParameter('some parameter', group_by='iterations', group_condition=99)

Here the input of :code:`param` is only visible if :code:`iterations` has a value of 99.
This works with any type of :code:`Parameter` as :code:`group_by` parameter.

To allow for even more flexibility, it is also possible to pass a (lambda)function as a condition:

.. code-block:: python

    iterations = IntegerParameter('Loop Iterations', default=100)
    param = FloatParameter('some parameter', group_by='iterations', group_condition=lambda v: 50 < v < 100)

Now the input of :code:`param` is only shown if the value of :code:`iterations` is between 51 and 99.

Using the :code:`hide_groups` keyword-argument of the :code:`ManagedWindow` you can choose between hiding the groups (:code:`hide_groups = True`) and disabling / graying-out the groups (:code:`hide_groups = False`).

Finally, it is also possible to provide multiple parameters to the :code:`group_by` argument, in which case the input will only be visible if all of the conditions are true.
Multiple parameters for grouping can either be passed as a dict of string: condition pairs, or as a list of strings, in which case the :code:`group_condition` can be either a single condition or a list of conditions:

.. code-block:: python

    iterations = IntegerParameter('Loop Iterations', default=100)
    toggle = BooleanParameter('A checkbox')
    param_A = FloatParameter('some parameter', group_by=['iterations', 'toggle'], group_condition=[lambda v: 50 < v < 100, True])
    param_B = FloatParameter('some parameter', group_by={'iterations': lambda v: 50 < v < 100, 'toggle': True})

Note that in this example, :code:`param_A` and :code:`param_B` are identically grouped: they're only visible if :code:`iterations` is between 51 and 99 and if the `toggle` checkbox is checked (i.e. True).

.. _pyqtgraph: http://www.pyqtgraph.org/
.. _PlotItem: http://www.pyqtgraph.org/documentation/graphicsItems/plotitem.html

Using the ManagedDockWindow
~~~~~~~~~~~~~~~~~~~~~~~~~~~

Building off the `Using the ManagedWindow`_ section where we used a :code:`ManagedWindow`, we can also use :class:`~pymeasure.display.windows.managed_dock_window.ManagedDockWindow` to build a graphical interface with multiple graphs that can be docked in the main GUI window or popped out into their own window.

To start with, let's make the following highlighted edits to the code example from `Using the ManagedWindow`_:

1. On line 10 we now import :class:`~pymeasure.display.windows.managed_dock_window.ManagedDockWindow`
2. On line 20, and lines 32 and 33, we add two new columns of data to be recorded :code:`'Random Number 2'` and :code:`'Random Number 3'`
3. On line 44 we make :code:`MainWindow` a subclass of :code:`ManagedDockWindow`
4. On line 51 we will pass in a list of strings from :code:`DATA_COLUMNS` to the :code:`x_axis` argument
5. On line 52 we will pass in a list of strings from :code:`DATA_COLUMNS` to the :code:`y_axis` argument

.. code-block:: python
   :emphasize-lines: 10,20,32,33,44,51,52

   import logging
   log = logging.getLogger(__name__)
   log.addHandler(logging.NullHandler())

   import sys
   import tempfile
   import random
   from time import sleep
   from pymeasure.display.Qt import QtWidgets
   from pymeasure.display.windows.managed_dock_window import ManagedDockWindow
   from pymeasure.experiment import Procedure, Results
   from pymeasure.experiment import IntegerParameter, FloatParameter, Parameter

   class RandomProcedure(Procedure):

       iterations = IntegerParameter('Loop Iterations', default=10)
       delay = FloatParameter('Delay Time', units='s', default=0.2)
       seed = Parameter('Random Seed', default='12345')

       DATA_COLUMNS = ['Iteration', 'Random Number 1', 'Random Number 2', 'Random Number 3']

       def startup(self):
           log.info("Setting the seed of the random number generator")
           random.seed(self.seed)

       def execute(self):
           log.info("Starting the loop of %d iterations" % self.iterations)
           for i in range(self.iterations):
               data = {
                   'Iteration': i,
                   'Random Number 1': random.random(),
                   'Random Number 2': random.random(),
                   'Random Number 3': random.random()
               }
               self.emit('results', data)
               log.debug("Emitting results: %s" % data)
               self.emit('progress', 100 * i / self.iterations)
               sleep(self.delay)
               if self.should_stop():
                   log.warning("Caught the stop flag in the procedure")
                   break


   class MainWindow(ManagedDockWindow):

       def __init__(self):
           super().__init__(
               procedure_class=RandomProcedure,
               inputs=['iterations', 'delay', 'seed'],
               displays=['iterations', 'delay', 'seed'],
               x_axis=['Iteration', 'Random Number 1'],
               y_axis=['Random Number 1','Random Number 2', 'Random Number 3']
           )
           self.setWindowTitle('GUI Example')


   if __name__ == "__main__":
       app = QtWidgets.QApplication(sys.argv)
       window = MainWindow()
       window.show()
       sys.exit(app.exec())

Now we can see our :code:`ManagedDockWindow`:

.. image:: managed_dock_window.png
    :alt: Managed dock window

As you can see from the above screenshot, our example code created three docks with following "X Axis" and "Y Axis" labels:

1. **X Axis:** "Iteration"        **Y Axis:** "Random Number 1"
2. **X Axis:** "Random Number 1"  **Y Axis:** "Random Number 2"
3. **X Axis:** "Random Number 1"  **Y Axis:** "Random Number 3"

The list of strings for :code:`x_axis` and :code:`y_axis` set the default labels for each dockable plot and the longest list determines how many dockable plots are created. To highlight this point, in our example we define :code:`x_axis` and :code:`y_axis` with the following lists::

   x_axis=['Iteration', 'Random Number 1'],
   y_axis=['Random Number 1','Random Number 2', 'Random Number 3']

If one list is longer than the last element if the other list is used as the default label for the rest of the dockable plots.
In our example that is why we have two **X Axis** labels with "Random Number 1".
The longest list between :code:`x_axis` and :code:`y_axis` determines the number of plots.
In our example :code:`y_axis` has the longest list with a length of three so three plots are created.

You can pop out a dockable plot from the main dock window to its own window by double clicking the blue "Dock #" title bar, which is to the left of each plot by default:

.. image:: managed_dock_window_popup.gif
    :alt: Pop up a managed dock window

You can return the popped out window to the main window by clicking the close icon X in the top right.

After positioning your dock windows, you can save the layout by right-clicking a dock widget and select "Save Dock Layout" from the context menu.
This will save the layout of all docks and the settings for each plot to a file. By default the file path is the current working directory of the python file
that started :code:`ManagedDockWindow`, and the default file name is '*procedure class* + "_dock_layout.json"'. For our example, that would be "./RandomProcedure_dock_layout.json"

When you run the python file that invokes :code:`ManagedDockWindow` again, it will look for and load the dock layout file if it exists.

.. image:: managed_dock_window_save.png
    :alt: Save dock window layout

You can drag a dockable plot to reposition it in reference to other plots in the main dock window in several ways. You can drag the blue "Dock #" title bar to the left or right side of another plot to reposition a plot to be side by side with another plot:

.. image:: managed_dock_window_side_drag.png
    :alt: Side drag managed dock window

.. image:: managed_dock_window_side_after.png
    :alt: Side position managed dock window

You can also drag the blue "Dock #" title bar to the top or bottom side of another plot to reposition a plot to rearrange the vertical order of the plots:

.. image:: managed_dock_window_top.png
    :alt: Top position managed dock window

You can drag the blue "Dock #" title bar to the middle of another plot to reposition a plot to create a tabbed view of the two plots:

.. image:: managed_dock_window_tab_drag.png
    :alt: Tab drag managed dock window

.. image:: managed_dock_window_tab_after.png
    :alt: Tab position managed dock window

Using the ManagedConsole
~~~~~~~~~~~~~~~~~~~~~~~~

The :class:`~pymeasure.display.console.ManagedConsole` is the most convenient tool for running measurements with your Procedure using a command line interface. The :class:`~pymeasure.display.console.ManagedConsole` allows to run an experiment with the same set of parameters available in the :class:`~pymeasure.display.windows.managed_window.ManagedWindow`, but they are defined using a set of command line switches.

It is also possible to define a test that uses both :class:`~pymeasure.display.console.ManagedConsole` or :class:`~pymeasure.display.windows.managed_window.ManagedWindow` according to user selection in the command line.

Enabling console mode is easy and straightforward and the following example demonstrates how to do it.

The following example is a variant of the code example from `Using the ManagedWindow`_ where some parts have been highlighted:

1. On line 8 we now import :class:`~pymeasure.display.console.ManagedConsole`
2. On line 73, we add the support for console mode


.. code-block:: python
   :emphasize-lines: 8,62,63,64

   import sys
   import random
   import tempfile
   from time import sleep
   
   from pymeasure.experiment import Procedure, IntegerParameter, Parameter, FloatParameter
   from pymeasure.experiment import Results
   from pymeasure.display.console import ManagedConsole
   from pymeasure.display.Qt import QtWidgets
   from pymeasure.display.windows import ManagedWindow
   import logging
   
   log = logging.getLogger('')
   log.addHandler(logging.NullHandler())
   
   
   class TestProcedure(Procedure):
       iterations = IntegerParameter('Loop Iterations', default=100)
       delay = FloatParameter('Delay Time', units='s', default=0.2)
       seed = Parameter('Random Seed', default='12345')
   
       DATA_COLUMNS = ['Iteration', 'Random Number']
   
       def startup(self):
           log.info("Setting up random number generator")
           random.seed(self.seed)
   
       def execute(self):
           log.info("Starting to generate numbers")
           for i in range(self.iterations):
               data = {
                   'Iteration': i,
                   'Random Number': random.random()
               }
               log.debug("Produced numbers: %s" % data)
               self.emit('results', data)
               self.emit('progress', 100 * (i + 1) / self.iterations)
               sleep(self.delay)
               if self.should_stop():
                   log.warning("Catch stop command in procedure")
                   break
   
       def shutdown(self):
           log.info("Finished")
   
   
   class MainWindow(ManagedWindow):
   
       def __init__(self):
           super(MainWindow, self).__init__(
               procedure_class=TestProcedure,
               inputs=['iterations', 'delay', 'seed'],
               displays=['iterations', 'delay', 'seed'],
               x_axis='Iteration',
               y_axis='Random Number'
           )
           self.setWindowTitle('GUI Example')
   
   
   if __name__ == "__main__":
       if len(sys.argv) > 1:
           # If any parameter is passed, the console mode is run
           # This criteria can be changed at user discretion
           app = ManagedConsole(procedure_class=TestProcedure)
       else:
           app = QtWidgets.QApplication(sys.argv)
           window = MainWindow()
           window.show()
   
       sys.exit(app.exec())

If we run the script above without any parameter, you will have the graphical user interface example.
If you run as follow, you will use the command line mode:

.. code-block:: bash

    python console.py --iterations 10 --result-file console_test

Console output is as follow (to show the progress bar, you need to install the optional module  `progressbar2 <https://pypi.org/project/progressbar2/>`_):

.. image:: console_output.png
    :alt: Console mode output

Other useful commands
#####################

To show all the command line switches:

.. code-block:: bash

    python console.py --help

To run an experiment with parameters retrieved from an existing result file.

.. code-block:: bash

    python console.py --use-result-file console_test2023-08-09_1.csv