1
"""
2
The parameters of any module are controlled by descriptors deriving from
3
:obj:`BaseAttribute`.
4

5
An attribute is a field that can be set or get by several means:
6

7
- programmatically: module.attribute = value
8
- graphically: attribute.create_widget(module) returns a widget to
9
  manipulate the value
10
- via loading the value in a config file for permanent value preservation
11

12
Of course, the gui/parameter file/actual values have to stay "in sync" each
13
time the attribute value is changed. The necessary mechanisms are happening
14
behind the scene, and they are coded in this file.
15
"""
16

17 3
from __future__ import division
18 3
from functools import partial
19 3
from .pyrpl_utils import recursive_getattr, recursive_setattr
20 3
from .widgets.attribute_widgets import BoolAttributeWidget, \
21
                                       FloatAttributeWidget, \
22
                                       FilterAttributeWidget, \
23
                                       IntAttributeWidget, \
24
                                       SelectAttributeWidget, \
25
                                       StringAttributeWidget, \
26
                                       BoolIgnoreAttributeWidget, \
27
                                       TextAttributeWidget, \
28
                                       CurveAttributeWidget, \
29
                                       DataAttributeWidget, \
30
                                       CurveSelectAttributeWidget, \
31
                                       LedAttributeWidget, \
32
                                       PlotAttributeWidget, \
33
                                       BasePropertyListPropertyWidget, \
34
                                       ComplexAttributeWidget
35

36 3
from .curvedb import CurveDB
37 3
from collections import OrderedDict
38 3
import logging
39 3
import sys
40 3
import numpy as np
41 3
import numbers
42

43 3
logger = logging.getLogger(name=__name__)
44

45
#way to represent the smallest positive value
46
#needed to set floats to minimum count above zero
47 3
epsilon = sys.float_info.epsilon
48

49

50 3
class BaseAttribute(object):
51
    """base class for attribute - only used as a placeholder"""
52

53 3
class BaseProperty(BaseAttribute):
54
    """
55
    A Property is a special type of attribute that is not mapping a fpga value,
56
    but rather an attribute _name of the module. This is used mainly in
57
    SoftwareModules
58

59
    An attribute is a field that can be set or get by several means:
60

61
    * programmatically: module.attribute = value
62
    * graphically: attribute.create_widget(module) returns a widget to
63
      manipulate the value
64
    * via loading the value in a config file for permanence
65

66
    The concrete derived class need to have certain attributes properly
67
    defined:
68

69
    * widget_class: the class of the widget to use for the gui (see
70
      attribute_widgets.py)
71
    * a function set_value(instance, value) that effectively sets the value
72
      (on redpitaya or elsewhere)
73
    * a function get_value(instance) that reads the value from
74
      wherever it is stored internally
75

76
    """
77 3
    _widget_class = None
78 3
    widget = None
79 3
    default = None
80

81 3
    def __init__(self,
82
                 default=None,
83
                 doc="",
84
                 ignore_errors=False,
85
                 call_setup=False):
86
        """
87
        default: if provided, the value is initialized to it
88
        """
89 3
        if default is not None:
90 3
            self.default = default
91 3
        self.call_setup = call_setup
92 3
        self.ignore_errors = ignore_errors
93 3
        self.__doc__ = doc
94

95 3
    def __set__(self, obj, value):
96
        """
97
        This function is called for any BaseAttribute, such that all the gui
98
        updating, and saving to disk is done automatically. The real work is
99
        delegated to self.set_value.
100
        """
101 3
        value = self.validate_and_normalize(obj, value)
102 3
        self.set_value(obj, value)
103
        # save new value in config, lauch signal and possibly call setup()
104 3
        self.value_updated(obj, value)#self.get_value(obj))
105

106 3
    def validate_and_normalize(self, obj, value):
107
        """
108
        This function should raise an exception if the value is incorrect.
109
        Normalization can be:
110

111
        - returning value.name if attribute "name" exists
112
        - rounding to nearest multiple of step for float_registers
113
        - rounding elements to nearest valid_frequencies for FilterAttributes
114
        """
115 0
        return value  # by default any value is valid
116

117 3
    def value_updated(self, module, value=None, appendix=[]):
118
        """
119
        Once the value has been changed internally, this function is called to perform the following actions:
120

121
        - launch the signal module._signal_launcher.attribute_changed (this is
122
          used in particular for gui update)
123
        - saves the new value in the config file (if flag
124
          module._autosave_active is True).
125
        - calls the callback function if the attribute is in module.callback
126

127
         Note for developers: We might consider moving the 2 last points in a connection behind the signal "attribute_changed".
128
        """
129 3
        if value is None:
130 0
            value = self.get_value(module)
131 3
        self.launch_signal(module, value, appendix=appendix)
132 3
        if module._autosave_active:  # (for module, when module is slaved, don't save attributes)
133 3
            if self.name in module._setup_attributes:
134 0
                self.save_attribute(module, value)
135 3
        if self.call_setup and not module._setup_ongoing:
136
            # call setup unless a bunch of attributes are being changed together.
137 0
            module._logger.info('Calling setup() for %s.%s ...', module.name, self.name)
138 0
            module.setup()
139 3
        return value
140

141 3
    def __get__(self, instance, owner):
142
        # self.parent = instance
143
        #store instance in memory <-- very bad practice: there is one Register for the class
144
        # and potentially many obj instances (think of having 2 redpitayas in the same python session), then
145
        # _read should use different clients depending on which obj is calling...)
146 3
        if instance is None:
147 3
            return self
148 3
        return self.get_value(instance)
149

150 3
    def launch_signal(self, module, new_value, appendix=[]):
151
        """
152
        Updates the widget and other subscribers with the module's value.
153
        """
154 3
        try:
155 3
            module._signal_launcher.update_attribute_by_name.emit(
156
                self.name,
157
                [new_value]+appendix)
158 0
        except AttributeError as e:  # occurs if nothing is connected (TODO:
159
            # remove this)
160 0
            module._logger.error("Erro in launch_signal of %s: %s",
161
                                 module.name, e)
162

163 3
    def save_attribute(self, module, value):
164
        """
165
        Saves the module's value in the config file.
166
        """
167 0
        module.c[self.name] = value
168

169 3
    def _create_widget(self, module, widget_name=None):
170
        """
171
        Creates a widget to graphically manipulate the attribute.
172
        """
173 0
        if self._widget_class is None:
174 0
            logger.warning("Module %s of type %s is trying to create a widget "
175
                           "for %s, but no _widget_class is defined!",
176
                           str(module), type(module), self.name)
177 0
            return None
178 0
        widget = self._widget_class(module, self.name, widget_name=widget_name)
179 0
        return widget
180

181 3
    def get_value(self, obj):
182 3
        if not hasattr(obj, '_' + self.name):
183 3
            setattr(obj, '_' + self.name, self.default)
184 3
        return getattr(obj, '_' + self.name)
185

186 3
    def set_value(self, obj, val):
187 3
        setattr(obj, '_' + self.name, val)
188

189

190 3
class BaseRegister(BaseProperty):
191
    """Registers implement the necessary read/write logic for storing an attribute on the redpitaya.
192
    Interface for basic register of type int. To convert the value between register format and python readable
193
    format, registers need to implement "from_python" and "to_python" functions"""
194 3
    default = None
195 3
    def __init__(self, address, bitmask=None, **kwargs):
196 3
        self.address = address
197 3
        self.bitmask = bitmask
198 3
        BaseProperty.__init__(self, **kwargs)
199

200 3
    def _writes(self, obj, addr, v):
201 0
        return obj._writes(addr, v)
202

203 3
    def _reads(self, obj, addr, l):
204 0
        return obj._reads(addr, l)
205

206 3
    def _write(self, obj, addr, v):
207 0
        return obj._write(addr, v)
