seQuencing logo

Parameters

Short introduction to attrs

sequencing relies on the attrs package for defining and instantiating classes with many parameters. attrs allows you to define classes with minimal boilerplate by automating the creation of important methods like __init__, __repr__, __str__, and comparison methods like __eq__. For full details on attrs, check out the documentation.

A class defined using attrs must be decorated with @attr.s (or attr.attrs). In place of an explicit __init__ method, attributes of a class decorated with @attr.s can have instance attributes defined at the class level using attr.ib() (or attr.attrib()). Adding attributes to a class using attr.ib() has many advantages:

  • Attributes may be required or have a default value

  • Defaults can be defined either with a specific value or with a “factory” function that generates a default value when called

  • Attributes can have a type associated with them, or a converter function that converts the user-specified value into the desired format

  • Attributes can have validators which raise an error if an invalid value is provided

For the full list of options see attr.ib()

Parameterized

sequencing builds on the functionality of attrs using a class called Parameterized. Parameterized objects must have a name and can have any number of parameters, which can be created using the functions defined in sequencing.parameters, or by using attrs directly via attr.ib().

Parameterized offers the following convenient features:

  • Recursive get() and set() methods for getting and setting attributes of nested Parameterized objects.

  • Methods for converting a Parameterized object into a Python dict, and creating a new Parameterized object from a dict.

  • Methods for serializing a Parameterized object to json and creating a new Parameterized object from json.

Notes:

  • Subclasses of Parameterized must be decorated with @attr.s

  • Subclasses of Parameterized can define an initialize() method, which takes no arguments. It will be called on instantiation after the attrs-generated __init__ method (see **attrs_post_init** for more details). If defined, the subclass’ initialize() method should always call super().initialize() to ensure that the superclass is correctly initialized.

[1]:
import json
from pprint import pprint
import attr
from sequencing.parameters import (
    Parameterized, StringParameter, BoolParameter, ListParameter, DictParameter,
    IntParameter, FloatParameter, NanosecondParameter, GigahertzParameter, RadianParameter,
)
[2]:
@attr.s
class Engine(Parameterized):
    cylinders = IntParameter(4)
    displacement = FloatParameter(2, unit='liter')
    current_rpm = FloatParameter(0, unit='rpm')
    turbo_charged = BoolParameter(False)

@attr.s
class Transmission(Parameterized):
    manual = BoolParameter(False)
    num_gears = IntParameter(5)
    current_gear = IntParameter(1)

    def initialize(self):
        super().initialize()
        # Add private attributes in initialize()
        self._is_broken = True

    @property
    def has_clutch(self):
        return self.manual

    def shift_to(self, gear):
        if gear not in range(self.num_gears+1):
            # 0 is reverse
            raise ValueError(f'Cannot shift into gear {gear}')
        if abs(gear - self.current_gear) > 1:
            raise ValueError('Cannot skip gears')
        self.current_gear = gear

@attr.s
class Car(Parameterized):
    VALID_CHASSIS = ['sedan', 'coupe', 'hatchback', 'suv']
    chassis = StringParameter('sedan', validator=attr.validators.in_(VALID_CHASSIS))
    num_doors = IntParameter(4, validator=attr.validators.in_([2,4]))
    miles_per_gallon = FloatParameter(30, unit='mpg')

    engine = attr.ib(factory=lambda: Engine('engine'))
    transmission = attr.ib(factory=lambda: Transmission('transmission'))
[3]:
car = Car('car') # All parameters other than name are optional because they have defaults
print(car)
Car(name='car', cls='__main__.Car', chassis='sedan', num_doors=4, miles_per_gallon=30.0, engine=Engine(name='engine', cls='__main__.Engine', cylinders=4, displacement=2.0, current_rpm=0.0, turbo_charged=False), transmission=Transmission(name='transmission', cls='__main__.Transmission', manual=False, num_gears=5, current_gear=1))
[4]:
pprint(car.as_dict())
{'chassis': 'sedan',
 'cls': '__main__.Car',
 'engine': {'cls': '__main__.Engine',
            'current_rpm': 0.0,
            'cylinders': 4,
            'displacement': 2.0,
            'name': 'engine',
            'turbo_charged': False},
 'miles_per_gallon': 30.0,
 'name': 'car',
 'num_doors': 4,
 'transmission': {'cls': '__main__.Transmission',
                  'current_gear': 1,
                  'manual': False,
                  'name': 'transmission',
                  'num_gears': 5}}
[5]:
car2 = Car.from_dict(car.as_dict())
print(car == car2)
True
[6]:
car.get('engine.displacement') == {'engine.displacement': car.engine.displacement}
[6]:
True
[7]:
car.set_param('engine.displacement', 2.5)
car.get_param('engine.displacement') == car.engine.displacement == 2.5
[7]:
True
[8]:
car.set(engine__displacement=3.0)
print(car.get('engine.displacement'))
{'engine.displacement': 3.0}
[9]:
print(f'RPM: {car.engine.current_rpm}, gear: {car.transmission.current_gear}')

with car.temporarily_set(engine__current_rpm=4000, transmission__current_gear=3):
    print(f'RPM: {car.engine.current_rpm}, gear: {car.transmission.current_gear}')

print(f'RPM: {car.engine.current_rpm}, gear: {car.transmission.current_gear}')
RPM: 0.0, gear: 1
RPM: 4000, gear: 3
RPM: 0.0, gear: 1
[10]:
try:
    convertible = Car('convertible', chassis='convertible')
except ValueError as e:
    print('ValueError:', e)
ValueError: ("'chassis' must be in ['sedan', 'coupe', 'hatchback', 'suv'] (got 'convertible')", Attribute(name='chassis', default='sedan', validator=<in_ validator with options ['sedan', 'coupe', 'hatchback', 'suv']>, repr=True, eq=True, eq_key=None, order=True, order_key=None, hash=None, init=True, metadata=mappingproxy({}), type=None, converter=<class 'str'>, kw_only=False, inherited=False, on_setattr=None), ['sedan', 'coupe', 'hatchback', 'suv'], 'convertible')
[11]:
try:
    three_door = Car('three_door', num_doors=3)
except ValueError as e:
    print('ValueError:', e)
ValueError: ("'num_doors' must be in [2, 4] (got 3)", Attribute(name='num_doors', default=4, validator=<in_ validator with options [2, 4]>, repr=True, eq=True, eq_key=None, order=True, order_key=None, hash=None, init=True, metadata=mappingproxy({}), type=None, converter=<class 'int'>, kw_only=False, inherited=False, on_setattr=None), [2, 4], 3)
[12]:
from qutip.ipynbtools import version_table
version_table()
[12]:
SoftwareVersion
QuTiP4.7.0
Numpy1.23.2
SciPy1.9.1
matplotlib3.5.3
Cython0.29.32
Number of CPUs2
BLAS InfoOPENBLAS
IPython8.4.0
Python3.8.6 (default, Oct 19 2020, 15:10:29) [GCC 7.5.0]
OSposix [linux]
Tue Aug 30 19:26:45 2022 UTC
[ ]: