diff --git a/gwinc/ifo/CE1/__init__.py b/gwinc/ifo/CE1/__init__.py index 7baa4055c4d34c6b16ab5b709cad340707552dad..c558f3f551ab27bc92b6c5efe0a5a5b4c6bfac7f 100644 --- a/gwinc/ifo/CE1/__init__.py +++ b/gwinc/ifo/CE1/__init__.py @@ -14,10 +14,11 @@ class QuantumVacuum(nb.Budget): noises = [ QuantumVacuumAS, QuantumVacuumArm, - QuantumVacuumSRC, + QuantumVacuumSEC, QuantumVacuumFilterCavity, QuantumVacuumInjection, QuantumVacuumReadout, + QuantumVacuumQuadraturePhase, ] diff --git a/gwinc/ifo/CE2/__init__.py b/gwinc/ifo/CE2/__init__.py index f4e0e69d0008982b6b01d346141f2ac4a10b9fc7..dd608d7f8e27b8dec0f6ee1ffb2ba2d3b9e3434b 100644 --- a/gwinc/ifo/CE2/__init__.py +++ b/gwinc/ifo/CE2/__init__.py @@ -14,10 +14,11 @@ class QuantumVacuum(nb.Budget): noises = [ QuantumVacuumAS, QuantumVacuumArm, - QuantumVacuumSRC, + QuantumVacuumSEC, QuantumVacuumFilterCavity, QuantumVacuumInjection, QuantumVacuumReadout, + QuantumVacuumQuadraturePhase, ] diff --git a/gwinc/ifo/CE2/ifo.yaml b/gwinc/ifo/CE2/ifo.yaml index 97f5f0d70a4c2ab7b8ce3c230acd42fcff23f373..40caefe3a6c872a3b54d1be6ec14ded0e74bba42 100644 --- a/gwinc/ifo/CE2/ifo.yaml +++ b/gwinc/ifo/CE2/ifo.yaml @@ -321,6 +321,7 @@ Squeezer: AmplitudedB: 15 # SQZ amplitude [dB] InjectionLoss: 0.02 # power loss to sqz SQZAngle: 0 # SQZ phase [radians] + LOAngleRMS: 10e-3 # quadrature noise [radians] # Parameters for frequency dependent squeezing FilterCavity: diff --git a/gwinc/ifo/noises.py b/gwinc/ifo/noises.py index c3826f4bbfdeb92cafd289cf4ec86f7d558305c4..7e5b05b9990d3eb2d0d4b04519fd33d6d45f54b5 100644 --- a/gwinc/ifo/noises.py +++ b/gwinc/ifo/noises.py @@ -174,8 +174,8 @@ def precomp_quantum(f, ifo): pbs, parm, _, _, _ = ifo_power(ifo) power = Struct(parm=parm, pbs=pbs) noise_dict = noise.quantum.shotrad(f, ifo, mirror_mass, power) - pc.ASvac = noise_dict['asvac'] - pc.SRC = noise_dict['src'] + pc.ASvac = noise_dict['ASvac'] + pc.SEC = noise_dict['SEC'] pc.Arm = noise_dict['arm'] pc.Injection = noise_dict['injection'] pc.PD = noise_dict['pd'] @@ -184,13 +184,19 @@ def precomp_quantum(f, ifo): # are noises from the unsqueezed vacuum injected at the back mirror # Right now there are at most one filter cavity in all the models; # if there were two, there would also be FC1 and FC1_unsqzd_back, etc. - keys = list(noise_dict.keys()) - fc_keys = [key for key in keys if 'FC' in key] + # keys = list(noise_dict.keys()) + fc_keys = [key for key in noise_dict.keys() if 'FC' in key] if fc_keys: pc.FC = np.zeros_like(pc.ASvac) for key in fc_keys: pc.FC += noise_dict[key] + if 'phase' in noise_dict.keys(): + pc.Phase = noise_dict['phase'] + + if 'ofc' in noise_dict.keys(): + pc.OFC = noise_dict['OFC'] + return pc @@ -280,18 +286,18 @@ class QuantumVacuumArm(nb.Noise): return quantum.Arm -class QuantumVacuumSRC(nb.Noise): - """Quantum vacuum due to SRC loss +class QuantumVacuumSEC(nb.Noise): + """Quantum vacuum due to SEC loss """ style = dict( - label='SRC Loss', + label='SEC Loss', color='xkcd:cerulean' ) @nb.precomp(quantum=precomp_quantum) def calc(self, quantum): - return quantum.SRC + return quantum.SEC class QuantumVacuumFilterCavity(nb.Noise): @@ -336,6 +342,19 @@ class QuantumVacuumReadout(nb.Noise): return quantum.PD +class QuantumVacuumQuadraturePhase(nb.Noise): + """Quantum vacuum noise due to quadrature phase noise + """ + style = dict( + label='Quadrature Phase', + color='xkcd:slate' + ) + + @nb.precomp(quantum=precomp_quantum) + def calc(self, quantum): + return quantum.Phase + + class StandardQuantumLimit(nb.Noise): """Standard Quantum Limit diff --git a/gwinc/noise/quantum.py b/gwinc/noise/quantum.py index 68fe6421455f99335bb0353b9e9f4505f0678f52..260a1ae428f0ce58e3aba5476f4ec44b772a9b1c 100644 --- a/gwinc/noise/quantum.py +++ b/gwinc/noise/quantum.py @@ -4,6 +4,7 @@ from __future__ import division import numpy as np from numpy import pi, sqrt, arctan, sin, cos, exp, log10, conj +from copy import deepcopy from .. import logger from .. import const @@ -17,71 +18,71 @@ def sqzOptimalSqueezeAngle(Mifo, eta): return alpha -def getSqzType(ifo): +def getSqzParams(ifo): + """Determine squeezer type, if any, and extract common parameters + + Returns a struct with the following attributes: + SQZ_DB: squeezing in dB + ANTISQZ_DB: anti-squeezing in dB + alpha: freq Indep Squeeze angle [rad] + lambda_in: loss to squeezing before injection [Power] + etaRMS: quadrature noise [rad] + eta: homodyne angle [rad] + """ + params = Struct() + if 'Squeezer' not in ifo: - sqzType = 'None' + sqzType = None else: sqzType = ifo.Squeezer.get('Type', 'Freq Independent') - return sqzType + params.sqzType = sqzType -def getSqzParams(ifo): - """Get squeezer parameters - - Returns: - SQZ_DB: squeezing [dB] - alpha: squeezing angle [rad] - lambda_in: loss to squeezing before injection [Power] - ANTISQZ_DB: anti-squeezing [dB] - etaRMS: quadrature phase noise [rad] - """ - # determine squeezer type, if any - # and extract common parameters - sqzType = getSqzType(ifo) - - # extract common parameters - if sqzType == 'None': - SQZ_DB = 0 # Squeeing in dB - alpha = 0 # Squeeze angle - lambda_in = 0 # Loss to squeezing before injection [Power] - ANTISQZ_DB = 0 # Anti squeezing in db - etaRMS = 0 - else: - SQZ_DB = ifo.Squeezer.AmplitudedB # Squeeing in dB - lambda_in = ifo.Squeezer.InjectionLoss # Loss to squeezing before injection [Power] - alpha = ifo.Squeezer.SQZAngle # Freq Indep Squeeze angle - ANTISQZ_DB = ifo.Squeezer.get('AntiAmplitudedB', SQZ_DB) # Anti squeezing in db - etaRMS = ifo.Squeezer.get('LOAngleRMS', 0) # quadrature noise + # extract squeezer parameters + if sqzType is None: + params.SQZ_DB = 0 + params.ANTISQZ_DB = 0 + params.alpha = 0 + params.lambda_in = 0 + params.etaRMS = 0 - # switch on squeezing type for other input squeezing modifications - if sqzType == 'None': - pass + else: + params.SQZ_DB = ifo.Squeezer.AmplitudedB + params.ANTISQZ_DB = ifo.Squeezer.get('AntiAmplitudedB', params.SQZ_DB) + params.alpha = ifo.Squeezer.SQZAngle + params.lambda_in = ifo.Squeezer.InjectionLoss + params.etaRMS = ifo.Squeezer.get('LOAngleRMS', 0) - elif sqzType == 'Freq Independent': - logger.debug('You are injecting %g dB of frequency independent squeezing' % SQZ_DB) + # Homodyne Readout phase + eta_orig = ifo.Optics.get('Quadrature', Struct()).get('dc', None) - elif sqzType == 'Optimal': - # compute optimal squeezing angle - alpha = sqzOptimalSqueezeAngle(Mifo, eta) + ifoRead = ifo.get('Squeezer', Struct()).get('Readout', None) + if ifoRead is None: + eta = eta_orig + if eta_orig is None: + raise Exception("must add Quadrature.dc or Readout...") - logger.debug('You are injecting %g dB of squeezing with optimal frequency dependent squeezing angle' % SQZ_DB) + elif ifoRead.Type == 'DC': + eta = np.sign(ifoRead.fringe_side) * \ + np.arccos((ifoRead.defect_PWR_W / ifoRead.readout_PWR_W)**.5) - elif sqzType == 'OptimalOptimal': - # compute optimal squeezing angle, assuming optimal readout phase - R = SQZ_DB / (20 * log10(exp(1))) - MnPD = sqzInjectionLoss(Mn, lambda_PD) - MsigPD = Msig * sqrt(1 - lambda_PD) - alpha = sqzOptimalSqueezeAngle(Mifo, [], [R, lambda_in], MsigPD, MnPD) + elif ifoRead.Type == 'Homodyne': + eta = ifoRead.Angle - logger.debug('You are injecting %g dB of squeezing with optimal FD squeezing angle, for optimal readout phase' % SQZ_DB) + else: + raise Exception("Unknown Readout Type") - elif sqzType == 'Freq Dependent': - logger.debug('You are injecting %g dB of squeezing with frequency dependent squeezing angle' % SQZ_DB) + if eta_orig is not None: + # logger.warn(( + # 'Quadrature.dc is redundant with ' + # 'Squeezer.Readout and is deprecated.' + # )) + if eta_orig != eta: + raise Exception("Quadrature.dc inconsistent with Readout eta") - else: - raise Exception('ifo.Squeezer.Type must be None, Freq Independent, Optimal, or Frequency Dependent, not "%s"' % sqzType) + params.eta = eta - return SQZ_DB, alpha, lambda_in, ANTISQZ_DB, etaRMS + return params ###################################################################### @@ -90,10 +91,11 @@ def getSqzParams(ifo): def shotrad(f, ifo, mirror_mass, power): - """Quantum noise noise strain spectrum + """Quantum noise strain spectrum :f: frequency array in Hz :ifo: gwinc IFO Struct + :mirror_mass: mirror mass in kg :power: gwinc power Struct :returns: displacement noise power spectrum at :f: @@ -105,10 +107,11 @@ def shotrad(f, ifo, mirror_mass, power): # Vacuum sources will be individually tracked with the Mnoise dict # # The keys are the following # # * arm: arm cavity losses # - # * src: SRC losses # - # * asvac: AS port vacuum # + # * SEC: SEC losses # + # * ASvac: AS port vacuum # # * pd: readout losses # # * injection: injection losses # + # * phase: quadrature phase noise # # # # If there is a filter cavity there are an additional set of keys # # starting with 'FC' for the filter cavity losses # @@ -116,153 +119,64 @@ def shotrad(f, ifo, mirror_mass, power): # If there is an output filter cavity the key 'OFC' gives its losses # ###################################################################### - ##################################################### - # Call IFO Quantum Model - ##################################################### - - Nfreq = len(f) - + # call the main IFO Quantum Model if 'Type' not in ifo.Optics: fname = shotradSignalRecycled else: namespace = globals() fname = namespace['shotrad' + ifo.Optics.Type] coeff, Mifo, Msig, Mn = fname(f, ifo, mirror_mass, power) - Mnoise = dict(arm=Mn[:, :2, :], src=Mn[:, 2:, :]) - ##################################################### - # Readout - ##################################################### + # separate arm and SEC loss + Mnoise = dict(arm=Mn[:, :2, :], SEC=Mn[:, 2:, :]) - # useful numbers - # TODO, adjust Struct to allow deep defaulted access - # Homodyne Readout phase - eta_orig = ifo.Optics.get('Quadrature', Struct()).get('dc', None) + sqz_params = getSqzParams(ifo) - ifoRead = ifo.get('Squeezer', Struct()).get('Readout', None) - if ifoRead is None: - eta = eta_orig - if eta_orig is None: - raise Exception("must add Quadrature.dc or Readout...") - elif ifoRead.Type == 'DC': - eta = np.sign(ifoRead.fringe_side) * np.arccos((ifoRead.defect_PWR_W / ifoRead.readout_PWR_W)**.5) - elif ifoRead.Type == 'Homodyne': - eta = ifoRead.Angle - else: - raise Exception("Unknown Readout Type") + if sqz_params.sqzType == 'Optimal': + # compute optimal squeezing angle + sqz_params.alpha = sqzOptimalSqueezeAngle(Mifo, sqz_params.eta) - if eta_orig is not None: - # logger.warn(( - # 'Quadrature.dc is redundant with ' - # 'Squeezer.Readout and is deprecated.' - # )) - if eta_orig != eta: - raise Exception("Quadrature.dc inconsistent with Readout eta") + vHD = np.array([[sin(sqz_params.eta), cos(sqz_params.eta)]]) + def homodyne(signal): + """Readout the eta quadrature of the signal signal + """ + return np.squeeze(np.sum(abs(getProdTF(vHD, signal))**2, axis=1)) + # optomechanical plant lambda_PD = 1 - ifo.Optics.PhotoDetectorEfficiency # PD losses + Msig = Msig * sqrt(1 - lambda_PD) + plant = homodyne(Msig) - ##################################################### - # Input Squeezing - ##################################################### - - #>>>>>>>> QUANTUM NOISE POWER SPECTRAL DENSITY WITH SQZ [BnC PRD 2004, 62] - #<<<<<<<<<<<<<<<<< Modified to include losses (KM) - #<<<<<<<<<<<<<<<<< Modified to include frequency dependent squeezing angle (LB) - - SQZ_DB, alpha, lambda_in, ANTISQZ_DB, etaRMS = getSqzParams(ifo) - sqzType = getSqzType(ifo) - - # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - # ------------------------------------------- equation 63 BnC PRD 2004 - # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - # Define input matrix of Squeezing - R = SQZ_DB / (20 * log10(exp(1))) # Squeeze factor - R_anti = ANTISQZ_DB / (20 * log10(exp(1))) # Squeeze factor - Msqz = np.array([[exp(-R), 0], [0, exp(R_anti)]]) - - # expand to Nfreq - Msqz = np.transpose(np.tile(Msqz, (Nfreq,1,1)), axes=(1,2,0)) - - # add input rotation - MsqzRot = make2x2TF(cos(alpha), -sin(alpha), sin(alpha), cos(alpha)) - Msqz = getProdTF(MsqzRot, Msqz) - Msqz = dict(asvac=Msqz) - - # Include losses (lambda_in=ifo.Squeezer.InjectionLoss) - Msqz = sqzInjectionLoss(Msqz, lambda_in, 'injection') - - # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - # Inject squeezed field into the IFO via some filter cavities - if sqzType == 'Freq Dependent' and 'FilterCavity' in ifo.Squeezer: - logger.debug(' Applying %d input filter cavities' % np.atleast_1d(ifo.Squeezer.FilterCavity).size) - Mr, Msqz = sqzFilterCavityChain( - f, np.atleast_1d(ifo.Squeezer.FilterCavity), Msqz) - - ##################################################### - # IFO Transfer and Output Filter Cavities - ##################################################### - - # apply the IFO dependent squeezing matrix to get the total noise - # due to quantum fluctuations coming in from the AS port - Msqz = {key: getProdTF(Mifo, nn) for key, nn in Msqz.items()} - - # add this to the other noises - Mnoise.update(Msqz) - - # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - # pass IFO output through some filter cavities - if 'OutputFilter' in ifo: - if ifo.OutputFilter.Type == 'None': - # do nothing, say nothing - pass - - elif ifo.OutputFilter.Type == 'Chain': - logger.debug(' Applying %d output filter cavities' % np.atleast_1d(ifo.OutputFilter.FilterCavity).size) - - Mr, Mnoise = sqzFilterCavityChain( - f, np.atleast_1d(ifo.OutputFilter.FilterCavity), Mnoise, key='OFC') - Msig = getProdTF(Mr, Msig) - # Mnoise = getProdTF(Mn, Mnoise); - - elif ifo.OutputFilter.Type == 'Optimal': - raise NotImplementedError("Cannot do optimal phase yet") - logger.debug(' Optimal output filtering!') + # get all the losses in the squeezed quadrature + Mnoise = propagate_noise_fc_ifo( + f, ifo, Mifo, Mnoise, sqz_params, sqz_params.alpha) - # compute optimal angle, including upcoming PD losses - MnPD = sqzInjectionLoss(Mnoise, lambda_PD) - zeta = sqzOptimalReadoutPhase(Msig, MnPD) + # add quadrature phase fluctuations if any + if sqz_params.etaRMS: + # get all losses in the anti-squeezed quadrature + Mnoise_antisqz = propagate_noise_fc_ifo( + f, ifo, Mifo, Mnoise, sqz_params, sqz_params.alpha + pi/2) - # rotate by that angle, less the homodyne angle - #zeta_opt = eta; - cs = cos(zeta - eta) - sn = sin(zeta - eta) - Mrot = make2x2TF(cs, -sn, sn, cs) - Mnoise = getProdTF(Mrot, Mnoise) - Msig = getProdTF(Mrot, Msig) - - else: - raise Exception('ifo.OutputFilter.Type must be None, Chain or Optimal, not "%s"' % ifo.OutputFilter.Type) - - # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - # add PD efficiency - Mnoise = sqzInjectionLoss(Mnoise, lambda_PD, 'pd') - Msig = Msig * sqrt(1 - lambda_PD) + # sum over these since they're not individually tracked + var_antisqz = np.zeros_like(f) + for nn in Mnoise_antisqz.values(): + var_antisqz += homodyne(nn) - # and compute the final noise - def HDnoise(eta, Mn): - vHD = np.array([[sin(eta), cos(eta)]]) - n = coeff * np.squeeze(np.sum(abs(getProdTF(vHD, Mn))**2, axis=1)) / \ - np.squeeze(np.sum(abs(getProdTF(vHD, Msig))**2, axis=1)) - return n + # The total variance is + # V(alpha) * cos(etaRMS)^2 + V(alpha + pi/2) * sin(etaRMS)^2 + Mnoise = {key: cos(sqz_params.etaRMS)**2 * homodyne(nn) + for key, nn in Mnoise.items()} + Mnoise['phase'] = sin(sqz_params.etaRMS)**2 * var_antisqz - # include quadrature noise (average of +- the RMS) - n = {key: (HDnoise(eta+etaRMS, Mn) + HDnoise(eta-etaRMS, Mn)) / 2 - for key, Mn in Mnoise.items()} + else: + Mnoise = {key: homodyne(nn) for key, nn in Mnoise.items()} - n = {key: nn * ifo.Infrastructure.Length**2 for key, nn in n.items()} + # calibrate into displacement + # coeff can be removed if shotradSignalRecycledBnC isn't used + Mnoise = {key: coeff*nn/plant for key, nn in Mnoise.items()} + psd = {key: nn * ifo.Infrastructure.Length**2 for key, nn in Mnoise.items()} - return n + return psd ###################################################################### @@ -625,6 +539,99 @@ def shotradSignalRecycledBnC(f, ifo, mirror_mass, power): return coeff, Mifo, Msig, Mnoise +def propagate_noise_fc_ifo(f, ifo, Mifo, Mnoise, sqz_params, alpha): + """Propagate quantum noise through the filter cavities and IFO + + f: frequency vector [Hz] + ifo: ifo Struct + Mifo: IFO input-output relation for the AS port + Mnoise: dictionary of existing noise sources + sqz_params: Struct of squeeze parameters + alpha: squeeze quadrature + + Returns a new Mnoise dict with the noises due to AS port vacuum, injection + loss, and any input or output filter cavities added. + """ + Mnoise = deepcopy(Mnoise) + + #>>>>>>>> QUANTUM NOISE POWER SPECTRAL DENSITY WITH SQZ [BnC PRD 2004, 62] + #<<<<<<<<<<<<<<<<< Modified to include losses (KM) + #<<<<<<<<<<<<<<<<< Modified to include frequency dependent squeezing angle (LB) + # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + # ------------------------------------------- equation 63 BnC PRD 2004 + # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + # Define input matrix of Squeezing + R = sqz_params.SQZ_DB / (20 * log10(exp(1))) # squeeze factor + R_anti = sqz_params.ANTISQZ_DB / (20 * log10(exp(1))) # anti-squeeze factor + Msqz = np.array([[exp(-R), 0], [0, exp(R_anti)]]) + + # expand to Nfreq + Msqz = np.transpose(np.tile(Msqz, (len(f), 1, 1)), axes=(1, 2, 0)) + + # add input rotation + MsqzRot = make2x2TF(cos(alpha), -sin(alpha), sin(alpha), cos(alpha)) + Msqz = getProdTF(MsqzRot, Msqz) + Msqz = dict(ASvac=Msqz) + + # Include losses (lambda_in=ifo.Squeezer.InjectionLoss) + Msqz = sqzInjectionLoss(Msqz, sqz_params.lambda_in, 'injection') + + # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + # Inject squeezed field into the IFO via some filter cavities + if sqz_params.sqzType == 'Freq Dependent' and 'FilterCavity' in ifo.Squeezer: + logger.debug(' Applying %d input filter cavities' % np.atleast_1d(ifo.Squeezer.FilterCavity).size) + Mr, Msqz = sqzFilterCavityChain( + f, np.atleast_1d(ifo.Squeezer.FilterCavity), Msqz) + + # apply the IFO dependent squeezing matrix to get the total noise + # due to quantum fluctuations coming in from the AS port + Msqz = {key: getProdTF(Mifo, nn) for key, nn in Msqz.items()} + + # add this to the other noises + Mnoise.update(Msqz) + + # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + # pass IFO output through some filter cavities + if 'OutputFilter' in ifo: + if ifo.OutputFilter.Type == 'None': + # do nothing, say nothing + pass + + elif ifo.OutputFilter.Type == 'Chain': + logger.debug(' Applying %d output filter cavities' % np.atleast_1d(ifo.OutputFilter.FilterCavity).size) + + Mr, Mnoise = sqzFilterCavityChain( + f, np.atleast_1d(ifo.OutputFilter.FilterCavity), Mnoise, key='OFC') + Msig = getProdTF(Mr, Msig) + # Mnoise = getProdTF(Mn, Mnoise); + + elif ifo.OutputFilter.Type == 'Optimal': + raise NotImplementedError("Cannot do optimal phase yet") + logger.debug(' Optimal output filtering!') + + # compute optimal angle, including upcoming PD losses + MnPD = sqzInjectionLoss(Mnoise, lambda_PD) + zeta = sqzOptimalReadoutPhase(Msig, MnPD) + + # rotate by that angle, less the homodyne angle + #zeta_opt = eta; + cs = cos(zeta - eta) + sn = sin(zeta - eta) + Mrot = make2x2TF(cs, -sn, sn, cs) + Mnoise = getProdTF(Mrot, Mnoise) + Msig = getProdTF(Mrot, Msig) + + else: + raise Exception('ifo.OutputFilter.Type must be None, Chain or Optimal, not "%s"' % ifo.OutputFilter.Type) + + # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + # add PD efficiency + lambda_PD = 1 - ifo.Optics.PhotoDetectorEfficiency # PD losses + Mnoise = sqzInjectionLoss(Mnoise, lambda_PD, 'pd') + + return Mnoise + + ###################################################################### # Auxiliary functions ######################################################################