# This code is part of Qiskit. # # (C) Copyright IBM 2020. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory # of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. # # Any modifications or derivative works of this code must retain this # copyright notice, and modified files need to carry a notice indicating # that they have been altered from the originals. """Container class for backend options.""" import io from collections.abc import Mapping class Options(Mapping): """Base options object This class is what all backend options are based on. The properties of the class are intended to be all dynamically adjustable so that a user can reconfigure the backend on demand. If a property is immutable to the user (eg something like number of qubits) that should be a configuration of the backend class itself instead of the options. Instances of this class behave like dictionaries. Accessing an option with a default value can be done with the `get()` method: >>> options = Options(opt1=1, opt2=2) >>> options.get("opt1") 1 >>> options.get("opt3", default="hello") 'hello' Key-value pairs for all options can be retrieved using the `items()` method: >>> list(options.items()) [('opt1', 1), ('opt2', 2)] Options can be updated by name: >>> options["opt1"] = 3 >>> options.get("opt1") 3 Runtime validators can be registered. See `set_validator`. Updates through `update_options` and indexing (`__setitem__`) validate the new value before peforming the update and raise `ValueError` if the new value is invalid. >>> options.set_validator("opt1", (1, 5)) >>> options["opt1"] = 4 >>> options["opt1"] 4 >>> options["opt1"] = 10 # doctest: +ELLIPSIS Traceback (most recent call last): ... ValueError: ... """ # Here there are dragons. # This class preamble is an abhorrent hack to make `Options` work similarly to a # SimpleNamespace, but with its instance methods and attributes in a separate namespace. This # is required to make the initial release of Qiskit Terra 0.19 compatible with already released # versions of Qiskit Experiments, which rely on both of # options.my_key = my_value # transpile(qc, **options.__dict__) # working. # # Making `__dict__` a property which gets a slotted attribute solves the second line. The # slotted attributes are not stored in a `__dict__` anyway, and `__slots__` classes suppress the # creation of `__dict__`. That leaves it free for us to override it with a property, which # returns the options namespace `_fields`. # # We need to make attribute setting simply set options as well, to support statements of the # form `options.key = value`. We also need to ensure that existing uses do not override any new # methods. We do this by overriding `__setattr__` to purely write into our `_fields` dict # instead. This has the highly unusual behavior that # >>> options = Options() # >>> options.validator = "my validator option setting" # >>> options.validator # {} # >>> options.get("validator") # "my validator option setting" # This is the most we can do to support the old interface; _getting_ attributes must return the # new forms where appropriate, but setting will work with anything. All options can always be # returned by `Options.get`. To initialise the attributes in `__init__`, we need to dodge the # overriding of `__setattr__`, and upcall to `object.__setattr__`. # # To support copying and pickling, we also have to define how to set our state, because Python's # normal way of trying to get attributes in the unpickle will fail. # # This is a terrible hack, and is purely to ensure that Terra 0.19 does not break versions of # other Qiskit-family packages that are already deployed. It should be removed as soon as # possible. __slots__ = ("_fields", "validator") # implementation of the Mapping ABC: def __getitem__(self, key): return self._fields[key] def __iter__(self): return iter(self._fields) def __len__(self): return len(self._fields) # Allow modifying the options (validated) def __setitem__(self, key, value): self.update_options(**{key: value}) # backwards-compatibilty with Qiskit Experiments: @property def __dict__(self): return self._fields # SimpleNamespace-like access to options: def __getattr__(self, name): # This does not interrupt the normal lookup of things like methods or `_fields`, because # those are successfully resolved by the normal Python lookup apparatus. If we are here, # then lookup has failed, so we must be looking for an option. If the user has manually # called `self.__getattr__("_fields")` then they'll get the option not the full dict, but # that's not really our fault. `getattr(self, "_fields")` will still find the dict. try: return self._fields[name] except KeyError as ex: raise AttributeError(f"Option {name} is not defined") from ex # setting options with the namespace interface is not validated def __setattr__(self, key, value): self._fields[key] = value # custom pickling: def __getstate__(self): return (self._fields, self.validator) def __setstate__(self, state): _fields, validator = state super().__setattr__("_fields", _fields) super().__setattr__("validator", validator) def __copy__(self): """Return a copy of the Options. The returned option and validator values are shallow copies of the originals. """ out = self.__new__(type(self)) out.__setstate__((self._fields.copy(), self.validator.copy())) return out def __init__(self, **kwargs): super().__setattr__("_fields", kwargs) super().__setattr__("validator", {}) # The eldritch horrors are over, and normal service resumes below. Beware that while # `__setattr__` is overridden, you cannot do `self.x = y` (but `self.x[key] = y` is fine). This # should not be necessary, but if _absolutely_ required, you must do # super().__setattr__("x", y) # to avoid just setting a value in `_fields`. def __repr__(self): items = (f"{k}={v!r}" for k, v in self._fields.items()) return "{}({})".format(type(self).__name__, ", ".join(items)) def __eq__(self, other): if isinstance(self, Options) and isinstance(other, Options): return self._fields == other._fields return NotImplemented def set_validator(self, field, validator_value): """Set an optional validator for a field in the options Setting a validator enables changes to an options values to be validated for correctness when :meth:`~qiskit.providers.Options.update_options` is called. For example if you have a numeric field like ``shots`` you can specify a bounds tuple that set an upper and lower bound on the value such as:: options.set_validator("shots", (1, 4096)) In this case whenever the ``"shots"`` option is updated by the user it will enforce that the value is >=1 and <=4096. A ``ValueError`` will be raised if it's outside those bounds. If a validator is already present for the specified field it will be silently overriden. Args: field (str): The field name to set the validator on validator_value (list or tuple or type): The value to use for the validator depending on the type indicates on how the value for a field is enforced. If a tuple is passed in it must have a length of two and will enforce the min and max value (inclusive) for an integer or float value option. If it's a list it will list the valid values for a field. If it's a ``type`` the validator will just enforce the value is of a certain type. Raises: KeyError: If field is not present in the options object ValueError: If the ``validator_value`` has an invalid value for a given type TypeError: If ``validator_value`` is not a valid type """ if field not in self._fields: raise KeyError("Field '%s' is not present in this options object" % field) if isinstance(validator_value, tuple): if len(validator_value) != 2: raise ValueError( "A tuple validator must be of the form '(lower, upper)' " "where lower and upper are the lower and upper bounds " "inclusive of the numeric value" ) elif isinstance(validator_value, list): if len(validator_value) == 0: raise ValueError("A list validator must have at least one entry") elif isinstance(validator_value, type): pass else: raise TypeError( f"{type(validator_value)} is not a valid validator type, it " "must be a tuple, list, or class/type" ) self.validator[field] = validator_value def update_options(self, **fields): """Update options with kwargs""" for field in fields: field_validator = self.validator.get(field, None) if isinstance(field_validator, tuple): if fields[field] > field_validator[1] or fields[field] < field_validator[0]: raise ValueError( f"Specified value for '{field}' is not a valid value, " f"must be >={field_validator[0]} or <={field_validator[1]}" ) elif isinstance(field_validator, list): if fields[field] not in field_validator: raise ValueError( f"Specified value for {field} is not a valid choice, " f"must be one of {field_validator}" ) elif isinstance(field_validator, type): if not isinstance(fields[field], field_validator): raise TypeError( f"Specified value for {field} is not of required type {field_validator}" ) self._fields.update(fields) def __str__(self): no_validator = super().__str__() if not self.validator: return no_validator else: out_str = io.StringIO() out_str.write(no_validator) out_str.write("\nWhere:\n") for field, value in self.validator.items(): if isinstance(value, tuple): out_str.write(f"\t{field} is >= {value[0]} and <= {value[1]}\n") elif isinstance(value, list): out_str.write(f"\t{field} is one of {value}\n") elif isinstance(value, type): out_str.write(f"\t{field} is of type {value}\n") return out_str.getvalue()