Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 90 additions & 1 deletion qcodes/instrument/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ class Instrument(Metadatable, DelegateAttributes, NestedAttrAccess):

shared_kwargs = ()

_all_instruments = {}

def __new__(cls, *args, server_name='', **kwargs):
"""Figure out whether to create a base instrument or proxy."""
if server_name is None:
Expand Down Expand Up @@ -207,16 +209,35 @@ def record_instance(cls, instance):
"""
Record (a weak ref to) an instance in a class's instance list.

Also records the instance in list of *all* instruments, and verifies
that there are no other instruments with the same name.

Args:
instance (Union[Instrument, RemoteInstrument]): Note: we *do not*
check that instance is actually an instance of ``cls``. This is
important, because a ``RemoteInstrument`` should function as an
instance of the instrument it proxies.

Raises:
KeyError: if another instance with the same name is already present
"""
wr = weakref.ref(instance)
name = instance.name

# First insert this instrument in the record of *all* instruments
# making sure its name is unique
existing_wr = cls._all_instruments.get(name)
if existing_wr and existing_wr():
raise KeyError('Another instrument has the name: {}'.format(name))

cls._all_instruments[name] = wr

# Then add it to the record for this specific subclass, using ``_type``
# to make sure we're not recording it in a base class instance list
if getattr(cls, '_type', None) is not cls:
cls._type = cls
cls._instances = []
cls._instances.append(weakref.ref(instance))
cls._instances.append(wr)

@classmethod
def instances(cls):
Expand Down Expand Up @@ -251,6 +272,74 @@ def remove_instance(cls, instance):
if wr in cls._instances:
cls._instances.remove(wr)

# remove from all_instruments too, but don't depend on the
# name to do it, in case name has changed or been deleted
all_ins = cls._all_instruments
for name, ref in list(all_ins.items()):
if ref is wr:
del all_ins[name]