208

209 3
    def _read(self, obj, addr):
210 0
        return obj._read(addr)
211

212 3
    def get_value(self, obj):
213
        """
214
        Retrieves the value that is physically on the redpitaya device.
215
        """
216
        # self.parent = obj  # store obj in memory
217 0
        if self.bitmask is None:
218 0
            return self.to_python(obj, obj._read(self.address))
219
        else:
220 0
            return self.to_python(obj, obj._read(self.address) & self.bitmask)
221

222 3
    def set_value(self, obj, val):
223
        """
224
        Sets the value on the redpitaya device.
225
        """
226 0
        if self.bitmask is None:
227 0
            obj._write(self.address, self.from_python(obj, val))
228
        else:
229 0
            act = obj._read(self.address)
230 0
            new = act & (~self.bitmask) | (int(self.from_python(obj, val)) & self.bitmask)
231 0
            obj._write(self.address, new)
232

233 3
    def __set__(self, obj, value):
234
        """
235
        this is very similar to the __set__ function of the parent,
236
        but here, value_updated is called with the return from
237
        validate_and_normalize instead of with the new from get_value in
238
        order to save one read operation.
239
        """
240 0
        value = self.validate_and_normalize(obj, value)
241 0
        self.set_value(obj, value)
242
        # save new value in config, lauch signal and possibly call setup()
243 0
        self.value_updated(obj, value)
244

245

246 3
class BoolProperty(BaseProperty):
247
    """
248
    A property for a boolean value
249
    """
250 3
    _widget_class = BoolAttributeWidget
251 3
    default = False
252

253 3
    def validate_and_normalize(self, obj, value):
254
        """
255
        Converts value to bool.
256
        """
257 0
        return bool(value)
258

259

260 3
class LedProperty(BoolProperty):
261 3
    _widget_class = LedAttributeWidget
262

263 3
    def __init__(self,
264
                 true_function = None,
265
                 false_function = None,
266
                 **kwargs):
267
        """
268
        default: if provided, the value is initialized to it
269
        """
270 3
        self.true_function = true_function or self.true_function
271 3
        self.false_function = false_function or self.false_function
272 3
        super(LedProperty, self).__init__(**kwargs)
273

274 3
    def set_value(self, obj, val):
275 0
        try:
276 0
            if val:
277 0
                self.true_function(obj)
278
            else:
279 0
                self.false_function(obj)
280 0
        except TypeError as e:
281 0
            obj._logger.debug('Cannot call %s of %s.%s: %s',
282
                              'true_function' if val else 'false_function',
283
                              obj.name, self.name, e)
284
        else:
285 0
            super(LedProperty, self).set_value(obj, val)
286

287

288 3
class BoolRegister(BaseRegister, BoolProperty):
289
    """Inteface for boolean values, 1: True, 0: False.
290
    invert=True inverts the mapping"""
291 3
    def __init__(self, address, bit=0, bitmask=None, invert=False, **kwargs):
292 3
        self.bit = bit
293 3
        assert type(invert) == bool
294 3
        self.invert = invert
295 3
        BaseRegister.__init__(self, address=address, bitmask=bitmask)
296 3
        BoolProperty.__init__(self, **kwargs)
297

298 3
    def to_python(self, obj, value):
299 0
        value = bool((value >> self.bit) & 1)
300 0
        if self.invert:
301 0
            value = not value
302 0
        return value
303

304 3
    def from_python(self, obj, val):
305 0
        if self.invert:
306 0
            val = not val
307 0
        if val:
308 0
            towrite = obj._read(self.address) | (1 << self.bit)
309
        else:
310 0
            towrite = obj._read(self.address) & (~(1 << self.bit))
311 0
        return towrite
312

313

314 3
class BoolIgnoreProperty(BoolProperty):
315
    """
316
    An attribute for booleans
317
    """
318 3
    _widget_class = BoolIgnoreAttributeWidget
319 3
    default = False
320

321 3
    def validate_and_normalize(self, obj, value):
322
        """
323
        Converts value to bool.
324
        """
325 0
        if isinstance(value, str):  # used to be basestring
326 0
            if value.lower() == 'true':
327 0
                return True
328 0
            elif value.lower() == 'false':
329 0
                return False
330
            else:
331 0
                return 'ignore'
332
        else:
333 0
            return bool(value)
334

335

336 3
class IORegister(BoolRegister):
337
    """Interface for digital outputs
338
    if argument outputmode is True, output mode is set, else input mode"""
339 3
    def __init__(self, read_address, write_address, direction_address,
340
                 outputmode=True, **kwargs):
341 3
        if outputmode:
342 3
            address = write_address
343
        else:
344 0
            address = read_address
345 3
        self.direction_address = direction_address
346
        # self.direction = BoolRegister(direction_address,bit=bit, **kwargs)
347 3
        self.outputmode = outputmode  # set output direction
348 3
        BoolRegister.__init__(self, address=address, **kwargs)
349

350 3
    def direction(self, obj, v=None):
351
        """ sets the direction (inputmode/outputmode) for the Register """
352 0
        if v is None:
353 0
            v = self.outputmode
354 0
        if v:
355 0
            v = obj._read(self.direction_address) | (1 << self.bit)
356
        else:
357 0
            v = obj._read(self.direction_address) & (~(1 << self.bit))
358 0
        obj._write(self.direction_address, v)
359

360 3
    def get_value(self, obj):
361 0
        self.direction(obj)
362 0
        return BoolRegister.get_value(self, obj)
363

364 3
    def set_value(self, obj, val):
365 0
        self.direction(obj)
366 0
        return BoolRegister.set_value(self, obj, val)
367

368

369 3
class NumberProperty(BaseProperty):
370
    """
371
    Abstract class for ints and floats
372
    """
373 3
    _widget_class = IntAttributeWidget
374 3
    default = 0
375

376 3
    def __init__(self,
377
                 min=-np.inf,
378
                 max=np.inf,
379
                 increment=0,
380
                 log_increment=False,  # if True, the widget has log increment
381
                 **kwargs):
382 3
        self.min = min
383 3
        self.max = max
384 3
        self.increment = increment
385 3
        self.log_increment = log_increment
386 3
        BaseProperty.__init__(self, **kwargs)
387

388 3
    def _create_widget(self, module, widget_name=None):
389 0
        widget = BaseProperty._create_widget(self, module,
390
                                             widget_name=widget_name)
391 0
        return widget
392

393 3
    def validate_and_normalize(self, obj, value):
394
        """
395
        Saturates value with min and max.
396
        """
397 3
        if value is None:  # setting a number to None essentially calls setup()
398 0
            value = self.get_value(obj)
399 3
        return max(min(value, self.max), self.min)
400

401

402 3
class IntProperty(NumberProperty):
403 3
    def __init__(self,
404
                 min=-np.inf,
405
                 max=np.inf,
406
                 increment=1,
407
                 log_increment=False,  # if True, the widget has log increment
408
                 **kwargs):
409 3
        super(IntProperty, self).__init__(min=min,
410
                                          max=max,
411
                                          increment=increment,
412
                                          log_increment=log_increment,
413
                                          **kwargs)
414

415 3
    def validate_and_normalize(self, obj, value):
416
        """
417
        Accepts float, but rounds to integer
418
        """
419 0
        if value is None:  # setting a number to None essentially calls setup()
420 0
            value = self.get_value(obj)
421 0
        return NumberProperty.validate_and_normalize(self,
422
                                                     obj,
423
                                                     int(round(value)))
424

425

426 3
class IntRegister(BaseRegister, IntProperty):
427
    """
428
    Register for integer values encoded on less than 32 bits.
429
    """
430 3
    def __init__(self, address, bits=32, bitmask=None, **kwargs):
431 3
        self.bits = bits
