Source code for finesse.components.readout

"""
A components sub-module containing classes for detecting power at
a physical point in a configuration.
"""

import numpy as np
import types
import finesse
import finesse.detectors._pdtypes as pdtypes
from finesse.components.general import Connector, borrows_nodes
from finesse.components.node import NodeDirection, NodeType, Port
from finesse.components.workspace import ConnectorWorkspace
from finesse.parameter import float_parameter
from finesse.detectors.compute.power import PD0Workspace, PD1Workspace, PD2Workspace
from finesse.element import ModelElement

from finesse.detectors.compute.quantum import (
    QShot0Workspace,
    QShotNWorkspace,
)

[docs]class ReadoutWorkspace(ConnectorWorkspace): pass
[docs]class ReadoutOutput(ModelElement): def __init__(self, name, readout): super().__init__(name) self.__readout = readout @property def readout(self): return self.__readout
[docs]class Readout(Connector): def __init__(self, name, optical_node, output_detectors=False): super().__init__(name) self.__output_detectors = output_detectors self._add_port("p1", NodeType.OPTICAL) if optical_node is not None: port = optical_node if isinstance(optical_node, Port) else optical_node.port other_node = tuple(o for o in port.nodes if o is not optical_node)[0] self.p1._add_node("i", None, optical_node) self.p1._add_node("o", None, other_node) else: self.p1._add_node("i", NodeDirection.INPUT) self.p1._add_node("o", NodeDirection.OUTPUT) def _get_output_workspaces(self, model): return None @property def optical_node(self): if self.p1.i.component != self: return self.p1.i @property def has_mask(self): return False @property def output_detectors(self): return self.__output_detectors @output_detectors.setter def output_detectors(self, value): self.__output_detectors = value
[docs]@borrows_nodes() class ReadoutDC(Readout): def __init__(self, name, optical_node=None, output_detectors=False): super().__init__(name, optical_node, output_detectors=output_detectors) self._add_port("DC", NodeType.ELECTRICAL) self.DC._add_node("o", NodeDirection.OUTPUT) self._register_node_coupling("P1i_DC", self.p1.i, self.DC.o) self.outputs = types.SimpleNamespace() self.outputs.DC = f'{self.name}_DC' def _on_add(self, model): model.add(ReadoutOutput(self.name+"_DC", self)) def _get_workspace(self, sim): if sim.signal: has_DC_node = self.DC.o.full_name in sim.signal.nodes if not has_DC_node: return None # Don't do anything if no nodes included ws = ReadoutWorkspace(self, sim, True, True) ws.I = np.eye(sim.model_data.num_HOMs, dtype=np.complex128) ws.signal.add_fill_function(self._fill_matrix, True) ws.frequencies = sim.signal.electrical_frequencies[self.DC.o].frequencies return ws else: return None def _get_output_workspaces(self, sim): from finesse.detectors.workspace import OutputInformation from finesse.detectors.compute.power import pd0_DC_output, PD0Workspace wss = [] for quadrature in ('DC',): # Setup a single demodulation photodiode detector for # using for outputs oinfo = OutputInformation( self.name + "_" + quadrature, (self.p1.i, ), np.float64, "W", None, "W", True, False ) ws = PD0Workspace(self, sim, oinfo=oinfo) ni = sim.carrier.get_node_info(oinfo.nodes[0]) ws.rhs_index = ni["rhs_index"] ws.size = ni["nfreqs"] * ni["nhoms"] ws.dc_node_id = sim.carrier.node_id(oinfo.nodes[0]) ws.set_output_fn(pd0_DC_output) wss.append(ws) if sim.signal: oinfo = OutputInformation( self.name + "_shot_noise", (self.p1.i, ), np.float64, "W/rtHz", None, "ASD", True, False ) wss.append( QShot0Workspace(self, sim, False, output_info=oinfo) ) return wss def _fill_matrix(self, ws): """ Computing E.conj() * upper + E * lower.conj() """ for freq in ws.sim.signal.optical_frequencies.frequencies: # Get the carrier HOMs for this frequency E = ws.sim.carrier.get_out(self.p1.i, freq.audio_carrier_index) # is_lower_sb = freq.audio_order < 0 for efreq in ws.frequencies: with ws.sim.signal.component_edge_fill3( ws.owner_id, ws.signal.connections.P1i_DC_idx, freq.index, efreq.index, ) as mat: mat[:] = E.conjugate()
[docs]@borrows_nodes() class ReadoutDCQPD(Readout): def __init__(self, name, optical_node=None, output_detectors=False): super().__init__(name, optical_node, output_detectors=output_detectors) self._add_port("x", NodeType.ELECTRICAL) self.x._add_node("o", NodeDirection.OUTPUT) self._add_port("y", NodeType.ELECTRICAL) self.y._add_node("o", NodeDirection.OUTPUT) self._register_node_coupling("P1i_x", self.p1.i, self.x.o) self._register_node_coupling("P1i_y", self.p1.i, self.y.o) self.outputs = types.SimpleNamespace() self.outputs.x = f'{self.name}_X' self.outputs.y = f'{self.name}_Y' def _on_add(self, model): model.add(ReadoutOutput(self.name + "_X", self)) model.add(ReadoutOutput(self.name + "_Y", self)) def _get_workspace(self, sim): if sim.signal: has_x_node = self.x.o.full_name in sim.signal.nodes has_y_node = self.y.o.full_name in sim.signal.nodes if not (has_x_node or has_y_node): return None # Don't do anything if no nodes included ws = ReadoutWorkspace(self, sim, True, True) ws.signal.add_fill_function(self._fill_matrix, True) ws.frequencies = sim.signal.electrical_frequencies[self.y.o].frequencies ws.Kx = pdtypes.construct_segment_beat_matrix( sim.model.mode_index_map, pdtypes.XSPLIT ) ws.Ky = pdtypes.construct_segment_beat_matrix( sim.model.mode_index_map, pdtypes.YSPLIT ) ws.node_car_id = sim.carrier.node_id(self.p1.i) return ws else: return None def _get_output_workspaces(self, sim): from finesse.detectors.workspace import OutputInformation from finesse.detectors.compute.power import pd0_DC_output_segmented, PD0Workspace wss = [] for dof, split in (('X', pdtypes.XSPLIT), ('Y', pdtypes.YSPLIT)): # Setup a single demodulation photodiode detector for # using for outputs oinfo = OutputInformation( self.name + "_" + dof, (self.p1.i, ), np.float64, "W", None, "W", True, False ) ws = PD0Workspace(self, sim, oinfo=oinfo) ws.dc_node_id = sim.carrier.node_id(oinfo.nodes[0]) ws.tmp = np.zeros(ws.size, dtype=complex) ws.K = pdtypes.construct_segment_beat_matrix( sim.model.mode_index_map, split ) ws.set_output_fn(pd0_DC_output_segmented) wss.append(ws) return wss def _fill_matrix(self, ws): for freq in ws.sim.signal.optical_frequencies.frequencies: # Get the carrier HOMs for this frequency cidx = freq.audio_carrier_index rhs_idx = ws.sim.carrier.field(self.p1.i, cidx, 0) Ec = np.conjugate(ws.sim.carrier.out_view[rhs_idx:(rhs_idx + ws.sim.model_data.num_HOMs)]) # is_lower_sb = freq.audio_order < 0 for efreq in ws.frequencies: if ws.signal.connections.P1i_x_idx > -1: with ws.sim.signal.component_edge_fill3( ws.owner_id, ws.signal.connections.P1i_x_idx, freq.index, efreq.index, ) as mat: mat[:] = np.dot(ws.Kx, Ec) if ws.signal.connections.P1i_y_idx > -1: with ws.sim.signal.component_edge_fill3( ws.owner_id, ws.signal.connections.P1i_y_idx, freq.index, efreq.index, ) as mat: mat[:] = np.dot(ws.Ky, Ec)
[docs]@borrows_nodes() @float_parameter("f", "Frequency") @float_parameter("phase", "Phase") class ReadoutRF(Readout): def __init__(self, name, optical_node=None, *, f=None, phase=0, output_detectors=False): super().__init__(name, optical_node, output_detectors=output_detectors) self.f = f self.phase = phase self._add_port("I", NodeType.ELECTRICAL) self.I._add_node("o", NodeDirection.OUTPUT) self._add_port("Q", NodeType.ELECTRICAL) self.Q._add_node("o", NodeDirection.OUTPUT) self._register_node_coupling("P1i_I", self.p1.i, self.I.o) self._register_node_coupling("P1i_Q", self.p1.i, self.Q.o) self.outputs = types.SimpleNamespace() self.outputs.I = f'{self.name}_I' self.outputs.Q = f'{self.name}_Q' @property def optical_node(self): if self.p1.i.component != self: return self.p1.i def _on_add(self, model): model.add(ReadoutOutput(self.name+"_I", self)) model.add(ReadoutOutput(self.name+"_Q", self)) def _get_workspace(self, sim): if sim.signal: has_I_node = self.I.o.full_name in sim.signal.nodes has_Q_node = self.Q.o.full_name in sim.signal.nodes if not (has_I_node or has_Q_node): return None # Don't do anything if no nodes included ws = ReadoutWorkspace(self, sim, True, True) ws.signal.add_fill_function(self._fill_matrix, True) ws.frequencies = sim.signal.electrical_frequencies[self.I.o if has_I_node else self.Q.o].frequencies ws.dc_node_id = sim.carrier.node_id(self.p1.i) return ws else: return None def _get_output_workspaces(self, sim): from finesse.detectors.workspace import OutputInformation from finesse.detectors.compute.power import pd1_DC_output wss = [] for quadrature in ('I', 'Q'): # Setup a single demodulation photodiode detector for # using for outputs oinfo = OutputInformation( self.name + "_" + quadrature, (self.p1.i, ), np.float64, "W", None, "W", True, False ) poff = 90 if quadrature == 'Q' else 0 ws = PD1Workspace(self, sim, phase_offset=poff, oinfo=oinfo) ws.is_f_changing = self.f.is_changing ws.is_phase_changing = self.phase.is_changing ws.output_real = True ws.dc_node_id = sim.carrier.node_id(oinfo.nodes[0]) ws.homs = ws.sim.model_data.homs_view ws.set_output_fn(pd1_DC_output) if not ws.is_f_changing: # precompute these things if nothing is changing ws.update_parameter_values() ws.update_beats() wss.append(ws) if sim.signal: oinfo = OutputInformation( self.name + "_shot_noise", (self.p1.i, ), np.float64, "W/rtHz", None, "ASD", True, False ) wss.append( QShotNWorkspace(self, sim, 1, False, output_info=oinfo) ) return wss def _fill_matrix(self, ws): # 1/2 from demod gain factorI = ( -1 # ddb - can't see why we need this minus sign. # Makes us match up with pd2 in v2 and v3 though * ws.sim.model_data.EPSILON0_C * np.exp(-1j * ws.values.phase * finesse.constants.DEG2RAD) ) cfactorI = factorI.conjugate() factorQ = 1j * factorI cfactorQ = factorQ.conjugate() # Need to zero everything first for f1 in ws.sim.carrier.optical_frequencies.frequencies: for f2 in ws.sim.carrier.optical_frequencies.frequencies: df = f1.f - f2.f if df == ws.values.f or df == -ws.values.f: if ws.signal.connections.P1i_I_idx >= 0: with ws.sim.signal.component_edge_fill3( ws.owner_id, ws.signal.connections.P1i_I_idx, ws.sim.carrier.optical_frequencies.get_info(f2.index)["audio_lower_index"], 0, ) as mat: mat[:] = 0 with ws.sim.signal.component_edge_fill3( ws.owner_id, ws.signal.connections.P1i_I_idx, ws.sim.carrier.optical_frequencies.get_info(f1.index)["audio_upper_index"], 0, ) as mat: mat[:] = 0 if ws.signal.connections.P1i_Q_idx >= 0: with ws.sim.signal.component_edge_fill3( ws.owner_id, ws.signal.connections.P1i_Q_idx, ws.sim.carrier.optical_frequencies.get_info(f2.index)["audio_lower_index"], 0, ) as mat: mat[:] = 0 with ws.sim.signal.component_edge_fill3( ws.owner_id, ws.signal.connections.P1i_Q_idx, ws.sim.carrier.optical_frequencies.get_info(f1.index)["audio_upper_index"], 0, ) as mat: mat[:] = 0 for f1 in ws.sim.carrier.optical_frequencies.frequencies: for f2 in ws.sim.carrier.optical_frequencies.frequencies: df = f1.f - f2.f # Get the carrier HOMs for this frequency rhs_idx = ws.sim.carrier.field(self.p1.i, f1.index, 0) E1 = ws.sim.carrier.out_view[rhs_idx:(rhs_idx + ws.sim.model_data.num_HOMs)] rhs_idx = ws.sim.carrier.field(self.p1.i, f2.index, 0) E2c = np.conjugate(ws.sim.carrier.out_view[rhs_idx:(rhs_idx + ws.sim.model_data.num_HOMs)]) if df == -ws.values.f: if ws.signal.connections.P1i_I_idx >= 0: with ws.sim.signal.component_edge_fill3( ws.owner_id, ws.signal.connections.P1i_I_idx, ws.sim.carrier.optical_frequencies.get_info(f2.index)["audio_lower_index"], 0, ) as mat: # This will apply a conjugation internally as it's # a lower SB connection mat[:] += factorI.conjugate() * E1 with ws.sim.signal.component_edge_fill3( ws.owner_id, ws.signal.connections.P1i_I_idx, ws.sim.carrier.optical_frequencies.get_info(f1.index)["audio_upper_index"], 0, ) as mat: mat[:] += factorI.conjugate() * E2c if ws.signal.connections.P1i_Q_idx >= 0: with ws.sim.signal.component_edge_fill3( ws.owner_id, ws.signal.connections.P1i_Q_idx, ws.sim.carrier.optical_frequencies.get_info(f2.index)["audio_lower_index"], 0, ) as mat: # This will apply a conjugation internally as it's # a lower SB connection mat[:] += factorQ.conjugate() * E1 with ws.sim.signal.component_edge_fill3( ws.owner_id, ws.signal.connections.P1i_Q_idx, ws.sim.carrier.optical_frequencies.get_info(f1.index)["audio_upper_index"], 0, ) as mat: mat[:] += factorQ.conjugate() * E2c if df == ws.values.f: if ws.signal.connections.P1i_I_idx >= 0: with ws.sim.signal.component_edge_fill3( ws.owner_id, ws.signal.connections.P1i_I_idx, ws.sim.carrier.optical_frequencies.get_info(f2.index)["audio_lower_index"], 0, ) as mat: mat[:] += factorI * E1 with ws.sim.signal.component_edge_fill3( ws.owner_id, ws.signal.connections.P1i_I_idx, ws.sim.carrier.optical_frequencies.get_info(f1.index)["audio_upper_index"], 0, ) as mat: mat[:] += factorI * E2c if ws.signal.connections.P1i_Q_idx >= 0: with ws.sim.signal.component_edge_fill3( ws.owner_id, ws.signal.connections.P1i_Q_idx, ws.sim.carrier.optical_frequencies.get_info(f2.index)["audio_lower_index"], 0, ) as mat: mat[:] += factorQ * E1 with ws.sim.signal.component_edge_fill3( ws.owner_id, ws.signal.connections.P1i_Q_idx, ws.sim.carrier.optical_frequencies.get_info(f1.index)["audio_upper_index"], 0, ) as mat: mat[:] += factorQ * E2c