[1]:
import quanguru as qg

named#

All the objects that are created/needed by the user have unique default names and user defined aliases, and the named class implement these two.

There can be any number of alias to a named object, but the aliases still need to be unique to that object, i.e. two objects cannot have the same alias, even if they have other aliases that are different.

named class uses its name mangled _named__name attribute (which is an instance of aliasClass) to store its name and aliases. Since aliasClass already ensures that the name cannot be changed, named class is simply responsible for creating a unique name for it during instantiation. See below examples for how these names are created.

Additionally, a weak-reference to all named instances are stored in an attribute _allInstacesDict (as class attribute), which is an instance of aliasDict, and we use the object names as the keys. Object names are also used as dictionary keys in various other parts of the library. In such cases, the those dictionaries, such as subSys, _paramBound, and allResults, are all instances of aliasDict. This approach enables us to give either the name or any alias of an object (both are strings) as a key to the dictionary when we are retrieving it. See below examples for more.

[2]:
# create a named and check its name
n1 = qg.named()

print(n1.name)

# also note that the string representation of a named object is its name
# so we can also directly print
print(n1)
named1
named1
[3]:
# name property has only the getter, no setter
# this will give an 'AttributeError' error
try:
    n1.name = "new name"
except AttributeError as a:
    print(a)

# NEVER change the name mangled attribute directly
# below will break many functionalities of the library
n2 = qg.base.named()
n2._named__name = "new name"
print(n2.name)
# because this breaks the composition inside the named
# meaning that the _named__name attribute is no longer an aliasClass, but a simple string

can't set attribute 'name'
new name

The names are created by using the class attribute label and the number of instances, which is again stored in class attributes.

label are always the same as class name. Therefore, for the above example, n1 is the first named created,and its name is

named.label + named._externalInstances -> "named" + 1

Here, the class attribute _externalInstances means that n1 is created externally by a user. QuanGuru also creates some objects internally to provide certain functionalities as well as house-keeping. Therefore, we distinguish between these, and the user does not need to know how many internal objects are created. For internal objects, the name is calculated as

"_" + named.label + named._internalInstances

The _ in the beginning is again to distinguish between internal and external instances, because names need to be unique for any named instance regardless it is internal or external.

Internally created objects are also labelled with a boolean stored inside the _internal attribute. We simply set this boolean to True when we create an internal object.

[4]:
# create an internal object
ni = qg.named(_internal=True)
#print its name. first internal named object so -> _ + named + 1
print(ni.name)
_named1
[5]:
# note that internal has to be a boolean type.
try:
    qg.base.named(_internal="not a bool")
except TypeError as te:
    print(te)

# 0 means False, but 0 is not a boolean, so it also fails, because _internal expect a boolean type
try:
    qg.base.named(_internal=0)
except TypeError as te:
    print(te)
_internal should be an instance of <class 'bool'>, <class 'NoneType'>, but <class 'str'> is given
_internal should be an instance of <class 'bool'>, <class 'NoneType'>, but <class 'int'> is given

As mentioned above a weak-reference to every named instance is stored in _allInstacesDict with objects name as the key. This is the enabling idea behind the getByNameOrAlias function/ality.

In order to demonstrate getByNameOrAlias and _allInstacesDict. Let’s first see how we set aliases to objects.

[6]:
# we can add a single of a list of aliases during instantiation
nAlias1 = qg.base.named(alias="single alias")
nAlias2 = qg.base.named(alias=["li1", "li2"])
# alias need to be unique
try:
    qg.base.named(alias=["single alias"])
except ValueError as te:
    print(te, nAlias1.name)

try:
    qg.base.named(alias="li1")
except ValueError as te:
    print(te, nAlias2.name)
Given alias (single alias) already exist and is assigned to: named3 named3
Given alias (li1) already exist and is assigned to: named4 named4
[7]:
# or we can add alias/es after instantiation

nAlias3 = qg.base.named()

