From 41d43d060e7b69f219c8229ce3a0e79ae76efd43 Mon Sep 17 00:00:00 2001 From: Giulio Ungaretti Date: Mon, 7 Nov 2016 19:05:17 +0100 Subject: [PATCH 1/5] feature: Add png for jupyter nb and saving --- qcodes/plots/pyqtgraph.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/qcodes/plots/pyqtgraph.py b/qcodes/plots/pyqtgraph.py index 996140316d36..494123ccf85a 100644 --- a/qcodes/plots/pyqtgraph.py +++ b/qcodes/plots/pyqtgraph.py @@ -382,3 +382,19 @@ def _cmap(self, scale): values, colors = scale return self.rpg.ColorMap(values, colors) + + def _repr_png_(self): + """ + Create a png representation of the current window. + """ + image = self.win.grab() + byte_array = self.rpg.QtCore.QByteArray() + buffer = self.rpg.QtCore.QBuffer(byte_array) + buffer.open(self.rpg.QtCore.QIODevice.ReadWrite) + image.save(buffer, 'PNG') + buffer.close() + return bytes(byte_array._getValue()) + + def save(self, filename=""): + image = self.win.grab() + image.save(filename, "PNG", 0) From eba6e95fa3b08692e26a7dbc694f1b13635861c1 Mon Sep 17 00:00:00 2001 From: Giulio Ungaretti Date: Tue, 8 Nov 2016 11:06:16 +0100 Subject: [PATCH 2/5] feature: Add save to all plotters --- qcodes/plots/base.py | 9 +++++++++ qcodes/plots/pyqtgraph.py | 12 +++++++++++- qcodes/plots/qcmatplotlib.py | 13 +++++++++++++ 3 files changed, 33 insertions(+), 1 deletion(-) diff --git a/qcodes/plots/base.py b/qcodes/plots/base.py index ad198980f46c..528b5baac88f 100644 --- a/qcodes/plots/base.py +++ b/qcodes/plots/base.py @@ -280,3 +280,12 @@ def halt(self): """ if hasattr(self, 'update_widget'): self.update_widget.halt() + + def save(self, filename=None): + """ + Save current plot to filename + + Args: + filename (Optional[str]): Location of the file + """ + raise NotImplementedError diff --git a/qcodes/plots/pyqtgraph.py b/qcodes/plots/pyqtgraph.py index 494123ccf85a..e65878840c90 100644 --- a/qcodes/plots/pyqtgraph.py +++ b/qcodes/plots/pyqtgraph.py @@ -395,6 +395,16 @@ def _repr_png_(self): buffer.close() return bytes(byte_array._getValue()) - def save(self, filename=""): + def save(self, filename=None): + """ + Save current plot to filename, by default + to the location corresponding to the default + title. + + Args: + filename (Optional[str]): Location of the file + """ + default = "{}.png".format(self.get_default_title()) + filename = filename or default image = self.win.grab() image.save(filename, "PNG", 0) diff --git a/qcodes/plots/qcmatplotlib.py b/qcodes/plots/qcmatplotlib.py index ac034f62f8bf..135edac3c9e4 100644 --- a/qcodes/plots/qcmatplotlib.py +++ b/qcodes/plots/qcmatplotlib.py @@ -204,3 +204,16 @@ def _draw_pcolormesh(self, ax, z, x=None, y=None, subplot=1, **kwargs): ax.qcodes_colorbar.set_label(self.get_label(z)) return pc + + def save(self, filename=None): + """ + Save current plot to filename, by default + to the location corresponding to the default + title. + + Args: + filename (Optional[str]): Location of the file + """ + default = "{}.png".format(self.get_default_title()) + filename = filename or default + self.fig.savefig(filename) From 55eab2485ea5194b3f2938c6c6132267cd883446 Mon Sep 17 00:00:00 2001 From: Giulio Ungaretti Date: Tue, 8 Nov 2016 11:45:06 +0100 Subject: [PATCH 3/5] feature: Add end_task Allow to programmatically do something after the loop is done. F.ex save the plot. --- qcodes/loops.py | 31 ++++++++++++++++++++++++------- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/qcodes/loops.py b/qcodes/loops.py index c04d0fed8554..56a9782c515d 100644 --- a/qcodes/loops.py +++ b/qcodes/loops.py @@ -180,6 +180,7 @@ def __init__(self, sweep_values, delay=0, station=None, self.actions = None self.then_actions = () self.bg_task = None + self.end_task = None self.bg_min_delay = None self.progress_interval = progress_interval @@ -249,9 +250,9 @@ def each(self, *actions): return ActiveLoop(self.sweep_values, self.delay, *actions, then_actions=self.then_actions, station=self.station, progress_interval=self.progress_interval, - bg_task=self.bg_task, bg_min_delay=self.bg_min_delay) + bg_task=self.bg_task, end_task=self.end_task, bg_min_delay=self.bg_min_delay) - def with_bg_task(self, task, min_delay=1): + def with_bg_task(self, task, end_task=None, min_delay=1): """ Attaches a background task to this loop. @@ -259,12 +260,15 @@ def with_bg_task(self, task, min_delay=1): task: A callable object with no parameters. This object will be invoked periodically during the measurement loop. + end_task: A callable object with no parameters. This object will be + invoked at the end of the measurement loop. + min_delay (default 1): The minimum number of seconds to wait between task invocations. Note that the actual time between task invocations may be much longer than this, as the task is only run between passes through the loop. """ - return _attach_bg_task(self, task, min_delay) + return _attach_bg_task(self, task, end_task, min_delay) @staticmethod def validate_actions(*actions): @@ -361,7 +365,7 @@ def _attach_then_actions(loop, actions, overwrite): return loop -def _attach_bg_task(loop, task, min_delay): +def _attach_bg_task(loop, task, end_task, min_delay): """Inner code for both Loop and ActiveLoop.bg_task""" if loop.bg_task is None: loop.bg_task = task @@ -369,6 +373,9 @@ def _attach_bg_task(loop, task, min_delay): else: raise RuntimeError('Only one background task is allowed per loop') + if end_task: + loop.end_task = end_task + return loop @@ -390,7 +397,7 @@ class ActiveLoop(Metadatable): def __init__(self, sweep_values, delay, *actions, then_actions=(), station=None, progress_interval=None, bg_task=None, - bg_min_delay=None): + end_task=None, bg_min_delay=None): super().__init__() self.sweep_values = sweep_values self.delay = delay @@ -399,6 +406,7 @@ def __init__(self, sweep_values, delay, *actions, then_actions=(), self.then_actions = then_actions self.station = station self.bg_task = bg_task + self.end_task = end_task self.bg_min_delay = bg_min_delay self.data_set = None @@ -441,7 +449,7 @@ def then(self, *actions, overwrite=False): then_actions=self.then_actions, station=self.station) return _attach_then_actions(loop, actions, overwrite) - def with_bg_task(self, task, min_delay=1): + def with_bg_task(self, task, end_task=None, min_delay=1): """ Attaches a background task to this loop. @@ -449,12 +457,15 @@ def with_bg_task(self, task, min_delay=1): task: A callable object with no parameters. This object will be invoked periodically during the measurement loop. + end_task: A callable object with no parameters. This object will be + invoked at the end of the measurement loop. + min_delay (default 1): The minimum number of seconds to wait between task invocations. Note that the actual time between task invocations may be much longer than this, as the task is only run between passes through the loop. """ - return _attach_bg_task(self, task, min_delay) + return _attach_bg_task(self, task, end_task, min_delay) def snapshot_base(self, update=False): """Snapshot of this ActiveLoop's definition.""" @@ -981,6 +992,12 @@ def _run_loop(self, first_delay=0, action_indices=(), for f in self._compile_actions(self.then_actions, ()): f() + # run the end_task from the bg_task: + if self.end_task is not None: + self.end_task() + + + def _wait(self, delay): if delay: finish_clock = time.perf_counter() + delay From dff292bb73f31f80e79f159a4b6131ab4242750c Mon Sep 17 00:00:00 2001 From: Giulio Ungaretti Date: Tue, 8 Nov 2016 11:46:56 +0100 Subject: [PATCH 4/5] docs: Add save example, use faster refresh rate The default was 1 second, but I think aiming for 100hz, and using that as default is a bit closer to reality. Then in the examples we can just use the default which makes for less typing for the users! --- docs/examples/Tutorial.ipynb | 1081 +++++----------------------------- qcodes/loops.py | 6 +- 2 files changed, 157 insertions(+), 930 deletions(-) diff --git a/docs/examples/Tutorial.ipynb b/docs/examples/Tutorial.ipynb index 26d7e134abe4..97a4a71dc4a7 100644 --- a/docs/examples/Tutorial.ipynb +++ b/docs/examples/Tutorial.ipynb @@ -451,914 +451,144 @@ "cell_type": "code", "execution_count": 4, "metadata": { - "collapsed": false, - "scrolled": true - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "DataSet:\n", - " mode = DataMode.LOCAL\n", - " location = 'data/2016-10-10/#002_testsweep_10-08-32'\n", - " | | | \n", - " Setpoint | gates_chan0_set | chan0 | (201,)\n", - " Measured | meter_amplitude | amplitude | (201,)\n", - "started at 2016-10-10 10:08:40\n" - ] - } - ], - "source": [ - "# Notice that one can use an explicit location and `overwrite=True` here so that\n", - "# running this notebook over and over won't result in extra files.\n", - "# If you leave these out, you get a new timestamped DataSet each time.\n", - "\n", - "loop = qc.Loop(c0.sweep(0,20,0.1), delay=0.001).each(meter.amplitude)\n", - "data = loop.get_data_set(name='testsweep')\n", - "plot = qc.QtPlot()\n", - "plot.add(data.meter_amplitude)\n", - "_ = loop.with_bg_task(plot.update, 0.0005).run()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Output of the loop\n", - "Notice the **\"DataSet\"**. \n", - "A loop returns a dataset. \n", - "The representation of the dataset shows what arrays it contains and where it is saved. \n", - "The dataset initially starts out empty (filled with NAN's) and get's filled while the Loop get's executed. " - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Once the measurement is done, take a look at the file in finder/explorer (the dataset.location should give you the relative path). \n", - "Note also the snapshot that captures the settings of all instruments at the start of the Loop. \n", - "This metadata is also accesible from the dataset and captures a snapshot of each instrument listed in the station. " - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "data": { - "text/plain": [ - "{'__class__': 'toymodel.MockMeter',\n", - " 'functions': {},\n", - " 'name': 'meter',\n", - " 'parameters': {'IDN': {'__class__': 'qcodes.instrument.parameter.StandardParameter',\n", - " 'instrument': 'toymodel.MockMeter',\n", - " 'instrument_name': 'meter',\n", - " 'label': 'IDN',\n", - " 'name': 'IDN',\n", - " 'ts': '2016-10-10 10:08:32',\n", - " 'units': '',\n", - " 'value': {'firmware': None,\n", - " 'model': 'MockMeter',\n", - " 'serial': 'meter',\n", - " 'vendor': None}},\n", - " 'amplitude': {'__class__': 'qcodes.instrument.parameter.StandardParameter',\n", - " 'instrument': 'toymodel.MockMeter',\n", - " 'instrument_name': 'meter',\n", - " 'label': 'Current (nA)',\n", - " 'name': 'amplitude',\n", - " 'ts': '2016-10-10 10:08:40',\n", - " 'units': '',\n", - " 'value': 0.117}}}" - ] - }, - "execution_count": 5, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "meter.snapshot()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Plotting the loop II\n", - "\n", - "QCodes supports both matplotlib inline plotting and pyqtgraph for plotting. \n", - "For a comparison see http://pyqtgraph.org/ (actually not that biased)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The same API works for plotting a measured dataset or an old dataset." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "`DataSet` objects are not intended to be instantiated directly, but\n", - "rather through the helper functions:\n", - "- `load_data` for existing data sets, including the data currently\n", - " being acquired.\n", - "- `new_data` to make an empty data set to be populated with new\n", - " measurements or simulation data. `new_data` is called internally by\n", - " `Loop.run()` so is also generally not needed directly." - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": { - "collapsed": false, - "scrolled": false - }, - "outputs": [ - { - "data": { - "application/javascript": [ - "/* Put everything inside the global mpl namespace */\n", - "window.mpl = {};\n", - "\n", - "mpl.get_websocket_type = function() {\n", - " if (typeof(WebSocket) !== 'undefined') {\n", - " return WebSocket;\n", - " } else if (typeof(MozWebSocket) !== 'undefined') {\n", - " return MozWebSocket;\n", - " } else {\n", - " alert('Your browser does not have WebSocket support.' +\n", - " 'Please try Chrome, Safari or Firefox ≥ 6. ' +\n", - " 'Firefox 4 and 5 are also supported but you ' +\n", - " 'have to enable WebSockets in about:config.');\n", - " };\n", - "}\n", - "\n", - "mpl.figure = function(figure_id, websocket, ondownload, parent_element) {\n", - " this.id = figure_id;\n", - "\n", - " this.ws = websocket;\n", - "\n", - " this.supports_binary = (this.ws.binaryType != undefined);\n", - "\n", - " if (!this.supports_binary) {\n", - " var warnings = document.getElementById(\"mpl-warnings\");\n", - " if (warnings) {\n", - " warnings.style.display = 'block';\n", - " warnings.textContent = (\n", - " \"This browser does not support binary websocket messages. \" +\n", - " \"Performance may be slow.\");\n", - " }\n", - " }\n", - "\n", - " this.imageObj = new Image();\n", - "\n", - " this.context = undefined;\n", - " this.message = undefined;\n", - " this.canvas = undefined;\n", - " this.rubberband_canvas = undefined;\n", - " this.rubberband_context = undefined;\n", - " this.format_dropdown = undefined;\n", - "\n", - " this.image_mode = 'full';\n", - "\n", - " this.root = $('
');\n", - " this._root_extra_style(this.root)\n", - " this.root.attr('style', 'display: inline-block');\n", - "\n", - " $(parent_element).append(this.root);\n", - "\n", - " this._init_header(this);\n", - " this._init_canvas(this);\n", - " this._init_toolbar(this);\n", - "\n", - " var fig = this;\n", - "\n", - " this.waiting = false;\n", - "\n", - " this.ws.onopen = function () {\n", - " fig.send_message(\"supports_binary\", {value: fig.supports_binary});\n", - " fig.send_message(\"send_image_mode\", {});\n", - " fig.send_message(\"refresh\", {});\n", - " }\n", - "\n", - " this.imageObj.onload = function() {\n", - " if (fig.image_mode == 'full') {\n", - " // Full images could contain transparency (where diff images\n", - " // almost always do), so we need to clear the canvas so that\n", - " // there is no ghosting.\n", - " fig.context.clearRect(0, 0, fig.canvas.width, fig.canvas.height);\n", - " }\n", - " fig.context.drawImage(fig.imageObj, 0, 0);\n", - " };\n", - "\n", - " this.imageObj.onunload = function() {\n", - " this.ws.close();\n", - " }\n", - "\n", - " this.ws.onmessage = this._make_on_message_function(this);\n", - "\n", - " this.ondownload = ondownload;\n", - "}\n", - "\n", - "mpl.figure.prototype._init_header = function() {\n", - " var titlebar = $(\n", - " '
');\n", - " var titletext = $(\n", - " '
');\n", - " titlebar.append(titletext)\n", - " this.root.append(titlebar);\n", - " this.header = titletext[0];\n", - "}\n", - "\n", - "\n", - "\n", - "mpl.figure.prototype._canvas_extra_style = function(canvas_div) {\n", - "\n", - "}\n", - "\n", - "\n", - "mpl.figure.prototype._root_extra_style = function(canvas_div) {\n", - "\n", - "}\n", - "\n", - "mpl.figure.prototype._init_canvas = function() {\n", - " var fig = this;\n", - "\n", - " var canvas_div = $('
');\n", - "\n", - " canvas_div.attr('style', 'position: relative; clear: both; outline: 0');\n", - "\n", - " function canvas_keyboard_event(event) {\n", - " return fig.key_event(event, event['data']);\n", - " }\n", - "\n", - " canvas_div.keydown('key_press', canvas_keyboard_event);\n", - " canvas_div.keyup('key_release', canvas_keyboard_event);\n", - " this.canvas_div = canvas_div\n", - " this._canvas_extra_style(canvas_div)\n", - " this.root.append(canvas_div);\n", - "\n", - " var canvas = $('');\n", - " canvas.addClass('mpl-canvas');\n", - " canvas.attr('style', \"left: 0; top: 0; z-index: 0; outline: 0\")\n", - "\n", - " this.canvas = canvas[0];\n", - " this.context = canvas[0].getContext(\"2d\");\n", - "\n", - " var rubberband = $('');\n", - " rubberband.attr('style', \"position: absolute; left: 0; top: 0; z-index: 1;\")\n", - "\n", - " var pass_mouse_events = true;\n", - "\n", - " canvas_div.resizable({\n", - " start: function(event, ui) {\n", - " pass_mouse_events = false;\n", - " },\n", - " resize: function(event, ui) {\n", - " fig.request_resize(ui.size.width, ui.size.height);\n", - " },\n", - " stop: function(event, ui) {\n", - " pass_mouse_events = true;\n", - " fig.request_resize(ui.size.width, ui.size.height);\n", - " },\n", - " });\n", - "\n", - " function mouse_event_fn(event) {\n", - " if (pass_mouse_events)\n", - " return fig.mouse_event(event, event['data']);\n", - " }\n", - "\n", - " rubberband.mousedown('button_press', mouse_event_fn);\n", - " rubberband.mouseup('button_release', mouse_event_fn);\n", - " // Throttle sequential mouse events to 1 every 20ms.\n", - " rubberband.mousemove('motion_notify', mouse_event_fn);\n", - "\n", - " rubberband.mouseenter('figure_enter', mouse_event_fn);\n", - " rubberband.mouseleave('figure_leave', mouse_event_fn);\n", - "\n", - " canvas_div.on(\"wheel\", function (event) {\n", - " event = event.originalEvent;\n", - " event['data'] = 'scroll'\n", - " if (event.deltaY < 0) {\n", - " event.step = 1;\n", - " } else {\n", - " event.step = -1;\n", - " }\n", - " mouse_event_fn(event);\n", - " });\n", - "\n", - " canvas_div.append(canvas);\n", - " canvas_div.append(rubberband);\n", - "\n", - " this.rubberband = rubberband;\n", - " this.rubberband_canvas = rubberband[0];\n", - " this.rubberband_context = rubberband[0].getContext(\"2d\");\n", - " this.rubberband_context.strokeStyle = \"#000000\";\n", - "\n", - " this._resize_canvas = function(width, height) {\n", - " // Keep the size of the canvas, canvas container, and rubber band\n", - " // canvas in synch.\n", - " canvas_div.css('width', width)\n", - " canvas_div.css('height', height)\n", - "\n", - " canvas.attr('width', width);\n", - " canvas.attr('height', height);\n", - "\n", - " rubberband.attr('width', width);\n", - " rubberband.attr('height', height);\n", - " }\n", - "\n", - " // Set the figure to an initial 600x600px, this will subsequently be updated\n", - " // upon first draw.\n", - " this._resize_canvas(600, 600);\n", - "\n", - " // Disable right mouse context menu.\n", - " $(this.rubberband_canvas).bind(\"contextmenu\",function(e){\n", - " return false;\n", - " });\n", - "\n", - " function set_focus () {\n", - " canvas.focus();\n", - " canvas_div.focus();\n", - " }\n", - "\n", - " window.setTimeout(set_focus, 100);\n", - "}\n", - "\n", - "mpl.figure.prototype._init_toolbar = function() {\n", - " var fig = this;\n", - "\n", - " var nav_element = $('
')\n", - " nav_element.attr('style', 'width: 100%');\n", - " this.root.append(nav_element);\n", - "\n", - " // Define a callback function for later on.\n", - " function toolbar_event(event) {\n", - " return fig.toolbar_button_onclick(event['data']);\n", - " }\n", - " function toolbar_mouse_event(event) {\n", - " return fig.toolbar_button_onmouseover(event['data']);\n", - " }\n", - "\n", - " for(var toolbar_ind in mpl.toolbar_items) {\n", - " var name = mpl.toolbar_items[toolbar_ind][0];\n", - " var tooltip = mpl.toolbar_items[toolbar_ind][1];\n", - " var image = mpl.toolbar_items[toolbar_ind][2];\n", - " var method_name = mpl.toolbar_items[toolbar_ind][3];\n", - "\n", - " if (!name) {\n", - " // put a spacer in here.\n", - " continue;\n", - " }\n", - " var button = $('