432 3
        self.size = int(np.ceil(float(self.bits) / 32))
433 3
        BaseRegister.__init__(self, address=address, bitmask=bitmask)
434 3
        if not 'min' in kwargs: kwargs['min'] = 0
435 3
        if not 'max' in kwargs: kwargs['max'] = 2**self.bits-1
436 3
        IntProperty.__init__(self,
437
                             **kwargs)
438

439 3
    def to_python(self, obj, value):
440 0
        return int(value)
441

442 3
    def from_python(self, obj, value):
443 0
        return int(value)
444

445

446 3
class ConstantIntRegister(IntRegister):
447
    """
448
    Implements an int register that only interacts with the FPGA once and
449
    subsequently returns the first read value from python memory.
450
    """
451 3
    def get_value(self, obj):
452 0
        try:
453 0
            return getattr(obj, '_' + self.name)
454 0
        except AttributeError:
455 0
            value = super(ConstantIntRegister, self).get_value(obj)
456 0
            setattr(obj, '_' + self.name, value)
457 0
            return value
458

459

460 3
class LongRegister(IntRegister):
461
    """Interface for register of python type int/long with arbitrary length 'bits' (effectively unsigned)"""
462 3
    def get_value(self, obj):
463 0
        values = obj._reads(self.address, self.size)
464 0
        value = int(0)
465 0
        for i in range(self.size):
466 0
            value += int(values[i]) << (32 * i)
467 0
        if self.bitmask is None:
468 0
            return self.to_python(obj, value)
469
        else:
470 0
            return (self.to_python(obj, value) & self.bitmask)
471

472 3
    def set_value(self, obj, val):
473 0
        val = self.from_python(obj, val)
474 0
        values = np.zeros(self.size, dtype=np.uint32)
475 0
        if self.bitmask is None:
476 0
            for i in range(self.size):
477 0
                values[i] = (val >> (32 * i)) & 0xFFFFFFFF
478
        else:
479 0
            act = obj._reads(self.address, self.size)
480 0
            for i in range(self.size):
481 0
                localbitmask = (self.bitmask >> 32 * i) & 0xFFFFFFFF
482 0
                values[i] = ((val >> (32 * i)) & localbitmask) | \
483
                            (int(act[i]) & (~localbitmask))
484 0
        obj._writes(self.address, values)
485

486

487 3
class FloatProperty(NumberProperty):
488
    """
489
    An attribute for a float value.
490
    """
491 3
    _widget_class = FloatAttributeWidget
492 3
    default = 0.0
493

494 3
    def validate_and_normalize(self, obj, value):
495
        """
496
        Try to convert to float, then saturates with min and max
497
        """
498 3
        return NumberProperty.validate_and_normalize(self,
499
                                                     obj,
500
                                                     float(value))
501

502

503 3
class ComplexProperty(FloatProperty):
504 3
    _widget_class = ComplexAttributeWidget
505 3
    def validate_and_normalize(self, obj, val):
506 0
        val = complex(val)
507 0
        re = super(ComplexProperty, self).validate_and_normalize(obj, val.real)
508 0
        im = super(ComplexProperty, self).validate_and_normalize(obj, val.imag)
509 0
        return complex(re, im)
510

511

512 3
class FloatRegister(IntRegister, FloatProperty):
513
    """Implements a fixed point register, seen like a (signed) float from python"""
514 3
    def __init__(self, address,
515
                 bits=14,  # total number of bits to represent on fpga
516
                 bitmask=None,
517
                 norm=1.0,  # fpga value corresponding to 1 in python
518
                 signed=True,  # otherwise unsigned
519
                 invert=False,  # if False: FPGA=norm*python, if True: FPGA=norm/python
520
                 **kwargs):
521 3
        IntRegister.__init__(self, address=address, bits=bits, bitmask=bitmask)
522 3
        self.invert = invert
523 3
        self.signed = signed
524 3
        self.norm = float(norm)
525 3
        if 'increment' not in kwargs:
526 3
            kwargs['increment'] = 1.0/self.norm
527 3
        if 'max' not in kwargs:
528 3
            kwargs['max'] = (float(2 ** (self.bits - int(self.signed)) - 1) / self.norm)
529 3
        if 'min' not in kwargs:
530 3
            if self.signed:
531 3
                kwargs['min'] = - float(2 ** (self.bits - int(self.signed))) / self.norm
532
            else:
533 3
                kwargs['min'] = 0
534 3
        FloatProperty.__init__(self, **kwargs)
535

536 3
    def to_python(self, obj, value):
537
        # 2's complement
538 0
        if self.signed:
539 0
            if value >= 2 ** (self.bits - 1):
540 0
                value -= 2 ** self.bits
541
        # normalization
542 0
        if self.invert:
543 0
            if value == 0:
544 0
                return float(0)
545
            else:
546 0
                return 1.0 / float(value) / self.norm
547
        else:
548 0
            return float(value) / self.norm
549

550 3
    def from_python(self, obj, value):
551
        # round and normalize
552 0
        if self.invert:
553 0
            if value == 0:
554 0
                v = 0
555
            else:
556 0
                v = int(round(1.0 / float(value) * self.norm))
557
        else:
558 0
            v = int(round(float(value) * self.norm))
559
            # make sure small float values are not rounded to zero
560 0
        if (v == 0 and value > 0):
561 0
            v = 1
562 0
        elif (v == 0 and value < 0):
563 0
            v = -1
564 0
        if self.signed:
565
            # saturation
566 0
            if (v >= 2 ** (self.bits - 1)):
567 0
                v = 2 ** (self.bits - 1) - 1
568 0
            elif (v < -2 ** (self.bits - 1)):
569 0
                v = -2 ** (self.bits - 1)
570
            # 2's complement
571 0
            if (v < 0):
572 0
                v += 2 ** self.bits
573
        else:
574 0
            v = abs(v)  # take absolute value
575
            # unsigned saturation
576 0
            if v >= 2 ** self.bits:
577 0
                v = 2 ** self.bits - 1
578 0
        return v
579

580 3
    def validate_and_normalize(self, obj, value):
581
        """
582
        For unsigned registers, takes the absolute value of the given value.
583
        Rounds to the nearest value authorized by the register granularity,
584
        then does the same as FloatProperty (==NumberProperty).
585
        """
586 0
        if not self.signed:
587 0
            value = abs(value)
588 0
        return FloatProperty.validate_and_normalize(self, obj,
589
                                round(value/self.increment)*self.increment)
590

591

592 3
class GainRegister(FloatRegister):
593
    """
594
    A register used mainly for gains, that replaces round-off to zero by
595
    round-off to the lowest-possible value.
596
    """
597 3
    avoid_round_off_to_zero = True
598

599 3
    def validate_and_normalize(self, obj, value):
600 0
        rounded_value = FloatRegister.validate_and_normalize(self, obj, value)
601 0
        if rounded_value == 0 and value != 0:  # value was rounded off to zero
602 0
            if self.avoid_round_off_to_zero:
603 0
                rounded_value = FloatRegister.validate_and_normalize(
604
                    self, obj, np.abs(self.increment)*np.sign(value))
605 0
                obj._logger.warning("Avoided rounding value %.1e of the "
606
                                    "gain register %s to zero. Setting it to %.1e "
607
                                    "instead. ", value, self.name, rounded_value)
608
            else:
609 0
                obj._logger.warning("Rounding value %.1e of the "
610
                                    "gain register %s to zero. ", value, self.name)
611 0
        if value > self.max or value < self.min:
612 0
            obj._logger.warning("Requested gain for %s.%s is outside the "
613
                                "bounds allowed by the hardware. Desired "
614
                                "gain of %.1e is capped to %.1e. ",
615
                                obj.name, self.name, value, rounded_value)
616 0
        return rounded_value
617

618

619 3
class FrequencyProperty(FloatProperty):
620
    """
621
    An attribute for frequency values
622
    Same as FloatAttribute, except it cannot become negative.
623
    """
624 3
    def __init__(self, **kwargs):
625 3
        if 'min' not in kwargs:
626 3
            kwargs['min'] = 0
627 3
        FloatProperty.__init__(self, **kwargs)
628

629

630 3
class FrequencyRegister(FloatRegister, FrequencyProperty):
631
    """Registers that contain a frequency as a float in units of Hz"""
632
    # attention: no bitmask can be defined for frequencyregisters
633 3
    CLOCK_FREQUENCY = 125e6
634

635 3
    def __init__(self, address, **kwargs):
636 3
        FloatRegister.__init__(self, address, **kwargs)
637 3
        self.min = 0
638 3
        self.max = self.CLOCK_FREQUENCY / 2.0
639 3
        self.increment = self.CLOCK_FREQUENCY / 2 ** self.bits
640

641 3
    def from_python(self, obj, value):
642
        # make sure small float values are not rounded to zero
643 0
        value = abs(float(value) / obj._frequency_correction)
644 0
        if (value == epsilon):
645 0
            value = 1
646
        else:
647
            # round and normalize
648 0
            value = int(round(
649
                value / self.CLOCK_FREQUENCY * 2 ** self.bits))  # Seems correct (should not be 2**bits -1): 125 MHz
650
            # out of reach because 2**bits is out of reach
651 0
        return value
652

653 3
    def to_python(self, obj, value):
654 0
        return 125e6 / 2 ** self.bits * float(
655
            value) * obj._frequency_correction
656

657 3
    def validate_and_normalize(self, obj, value):
658
        """
659
        Same as FloatRegister, except the value should be positive.
660
        """
661 0
        return FrequencyProperty.validate_and_normalize(self, obj,
662
                        FloatRegister.validate_and_normalize(self, obj, value))
663

664

665 3
class PhaseProperty(FloatProperty):
666
    """
667
    An attribute to represent a phase
668
    """
669 3
    def validate_and_normalize(self, obj, value):
670
        """
671
        Rejects anything that is not float, and takes modulo 360
672
        """
673 0
        return FloatProperty.validate_and_normalize(self,
674
                                                    obj,
675
                                                    value % 360.)
676

677

678 3
class PhaseRegister(FloatRegister, PhaseProperty):
679
    """Registers that contain a phase as a float in units of degrees."""
680 3
    def __init__(self, address, bits=32, bitmask=None, invert=False, **kwargs):
681 3
        FloatRegister.__init__(self, address=address, bits=bits,
682
                               bitmask=bitmask, invert=invert)
683 3
        PhaseProperty.__init__(self, increment=360. / 2 ** bits, **kwargs)
684

685 3
    def from_python(self, obj, value):
686 0
        if self.invert:
687 0
            value = float(value) * (-1)
688 0
        return int(round(float(value) / 360 * 2 ** self.bits) % 2 ** self.bits)
689

690 3
    def to_python(self, obj, value):
691 0
        phase = float(value) / 2 ** self.bits * 360
692 0
        if self.invert:
693 0
            phase *= -1
694 0
        return phase % 360.0
695

696 3
    def validate_and_normalize(self, obj, value):
697
        """
698
        Rounds to nearest authorized register value and take modulo 360
699
        """
700 0
        return ((int(round(float(value) / 360 * 2 ** self.bits)) / 2 ** self.bits) * 360.) % 360.0
701

702

703 3
class FilterProperty(BaseProperty):
704
    """
705
    An attribute for a list of bandwidth. Each bandwidth has to be chosen in a list given by
706
    self.valid_frequencies(module) (evaluated at runtime). If floats are provided, they are normalized to the
707
    nearest values in the list. Individual floats are also normalized to a singleton.
708
    The number of elements in the list are also defined at runtime.
709
    A property for a list of float values to be chosen in valid_frequencies(module).
710
    """
711 3
    _widget_class = FilterAttributeWidget
712

713 3
    def validate_and_normalize(self, obj, value):
714
        """
715
        Returns a list with the closest elements in module.valid_frequencies
716
        """
717 0
        if not np.iterable(value):
718 0
            value = [value]
719 0
        value = [min([opt for opt in self.valid_frequencies(obj)],
720
                      key=lambda x: abs(x - val)) for val in value]
721 0
        if len(value) == 1:
722 0
            return value[0]
723
        else:
724 0
            return value
725

726 3
    def get_value(self, obj):
727 0
        if not hasattr(obj, '_' + self.name):
728
            # choose any value in the options as default.
729 0
            default = self.valid_frequencies(obj)[0]
730 0
            setattr(obj, '_' + self.name, default)
731 0
        return getattr(obj, '_' + self.name)
732

733 3
    def set_value(self, obj, value):
734 0
        return BaseProperty.set_value(self, obj, value)
735

736 3
    def valid_frequencies(self, module):
737 0
        raise NotImplementedError("this is a baseclass, your derived class "
738
                                  "must implement the following function")
739

740 3
    def refresh_options(self, module):
741 0
        module._signal_launcher.refresh_filter_options.emit(self.name)
742

743

744 3
class FilterRegister(BaseRegister, FilterProperty):
745
    """
746
    Interface for up to 4 low-/highpass filters in series (filter_block.v)
747
    """
748 3
    _widget_class = FilterAttributeWidget
749

750 3
    def __init__(self, address, filterstages, shiftbits, minbw, **kwargs):
751 3
        self.filterstages = filterstages
752 3
        self.shiftbits = shiftbits
753 3
        self.minbw = minbw
754 3
        BaseRegister.__init__(self, address=address)
755 3
        FilterProperty.__init__(self, **kwargs)
756

757 3
    def read_and_save(self, obj, attr_name):
758
        # save the value of constants saved in the fpga upon first execution
759
        # in order to only read the corresponding register once
760 0
        var_name = "_" + self.name + "_" + attr_name
761 0
        if not hasattr(obj, var_name):
762 0
            setattr(obj, var_name, obj._read(getattr(self, attr_name)))
763 0
        return getattr(obj, var_name)
764

765 3
    def _FILTERSTAGES(self, obj):
766 0
        return self.read_and_save(obj, "filterstages")
767

768 3
    def _SHIFTBITS(self, obj):
769 0
        return self.read_and_save(obj, "shiftbits")
770

771 3
    def _MINBW(self, obj):
772 0
        return self.read_and_save(obj, "minbw")
773

774 3
    def _MAXSHIFT(self, obj):
775 0
        def clog2(x):
776
            """ mirrors the function clog2 in verilog code """
777 0
            if x < 2:
778 0
                return 1
779 0
            elif x > 2**32:
780 0
                return -1
781 0
            elif x > 2**31:
782 0
                return 32
783
            else:
784 0
                return int(np.floor(np.log2(float(x))))+1
785 0
        return clog2(125000000.0/float(self._MINBW(obj)))
786

787
    #def _ALPHABITS(self, obj):
788
    #    return int(np.ceil(np.log2(125000000.0 / self._MINBW(obj))))
789

790 3
    def valid_frequencies(self, obj):
791
        """ returns a list of all valid filter cutoff frequencies"""
792
        #valid_bits = range(0, self._MAXSHIFT(obj)-1)  # this is possible
793 0
        valid_bits = range(0, self._MAXSHIFT(obj)-2)  # this gives reasonable results (test_filter)
794 0
        pos = list([self.to_python(obj, b | 0x1 << 7) for b in valid_bits])
795 0
        pos = [val if not np.iterable(val) else val[0] for val in pos]
796 0
        neg = [-val for val in reversed(pos)]
797 0
        valid_frequencies = neg + [0] + pos
798 0
        if obj is not None and not hasattr(obj.__class__,
799
                                           self.name+'_options') and not hasattr(obj, self.name+'_options'):
