"""configuration
==================
Here we implement the ``Configuration`` class, that allows to define
consistent configurations that are later fed to the ``Simulation`` objects.
Quick description
--------------------
A configuration consists of:
* a atom (``Atom``)
* a collection of laser beams (``LaserBeam``)
* a collection of magnetic Fields (``MagneticField``)
* a collection of forces (``Force``)
* a collection of zones (``Zone``)
The coupling between atoms and lasers is stored in a ``atomlight`` dictionnary, that
is setup with the ``add_atomlight_coupling()`` method.
"""
# % IMPORTS
import warnings
import numpy as np
from copy import copy, deepcopy
# % LOCAL IMPORTS
from ..environment import LaserBeam, MagneticField, Zone, SuperZone, Force
from ..environment.envbase import EnvObject
from ..atoms import Atom
from ..utils.infostring import InfoString
# % CONSTANTS
SEP_STR = "# ------------ {} ------------ #"
# % DEFINE THE CLASS
[docs]
class Configuration(object):
"""Defines a configuration for the simulation
Parameters
----------
object_list : EnvObject | list, optional
list of environment objects (lasers, magnetic fields, zones, forces)
to include in the configuration, by default None
atom : Atom, optional
atom for the simulation, by default None
Examples
---------
.. code-block:: python
# - imports
from atomsmltr.environment import GaussianLaserBeam, MagneticOffset, Limits
from atomsmltr.atoms import Ytterbium
from atomsmltr.simulation import Configuration
# - setup environment objects
laser = GaussianLaserBeam(tag="laser")
mag_offset = MagneticOffset(offset=(0,1,0), tag="offset")
xlim = Limits(min=0, max=10, axis=0, target="position")
# - setup atom
yb = Ytterbium()
# - init configuration
config = Configuration(object_list=[laser, mag_offset, xlim], atom=yb)
# - print info
config.print_info()
"""
def __init__(self, object_list: EnvObject | list = None, atom: Atom = None):
# - initialize collections
self.__lasers = {}
self.__zones = {}
self.__magfields = {}
self.__forces = {}
self.__atomlight = {}
self.__atom = None
self.__implemented_collections = {
"laser": self.__lasers,
"magnetic field": self.__magfields,
"force": self.__forces,
"zone": self.__zones,
}
# - init atom
if atom is not None:
self.atom = atom
if object_list is not None:
self.add_objects(object_list)
# -- ATOM-LIGHT INTERACTION HANDLING
[docs]
def get_atomlight_couples(self) -> list:
"""Returns a list of (transition, laser, detuning) tuples
Returns
-------
list
a list of tuples with (transition, laser, detuning)
"""
list = []
for transition_tag, laser_dict in self.__atomlight.items():
transition = self.atom.trans[transition_tag]
for laser_tag, coupling_info in laser_dict.items():
laser = self.__lasers[laser_tag]
detuning = coupling_info["detuning"]
list.append((transition, laser, detuning))
return list
[docs]
def add_atomlight_coupling(
self,
laser: str | LaserBeam,
transition: str,
detuning: float,
verbose: bool = False,
override: bool = False,
):
"""Adds a atom-light coupling element in the configuration
Parameters
----------
laser : str | LaserBeam
either a laser tag or a laser object. This object/tag has to be in the configuration laser list
transition : str
the tag of the transition. Should be part of the collection's atom transition list
detuning : float
detuning of the laser w.r.t to transition (rad/s), see notes for the definition
verbose : bool, optional
if True, will print messages when adding the couplint, by default False
override : bool, optional
if set to True, if a coupling between the laser and the transition already
exists, then it will be overriden. Otherwise it will raise an error, by default False
Notes
------
The detuning δ is defined as:
δ = ωL - ω0
Where ωL is the laser pulsation and ω0 the atomic transition pulsation (hence, in rad/s)
Stated otherwise, detuning units is in units of 2π x Hz
"""
# - checking inputs
# check laser argument
if not isinstance(laser, (str, LaserBeam)):
raise TypeError("'laser' should be a tag (string) or a Laser object")
if not isinstance(laser, str):
laser = laser.tag
# check that laser is there
if laser not in self.__lasers:
msg = f"No entry for laser tag '{laser}'. "
msg += f" Available lasers are {list(self.__lasers)}."
raise KeyError(msg)
# check that transition is there
if self.atom is None:
raise ValueError("No atom was defined for this config")
if transition not in self.__atomlight:
msg = f"No entry for transition '{transition}'. "
msg += f" Available transitions are {list(self.__atomlight)}."
raise KeyError(msg)
# - check that there is no link
if laser in self.__atomlight[transition]:
msg = f"There is alreay a link between laser '{laser}' and transition '{transition}'. "
if not override:
msg += "Since 'override' is set to 'False', we stop here with an error."
raise KeyError(msg)
else:
msg += "Since 'override' is set to 'True', we go on."
if verbose:
print(" > " + msg)
# - store
self.__atomlight[transition][laser] = {"detuning": detuning}
[docs]
def rm_atomlight_coupling(
self,
laser: str | LaserBeam,
transition: str,
):
"""Removes an atom-light coupling
Parameters
----------
laser : str | LaserBeam
laser coupled : tag (str) or directly the object
transition : str
transition tag
"""
# - checking inputs
# check laser argument
if not isinstance(laser, (str, LaserBeam)):
raise TypeError("'laser' should be a tag (string) or a Laser object")
if not isinstance(laser, str):
laser = laser.tag
# - remove
success = False
if transition in self.__atomlight:
if laser in self.__atomlight[transition]:
self.__atomlight[transition].pop(laser)
success = True
if not success:
msg = f"There is no link between '{laser}' and '{transition}'."
raise KeyError(msg)
def reset_atomlight_coupling(self):
for transition in self.__atomlight:
self.__atomlight[transition].clear()
# -- GETTING VALUES
[docs]
def getB(self, position: np.ndarray) -> np.ndarray:
"""Returns the total magnetic field at a given position in the lab frame
Parameters
----------
position : array, shape (3,) or (n1, n2, .., 3)
array of cartesian coordinates in the lab frame
Returns
-------
B : array, shape (3,) or (n1, n2, .., 3)
magnetic field at the position. shape matches the one of ``position``
Notes
------
position is an array_like object, with shape (3,) or (n1, n2, .., 3).
In all cases, the last dimension contains cordinates (x, y, z),
in meter and in the lab frame
Example
-------
.. code-block:: python
... init a proper config first
import numpy as np
# generate a grid of 100 x 100 points in the (x, y) plane
grid = np.mgrid[-10:10:100j, -10:-10:100j, 0:0:1j]
grid = np.squeeze(grid)
# get coordinates arrays (for plotting for instance)
X, Y, Z = grid
# generate the requested (..., 3) shaped position array
position = grid.T
# compute magnetic field
B = config.getB(position)
# get magnetic field components
Bx, By, Bz = B.T
# show shapes (to illustrate what we did)
print(f"{grid.shape=}")
print(f"{position.shape=}")
print(f"{B.shape=}")
print(f"{X.shape=}")
print(f"{Bx.shape=}")
This returns
.. code-block:: python
grid.shape=(3, 100, 100)
position.shape=(100, 100, 3)
B.shape=(100, 100, 3)
X.shape=(100, 100)
Bx.shape=(100, 100)
"""
B = np.zeros_like(position, dtype=float)
if self.__magfields:
for magfield in self.__magfields.values():
B += magfield.get_value(position)
return B
[docs]
def getBnorm(self, position):
"""Returns the magnetic field amplitude (norm) at a given lab position
Parameters
----------
position : array, shape (3,) or (n1, n2, .., 3)
array of cartesian coordinates in the lab frame
Returns
-------
B_norm : array, shape (1,) or (n1, n2, .., 1)
magnetic field norm the position. shape matches the one of ``position``
Notes
------
position is an array_like object, with shape (3,) or (n1, n2, .., 3).
In all cases, the last dimension contains cordinates (x, y, z),
in meter and in the lab frame
"""
B = self.getB(position)
Bx, By, Bz = B.T
B_norm = np.sqrt(Bx**2 + By**2 + Bz**2).T
return B_norm
# -- GETTING ZONES
[docs]
def get_stop_zones(self):
"""Returns two list of the zones whose ``action`` are set to ``stop``
Returns
-------
stop_position: list
list of position stop zones (target=position)
stop_speed: list
list of speed stop zones (target=speed)
"""
return self._get_zones(action="stop")
[docs]
def get_all_zones(self):
"""Returns zones sorted in according to their target
Returns
-------
stop_position: list
list of position stop zones (target=position)
stop_speed: list
list of speed stop zones (target=speed)
"""
return self._get_zones(action="all")
def _get_zones(self, action: str = "stop"):
"""Returns two list of the zones whose ``action`` are set to a given value
Parameters
----------
action : str, optional
action to target, by default "stop"
Returns
-------
stop_position: list
list of position zones (target=position)
stop_speed: list
list of speed zones (target=speed)
"""
stop_speed = []
stop_position = []
for zone in self.__zones.values():
if zone.action == action or action == "all":
if zone.target == "speed":
stop_speed.append(deepcopy(zone))
elif zone.target == "position":
stop_position.append(deepcopy(zone))
return stop_position, stop_speed
[docs]
def in_zone(self, pos_speed_vector: np.ndarray, action: str = "stop") -> np.ndarray:
"""Evaluates whether 'pos_speed_vector' is in the zones corresponding to a given action
Parameters
----------
vector : array of shape (6,) or (n1, n2, ..., 6)
cartesian coordinates of the vectors in the lab frame
action : str, optionnal
the action of the zones to consider by default "stop"
Returns
-------
in_zone : array of shape (1,) or (n1, n2, ..., 1)
whether the vector is 'in the zone'
Notes
-----
``vector`` should be an array of shape (6,) or (n1, n2, .., 6), where last axis contains
the coordinates (position & speed) to evaluate.
In all cases, the last dimension contains cordinates (x, y, z, vx, vy, vz),
"""
# -- init a SuperZone
collection = SuperZone(logic="AND")
# -- populate
for zone in self.__zones.values():
if zone.action == action:
collection += zone
# -- return results
return collection.get_value(pos_speed_vector)
# -- GETTING FORCES
[docs]
def get_all_forces(self) -> list:
"""Returns a list of all forces
Returns
-------
force_list (list)
a list of all forces in the configuration
"""
force_list = self.__forces.values()
return force_list
# -- COLLECTION HANDLING METHODS
# ADDING
[docs]
def add_objects(self, obj: EnvObject | list, verbose=False):
"""Add environment objects to the configuration.
Parameters
----------
obj : EnvObject | list
a environment object or a list of objects
verbose : bool, optional
if set to True messages are displayed. Defaults to False.
Notes
-----
The function takes a single environment object (laser, magnetic field...) or a collection
of objects in the form of a tuple or a list.
Objects of different subtypes can be added at the same time: the method will add them
to the correct collection based on their classes
Note
----
The addition operator ``+`` also allows to add objects.
Hence, ``conf.add_objects([obj1, obj2, ...])`` is equivalent to
``conf += obj1, obj2 , ...``
Examples
--------
.. code-block:: python
... init a proper config first and env objects
# add objects
config.add_objects(laser1)
config.add_objects([mag_field, zone1, zone2, laser2])
# also works with += operator*
config += laser3, laser4
"""
# - check argument
self.__check_objects_arg(obj)
# - recursive add if list
if isinstance(obj, (list, tuple)):
for element in obj:
self.add_objects(element, verbose)
return
# - add object
if isinstance(obj, MagneticField):
collection = self.__magfields
name = "magnetic fields"
elif isinstance(obj, LaserBeam):
collection = self.__lasers
name = "lasers"
elif isinstance(obj, Force):
collection = self.__forces
name = "forces"
elif isinstance(obj, Zone):
collection = self.__zones
name = "zones"
else:
msg = f"Objects of type {type(obj)} are not handled yet.. where did you find this ?"
raise TypeError(msg)
self.__add_obj(obj, collection, name)
if verbose:
msg = f"(+) sucessfully added object '{obj.tag}' in the {name} collection"
print(msg)
def __add_obj(self, obj, collection, name):
"""Internal method to add objects"""
# - copy
obj = copy(obj)
# - check that object tag not present
msg = f"We already have an element with tag '{obj.tag}' in our {name} collection. "
msg += "Remove or update this element."
if obj.tag in collection:
raise ValueError(msg)
# - add the object >>> we use a copy to avoid unwanted modifications
collection[obj.tag] = obj
# UPDATING
[docs]
def update_objects(self, obj: EnvObject | list, verbose=False, error_on_fail=False):
"""Update an object or a list of objects
Parameters
----------
obj : EnvObject | list
a environment object or a list of objects
verbose : bool, optional
if set to True messages are displayed. Defaults to False.
error_on_fail : bool, optional
if set to True, raises an error if it fails. Otherwise,
just raises a warning and continues. Defaults to False.
Notes
------
The function takes a single environment object (laser, magnetic field...) or a collection
of objects in the form of a tuple or a list.
For each object given as an input, if there is an object with:
(1) same type (laser, magnetic field) **and**
(2) same tag
then this object is replaced by the new one.
"""
# - check argument
self.__check_objects_arg(obj)
# - recursive add if list
if isinstance(obj, (list, tuple)):
for element in obj:
self.update_objects(element, verbose, error_on_fail)
return
# - add object
if isinstance(obj, MagneticField):
collection = self.__magfields
name = "magnetic fields"
elif isinstance(obj, Force):
collection = self.__forces
name = "forces"
elif isinstance(obj, Zone):
collection = self.__zones
name = "zones"
elif isinstance(obj, LaserBeam):
collection = self.__lasers
name = "lasers"
else:
msg = f"Objects of type {type(obj)} are not handled yet.. where did you find this ?"
raise TypeError(msg)
success = self.__upd_obj(obj, collection, name, error_on_fail)
if verbose:
if success:
msg = f"(>) sucessfully updated object '{obj.tag}' in the {name} collection"
else:
msg = f"(x) could not update '{obj.tag}' in the {name} collection"
print(msg)
def __upd_obj(self, obj, collection, name, error_on_fail) -> bool:
# - copy
obj = copy(obj)
# - check that object tag not present
msg = f"There is no element with tag '{obj.tag}' in our {name} collection. "
if not obj.tag in collection:
if error_on_fail:
raise KeyError(msg)
else:
warnings.warn(msg)
return False
# - update the object
collection[obj.tag] = obj
return True
# LISTING
[docs]
def list_lasers(self) -> list:
"""Returns the list of laser's tags in the current config"""
return list(self.__lasers)
[docs]
def list_magnetic_fields(self):
"""Returns the list of magnetic fields' tags in the current config"""
return list(self.__magfields)
[docs]
def list_zones(self):
"""Returns the list of zones' tags in the current config"""
return list(self.__zones)
[docs]
def list_forces(self):
"""Returns the list of forces' tags in the current config"""
return list(self.__forces)
# REMOVING
[docs]
def rm_object(self, collection: str, tag: str):
"""Remove object from 'collection' with 'tag'
Collection must be in ['laser', 'magnetic field', 'zone', 'force']
Parameters
----------
collection : str
the collection from which the object should be removed
tag : str
the tag of the object
"""
coll = self.__check_object_in_coll(collection, tag)
del coll[tag]
[docs]
def rm_laser(self, tag: str):
"""Removes laser by tag
Parameters
----------
tag : str
laser tag
"""
return self.rm_object("laser", tag)
[docs]
def rm_magnetic_field(self, tag):
"""Removes magnetic field by tag
Parameters
----------
tag : str
magnetic field tag
"""
return self.rm_object("magnetic field", tag)
[docs]
def rm_zone(self, tag):
"""Removes zone by tag
Parameters
----------
tag : str
zone tag
"""
return self.rm_object("zone", tag)
[docs]
def rm_force(self, tag):
"""Removes force by tag
Parameters
----------
tag : str
force tag
"""
return self.rm_object("force", tag)
[docs]
def rm_all_objects(self):
"""Remove all objects"""
self.rm_all_lasers()
self.rm_all_magnetic_fields()
self.rm_all_zones()
self.rm_all_forces()
[docs]
def rm_all_lasers(self):
"""Remove all lasers"""
self.__lasers.clear()
[docs]
def rm_all_magnetic_fields(self):
"""Remove all magnetic fields"""
self.__magfields.clear()
[docs]
def rm_all_zones(self):
"""Remove all zones"""
self.__zones.clear()
[docs]
def rm_all_forces(self):
"""Remove all forces"""
self.__forces.clear()
# -- INFOS
[docs]
def gen_object_infostring_object(self, collection: str, tag: str) -> InfoString:
"""Generate infostring object for an object from 'collection' with 'tag'
Collection must be in ['laser', 'magnetic field', 'zone']
Parameters
----------
collection : str
the collection from which the object should be taken
tag : str
the tag of the object
Returns
-------
infostring: Infostring
an infostring object
See also
---------
atomsmltr.utils.infostring
"""
coll = self.__check_object_in_coll(collection, tag)
info = coll[tag].gen_infostring_obj()
info.title = f"{collection} | {tag=}"
return info
[docs]
def print_object_info(self, collection: str, tag: str):
"""Print info for an object from 'collection' with 'tag'
Collection must be in ['laser', 'magnetic field', 'zone' ]
Parameters
----------
collection : str
the collection from which the object should be taken
tag : str
the tag of the object
"""
info = self.gen_object_infostring_object(collection, tag)
print(info.generate())
[docs]
def print_laser_info(self, tag: str):
"""Print info of the laser indentified by 'tag'
Parameters
----------
tag : str
the tag of the laser
"""
return self.print_object_info("laser", tag)
[docs]
def print_magnetic_field_info(self, tag: str):
"""Print info of the magnetic field indentified by 'tag'
Parameters
----------
tag : str
the tag of the magnetic field
"""
return self.print_object_info("magnetic field", tag)
[docs]
def print_zone_info(self, tag: str):
"""Print info of the zone indentified by 'tag'
Parameters
----------
tag : str
the tag of the zone
"""
return self.print_object_info("zone", tag)
[docs]
def print_force_info(self, tag: str):
"""Print info of the force indentified by 'tag'
Parameters
----------
tag : str
the tag of the force
"""
return self.print_object_info("force", tag)
# -- GET OBJECTS
[docs]
def get_object_copy(self, collection: str, tag: str) -> EnvObject:
"""Returns a copy of an object from 'collection' with 'tag'
Collection must be in ['laser', 'magnetic field', 'zone', 'force' ]
Parameters
----------
collection : str
the collection from which the object should be taken
tag : str
the tag of the object
"""
coll = self.__check_object_in_coll(collection, tag)
return copy(coll[tag])
[docs]
def get_laser_copy(self, tag: str):
"""Returns a copy of the laser indentified by 'tag'
Parameters
----------
tag : str
the tag of the laser
"""
return self.get_object_copy("laser", tag)
[docs]
def get_magnetic_field_copy(self, tag: str):
"""Returns a copy of the magnetic field indentified by 'tag'
Parameters
----------
tag : str
the tag of the magnetic field
"""
return self.get_object_copy("magnetic field", tag)
[docs]
def get_zone_copy(self, tag: str):
"""Returns a copy of the zone indentified by 'tag'
Parameters
----------
tag : str
the tag of the zone
"""
return self.get_object_copy("zone", tag)
[docs]
def get_force_copy(self, tag: str):
"""Returns a copy of the force indentified by 'tag'
Parameters
----------
tag : str
the tag of the force
"""
return self.get_object_copy("force", tag)
# -- COMMON METHODS
def __check_object_in_coll(self, collection, tag) -> dict:
implemented_collections = self.__implemented_collections
if collection not in implemented_collections:
msg = f"Wrong collection. implemented collections are {list(implemented_collections)}"
raise ValueError(msg)
coll = implemented_collections[collection]
if tag not in coll:
msg = f"There is no {collection} with tag {tag}"
raise KeyError(msg)
return coll
def __check_objects_arg(self, obj):
"""Called in add_objects & update_objects"""
type_err_msg = "passed argument should be an EnvObject or a list of EnvObjects"
if isinstance(obj, (list, tuple)):
for element in obj:
if not isinstance(element, EnvObject):
raise TypeError(type_err_msg)
elif not isinstance(obj, EnvObject):
raise TypeError(type_err_msg)
# -- GETTERS & SETTERS
@property
def atom(self) -> Atom:
""" "Atom: the configuration atom"""
return self.__atom
@atom.setter
def atom(self, atom: Atom):
# - set atom
if not isinstance(atom, Atom):
raise TypeError("'atom' should be an atom")
self.__atom = atom
# - prepare atomlight dict
# issue warning if already some entries
if not self.__atomlight:
# if dict not empty, clear it
self.__atomlight.clear()
# warnings.warn("Resetting atom-light dictionnary...")
for transition_tag in self.atom.list_transitions():
self.__atomlight[transition_tag] = {}
@property
def objects(self) -> dict:
"""dict: the collection of objects"""
out = {}
for k, v in self.__implemented_collections.items():
out[k] = copy(v)
return out
# -- INFO PRINTER
def gen_atomlight_infostring_obj(self):
info = InfoString("Atom-light couplings")
for transition, couplings in self.__atomlight.items():
info.add_section(f"transition > '{transition}'")
if couplings:
for laser, params in couplings.items():
detuning = params["detuning"]
trans_Gamma = self.atom.trans[transition].Gamma
det_str = f"{detuning=:.3g}"
det_str += f" ({detuning / trans_Gamma:.2f}Γ)"
info.add_element(f"laser '{laser}'", det_str)
else:
info.add_element("empty")
return info
[docs]
def print_atomlight_info(self):
"""Prints atom-light coupling information"""
print(self.gen_atomlight_infostring_obj().generate())
def gen_infostring_obj_list(self):
# - prepare output
info_list = []
# - general infostring
info = InfoString("General informations")
# atom
info.add_section("atom")
info.add_element("name", self.atom.name)
# collections
for name, coll in self.__implemented_collections.items():
info.add_section(name + "s")
if coll:
for tag in coll:
info.add_element(tag)
else:
info.add_element("empty")
# append to list
info_list.append(info)
# - atom info
info = self.atom.gen_infostring_obj()
info.title = f"atom | {self.atom.name.lower()}"
info_list.append(info)
# - collections
for name, coll in self.__implemented_collections.items():
for tag in coll:
info = self.gen_object_infostring_object(name, tag)
info_list.append(info)
# - atom light
info = self.gen_atomlight_infostring_obj()
info_list.append(info)
return info_list
[docs]
def print_info(self):
"""Prints informations on the configuration"""
info_list = self.gen_infostring_obj_list()
print(SEP_STR.format("CONFIG INFO > START"))
for info in info_list:
print(info.generate())
print(SEP_STR.format("CONFIG INFO > STOP "))
# -- OPERATORS OVERLOADING
def __add__(self, object: EnvObject):
"""returns a copy of the configuration, with the object added"""
new_config = deepcopy(self)
new_config.add_objects(object)
return new_config
def __iadd__(self, object: EnvObject):
"""adds the objects"""
self.add_objects(object)
return self
def __imod__(self, object: EnvObject):
"""updates the objects"""
self.update_objects(object)
return self