@classmethod
def find_instrument(cls, name, instrument_class=None):
"""
Find an existing instrument by name.

Args:
name (str)
instrument_class (Optional[class]): The type of instrument
you are looking for.

Returns:
Union[Instrument, RemoteInstrument]

Raises:
KeyError: if no instrument of that name was found, or if its
reference is invalid (dead).
TypeError: if a specific class was requested but a different
type was found
"""
ins = cls._all_instruments[name]()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am confused by the difference between
cls._all_instruments
and
Instrument._all_instruments
on line 273, is there a difference? or should one be changed into the other?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cls is Instrument here - even if you call find_instrument from a subclass, a @classmethod gets as an argument the class it was defined in (which I wasn't actually clear on myself, had to try it 😄 though it would still work if cls were the subclass, it would find the object in the base class). Anyway, generally it's good practice to refer to the class by reference rather than by name, just in case the name changes later.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, I was wrong above... made a mistake trying this out 🙈
cls is the class you called from, or the class of the instance you called from - a fact I made use of already in record_instance! Anyway, the conclusion stands, I will update to use cls consistently.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

interesting, but this sounds like sensible behavior, as long as we don't start mixing the name and cls without cause


if ins is None:
del cls._all_instruments[name]
raise KeyError('Instrument {} has been removed'.format(name))

if instrument_class is not None:
if not isinstance(ins, instrument_class):
raise TypeError(
'Instrument {} is {} but {} was requested'.format(
name, type(ins), instrument_class))

return ins

@classmethod
def find_component(cls, name_attr, instrument_class=None):
"""
Find a component of an existing instrument by name and attribute.

Args:
name_attr (str): A string in nested attribute format:
<name>.<attribute>[.<subattribute>] and so on.
For example, <attribute> can be a parameter name,
or a method name.
instrument_class (Optional[class]): The type of instrument
you are looking for this component within.

Returns:
Any: The component requested.
"""

if '.' in name_attr:
name, attr = name_attr.split('.', 1)
ins = cls.find_instrument(name, instrument_class=instrument_class)
return ins.getattr(attr)

else:
# allow find_component to return the whole instrument,
# if no attribute was specified, for maximum generality.
return cls.find_instrument(name_attr,
instrument_class=instrument_class)

def add_parameter(self, name, parameter_class=StandardParameter,
**kwargs):
"""
Expand Down
21 changes: 20 additions & 1 deletion qcodes/instrument/remote.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,11 @@ def __init__(self, *args, instrument_class=None, server_name='',
self._args = args
self._kwargs = kwargs

instrument_class.record_instance(self)
self.connect()

# must come after connect() because that sets self.name
instrument_class.record_instance(self)

def connect(self):
"""Create the instrument on the server and replicate its API here."""

Expand Down Expand Up @@ -181,6 +183,23 @@ def instances(self):
"""
return self._instrument_class.instances()

def find_instrument(self, name, instrument_class=None):
"""
Find an existing instrument by name.

Args:
name (str)

Returns:
Union[Instrument, RemoteInstrument]

Raises:
KeyError: if no instrument of that name was found, or if its
reference is invalid (dead).
"""
return self._instrument_class.find_instrument(
name, instrument_class=instrument_class)

def close(self):
"""Irreversibly close and tear down the server & remote instruments."""
if hasattr(self, '_manager'):
Expand Down
7 changes: 7 additions & 0 deletions qcodes/instrument/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,13 @@ def __init__(self, query_queue, response_queue, shared_kwargs):
self.instruments = {}
self.next_id = 0

# Ensure no references of instruments defined in the main process
# are copied to the server process. With the spawn multiprocessing
# method this is not an issue, as the class is reimported in the
# new process, but with fork it can be a problem ironically.
from qcodes.instrument.base import Instrument
Instrument._all_instruments = {}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Holiday comment 🍸
this is a no go in my book.

It's hinting that there is a design issue somewhere. Either in the class design or in the server/proxy design.

It's a fine workaround, but shall not go into master IMHO.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you have an alternative to propose (other than "yes, I'm working on a new architecture")? This just exists to smooth out a difference between the inheritance models of the two multiprocessing methods, so presumably will go away entirely with the new architecture, but what do we do until then?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nops, hence the holiday remark in the first line.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but idk, it seems like adding one more hack makes people happy so whatevs.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also not really sure it is about inheritance, but rather namespaces.


self.run_event_loop()

def handle_new_id(self):
Expand Down
29 changes: 23 additions & 6 deletions qcodes/tests/instrument_mocks.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,23 @@ def meter_get(self, parameter):
elif parameter[:5] == 'echo ':
return self.fmt(float(parameter[5:]))

# alias because we need new names when we instantiate an instrument
# locally at the same time as remotely
def gateslocal_set(self, parameter, value):
return self.gates_set(parameter, value)

def gateslocal_get(self, parameter):
return self.gates_get(parameter)

def sourcelocal_set(self, parameter, value):
return self.source_set(parameter, value)

def sourcelocal_get(self, parameter):
return self.source_get(parameter)

def meterlocal_get(self, parameter):
return self.meter_get(parameter)


class ParamNoDoc:

Expand Down Expand Up @@ -115,8 +132,8 @@ def add5(self, b):

class MockGates(MockInstTester):

def __init__(self, model=None, **kwargs):
super().__init__('gates', model=model, delay=0.001, **kwargs)
def __init__(self, name='gates', model=None, **kwargs):
super().__init__(name, model=model, delay=0.001, **kwargs)

for i in range(3):
cmdbase = 'c{}'.format(i)
Expand Down Expand Up @@ -164,8 +181,8 @@ def slow_neg_set(self, val):

class MockSource(MockInstTester):

def __init__(self, model=None, **kwargs):
super().__init__('source', model=model, delay=0.001, **kwargs)
def __init__(self, name='source', model=None, **kwargs):
super().__init__(name, model=model, delay=0.001, **kwargs)

self.add_parameter('amplitude', get_cmd='ampl?',
set_cmd='ampl:{:.4f}', get_parser=float,
Expand All @@ -175,8 +192,8 @@ def __init__(self, model=None, **kwargs):

class MockMeter(MockInstTester):

def __init__(self, model=None, **kwargs):
super().__init__('meter', model=model, delay=0.001, **kwargs)
def __init__(self, name='meter', model=None, **kwargs):
super().__init__(name, model=model, delay=0.001, **kwargs)

self.add_parameter('amplitude', get_cmd='ampl?', get_parser=float)
self.add_function('echo', call_cmd='echo {:.2f}?',
Expand Down
Loading