800 0
            setattr(obj, self.name+'_options', valid_frequencies)
801 0
        return valid_frequencies
802

803
    # empirical correction factors for the cutoff frequencies in order to be
804
    # able to accurately model implemented bandwidth with an analog
805
    # butterworth filter. Works well up to 5 MHz. See unittest test_inputfilter
806 3
    correction_factors = {0.5: 0.7,
807
                          0.25: 1.65,
808
                          0.125: 1.17,
809
                          0.0625: 1.08,
810
                          0.03125: 1.04,
811
                          0.015625: 1.02,
812
                          0.0078125: 1.01,
813
                          0.001953125: 1.0,
814
                          0.00390625: 1.0}
815

816 3
    def to_python(self, obj, value):
817
        """
818
        returns a list of bandwidths for the low-pass filter cascade before the module
819
        negative bandwidth stands for high-pass instead of lowpass, 0 bandwidth for bypassing the filter
820
        """
821 0
        filter_shifts = value
822 0
        bandwidths = []
823 0
        for i in range(self._FILTERSTAGES(obj)):
824 0
            v = (filter_shifts >> (i * 8)) & 0xFF
825 0
            shift = v & (2 ** self._SHIFTBITS(obj) - 1)
826 0
            filter_on = ((v >> 7) == 0x1)
827 0
            highpass = (((v >> 6) & 0x1) == 0x1)
828 0
            if filter_on:
829
                # difference equation is
830
                # y[n] = (1-alpha)*y[n-1] + alpha*x[n]
831 0
                alpha = float(2 ** shift) / (2 ** self._MAXSHIFT(obj))
832
                # old formula
833
                #bandwidth = alpha * 125e6 / 2 / np.pi
834
                # new, more correct formula (from Oppenheim-Schafer p. 70)
835 0
                bandwidth = -np.log(1.0-alpha)/2.0/np.pi*125e6
836
                # here comes a nasty bugfix to make it work (see issue 242)
837 0
                if alpha in self.correction_factors:
838 0
                    bandwidth *= self.correction_factors[alpha]
839 0
                if highpass:
840 0
                    bandwidth *= -1.0
841
            else:
842 0
                bandwidth = 0
843 0
            bandwidths.append(bandwidth)
844 0
        if len(bandwidths) == 1:
845 0
            return bandwidths[0]
846
        else:
847 0
            return bandwidths
848

849 3
    def from_python(self, obj, value):
850 0
        try:
851 0
            v = list(value)[:self._FILTERSTAGES(obj)]
852 0
        except TypeError:
853 0
            v = list([value])[:self._FILTERSTAGES(obj)]
854 0
        filter_shifts = 0
855 0
        for i in range(self._FILTERSTAGES(obj)):
856 0
            if len(v) <= i:
857 0
                bandwidth = 0
858
            else:
859 0
                bandwidth = float(v[i])
860 0
            if bandwidth == 0:
861 0
                continue
862
            else:
863
                # old formula
864
                #alpha = np.abs(bandwidth)*2*np.pi/125e6
865
                # new formula
866 0
                alpha = 1.0 - np.exp(-np.abs(bandwidth)*2.0*np.pi/125e6)
867 0
                if alpha in self.correction_factors:
868 0
                    bandwidth /= self.correction_factors[alpha]
869 0
                    alpha = 1.0 - np.exp(-np.abs(bandwidth)*2.0*np.pi/125e6)
870 0
                shift = int(np.round(np.log2(alpha*(2**self._MAXSHIFT(obj)))))
871 0
                if shift < 0:
872 0
                    shift = 0
873 0
                elif shift > (2**self._SHIFTBITS(obj) - 1):
874 0
                    shift = (2**self._SHIFTBITS(obj) - 1)
875 0
                shift += 2**7  # turn this filter stage on
876 0
                if bandwidth < 0:
877 0
                    shift += 2**6  # turn this filter into a highpass
878 0
                filter_shifts += shift * 2**(8*i)
879 0
        return filter_shifts
880

881

882 3
class AttributeList(list):
883
    """
884
    A list of attributes.
885

886
    This class is not an attribute/property by itself, but is the object
887
    returned by AttributeListProperty that correctly extends list methods to
888
    communicate a change in the list throughout pyrpl.
889

890
    When a list-specific operation is performed that alters the values,
891
    the AttributeListProperty object is informed about this and will ensure
892
    the correct propagation of the signal.
893
    """
894 3
    def __init__(self, parent, module, *args, **kwargs):
895 0
        self._parent = parent
896 0
        self._module = module
897 0
        super(AttributeList, self).__init__(*args, **kwargs)
898

899
    # insert, __setitem__, and __delitem__ completely describe the behavior
900 3
    def insert(self, index, new=None):
901 0
        if new is None:
902 0
            new = self._parent.default_element or self._parent.element_cls.default
903 0
        new = self._parent.validate_and_normalize_element(self._module, new)
904 0
        super(AttributeList, self).insert(index, new)
905 0
        self._parent.list_changed(self._module, "insert", index, new)
906 0
        self.selected = index
907

908 3
    def __setitem__(self, index, value):
909
        # rely on parent's validate_and_normalize function
910 0
        value = self._parent.validate_and_normalize_element(self._module, value)
911
        # set value
912 0
        super(AttributeList, self).__setitem__(index, value)
913 0
        self._parent.list_changed(self._module, "setitem", index, value)
914 0
        self.selected = index
915

916 3
    def __delitem__(self, index=-1):
917
        # unselect if selected
918 0
        if self.selected == self._get_unique_index(index):
919 0
            self.selected = None
920
        # remove and send message
921 0
        super(AttributeList, self).pop(index)
922 0
        self._parent.list_changed(self._module, "delitem", index)
923

924 3
    @property
925
    def selected(self):
926 0
        if not hasattr(self, '_selected'):
927 0
            self._selected = None
928 0
        return self._selected
929

930 3
    @selected.setter
931
    def selected(self, index):
932
        # old = self.selected
933 0
        self._selected = self._get_unique_index(index)
934 0
        self._parent.list_changed(self._module, 'select', self.selected)
935

936 3
    def _get_unique_index(self, index):
937 0
        try:
938 0
            return self.index(self[index])
939 0
        except:
940 0
            return None
941

942 3
    def select(self, value):
943
        """ selects the element with value, or None if it does not exist """
944 0
        try:
945 0
            self.selected = self.index(value)
946 0
        except IndexError:
947 0
            self.selected = None
948

949
    # other convenience functions that are based on above axioms
950 3
    def append(self, new=None):
951 0
        self.insert(self.__len__(), new)
952

953 3
    def extend(self, iterable=[]):
954 0
        for i in iterable:
955 0
            self.append(i)
956

957 3
    def pop(self, index=-1):
958
        # get attributes
959 0
        item = self[index]
960 0
        self.__delitem__(index)
961 0
        return item
962

963 3
    def remove(self, value):
964 0
        self.__delitem__(self.index(value))
965

966 3
    def clear(self):
967 0
        while len(self) > 0:
968 0
            self.__delitem__()
969

970 3
    def copy(self):
971 0
        return list(self)
972

973 3
    def sort(self, key=None, reverse=False):
974 0
        sorted = self.copy().sort(key=key, reverse=reverse)
975 0
        for i, v in enumerate(sorted):
976 0
            self[i] = v
977

978 3
    def reverse(self):
979 0
        reversed = self.copy()
980 0
        reversed.reverse()
981 0
        for i, v in enumerate(reversed):
982 0
            self[i] = v
983

984

985 3
class BasePropertyListProperty(BaseProperty):
986
    """
987
    An arbitrary length list of items that behave like BaseProperty.
988

989
    A derived class FloatPropertyListProperty(BasePropertyListProperty)
990
    will behave as a list of FloatProperty-like items.
991
    """
992 3
    default = []
993 3
    _widget_class = BasePropertyListPropertyWidget
994

995 3
    def __init__(self, *args, **kwargs):
996
        """
997
        default is the default list
998
        default_element: default new element
999
        """
1000 3
        self.default_element = kwargs.pop('default_element', None)
1001 3
        super(BasePropertyListProperty, self).__init__(*args, **kwargs)
1002

1003 3
    @property
1004
    def element_cls(self):
1005
        """ the class of the elements of the list """
1006 0
        return super(BasePropertyListProperty, self)
1007

1008 3
    def validate_and_normalize(self, obj, value):
1009
        """
1010
        Converts the value into a list.
1011
        """
1012 0
        return list(value)
1013

1014 3
    def validate_and_normalize_element(self, obj, val):
1015 0
        return self.element_cls.validate_and_normalize(obj, val)
1016

1017 3
    def get_value(self, obj):
1018 0
        if not hasattr(obj, '_' + self.name):
1019
            # make a new AttributeList, pass to it the instance of obj
1020 0
            value = AttributeList(self, obj, self.default)
1021 0
            setattr(obj, '_' + self.name, value)
1022 0
        return getattr(obj, '_' + self.name)
1023

1024 3
    def set_value(self, obj, val):
1025 0
        current = self.get_value(obj)
1026 0
        try:  # block repetitive calls to setup
1027 0
            call_setup, self.call_setup = self.call_setup, False
1028
            # replace the already existing list elements and append new ones
1029 0
            for i, v in enumerate(val):
1030 0
                try:
1031 0
                    current[i] = v
1032 0
                except IndexError:
1033 0
                    current.append(v)
1034
            # remove the trailing items
1035 0
            while len(current) > len(val):
1036 0
                current.pop()
1037
        finally:
1038 0
            self.call_setup = call_setup
1039

1040 3
    def list_changed(self, module, operation, index, value=None):
1041 0
        if operation == 'selecti':
1042
            # only launch signal in this case, do not call setup
1043
            # value can be None in this case, as it is not used
1044 0
            if value is None:
1045 0
                value = self.get_value(module)
1046 0
            self.launch_signal(module, value, appendix=[operation, index, value])
1047
        else:
1048
            # launches signal and calls setup()
1049 0
            self.value_updated(module, appendix=[operation, index, value])
1050

1051

1052 3
class FloatAttributeListProperty(BasePropertyListProperty, FloatProperty):
1053 3
    pass
1054

1055

1056 3
class ComplexAttributeListProperty(BasePropertyListProperty, ComplexProperty):
1057 3
    pass
1058

1059

1060 3
class PWMRegister(FloatRegister):
1061
    """
1062
    FloatRegister that defines the PWM voltage similar to setting a float.
1063
    """
1064
    # See FPGA code for a more detailed description on how the PWM works
1065 3
    def __init__(self, address, CFG_BITS=24, PWM_BITS=8, **kwargs):
1066 3
        self.CFG_BITS = int(CFG_BITS)
1067 3
        self.PWM_BITS = int(PWM_BITS)
1068 3
        FloatRegister.__init__(self, address=address, bits=14, norm=1, **kwargs)
1069 3
        self.min = 0   # voltage of pwm outputs ranges from 0 to 1.8 volts
1070 3
        self.max = 1.8
1071 3
        self.increment = (self.max-self.min)/2**(self.bits-1)  # actual resolution is 14 bits (roughly 0.1 mV incr.)
1072

1073 3
    def to_python(self, obj, value):
1074 0
        value = int(value)
1075 0
        pwm = float(value >> (self.CFG_BITS - self.PWM_BITS) & (2 ** self.PWM_BITS - 1))
1076 0
        mod = value & (2 ** (self.CFG_BITS - self.PWM_BITS) - 1)
1077 0
        postcomma = float(bin(mod).count('1')) / (self.CFG_BITS - self.PWM_BITS)
1078 0
        voltage = 1.8 * (pwm + postcomma) / 2 ** self.PWM_BITS
1079 0
        if voltage > 1.8:
1080 0
            logger.error("Readout value from PWM (%h) yields wrong voltage %f",
1081
                         value, voltage)
1082 0
        return voltage
1083

1084 3
    def from_python(self, obj, value):
1085
        # here we don't bother to minimize the PWM noise
1086
        # room for improvement is in the low -> towrite conversion
1087 0
        value = 0 if (value < 0) else float(value) / 1.8 * (2 ** self.PWM_BITS)
1088 0
        high = np.floor(value)
1089 0
        if (high >= 2 ** self.PWM_BITS):
1090 0
            high = 2 ** self.PWM_BITS - 1
1091 0
        low = int(np.round((value - high) * (self.CFG_BITS - self.PWM_BITS)))
1092 0
        towrite = int(high) << (self.CFG_BITS - self.PWM_BITS)
1093 0
        towrite += ((1 << low) - 1) & ((1 << self.CFG_BITS) - 1)
1094 0
        return towrite
1095

1096

1097 3
class StringProperty(BaseProperty):
1098
    """
1099
    An attribute for string (there is no corresponding StringRegister).
1100
    """
1101 3
    _widget_class = StringAttributeWidget
1102 3
    default = ""
1103

1104 3
    def validate_and_normalize(self, obj, value):
1105
        """
1106
        Convert argument to string
1107
        """
1108 0
        return str(value)
1109

1110

1111 3
class TextProperty(StringProperty):
1112
    """
1113
    Same as StringProperty, but the gui displays it as multi-line text.
1114
    """
1115 3
    _widget_class = TextAttributeWidget
1116

1117

1118 3
class SelectProperty(BaseProperty):
1119
    """
1120
    An attribute for a multiple choice value.
1121

1122
    The options can be specified at the object creation as a list or an
1123
    (ordered) dict, or as a callable with one argument (which is None or the
1124
    module that contains this attribute, depending on when the call is made).
1125
    Options can be specified at attribute creation, but it can also be updated
1126
    later on a per-module basis using change_options(new_options). If
1127
    options are callable, they are evaluated every time they are needed.
1128
    """
1129 3
    _widget_class = SelectAttributeWidget
1130 3
    default = None
1131

1132 3
    def __init__(self,
1133
                 options=[],
1134
                 **kwargs):
1135 3
        self.default_options = options
1136 3
        BaseProperty.__init__(self, **kwargs)
1137

1138 3
    @property
1139
    def __doc__(self):
1140
        # Append available options to docstring
1141 3
        return self.doc + "\r\nOptions:\r\n" + str(list(self.options(None)))
1142

1143 3
    @__doc__.setter
1144
    def __doc__(self, value):
1145 3
        self.doc = value
1146

1147 3
    def get_default(self, instance):
1148
        """ returns the default value. default is pre-defined value
1149
        if that is not a valid option. Otherwise the first valid option
1150
        is taken, and if that is not possible (no options), None is taken. """
1151 3
        default = self.default  # internal default
1152
        # at startup, we cannot access the instance, so we must continue without it
1153 3
        if instance is not None:
1154
            # make sure default is stored in the instance, such that it can be easily modified
1155 3
            if not hasattr(instance, '_' + self.name + '_' + 'default'):
1156 3
                setattr(instance, '_' + self.name + '_' + 'default', default)
1157 3
            default = getattr(instance, '_' + self.name + '_' + 'default')
1158
        # make sure default is a valid option
1159 3
        options = self.options(instance)
1160 3
        if not default in options:
1161
            # if not valid, default default is the first options
1162 0
            default = list(options)[0]
1163
        # if no options are availbale, fall back to None
1164 3
        if default is None:
1165 0
            logger.warning("Default of SelectProperty %s "
1166
                           "is None. ", self.name)
1167 3
        return default