nAlias3.alias = "first alias"
print(nAlias3.alias)
# NOTE THAT ALIASES DOES NOT HAVE TO BE A STRING.
# we want to provide this flexibility, but strings should be preferred unless there is a good reason
# so let's give a dictionary as an alias
nAlias3.alias = ["2", {"a": 1, "b": 2}] # also NOTE that this alias setter adds to aliases not replace
print(nAlias3.alias)
['first alias']
['first alias', '2', {'a': 1, 'b': 2}]
[8]:
# if want to truly replace an alias, we can call alias getter
# which returns the alias list, and we can use usual list methods to modify the aliases
# see also aliasClass tutorial

# append an alias
nAlias3.alias.append("new alias")
print(nAlias3.alias)

# remove an alias
nAlias3.alias.remove("2")
print(nAlias3.alias)

# change the alias a specific index
nAlias3.alias[0] = 1
print(nAlias3.alias)
['first alias', '2', {'a': 1, 'b': 2}, 'new alias']
['first alias', {'a': 1, 'b': 2}, 'new alias']
[1, {'a': 1, 'b': 2}, 'new alias']

Notice that in above examples, we used the keyword alias or name both at instantiation and/or also after it, and these are not simply object attributes but properties (as might already be clear from above).

name property has only a getter

alias has both getter and setter.

We want them to be properties, because this enable us to implement further functionalities and side effects, which need to happen also during instantiation. For this purpose, we introduced and used __setKwargs method in every __init__ method. __setKwargs, as the name suggests, sets these keyword arguments.

So far, we created many named instances, so let’s use getByNameOrAlias to obtain references to them.

[9]:
# first lets print _allInstacesDict
print(n1._allInstacesDict)
{'named1': <weakref at 0x10b0a24d0; to 'named' at 0x10b04be80>, 'named2': <weakref at 0x106c722a0; to 'named' at 0x10b0aed00>, '_named1': <weakref at 0x1098473d0; to 'named' at 0x10b0af0c0>, 'named3': <weakref at 0x10a0f2b10; to 'named' at 0x10b0b4980>, 'named4': <weakref at 0x106beb920; to 'named' at 0x10b0b4940>, 'named7': <weakref at 0x10b0a2520; to 'named' at 0x10b0b6880>}
[10]:
# we have weak-references together with names of the object in _allInstacesDict
# getByNameOrAlias uses this dictionary to return us a reference
# and we can use getByNameOrAlias on any object because _allInstacesDict is an class attribute
# get a reference to nAlias3 using its alias "new alias"
newRef1 = n1.getByNameOrAlias("new alias")
print(newRef1 is nAlias3)

# get a reference to first internal instance of named with its name _named1
newRef2 = n1.getByNameOrAlias("_named1")
print(newRef2 is ni)

True
True

In all the examples above, we already had a reference to each object, so the purpose of getByNameOrAlias method might not be clear. Firstly, it is useful when we want get a reference to an internally created object. More importantly, when we write compute or calculate functions (see later tutorials), if we use the existing references: - the simulations runs fine, if we are not doing multi-processing - if we are multi-processing, then we want to get a reference to pickled version of the objects. Because the sweeps (see later tutorials) might have changed the values stored in the object, and this causes undefined behaviors.

Additionally, for the multi-processing support, any named instance should be pickled and loaded properly, and the below examples show that they do.

[11]:
import pickle

nbefore = qg.named(_internal=False, alias=["t", "z"])
npickled = pickle.dumps(nbefore)
nafter = pickle.loads(npickled)

print(nbefore.name == nafter.name)
print(nbefore.alias == nafter.alias)
True
True

Finally, if we want to empty the _allInstacesDict and reset all the instance counters to zero, we can simply use resetAll method on any named instance. This is useful, when we are working on a notebook and want to reset our objects without restarting the kernel.

[12]:
# resetAll the object
n1._resetAll()

# create a new instance an its name should be named1 now
newN1 = qg.named()
print(newN1.name)

# note that this does not mean n1 is destroyed/garbage-collected.
# we still have reference to it
# it is just no-longer accessible through the methods such as getByNameOrAlias
print(newN1 == n1)


named1
False