Source code for pzp_hardware.generic.control.proportional

# This file is a part of pzp-hardware, a library of laboratory hardware support Pieces
# for the puzzlepiece GUI & automation framework. Check out https://pzp-hardware.readthedocs.io
# Licensed under the Apache License 2.0 - https://github.com/jdranczewski/pzp-hardware/blob/main/LICENSE

"""
:module_title:`Proportional`

A basic proportional controller, changing one param in an attempt to shift another param
towards a desired value.

Example usage (see :ref:`getting-started` for more details on using Pieces in general)::

    import puzzlepiece as pzp
    from pzp_hardware.generic.control import proportional

    app = pzp.QApp()
    puzzle = pzp.Puzzle(debug=False)
    puzzle.add_piece("dummy", proportional.Dummy, 0, 0)
    puzzle.add_piece("proportional", proportional.Piece, 1, 0)
    puzzle.show()
    app.exec()

"""

import puzzlepiece as pzp
import numpy as np
import pyqtgraph as pg
from qtpy import QtWidgets, QtCore

[docs] class Dummy(pzp.Piece): """ Test Piece with an input param and an output param that's proportional to it. """ def define_params(self): pzp.param.spinbox(self, "in", 0.)(None) pzp.param.spinbox(self, "mult", 10.)(None) pzp.param.spinbox(self, "rand", .1)(None) @pzp.param.readout(self, "out", format="{:.4f}") def out(): return self["in"].value * self["mult"].value + np.random.random() * self["rand"].value
[docs] class Piece(pzp.Piece): """ Proportional control Piece. .. image:: ../images/pzp_hardware.generic.control.proportional.Piece.png """ update_plot = QtCore.Signal(float, float) param_wrap=2 def define_params(self): pzp.param.text(self, 'control', 'dummy:in')(None) pzp.param.text(self, 'measure', 'dummy:out')(None) pzp.param.spinbox(self, "unit_10e", 0, visible=False)(None) pzp.param.spinbox(self, 'goal', 1., v_step=.1)(None) pzp.param.spinbox(self, 'tolerance', 0.1, v_step=.1, visible=False)(None) pzp.param.spinbox(self, "good_to_stop", 5, visible=False)(None) pzp.param.spinbox(self, "step_loop_limit", 100, visible=False)(None) pzp.param.spinbox(self, "prop", 0.0100, v_step=.01)(None) self["prop"].input.setDecimals(4) @pzp.param.spinbox(self, "dt", .1, visible=False) def dt(value): self.timer.sleep = value def define_actions(self): @pzp.action.define(self, "Step") def step(): value = pzp.parse.parse_params(self["measure"].value, self.puzzle)[0].get_value() output = pzp.parse.parse_params(self["control"].value, self.puzzle)[0].get_value() goal = self["goal"].value * 10**self["unit_10e"].value error = goal - value output += self["prop"].value * error pzp.parse.parse_params(self["control"].value, self.puzzle)[0].set_value(output) new_value = pzp.parse.parse_params(self["measure"].value, self.puzzle)[0].get_value() self.update_plot.emit(output, new_value) return goal - new_value @pzp.action.define(self, "Step loop") def step_loop(): self.stop = False count_good = 0 for i in range(self["step_loop_limit"].value): error = self.actions["Step"]() self.puzzle.process_events() tolerance = self.params['tolerance'].get_value() * 10**self["unit_10e"].value if abs(error) < tolerance: count_good += 1 else: count_good = 0 if count_good >= self["good_to_stop"].value or self.stop: break @pzp.action.define(self, "Clear") def clear(): self._ins = [] self._outs = [] self._line_in.setData(self._ins) self._line_out.setData(self._outs) pzp.action.settings(self) def custom_layout(self): layout = QtWidgets.QVBoxLayout() # Add a PuzzleTimer for live view self.timer = pzp.threads.PuzzleTimer('Live', self.puzzle, self.actions['Step'], 0.1) self["dt"].set_value() layout.addWidget(self.timer) # Make the plots self.gl = pg.GraphicsLayoutWidget() layout.addWidget(self.gl) plot_in = self.gl.addPlot(0, 0) self._line_in = line_in = plot_in.plot() plot_out = self.gl.addPlot(1, 0) self._line_out = line_out = plot_out.plot() plot_out.addItem( plot_region := pg.LinearRegionItem( values=[self["goal"].value-self["tolerance"].value, self["goal"].value+self["tolerance"].value], orientation='horizontal', movable=False ) ) plot_out.addItem(il := pg.InfiniteLine(self["goal"].value, 0)) def update_items(): il.setValue(self["goal"].value) plot_region.setRegion((self["goal"].value-self["tolerance"].value, self["goal"].value+self["tolerance"].value)) self["goal"].changed.connect(update_items) self["tolerance"].changed.connect(update_items) self._ins = [] self._outs = [] def add_point(a, b): self._ins.append(a) self._outs.append(b / 10**self["unit_10e"].value) line_in.setData(self._ins) line_out.setData(self._outs) self.update_plot.connect(add_point) return layout
if __name__ == "__main__": app = pzp.QApp([]) puzzle = pzp.Puzzle() puzzle.add_piece("dummy", Dummy, 0, 0) puzzle.add_piece("proportional", Piece, 1, 0) puzzle.show() app.exec()