1168

1169 3
    def options(self, instance=None):
1170
        """
1171
        options are evaluated at run time. options may be callable with instance as optional argument.
1172
        """
1173 3
        options = self.default_options
1174
        # at startup, we cannot access the instance, so we must continue without it
1175 3
        if instance is not None:
1176
            # make sure default is stored in the instance, such that it can be easily modified
1177 3
            if not hasattr(instance, '_' + self.name + '_' + 'options'):
1178 3
                setattr(instance, '_' + self.name + '_' + 'options', options)
1179 3
            options = getattr(instance, '_' + self.name + '_' + 'options')
1180 3
        if callable(options):
1181 3
            try:
1182 3
                options = options(instance)
1183 3
            except (TypeError, AttributeError):
1184 3
                try:
1185 3
                    options = options()
1186 3
                except (TypeError, AttributeError):
1187 3
                    options = OrderedDict()
1188 3
        if not hasattr(options, "keys"):
1189 3
            options = OrderedDict([(v, v) for v in options])
1190 3
        if len(options) == 0:
1191 3
            logger.debug("SelectProperty %s of module %s has no options!", self.name, instance)
1192 3
            options = {None: None}
1193
        # check whether options keys have changed w.r.t. last time and emit a signal in that
1194
        # case. Also create a list of valid options in the parent module called
1195
        # self.name+'_options'.
1196 3
        if instance is not None:
1197 3
            try:
1198 3
                lastoptions = getattr(instance, '_' + self.name + '_lastoptions')
1199 3
            except AttributeError:
1200 3
                lastoptions = None
1201 3
            if options != lastoptions:
1202 3
                setattr(instance, '_' + self.name + '_lastoptions', options)
1203
                # save the keys for the user convenience
1204 3
                setattr(instance, self.name + '_options', list(options.keys()))
1205 3
                instance._signal_launcher.change_options.emit(self.name,
1206
                                                              list(options))
1207
        # return the actual options
1208 3
        return options
1209

1210 3
    def change_options(self, instance, new_options):
1211
        """
1212
        Changes the possible options acceptable by the Attribute:
1213

1214
        - New validation takes effect immediately (otherwise a script
1215
          involving 1. changing the options / 2. selecting one of the
1216
          new options could not be executed at once)
1217
        - Update of the ComboxBox is performed behind a signal-slot
1218
          mechanism to be thread-safe
1219
        - If the current value is not in the new_options, then value
1220
          is changed to some available option
1221
        """
1222 3
        setattr(instance, '_' + self.name + '_' + 'options', new_options)
1223
        # refresh default options in case options(None) is called (no instance in argument)
1224
        # this also triggers the signal emission in the method options()
1225 3
        self.default_options = self.options(instance)
1226

1227 3
    def validate_and_normalize(self, obj, value):
1228 3
        options = self.options(obj)
1229 3
        if not (value in options):
1230 0
            msg = "Value '%s' is not an option for SelectAttribute %s of " \
1231
                  "module %s with options %s" \
1232
                  % (value, self.name, obj.name, options)
1233 0
            if self.ignore_errors:
1234 0
                value = self.get_default(obj)
1235 0
                logger.warning(msg + ". Picking an arbitrary value %s instead."
1236
                               % str(value))
1237
            else:
1238 0
                logger.error(msg)
1239 0
                raise ValueError(msg)
1240 3
        return value
1241

1242 3
    def get_value(self, obj):
1243 3
        if not hasattr(obj, '_' + self.name):
1244 3
            setattr(obj, '_' + self.name, self.get_default(obj))
1245 3
        value = getattr(obj, '_' + self.name)
1246
        # make sure the value is a valid option
1247 3
        value = self.validate_and_normalize(obj, value)
1248 3
        return value
1249

1250 3
    def set_value(self, obj, value):
1251 3
        BaseProperty.set_value(self, obj, value)
1252

1253

1254 3
class SelectRegister(BaseRegister, SelectProperty):
1255
    """
1256
    Implements a selection register, such as for multiplexers.
1257

1258
    The options must be a dict, where the keys indicate the available
1259
    options and the values indicate the corresponding fpga register values.
1260
    If different keys point to the same register value, the keys are
1261
    nevertheless distinguished (allows implementing aliases that may vary
1262
    over time if options is a callable object). """
1263 3
    def __init__(self, address,
1264
                 bitmask=None,
1265
                 options={},
1266
                 **kwargs):
1267 3
        BaseRegister.__init__(self, address=address, bitmask=bitmask)
1268 3
        SelectProperty.__init__(self, options=options, **kwargs)
1269

1270 3
    def get_default(self, obj):
1271 0
        default = SelectProperty.get_default(self, obj)
1272 0
        if default is None and obj is not None:
1273
            # retrieve default value from FPGA if nothing more reasonable is available
1274 0
            value = BaseRegister.get_value(self, obj)
1275 0
            for k, v in self.options(obj).items():
1276 0
                if v == value:
1277 0
                    default = k
1278 0
                    break
1279 0
        return default
1280

1281 3
    def get_value(self, obj):
1282 0
        value = SelectProperty.get_value(self, obj)
1283
        # make sure the register value corresponds to the selected option
1284 0
        expected_value = self.options(obj)[value]
1285 0
        raw_value = BaseRegister.get_value(self, obj)
1286 0
        if raw_value != expected_value:
1287 0
            obj._logger.warning("Register %s of module %s has value %s, "
1288
                                "which does not correspond to selected "
1289
                                "option %s. Setting to '%s'. ",
1290
                                self.name, obj.name,
1291
                                raw_value, expected_value, value)
1292 0
            BaseRegister.set_value(self, obj, expected_value)
1293 0
        return value
1294

1295 3
    def set_value(self, obj, value):
1296 0
        SelectProperty.set_value(self, obj, value)
1297 0
        BaseRegister.set_value(self, obj, self.options(obj)[value])
1298

1299 3
    def to_python(self, obj, value):
1300 0
        return int(value)
1301

1302 3
    def from_python(self, obj, value):
1303 0
        return int(value)
1304

1305

1306 3
class ProxyProperty(BaseProperty):
1307
    """
1308
    An attribute that is a proxy to another attribute.
1309

1310
    This attribute essentially behaves like the one that is reached by
1311
    instance.path_to_target, always staying in synch.
1312
    """
1313 3
    def __init__(self,
1314
                 path_to_target,
1315
                 **kwargs):
1316 3
        self.path_to_target = path_to_target
1317 3
        lastpart = path_to_target.split('.')[-1]
1318 3
        self.target_attribute = lastpart
1319 3
        self.path_to_target_module = path_to_target[:-(len(lastpart)+1)] #+1 for the dot
1320 3
        self.path_to_target_descriptor = self.path_to_target_module \
1321
                                         + '.__class__.' \
1322
                                         + lastpart
1323 3
        BaseProperty.__init__(self, **kwargs)
1324

1325 3
    def _target_to_proxy(self, obj, target):
1326
        """ override this function to implement conversion between target
1327
        and proxy"""
1328 3
        return target
1329

1330 3
    def _proxy_to_target(self, obj, proxy):
1331
        """ override this function to implement conversion between target
1332
        and proxy"""
1333 3
        return proxy
1334

1335 3
    def __get__(self, instance, owner):
1336 3
        if instance is None:
1337 3
            return self
1338 3
        self.instance = instance
1339
        # dangerous, but works because we only call __getattribute__
1340
        # immediately after __set__ or __get__
1341 3
        self.connect_signals(instance)
1342 3
        return self._target_to_proxy(instance,
1343
                                     recursive_getattr(instance,
1344
                                                       self.path_to_target))
1345

1346 3
    def __set__(self, obj, value):
1347 3
        self.instance = obj
1348 3
        self.connect_signals(obj)
