"""The atomiq components are a mechanism to put the complexity of the experimental setup into a configuration and to allowto work with the setup at a higher abstraction level. The key ideas behind it are: * Have abstract, generic classes to represent different types of lab equipment (e.g. :class:`~electronics.rfsource.RFSource`, :class:`~electronics.voltagesource.DAC`, :class:`~optoelectronics.lightmodulator.AOM`, :class:`~coil.Coil`, :class:`~laser.Laser`, etc) * Derive more specific classes from the generic ones. For example a channel of the Sinara Urukul is an RFSource but also a standalone AOM driver that is controlled by analog voltages is an RFSource. Thus, both, :class:`~sinara.urukul.UrukulChannel` and :class:`~electronics.rfsource.VoltageControlledRFSource` inherit from :class:`~electronics.rfsource.RFSource`. * Allow to reference other components in a component definition and recursively build all need components.These ideas allow to move hardware specific information (like what devices are used and what device is plugged to whichother device) into a configuration and to work with generic software objects that represent the actual hardware in thelab (like AOMs, Coils, Lasers, etc). It allows to define your experiment hardware in a simple dictionary and todynamically link them together.To get the idea, lets start with a simple example:.. literalinclude:: ../examples/aom/components.py :language: pythonEach entry in the components dict is identified by a name that can be chosen to your liking... note:: Since the component dict is flat, it might make sense to think about a proper naming scheme like `<component_type>_<component_name>` (e.g. `aom_cooler`, `pd_repumper`, `coil_north`, `dds_cooler`, etc.).Every component needs to define a `classname` which defines the class the software object should have. Atomiqautomatically builds these objects from the defined class, if you define that you require them in your experimentcode. The arguments field takes the parameters that are needed to create the object. Look for the constructor methodof the defined class to find what arguments are required/supported. These arguments are the basic means of addingdefault configuration for your components and to interconnect them.Typically atomiq components rely on other components to do their job. It is thus necessary to reference to othercomponents when writing the configuration for a certain component. Such interconnections between components areprovided by starting an argument either with "&" (which refers to another component) or with "@" with refers toa device in your ARTIQ `device_db.py`. When creating the objects from the configuration, atomiq detects thesereferences and recursively builds the components required by the component you need... warning:: Please note that a restart of your ARTIQ master is required if you change your components definition since the `device_db` is only read upon starting the master.Once you defined your components you can request atomiq to build it for you by adding it to the class-level componentslist of your experiment classAtomiq comes with a lot of the most used classes already. See the following list to see what exists."""importartiqimportartiq.master.worker_dbfromatomiq.helperimportget_class_by_nameimportloggingimportrandomimportstringlogging.basicConfig()logger=logging.getLogger(__name__)components=Nonesuservo_replacements=[]def_create_device_replace(desc,device_mgr,*args,**kwargs):globalsuservo_replacementsifdescinsuservo_replacements:desc["class"]+="_Suservo"fori,existinginenumerate(device_mgr.active_devices):# Avoid double init due to change in class name from original class as in ddbifdesc==existing[0]:deldevice_mgr.active_devices[i]returnexisting[1]returnartiq.master.worker_db._create_device_original(desc,device_mgr,*args,**kwargs)artiq.master.worker_db._create_device_original=artiq.master.worker_db._create_deviceartiq.master.worker_db._create_device=_create_device_replace
[docs]defbuild_object_from_device_db(parent,object_id):globalsuservo_replacementslogger.debug(f"building device_db object {object_id} with parent {parent}")# workaroud for https://forum.m-labs.hk/d/273-using-su-servo-mode-and-freestanding-urukul-in-the-same-experiment/3# this should not be necessary anymore once we run nac3deffixup_suservo_class(node):forclassnamein["CPLD","AD9910"]:if"class"innodeandnode["class"]==classname:suservo_replacements.append(node)logger.debug(f"fixing class for {node}")if"compiler"indir(artiq):ddb=parent.get_device_db()ifobject_idinddb:entry=ddb[object_id]logger.info(f"checking {object_id}")if"class"inentryandentry["class"]=="SUServo":fornodeinentry["arguments"]["cpld_devices"]:fixup_suservo_class(ddb[node])fornodeinentry["arguments"]["dds_devices"]:fixup_suservo_class(ddb[node])returnparent.get_device(object_id)
[docs]defbuild_object(classname,arg_dict):logger.debug(f"building object of class {classname}")# fixup for artiq compilers prior to nac3 where the type inferral complains when two objects of the# same class differ in the types of the attributes. Here we create unique dummy classes for each object# to work around the issue. When we are running on the legacy compiler artiq.compiler submodule should exist.if"compiler"indir(artiq):ephemeral_classname=f"{classname}_{''.join(random.choice(string.ascii_lowercase)foriinrange(8))}"logger.debug(f"apply workaround for old artiq type inferral {classname} -> {ephemeral_classname}")target_class=type(ephemeral_classname,(get_class_by_name(classname),),{})else:target_class=get_class_by_name(classname)returntarget_class(**arg_dict)
[docs]@staticmethoddefproduce(name,parent):globalcomponentsifcomponentsisNone:ddb=parent.get_device_db()components=ddb["components"]if"components"inddbelse{}logger.debug(f"Try to produce {name}")ifnameincomponents:returnComponentFactory._produce_from_dict(name,parent,components[name])else:raiseKeyError(f"component '{name}' required by {parent.__class__.__name__} does not exist in component"\
"definition dict")
@staticmethoddef_produce_from_dict(identifier,parent,dictionary):globalcomponentsif"obj"indictionary:# component was already built. Return itreturndictionary["obj"]else:# Build component. If arguments are themselfes components, build them recursivelyif"classname"notindictionary:raiseKeyError(f"component '{identifier}' required by {parent.__class__.__name__} does not have the"\
"mandatory argument `classname`")arg_dict={"identifier":identifier,"parent":parent}if"arguments"indictionaryandtype(dictionary["arguments"])isdict:forargname,argvalindictionary["arguments"].items():iftype(argval)isstrandargval.startswith("&"):obj=ComponentFactory.produce(argval[1:],parent)arg_dict[argname]=objeliftype(argval)isstrandargval.startswith("@"):arg_dict[argname]=build_object_from_device_db(parent,argval[1:])else:arg_dict[argname]=argvaltry:obj=build_object(dictionary["classname"],arg_dict)dictionary["obj"]=objreturnobjexceptExceptionase:# Make a more user friendly output that names the component that cannot be builtlogger.error(f"Cannot build component {identifier} due to following error: {e}")raisee