1349 3
        recursive_setattr(obj,
1350
                          self.path_to_target,
1351
                          self._proxy_to_target(obj, value))
1352

1353 3
    def __getattribute__(self, item):
1354 3
        try:
1355 3
            return BaseProperty.__getattribute__(self, item)
1356 0
        except AttributeError:
1357 0
            attr = recursive_getattr(self.instance,
1358
                                     self.path_to_target_descriptor + '.' + item)
1359
            #if callable(attr):
1360
            #    return partial(attr, self.instance)
1361
            #else:
1362 0
            return attr
1363

1364
    # special functions for SelectProperties, which transform the argument
1365
    # 'obj' from the hosting module to the target module to avoid redundant
1366
    # saving of options
1367 3
    def options(self, obj):
1368 3
        if obj is None:
1369 3
            obj = self.instance
1370 3
        module = recursive_getattr(obj, self.path_to_target_module)
1371 3
        options = recursive_getattr(obj, self.path_to_target_descriptor +
1372
                                    '.options')(module)
1373 3
        return OrderedDict([(self._target_to_proxy(obj, k), v)
1374
                            for k, v in options.items()])
1375

1376 3
    def change_options(self, obj, new_options):
1377 3
        if obj is None:
1378 0
            obj = self.instance
1379 3
        module = recursive_getattr(obj, self.path_to_target_module)
1380 3
        return recursive_getattr(obj, self.path_to_target_descriptor + '.change_options')(module, new_options)
1381

1382 3
    def __repr__(self):
1383 3
        try:
1384 3
            targetdescr = " (target: " \
1385
                          + recursive_getattr(self.instance,
1386
                                              self.path_to_target_descriptor).__repr__() \
1387
                          + ")"
1388 0
        except:
1389 0
            targetdescr = ""
1390 3
        return super(ProxyProperty, self).__repr__() + targetdescr
1391

1392 3
    def connect_signals(self, instance):
1393
        """ function that takes care of forwarding signals from target to
1394
        signal_launcher of proxy module """
1395 3
        if hasattr(instance, '_' + self.name + '_connected'):
1396 3
            return  # skip if connection has already been set up
1397
        else:
1398 3
            module = recursive_getattr(instance, self.path_to_target_module)
1399

1400 3
            def forward_update_attribute_by_name(name, value):
1401
                """ forward the signal, but change attribute name """
1402 3
                if name == self.target_attribute:
1403 3
                    instance._signal_launcher.update_attribute_by_name.emit(
1404
                        self.name, [self._target_to_proxy(instance,
1405
                                                          value[0])])
1406 3
                    if self.call_setup:
1407 3
                        instance.setup()
1408 3
            module._signal_launcher.update_attribute_by_name.connect(
1409
                forward_update_attribute_by_name)
1410

1411 3
            def forward_change_options(name, new_options):
1412
                """ forward the signal, but change attribute name """
1413 3
                if name == self.target_attribute:
1414
                    # update local list of options
1415 3
                    setattr(instance, self.name + '_options', new_options)
1416
                    # forward the signal
1417 3
                    instance._signal_launcher.change_options.emit(
1418
                        self.name, new_options)
1419 3
            module._signal_launcher.change_options.connect(
1420
                forward_change_options)
1421

1422
            # remember that we are now connected
1423 3
            setattr(instance, '_' + self.name + '_connected', True)
1424

1425 3
    def _create_widget(self, module, widget_name=None, **kwargs):
1426 0
        target_module = recursive_getattr(module, self.path_to_target_module)
1427 0
        target_descriptor = recursive_getattr(module, self.path_to_target_descriptor)
1428 0
        if widget_name is None:
1429 0
            widget_name = self.name
1430
        #return recursive_getattr(module,
1431
        #                         self.path_to_target_descriptor +
1432
        #                         '._create_widget')(target_module,
1433
        #                                            widget_name=widget_name,
1434
        #                                            **kwargs)
1435 0
        self._widget_class = recursive_getattr(module,
1436
                                 self.path_to_target_descriptor +
1437
                                 '._widget_class')
1438 0
        try:  # try to make a widget for proxy
1439 0
            return recursive_getattr(module,
1440
                                 self.path_to_target_descriptor +
1441
                                 '.__class__._create_widget')(self, module,
1442
                                                    #widget_name=widget_name,
1443
                                                    **kwargs)
1444 0
        except:  # make a renamed widget for target
1445 0
            return recursive_getattr(module,
1446
                                     self.path_to_target_descriptor +
1447
                                     '.__class__._create_widget')(target_descriptor, target_module,
1448
                                                                  widget_name=widget_name,
1449
                                                                  **kwargs)
1450

1451

1452 3
class ModuleAttribute(BaseProperty):
1453
    """
1454
    This is the base class for handling submodules of a module.
1455

1456
    The actual implementation is found in module_attributes.ModuleProperty.
1457
    This object is only used inside the Module class
1458
    """
1459

1460 3
class CurveSelectProperty(SelectProperty):
1461
    """
1462
    An attribute to select a curve from all available ones.
1463

1464
    The curve object is loaded to instance._name_object, where 'name' stands
1465
    for the name of this attribute. The property can be set by either passing
1466
    a CurveDB object, or a curve id.
1467
    """
1468 3
    def __init__(self,
1469
                 no_curve_first=False,
1470
                 show_childs=False,
1471
                 **kwargs):
1472 3
        self.no_curve_first = no_curve_first
1473 3
        self.show_childs = show_childs
1474 3
        SelectProperty.__init__(self, options=self._default_options, **kwargs)
1475

1476 3
    def _default_options(self):
1477 0
        if self.no_curve_first:
1478 0
            return [-1] + [curve.pk for curve in CurveDB.all()]
1479
        else:
1480 0
            return [curve.pk for curve in CurveDB.all()] + [-1]
1481
        #return OrderedDict([(k, k) for k in (CurveDB.all()) + [-1]])
1482

1483 3
    def validate_and_normalize(self, obj, value):
1484
        # returns none or a valid curve corresponding to the given curve or id
1485 0
        if isinstance(value, CurveDB):
1486 0
            value = value.pk
1487 0
        try:
1488 0
            pk = int(value)
1489 0
        except:
1490 0
            pk = -1
1491 0
        return pk
1492

1493 3
    def set_value(self, obj, pk):
1494 0
        SelectProperty.set_value(self, obj, pk)
1495 0
        try:
1496 0
            curve = CurveDB.get(pk)
1497 0
        except:
1498 0
            curve = None
1499 0
        setattr(obj, '_' + self.name + '_object', curve)
1500

1501

1502 3
class CurveProperty(CurveSelectProperty):
1503
    """ property for a curve whose widget plots the corresponding curve.
1504

1505
    Unfortunately, the widget does not allow to select the curve,
1506
    i.e. selection must be implemented with another CurveSelectProperty.
1507
    """
1508 3
    _widget_class = CurveAttributeWidget
1509

1510

1511 3
class CurveSelectListProperty(CurveSelectProperty):
1512
    """ same as above, but widget is a list to select from """
1513 3
    _widget_class = CurveSelectAttributeWidget
1514

1515

1516 3
class Plotter(BaseProperty):
1517
    """ property for plotting in the GUI window.
1518

1519
    passing a value, list of values, or a dict of color-value pairs
1520
    results in plotting the values as a function of time in the GUI
1521
    """
1522 3
    _widget_class = PlotAttributeWidget
1523 3
    def __init__(self, legend="value"):
1524 3
        self.legend = legend
1525 3
        super(Plotter, self).__init__()
1526

1527

1528 3
class DataProperty(BaseProperty):
1529
    """
1530
    Property for a dataset (real or complex), that can be plotted.
1531
    """
1532 3
    _widget_class = DataAttributeWidget
1533

1534

Read our documentation on viewing source code .

Loading