Fitting Data and Making FOMs

Here I will go through an example of how to fit data to a transfer function. Later this TF will be used to make a full system model.

First imports:

%load_ext autoreload
%autoreload 2
import numpy as np
import matplotlib.pyplot as plt
import control
import scipy.signal as signal
from scipy import interpolate
import scipy.linalg
from scipy.stats import gmean
import gwinc
import yaml
from wield.iirrational.v2 import data2filter
from wield.control.AAA import tfAAA
from wield.utilities.file_io import load, save
from wield.control.ss_bare.ss import BareStateSpace
from scipy.signal import cheby2
from wield.control import SISO
from buzz import ssutil
from physunits import m, s, kg, Hz
from scipy.constants import c, G

Next we will define a function to grab the BNS waveform

# from https://github.com/wield/wield-control/blob/main/src/wield/control/AAA/test/test_AAA_present.py
def gen_waveform(**params):
    """Generate frequency-domain inspiral waveform

    Returns a tuple of (freq, h_plus^tilde, h_cross^tilde).

    The waveform is generated with the lalsimulation
    SimInspiralChooseFDWaveform() function.  Keyword arguments are
    used to update the default waveform parameters (see DEFAULT_PARAMS
    macro).  The mass parameters ('m1' and 'm2') should be specified
    in solar masses and the 'distance' parameter should be specified
    in parsecs**.  Waveform approximants may be given as string names
    (see `lalsimulation` documentation for more info).

    For example, to generate a 20/20 Msolar BBH waveform:

    >>> hp,hc = waveform.gen_waveform('m1'=20, 'm2'=20)

    **NOTE: The requirement that masses be specified in solar masses
    and distances in parsecs is different than that of the underlying
    lalsimulation method which expects mass and distance parameters to
    be in SI units.

    """
    import lalsimulation
    from inspiral_range import waveform
    from inspiral_range import const

    iparams = dict(waveform.DEFAULT_PARAMS)
    iparams.update(**params)

    # convert to SI units
    iparams["distance"] *= const.PC_SI
    iparams["m1"] *= const.MSUN_SI
    iparams["m2"] *= const.MSUN_SI
    iparams["approximant"] = lalsimulation.SimInspiralGetApproximantFromString(
        iparams["approximant"]
    )

    m = iparams["m1"] + iparams["m2"]

    # calculate delta F based on frequency of inner-most stable
    # circular orbit ("fisco")
    fisco = (const.c ** 3) / (const.G * (6 ** 1.5) * 2 * np.pi * m)
    df = 2 ** (np.max([np.floor(np.log(fisco / 4096) / np.log(2)), -6]))

    # FIXME: are these limits reasonable?
    if iparams["deltaF"] is None:
        iparams["deltaF"] = df
    # iparams['f_min'] = 0.1
    # iparams['f_max'] = 10000

    hp, hc = lalsimulation.SimInspiralChooseFDWaveform(**iparams)
    
    freq = hp.f0 + np.arange(len(hp.data.data)) * hp.deltaF

    print("f0", hp.f0)
    select = abs(hp.data.data) > 0

    return freq[select], hp.data.data[select], hc.data.data[select]

Next we will define the parameters of our BNS model and plot its PSD

params = dict()
params["m1"] = 1.4
params["m2"] = 1.4
# rest of params from first BNS detection see https://journals.aps.org/prl/pdf/10.1103/PhysRevLett.119.161101
params["f_min"] = 0.01
params["f_max"] = 1000000
params["deltaF"] = 0.01
params["distance"] = 1e6
F_Hz, hp, hc = gen_waveform(**params) # assuming that this is giving an ASD I.M. Jan 2024

BNS_intrp = interpolate.interp1d(F_Hz, abs(hp)**2) ## we add interpolation to put all of our data with the same FQ spacing

print("F_Hz max", max(F_Hz))

plt.loglog(F_Hz, abs(hp)**2, label = "hp")
plt.loglog(F_Hz, abs(hc)**2, linestyle = ":", label = "hc")
plt.title(str(params["m1"]) +"/"+str(params["m1"])+ "Solarmass BNS Spec")
plt.xlabel("Frequency [Hz]")
plt.ylabel("Power spectral density [strain^2/Hz^2]")
plt.legend()
plt.show()
f0 0.0
F_Hz max 14501.800000000001
../../_images/6a80f6cd6021ac2ffd3889b0ae4d8e5b7a0216349454bcc31e3bf6a63e357c64.png
# # compare the waveform with part of equation 3 from https://arxiv.org/abs/1709.08079
m1 = (params["m1"] * 1.988416e30) * kg
m2 = (params["m2"] * 1.988416e30) * kg
chirp_mass = (m1*m2)**(3/5)/(m1+m2)**(1/5)
print('chirpmass: \t', chirp_mass, ' kg\n\t\t', chirp_mass/1.988416e30, ' solar masses')


# N = kg* m/(s**2)
# c = c * m/s
# G = G * N * m**2 / (kg**2)
# F_Hz = F_Hz * Hz
# eq3 = ((5/24)**0.5 * c * ( G * chirp_mass * (c**-3))**(5/6)* 1/(np.pi**(2/3)))**2 * F_Hz**(-7/3)
# m2Mpc = 1/3.086e22
# eq3_Mpc = eq3 * m2Mpc

# print("eq3[0]", eq3[0])
# print("eq3_Mpc[0]", eq3_Mpc[0])

# print("BNS hp[0]", abs(hp[0])**2)

# plt.loglog(F_Hz, eq3_Mpc, label="eq3")
# plt.loglog(F_Hz, abs(hp)**2, label="BNS Strain from LAL Simulation")
# plt.title(str(params["m1"]) +"/"+str(params["m1"])+ "Solarmass BNS Spec")
# plt.xlabel("Frequency [Hz]")
# plt.ylabel("Power spectral density [strain^2/Hz^2]")
# plt.legend()
# plt.show()
chirpmass: 	 2.423423336413827e+30  kg
		 1.2187707886145691  solar masses

Getting the LIGO Sensitivity Curve

Now I must multiply this by the GWINC curve to get our final FOM

F_bug = np.logspace(np.log10(min(F_Hz)), np.log10(max(F_Hz)-1), 10000)
budget = gwinc.load_budget('aLIGO', freq=F_bug)
trace = budget.run(freq=F_bug)

plt.title("ALIGO BUDGET")
plt.loglog(F_bug, trace.psd)
plt.xlabel("Frequency [Hz]")
plt.ylabel("Power Spectral Density [strain/hz]")
# plt.xlim([6, max(F_Hz)])
# plt.ylim([1e-20, 1e-17])
Text(0, 0.5, 'Power Spectral Density [strain/hz]')
../../_images/7d14129023e95621c6745ab2d683ea1e3a6fa35bca53d45718d14054798097c3.png
prefactor = (2*(5/96)**0.5 * c * ( G * chirp_mass * (c**3))**(5/6) * 1/(np.pi**(-2/3))* 2 / 2.26)**2 
I7 = np.trapz(F_bug**(-7/3)/trace.psd, F_bug)
eq3 = prefactor * I7**0.5
print("eq3: ", eq3/3.086e22, ' Mpc')
eq3:  4.8671221455438923e+92  Mpc
/var/folders/mg/zt3ys6wx6dv2lp4fp2v08nn80000gn/T/ipykernel_49986/2399436929.py:2: DeprecationWarning: `trapz` is deprecated. Use `trapezoid` instead, or one of the numerical integration functions in `scipy.integrate`.
  I7 = np.trapz(F_bug**(-7/3)/trace.psd, F_bug)
from inspiral_range import inspiral_range as ir
from inspiral_range import waveform
from inspiral_range import const

DETECTION_SNR = 8.0
def ian_sensemon_range(freq, m1=1.4, m2=1.4, horizon=False, integrate=False, detection_snr=DETECTION_SNR):
    """Detector inspiral range from closed form expression

    Masses `m1` and `m2` should be specified in solar masses (default:
    m1=m2=1.4).  If the `horizon` keyword is specified the "horizon"
    range will be returned, which differs from the angle-averaged
    range by ~2.26.

    @returns distance in Mpc as a float

    """
    if horizon:
        theta = 4
    else:
        theta = 1.77
    theta /= 1e6 * const.PC_SI
    M_chirp = waveform.M_chirp(m1, m2) * const.MSUN_SI

    i73 = freq ** (-7/3)
    val = theta / detection_snr \
        * waveform.habs_nsp_prefactor(M_chirp) \
        * np.sqrt(i73) / 2

    return val
irdata = ian_sensemon_range(F_bug, integrate=False, horizon=True)**2

range_ir = ir.sensemon_range(F_bug, psd=trace.psd, m1=1.4, m2=1.4, horizon=False, integrate=True, detection_snr=DETECTION_SNR)
print("range: ", range_ir)

#np.savetxt('FBNS_Current_range.txt', [range])

BNS_PSD = BNS_intrp(F_bug)
IR_interp = interpolate.interp1d(F_bug, irdata)

midpoint = len(F_bug)//2
print('relative difference: ', BNS_PSD[midpoint]/irdata[midpoint])


plt.loglog(F_bug, BNS_PSD)
plt.loglog(F_bug, irdata)
plt.scatter(F_bug[midpoint], BNS_PSD[midpoint])
plt.scatter(F_bug[midpoint], irdata[midpoint])
plt.xlabel("Frequency [Hz]")
plt.ylabel("Power spectral density");
range:  194.97356835627136
relative difference:  15.834426262915697
../../_images/03c09e9f319d3f376145eaee9bfa5d5b1d449a5b00147237640e50b5e8202883.png

Choose which inspiral waveform PSD to use

#BNS_PSD = BNS_intrp(F_bug) # us the custom inspiral equatin from LAL
BNS_PSD = IR_interp(F_bug) # use the inspiral range aproximation and calibration 

Now I will put the BNS waveform in the Fq space of the GWINC curve

print("BNS_PSD", BNS_PSD)
print("irdata", irdata)
plt.loglog(F_bug, BNS_PSD)
plt.xlabel("Frequency [Hz]")
plt.ylabel("Power spectral density")
BNS_PSD [2.45847170e-35 2.45034599e-35 2.44224714e-35 ... 1.03982336e-49
 1.03638655e-49 1.03296110e-49]
irdata [2.45847170e-35 2.45034599e-35 2.44224714e-35 ... 1.03982336e-49
 1.03638655e-49 1.03296110e-49]
Text(0, 0.5, 'Power spectral density')
../../_images/32f98ed6eb185cc3f37cbaa1a6d84da1ec02a18e5790f08ed45754f231b85c88.png

Lastly for this step we will combine the twu curves to make a FOM. we divide the BNS PSD by the LIGO Noise spectrum without the controls noise. From the paper: $$\mathrm{SNR}^2 = 4\int_{0}^{\infty} \frac{\left |h_\mathrm{gw}(f)\right |^2}{S_h(f)} df$$

where $S_h(f)$ is the noise spectrum of the detector. This can be written as:

$$ S_h(f) = S_{\mathrm{det}}(f) + |C(f)|^2 S_{\mathrm{p}}(f)$$

where $S_{\mathrm{det}}(f)$ is the detector noise spectrum without controls noise, $S_{\mathrm{p}}(f)$ is the control noise spectrum, and $C(f)$ is the coupling function from the controls noise in the control loop to DARM. By using the taylor series of A/(B+x) to linearize the integral. i.e.:

$$\frac{A}{B+x}=\frac{A}{B} - \frac{Ax}{B^2} + \frac{Ax^2}{B^3} - \frac{Ax^3}{B^4} + \frac{Ax^4}{B^5} - \frac{Ax^5}{B^6} + O(x^6)$$

taking the first two terms of the taylor series and applying it to the SNR equation we get:

$$\mathrm{SNR}^2 = 4\int_{0}^{\infty} \frac{\left | h_{\mathrm{signal}}(f)\right |^2}{S_{\mathrm{det}}(f)} \ df - 4\int_{0}^{\infty} \frac{\left |C\right |^2\left | h_{\mathrm{signal}}(f)\right |^2 }{S_{\mathrm{det}}(f)^2} S_{\mathrm{p}}(f) \ df$$

To get range we need to use the $\mathrm{SNR}$ not the $\mathrm{SNR}^2$ so we take the square root of the above equation which has a different linearization. as $A \rightarrow \infty$

$$\sqrt{A+B} = \sqrt{A} \sqrt{1+\frac{B}{A}} = \sqrt{A} (1+\frac{B}{2 \sqrt{A}})$$

Div_PSD = BNS_PSD / (trace.psd)**2 # divide the BNS PSD by the aLIGO noise psd squared. this will give a PSD of LIGO's sensitivity to a BNS signal
#Div_PSD = Div_PSD * 4000**2 # to put it in units of displacement
F_Div = F_bug
plt.loglog(F_Div, Div_PSD)
plt.xlabel("Frequency [Hz]")
plt.ylabel("Power spectral density")
plt.title("PSD of FOM for BNS Signal")
Text(0.5, 1.0, 'PSD of FOM for BNS Signal')
../../_images/aad5ef04daf9c33e4adc3b0cc54eb351ed82f6528b8fbfafd54514495f94ea6e.png

Fitting the Data to a Transfer Function

First we will Define a function to reduce the number of points to fit to

# edited from https://git.ligo.org/wield/wield-ligo-mcculler/-/blob/main/src/wield/LIGO/mcculler/filter_cavity/test_FC1_SUSPOINT_fits.py
def reduce(F_Hz, PSD):
    F_groups = np.logspace(-2, 4, 50) # originally 200 length
    F_groups = np.geomspace(min(F_Hz), max(F_Hz), 50) # originally 200 length
    
    idx_groups = np.searchsorted(F_Hz, F_groups)
    idx_pairs = list(zip(idx_groups[:-1], idx_groups[1:]))

    lPSDs_min = []
    lPSDs_max = []
    lPSDs_med = []
    lPSDs_mean = []
    lF_Hz = []
    for idx1, idx2 in idx_pairs:
        if idx1 == idx2:
            continue
        lPSDs_max.append(
            np.nanmax(PSD[idx1:idx2])
        )
        lPSDs_med.append(
            np.nanmedian(PSD[idx1:idx2])
        )
        lPSDs_min.append(
            np.nanmin(PSD[idx1:idx2])
        )
        lPSDs_mean.append(
            np.nanmean(PSD[idx1:idx2])
        )
        lF_Hz.append(np.mean(F_Hz[idx1:idx2]))

    return np.asarray(lF_Hz), np.asarray(lPSDs_min), np.asarray(lPSDs_med), np.asarray(lPSDs_max), np.asarray(lPSDs_mean)

Now we will reduce the LIGO sensitivity curve and its reduced form

F_FOM_dwn, PSD_min, PSD_med, PSD_max, PSD_mean = reduce(F_bug, Div_PSD)
FOM_PSD = PSD_mean
# The current form of FOM_PSD is the ASD
FOM_ASD = FOM_PSD**0.5

plt.loglog(F_bug, Div_PSD**0.5, label = 'FOM ASD')
plt.loglog(F_FOM_dwn, FOM_ASD, label = 'Downsampled FOM ASD')
plt.legend()
<matplotlib.legend.Legend at 0x16ca8e8c0>
../../_images/cf982ef7b779a82aaa48039c6b9d8fa6750b5a6e294859dd7a3cd9ee914cb62d.png

Now we will define some functions to work with these transfer functions

def FBNSsimpSS(gain=1, return_name=False, lo_ord=4):
    if return_name:
        name = "FOM BNS Simple"
        return name
    F_Fq_lo = 10 * 2 * np.pi
    F_Fq_hi = 120 * 2 * np.pi
    hp_order = lo_ord
    lp_order = 2
    F_p = []
    F_z = []
    for ord in range(hp_order):
        F_p.append(-F_Fq_lo + 0*1j*F_Fq_lo)
        F_p.append(-F_Fq_lo - 0*1j*F_Fq_lo)
        F_z.append(0)
        F_z.append(0)

    for ord in range(lp_order):
        F_p.append(-F_Fq_hi)
    F_k = 1
    c_zpk = cheby2(8, 170, 1.25*2*np.pi, btype='high', analog=True, output='zpk')
    F_z.extend(c_zpk[0])
    F_p.extend(c_zpk[1])
    F_k *= c_zpk[2]

    ian_lfq = -0.7
    ian_lfq2 = 2
    ian_lfq3 = 0.5
    F_z.append(0)
    F_p.append(ian_lfq)
    F_z.append(0)
    F_p.append(ian_lfq)

    F_z.append(ian_lfq3)
    F_z.append(ian_lfq3)
    F_p.append(ian_lfq2)
    F_p.append(ian_lfq2)
    
    F_p = F_p
    F_z = F_z

    #F_mod = ws_tf2ss(F_z, F_p, F_k)
    #F_mod = ssutil.normalize_gain(F_mod) * gain 
    #F_iod = {"FBNS.in": 0, "FBNS.out": 0}
    F_mod = SISO.zpk(F_z, F_p, F_k, angular=True, fiducial_rtol=1e-5, fiducial_atol=1e-10)
    F_mod = F_mod * F_mod
    F_mod = F_mod.asSS
    F_mod = F_mod / abs(F_mod.asSS.Linf_norm()[0]) 


    F_mod = F_mod * gain
    F_mod.balance_and_truncate()

    #F = ssutil.wieldSS(F_mod, F_iod)
    
    return F_mod.mimo("FBNS.in", "FBNS.out")

plt.loglog(F_bug, Div_PSD**0.5, label = 'FOM ASD')
plt.loglog(F_FOM_dwn, FOM_ASD, label = 'Downsampled FOM ASD')
mag, phase, omega = control.bode(FBNSsimpSS(gain=4 * 100**2).mod, Hz=False, plot=False, omega=F_FOM_dwn*2*np.pi, label = "BNS Simple")
#plt.loglog(omega/(2*np.pi), mag, label="BNS Simple")
plt.legend()
/Users/Ian/miniconda3/envs/controls/lib/python3.10/site-packages/wield/control/SISO/zpk.py:164: NumericalWarning: StateSpace is large (>50 states), using reduced response fiducial auditing heuristics. TODO to make this smarter
  warnings.warn(f"StateSpace is large (>{self.N_MAX_FID} states), using reduced response fiducial auditing heuristics. TODO to make this smarter", util.NumericalWarning)
/Users/Ian/miniconda3/envs/controls/lib/python3.10/site-packages/slycot/exceptions.py:241: SlycotResultWarning: 
The selected order 44 is greater
than `nsmin`, the sum of the order of the
0-unstable part and the order of a minimal
realization of the 0-stable part of the given
system. The resulting `nr`  is set to `nsmin` = 33
  warn(globals()[warning](fmessage, iwarn, info))
<matplotlib.legend.Legend at 0x14f5f00a0>
../../_images/cf982ef7b779a82aaa48039c6b9d8fa6750b5a6e294859dd7a3cd9ee914cb62d.png
from scipy.signal import cheby2
from wield.control import SISO

def FBNSsimpSS(gain=1, return_name=False, lo_ord=4, return_scale=False):
    if return_name:
        name = "FOM BNS Simple"
        return name
    
    F_Fq_lo = 40 * 2 * np.pi
    # aggressive version
    # F_Fq_lo = 30 * 2 * np.pi
    F_Fq_hi = 300 * 2 * np.pi
    hp_order = 1 #lo_ord
    lp_order = 2
    F_p = []
    F_z = []
    for ord in range(hp_order):
        F_p.append(-F_Fq_lo + 0*1j*F_Fq_lo)
        F_p.append(-F_Fq_lo - 0*1j*F_Fq_lo)
        F_z.append(0)
        F_z.append(0)
    for ord in range(lp_order):
        F_p.append(-F_Fq_hi)
    F_k = gain * 1.5e19
    c_zpk = cheby2(8, 160, 4*2*np.pi, btype='high', analog=True, output='zpk')
    # aggressive version
    # c_zpk = cheby2(8, 120, 4*2*np.pi, btype='high', analog=True, output='zpk')
    F_z.extend(c_zpk[0])
    F_p.extend(c_zpk[1])
    F_k *= c_zpk[2]
    
    F = SISO.zpk(F_z, F_p, F_k)
    return F*F
sys_FBNS_simp = FBNSsimpSS()

zpk_dict = {
            'z' : [str(zero) for zero in np.asarray(tuple(sys_FBNS_simp.z)).tolist()],
            'p' : [str(pole) for pole in np.asarray(tuple(sys_FBNS_simp.p)).tolist()],
            'k' : float(sys_FBNS_simp.k)
            }

save('FBNSsimp_FOM.yml', zpk_dict)
with open('BNS_FOM_handfit.yml', 'r') as file:
    filt_dict = yaml.safe_load(file)
p_hand = np.array(filt_dict['p'], dtype=np.complex128)
z_hand = np.array(filt_dict['z'], dtype=np.complex128)
k_hand = np.float64(filt_dict['k'])

hand_zpk = SISO.zpk(z_hand, p_hand, k_hand)
hand_xfr = hand_zpk.fresponse(f=F_FOM_dwn)
BNSsimp_xfr = sys_FBNS_simp.fresponse(f=F_FOM_dwn)

plt.loglog(F_bug, Div_PSD**0.5, label = 'FOM ASD')
plt.loglog(F_FOM_dwn, FOM_ASD, label = 'Downsampled FOM ASD')
plt.loglog(F_FOM_dwn, hand_xfr.mag, label = 'Handfit ASD')
plt.loglog(F_FOM_dwn, BNSsimp_xfr.mag, label = 'BNSSimp ASD')
plt.legend()
<matplotlib.legend.Legend at 0x3262ada50>
../../_images/a62db8769727f1e2bb33914e438de4acb89c9ead42125dd019fe9e2eb16ce6ce.png
# Divide the FOM ASD by the hand fit to get the reduced ASD
reduced_ASD = FOM_ASD/hand_xfr.mag

plt.loglog(F_FOM_dwn, reduced_ASD, label = 'reduced ASD')
plt.legend()
<matplotlib.legend.Legend at 0x3265c1300>
../../_images/5fe5220dbbba4fcff754647feaca998318ceeb41748c30cdb4536632ecef5d6e.png

Now we form a guess of the TF

#results = tfAAA(F_Hz=F_FOM_dwn, xfer=FOM_ASD)
results = tfAAA(F_Hz=F_FOM_dwn, xfer=reduced_ASD)

Now we play with the poles and zeros to get a good fit

#print("poles", results.poles)
#print("zeros", results.zeros)
#print("gain", results.gain)

all_z = results.zeros
all_p = results.poles
k = results.gain

for i in range(0, len(all_z)):
    if all_z[i] >= 0:
        all_z[i] = -all_z[i]
        
for i in range(0, len(all_p)):
    if all_p[i] >= 0:
        all_p[i] = -all_p[i]

AAA_zpk = SISO.zpk(all_z, all_p, k, angular=False, fiducial_rtol=1e-5, fiducial_atol=1e-10)
recalibrated_zpk =  hand_zpk * AAA_zpk 
# recalibrated_zpk =  AAA_zpk 
print(recalibrated_zpk.zeros)
2π·[-0.3958406743523140, -0.3958406743523140, -0.3958406743523140,
 -0.3958406743523140, -0.3958406743523140, -0.3958406743523140,
 -0.3958406743523140, -0.3958406743523140, -0.3958406743523140,
 -0.3958406743523140, -0.3958406743523140, -0.3958406743523140,
 -0.3958406743523140, -0.3958406743523140, -0.3958406743523140,
 -0.3958406743523140, -0.3958406743523140, -0.3958406743523140,
 -0.01203291268450893, -0.01203291245508902, -2.972731813094795e-10,
 -0.04867452063486528, -0.04867452070907659, -1.998523750481311,
 -1.998523750486431, -2665.214389252330, -2665.214389239281,
 -92.39465985988777     ± 57.24588444085094j,
 -92.39465986028216     ± 57.24588444036313j,
 -3.274971594925781e-14 ± 9.399266058512616j,
 -2.736440429942560e-13 ± 1.357591960953234j,
 -0.01049742774282587   ± 0.2673257560166767j,
 -0.01049742774343546   ± 0.2673257560021555j,
 -1.092192520377326e-12 ± 0.3543630287392169j,
 -0.2711283246749082    ± 2.729288190523091j,
 -0.2711283246761620    ± 2.729288190522066j,
 -13.03329719065969     ± 14.32232176510654j,
 -13.03329719064406     ± 14.32232176511557j]
plt.loglog(F_FOM_dwn, reduced_ASD, label = 'Reduced ASD')
plt.loglog(F_bug, AAA_zpk.fresponse(f=F_bug).mag, label = 'AAA Fit', linestyle = '--')
plt.legend()
<matplotlib.legend.Legend at 0x3307ee1d0>
../../_images/824356ed65326ae499a5f0c25611cfa0a9b03e6c292821f30dd13edccef4d2ee.png
# Get the ZPK and the minreal ZPK for all poles and zeros
B_res_all = control.zpk(all_z, all_p, k)
B_res_all_minreal = control.minreal(B_res_all)
minreal_all_z, minreal_all_p, minreal_all_k = signal.tf2zpk(B_res_all_minreal.num[0][0], B_res_all_minreal.den[0][0])

# Get only the real poles and zeros
z = [lz for lz in all_z if lz.real < 0]
p = [lp for lp in all_p if lp.real < 0]

# Get the ZPK for the real poles and zeros and the minreal ZPK for only the real poles and zeros
B_res = control.zpk(z, p, k)
B_res_minreal = control.minreal(B_res)
minreal_z, minreal_p, minreal_k = signal.tf2zpk(B_res_minreal.num[0][0], B_res_minreal.den[0][0])
0 states have been removed from the model
0 states have been removed from the model
/Users/Ian/miniconda3/envs/controls/lib/python3.10/site-packages/control/xferfcn.py:1047: ComplexWarning: Casting complex values to real discards the imaginary part
  den[j, :maxindex+1] = poly(poles[j])
/Users/Ian/miniconda3/envs/controls/lib/python3.10/site-packages/control/xferfcn.py:1077: ComplexWarning: Casting complex values to real discards the imaginary part
  num[i, j, maxindex+1-len(numpoly):maxindex+1] = numpoly

Now we will plot our guess and our othe tfs

_, TF_all = scipy.signal.freqs_zpk(all_z, all_p, k, worN = F_Hz)
_, TF_minreal = scipy.signal.freqs_zpk(minreal_all_z, minreal_all_p, minreal_all_k, worN = F_Hz)

recalibrated_zpk_xfr = recalibrated_zpk.fresponse(f=F_Hz)

plt.loglog(F_bug, Div_PSD**0.5, label = 'Full FOM ASD')
plt.loglog(F_FOM_dwn, hand_xfr.mag, label = 'Handfit ASD')
plt.loglog(F_Hz, recalibrated_zpk_xfr.mag, label = 'Recalibrated AAA')
plt.loglog(F_FOM_dwn, FOM_ASD, label = 'Downsampled FOM ASD')
#plt.loglog(F_Hz, abs(TF_all), label = 'ALL ZPK Fit Bary. (order {})'.format(results.order))
#plt.loglog(F_Hz, abs(TF_minreal), label = 'Minreal ALL ZPK Fit Bary.', linestyle = '--')
plt.legend()
<matplotlib.legend.Legend at 0x330b8f790>
../../_images/9bfee52b6b6025afa2c4ff514597c085cab87ef4dfc43e2d11e03377766e229c.png

Now we fit for the poles and zeros

snr = np.ones_like(F_FOM_dwn)
snr[:-1] = (F_FOM_dwn[1:] - F_FOM_dwn[:-1])**-0.001
from wield.iirrational import fitters_ZPK

all_z = (np.array(recalibrated_zpk.zeros))
all_p = (np.array(recalibrated_zpk.poles))
k = float(recalibrated_zpk.k)

z_iir = tuple(np.array(all_z) / (np.pi * 2))
p_iir = tuple(np.array(all_p) / (np.pi * 2))
print(z_iir)
print(p_iir)

iir_results = data2filter(
    F_Hz=F_FOM_dwn,
    xfer=FOM_ASD,
    #xfer=reduced_ASD,
    mode='reduce',
    zeros=z_iir, 
    poles=p_iir,
    gain=k * (2*np.pi)**(len(z_iir) - len(p_iir)),
    SNR_phase_rel=0,
    SNR=snr,
    #relative_degree=-4,
    # resavg_RthreshOrdDn=1.01,
    baseline_only=True,
    #coding_map=fitters_ZPK.codings_s.coding_maps.RI
    # trust_SNR = True,
)
print("C", iir_results.fit_aid._fitters[0].fitter.poles.fullplane)
print(tuple(np.array(all_p) / (np.pi * 2)))
(np.float64(-0.395840674352314), np.float64(-0.395840674352314), np.float64(-0.395840674352314), np.float64(-0.395840674352314), np.float64(-0.395840674352314), np.float64(-0.395840674352314), np.float64(-0.395840674352314), np.float64(-0.395840674352314), np.float64(-0.395840674352314), np.float64(-0.395840674352314), np.float64(-0.395840674352314), np.float64(-0.395840674352314), np.float64(-0.395840674352314), np.float64(-0.395840674352314), np.float64(-0.395840674352314), np.float64(-0.395840674352314), np.float64(-0.395840674352314), np.float64(-0.395840674352314), np.float64(-0.012032912684508928), np.float64(-0.01203291245508902), np.float64(-2.972731813094795e-10), np.float64(-0.048674520634865284), np.float64(-0.048674520709076595), np.float64(-1.9985237504813107), np.float64(-1.9985237504864306), np.float64(-2665.21438925233), np.float64(-2665.2143892392814), np.complex128(-92.39465985988777+57.245884440850936j), np.complex128(-92.39465985988777-57.245884440850936j), np.complex128(-92.39465986028216+57.24588444036313j), np.complex128(-92.39465986028216-57.24588444036313j), np.complex128(-3.274971594925781e-14+9.399266058512616j), np.complex128(-3.274971594925781e-14-9.399266058512616j), np.complex128(-2.73644042994256e-13+1.357591960953234j), np.complex128(-2.73644042994256e-13-1.357591960953234j), np.complex128(-0.010497427742825865+0.26732575601667674j), np.complex128(-0.010497427742825865-0.26732575601667674j), np.complex128(-0.010497427743435463+0.26732575600215547j), np.complex128(-0.010497427743435463-0.26732575600215547j), np.complex128(-1.0921925203773264e-12+0.35436302873921693j), np.complex128(-1.0921925203773264e-12-0.35436302873921693j), np.complex128(-0.2711283246749082+2.7292881905230915j), np.complex128(-0.2711283246749082-2.7292881905230915j), np.complex128(-0.271128324676162+2.729288190522066j), np.complex128(-0.271128324676162-2.729288190522066j), np.complex128(-13.033297190659693+14.322321765106544j), np.complex128(-13.033297190659693-14.322321765106544j), np.complex128(-13.033297190644062+14.322321765115568j), np.complex128(-13.033297190644062-14.322321765115568j))
(np.float64(-200.0), np.float64(-200.0), np.float64(-1999.9999999999998), np.float64(-0.02007827991778461), np.float64(-0.020078279659776748), np.float64(-2.580708621122238e-10), np.float64(-15.278039002751779), np.float64(-15.27803900294391), np.float64(-37.5875499182891), np.float64(-37.58754992011324), np.float64(-1270.3160778915844), np.float64(-1270.3160778760625), np.complex128(-8.482300164692441+0.8482300164692441j), np.complex128(-8.482300164692441-0.8482300164692441j), np.complex128(-8.482300164692441+0.8482300164692441j), np.complex128(-8.482300164692441-0.8482300164692441j), np.complex128(-8.482300164692441+0.8482300164692441j), np.complex128(-8.482300164692441-0.8482300164692441j), np.complex128(-8.482300164692441+0.8482300164692441j), np.complex128(-8.482300164692441-0.8482300164692441j), np.complex128(-8.482300164692441+0.8482300164692441j), np.complex128(-8.482300164692441-0.8482300164692441j), np.complex128(-8.482300164692441+0.8482300164692441j), np.complex128(-8.482300164692441-0.8482300164692441j), np.complex128(-8.482300164692441+0.8482300164692441j), np.complex128(-8.482300164692441-0.8482300164692441j), np.complex128(-8.482300164692441+0.8482300164692441j), np.complex128(-8.482300164692441-0.8482300164692441j), np.complex128(-8.482300164692441+0.8482300164692441j), np.complex128(-8.482300164692441-0.8482300164692441j), np.complex128(-86.35143298229573+19.216867768387107j), np.complex128(-86.35143298229573-19.216867768387107j), np.complex128(-86.35143298333351+19.216867758926814j), np.complex128(-86.35143298333351-19.216867758926814j), np.complex128(-7.184546295609813e-13+9.719862477185815j), np.complex128(-7.184546295609813e-13-9.719862477185815j), np.complex128(-1.835561875225273e-13+1.2313225699476558j), np.complex128(-1.835561875225273e-13-1.2313225699476558j), np.complex128(-1.0967543329481134e-12+0.2850867195563693j), np.complex128(-1.0967543329481134e-12-0.2850867195563693j), np.complex128(-0.03919349729548207+0.08045695915767265j), np.complex128(-0.03919349729548207-0.08045695915767265j), np.complex128(-0.03919349729706598+0.0804569591557125j), np.complex128(-0.03919349729706598-0.0804569591557125j), np.complex128(-1.0663904270747737e-12+0.5688213957405965j), np.complex128(-1.0663904270747737e-12-0.5688213957405965j), np.complex128(-1.078838929878826e-12+0.6261479514724254j), np.complex128(-1.078838929878826e-12-0.6261479514724254j), np.complex128(-1.0344028607168105+6.179659433092568j), np.complex128(-1.0344028607168105-6.179659433092568j), np.complex128(-1.0344028607159552+6.179659433094185j), np.complex128(-1.0344028607159552-6.179659433094185j))
TEE_LOGFILE None
A [-2.00000000e+02 +0.j         -2.00000000e+02 +0.j
 -2.00000000e+03 +0.j         -2.00782799e-02 +0.j
 -2.00782797e-02 +0.j         -2.58070862e-10 +0.j
 -1.52780390e+01 +0.j         -1.52780390e+01 +0.j
 -3.75875499e+01 +0.j         -3.75875499e+01 +0.j
 -1.27031608e+03 +0.j         -1.27031608e+03 +0.j
 -8.48230016e+00 +0.84823002j -8.48230016e+00 -0.84823002j
 -8.63514330e+01+19.21686777j -8.63514330e+01-19.21686777j
 -8.63514330e+01+19.21686776j -8.63514330e+01-19.21686776j
 -7.18454630e-13 +9.71986248j -7.18454630e-13 -9.71986248j
 -1.83556188e-13 +1.23132257j -1.83556188e-13 -1.23132257j
 -1.09675433e-12 +0.28508672j -1.09675433e-12 -0.28508672j
 -3.91934973e-02 +0.08045696j -3.91934973e-02 -0.08045696j
 -3.91934973e-02 +0.08045696j -3.91934973e-02 -0.08045696j
 -1.06639043e-12 +0.5688214j  -1.06639043e-12 -0.5688214j
 -1.07883893e-12 +0.62614795j -1.07883893e-12 -0.62614795j
 -1.03440286e+00 +6.17965943j -1.03440286e+00 -6.17965943j
 -1.03440286e+00 +6.17965943j -1.03440286e+00 -6.17965943j
 -8.48230016e+00 +0.84823002j -8.48230016e+00 -0.84823002j
 -8.48230016e+00 +0.84823002j -8.48230016e+00 -0.84823002j
 -8.48230016e+00 +0.84823002j -8.48230016e+00 -0.84823002j
 -8.48230016e+00 +0.84823002j -8.48230016e+00 -0.84823002j
 -8.48230016e+00 +0.84823002j -8.48230016e+00 -0.84823002j
 -8.48230016e+00 +0.84823002j -8.48230016e+00 -0.84823002j
 -8.48230016e+00 +0.84823002j -8.48230016e+00 -0.84823002j
 -8.48230016e+00 +0.84823002j -8.48230016e+00 -0.84823002j]
B [-1.52780392e+01+0.00000000e+00j -1.52780388e+01+0.00000000e+00j
 -2.00782797e-02+0.00000000e+00j -3.90376294e-03+0.00000000e+00j
 -2.00000000e+03+0.00000000e+00j -2.00782799e-02+0.00000000e+00j
 -1.27031608e+03+1.52587891e-05j -1.27031608e+03-1.52587891e-05j
 -3.75875499e+01+4.76837158e-07j -3.75875499e+01-4.76837158e-07j
 -2.00000000e+02+3.81469727e-06j -2.00000000e+02-3.81469727e-06j
 -8.48230016e+00+8.48230016e-01j -8.48230016e+00-8.48230016e-01j
 -8.48230016e+00+8.48230016e-01j -8.48230016e+00-8.48230016e-01j
 -8.48230016e+00+8.48230016e-01j -8.48230016e+00-8.48230016e-01j
 -8.48230016e+00+8.48230016e-01j -8.48230016e+00-8.48230016e-01j
 -8.48230016e+00+8.48230016e-01j -8.48230016e+00-8.48230016e-01j
 -8.48230016e+00+8.48230016e-01j -8.48230016e+00-8.48230016e-01j
 -8.48230016e+00+8.48230016e-01j -8.48230016e+00-8.48230016e-01j
 -8.48230016e+00+8.48230016e-01j -8.48230016e+00-8.48230016e-01j
 -1.03440286e+00+6.17965943e+00j -1.03440286e+00-6.17965943e+00j
 -1.03440286e+00+6.17965943e+00j -1.03440286e+00-6.17965943e+00j
 -4.68916991e-04+6.26147951e-01j -4.68916991e-04-6.26147951e-01j
 -4.25985610e-04+5.68821396e-01j -4.25985610e-04-5.68821396e-01j
 -3.91934973e-02+8.04569592e-02j -3.91934973e-02-8.04569592e-02j
 -3.91934973e-02+8.04569592e-02j -3.91934973e-02-8.04569592e-02j
 -8.88613204e-03+2.85086720e-01j -8.88613204e-03-2.85086720e-01j
 -8.88881768e-02+1.23132257e+00j -8.88881768e-02-1.23132257e+00j
 -2.85282769e+00+9.71986248e+00j -2.85282769e+00-9.71986248e+00j
 -8.63514330e+01+1.92168678e+01j -8.63514330e+01-1.92168678e+01j
 -8.63514330e+01+1.92168678e+01j -8.63514330e+01-1.92168678e+01j
 -8.48230016e+00+8.48230016e-01j -8.48230016e+00-8.48230016e-01j]
/Users/Ian/miniconda3/envs/controls/lib/python3.10/site-packages/wield/iirrational/fitters_ZPK/codings_s/cplx_sos_NL.py:176: RuntimeWarning: divide by zero encountered in scalar divide
  V_D_c1 = -pD_c1 * c1 / disc * V_D
/Users/Ian/miniconda3/envs/controls/lib/python3.10/site-packages/wield/iirrational/fitters_ZPK/codings_s/cplx_sos_NL.py:176: RuntimeWarning: invalid value encountered in scalar multiply
  V_D_c1 = -pD_c1 * c1 / disc * V_D
/Users/Ian/miniconda3/envs/controls/lib/python3.10/site-packages/wield/iirrational/fitters_ZPK/codings_s/cplx_sos_NL.py:177: RuntimeWarning: divide by zero encountered in scalar divide
  V_D_c2 = -pD_c2 / (2 * disc) * V_D
/Users/Ian/miniconda3/envs/controls/lib/python3.10/site-packages/wield/iirrational/fitters_ZPK/codings_s/cplx_sos_NL.py:177: RuntimeWarning: invalid value encountered in scalar multiply
  V_D_c2 = -pD_c2 / (2 * disc) * V_D
3W   0.23  Fitter_checkpoint improvement succeed, None
------------:Q-ranked order reduction:
4P   0.32    order reduced annealing
5P   0.56  zero flipping, maxzp 44, residuals=-1.49e-01, -4.21e-01, reldeg=-3
2A  15.32  Baseline fit residuals: -4.21e-01, at order 44
BASELINE:  44
------------:investigations:
2I  15.32    max(z, p)       ChiSq.
                   order    avg. res.    med. res.    max. res.
             -----------  -----------  -----------  -----------
                      52    -0.148973    0.0208649      5.13889
C [-1.52780392e+01+0.00000000e+00j -1.52780388e+01+0.00000000e+00j
 -2.00782797e-02+0.00000000e+00j -3.90376294e-03+0.00000000e+00j
 -2.00000000e+03+0.00000000e+00j -2.00782799e-02+0.00000000e+00j
 -1.27031608e+03+1.52587891e-05j -1.27031608e+03-1.52587891e-05j
 -3.75875499e+01+4.76837158e-07j -3.75875499e+01-4.76837158e-07j
 -2.00000000e+02+3.81469727e-06j -2.00000000e+02-3.81469727e-06j
 -8.48230016e+00+8.48230016e-01j -8.48230016e+00-8.48230016e-01j
 -8.48230016e+00+8.48230016e-01j -8.48230016e+00-8.48230016e-01j
 -8.48230016e+00+8.48230016e-01j -8.48230016e+00-8.48230016e-01j
 -8.48230016e+00+8.48230016e-01j -8.48230016e+00-8.48230016e-01j
 -8.48230016e+00+8.48230016e-01j -8.48230016e+00-8.48230016e-01j
 -8.48230016e+00+8.48230016e-01j -8.48230016e+00-8.48230016e-01j
 -8.48230016e+00+8.48230016e-01j -8.48230016e+00-8.48230016e-01j
 -8.48230016e+00+8.48230016e-01j -8.48230016e+00-8.48230016e-01j
 -1.03440286e+00+6.17965943e+00j -1.03440286e+00-6.17965943e+00j
 -1.03440286e+00+6.17965943e+00j -1.03440286e+00-6.17965943e+00j
 -4.68916991e-04+6.26147951e-01j -4.68916991e-04-6.26147951e-01j
 -4.25985610e-04+5.68821396e-01j -4.25985610e-04-5.68821396e-01j
 -3.91934973e-02+8.04569592e-02j -3.91934973e-02-8.04569592e-02j
 -3.91934973e-02+8.04569592e-02j -3.91934973e-02-8.04569592e-02j
 -8.88613204e-03+2.85086720e-01j -8.88613204e-03-2.85086720e-01j
 -8.88881768e-02+1.23132257e+00j -8.88881768e-02-1.23132257e+00j
 -2.85282769e+00+9.71986248e+00j -2.85282769e+00-9.71986248e+00j
 -8.63514330e+01+1.92168678e+01j -8.63514330e+01-1.92168678e+01j
 -8.63514330e+01+1.92168678e+01j -8.63514330e+01-1.92168678e+01j
 -8.48230016e+00+8.48230016e-01j -8.48230016e+00-8.48230016e-01j]
(np.float64(-200.0), np.float64(-200.0), np.float64(-1999.9999999999998), np.float64(-0.02007827991778461), np.float64(-0.020078279659776748), np.float64(-2.580708621122238e-10), np.float64(-15.278039002751779), np.float64(-15.27803900294391), np.float64(-37.5875499182891), np.float64(-37.58754992011324), np.float64(-1270.3160778915844), np.float64(-1270.3160778760625), np.complex128(-8.482300164692441+0.8482300164692441j), np.complex128(-8.482300164692441-0.8482300164692441j), np.complex128(-8.482300164692441+0.8482300164692441j), np.complex128(-8.482300164692441-0.8482300164692441j), np.complex128(-8.482300164692441+0.8482300164692441j), np.complex128(-8.482300164692441-0.8482300164692441j), np.complex128(-8.482300164692441+0.8482300164692441j), np.complex128(-8.482300164692441-0.8482300164692441j), np.complex128(-8.482300164692441+0.8482300164692441j), np.complex128(-8.482300164692441-0.8482300164692441j), np.complex128(-8.482300164692441+0.8482300164692441j), np.complex128(-8.482300164692441-0.8482300164692441j), np.complex128(-8.482300164692441+0.8482300164692441j), np.complex128(-8.482300164692441-0.8482300164692441j), np.complex128(-8.482300164692441+0.8482300164692441j), np.complex128(-8.482300164692441-0.8482300164692441j), np.complex128(-8.482300164692441+0.8482300164692441j), np.complex128(-8.482300164692441-0.8482300164692441j), np.complex128(-86.35143298229573+19.216867768387107j), np.complex128(-86.35143298229573-19.216867768387107j), np.complex128(-86.35143298333351+19.216867758926814j), np.complex128(-86.35143298333351-19.216867758926814j), np.complex128(-7.184546295609813e-13+9.719862477185815j), np.complex128(-7.184546295609813e-13-9.719862477185815j), np.complex128(-1.835561875225273e-13+1.2313225699476558j), np.complex128(-1.835561875225273e-13-1.2313225699476558j), np.complex128(-1.0967543329481134e-12+0.2850867195563693j), np.complex128(-1.0967543329481134e-12-0.2850867195563693j), np.complex128(-0.03919349729548207+0.08045695915767265j), np.complex128(-0.03919349729548207-0.08045695915767265j), np.complex128(-0.03919349729706598+0.0804569591557125j), np.complex128(-0.03919349729706598-0.0804569591557125j), np.complex128(-1.0663904270747737e-12+0.5688213957405965j), np.complex128(-1.0663904270747737e-12-0.5688213957405965j), np.complex128(-1.078838929878826e-12+0.6261479514724254j), np.complex128(-1.078838929878826e-12-0.6261479514724254j), np.complex128(-1.0344028607168105+6.179659433092568j), np.complex128(-1.0344028607168105-6.179659433092568j), np.complex128(-1.0344028607159552+6.179659433094185j), np.complex128(-1.0344028607159552-6.179659433094185j))
iir_results.fit_aid._fitters
[Bunch(
     checkpoint_idx = -1,
     fitter = <wield.iirrational.fitters_ZPK.MRF.MultiReprFilterS at 0x337c8e710>,
     log_idx = 0,
     valid = True,
     ),
 Bunch(
     checkpoint_idx = -1,
     fitter = <wield.iirrational.fitters_ZPK.MRF.MultiReprFilterS at 0x337cd2e00>,
     log_idx = 1,
     valid = True,
     ),
 Bunch(
     checkpoint_idx = -1,
     fitter = <wield.iirrational.fitters_ZPK.MRF.MultiReprFilterS at 0x337cd2d10>,
     log_idx = 12,
     valid = True,
     ),
 Bunch(
     checkpoint_idx = -1,
     fitter = <wield.iirrational.fitters_ZPK.MRF.MultiReprFilterS at 0x337cd1cc0>,
     log_idx = 14,
     valid = True,
     )]

And plot the result

#_, TF_all = scipy.signal.freqs_zpk(all_z, all_p, k, worN = F_Hz)
#_, TF_minreal = scipy.signal.freqs_zpk(minreal_all_z, minreal_all_p, minreal_all_k, worN = F_Hz)


plt.loglog(F_bug, Div_PSD**0.5, label = "Full FOM ASD")
plt.loglog(F_FOM_dwn, FOM_ASD, label = "Downsampled FOM ASD")
plt.loglog(F_FOM_dwn, hand_xfr.mag, label = 'Handfit ASD')
plt.loglog(F_Hz, recalibrated_zpk_xfr.mag, label = 'Recalibrated AAA')
plt.loglog(F_FOM_dwn, reduced_ASD, label = "reduced ASD")
#plt.loglog(F_Hz, abs(TF_all), label = 'ALL ZPK Fit Bary. (order {})'.format(results.order))
#plt.loglog(F_Hz, abs(TF_minreal), label = 'Minreal ALL ZPK Fit Bary.', linestyle = '--')
plt.loglog(F_Hz, np.abs(iir_results.fitter.xfer_eval(F_Hz)), label = "data2filter Fit FOM", ls='--', linewidth=4)
plt.grid()
plt.legend()
<matplotlib.legend.Legend at 0x337d060b0>
../../_images/2b1c8fcff263828c57a3ba1d0e8fa0b00c279c5f425356e3dd274fa74d1c5e52.png
iir_results.fitter.xfer_eval(F_Hz)
array([-8.40033252e-01-1.89162563e+00j,  8.80201515e-01-3.31859995e+00j,
        3.63020982e+00-3.82718193e+00j, ...,
       -6.28908229e+17-5.70991599e+19j, -6.28906692e+17-5.70990405e+19j,
       -6.28905155e+17-5.70989212e+19j], shape=(1450180,))
# Python code to merge dict using a single 
# expression
def Merge(dict1, dict2):
	res = {**dict1, **dict2}
	return res
	
# Driver code
dict1 = {'a': 10, 'b': 8}
dict2 = {'d': 6, 'c': 4}
dict3 = Merge(dict1, dict2)
print(dict3)
print(dict1)
print(dict2)
{'a': 10, 'b': 8, 'd': 6, 'c': 4}
{'a': 10, 'b': 8}
{'d': 6, 'c': 4}
# Hand Fit the DARM**2 ZPK
shift_sq = 0.9
z_hand_sq = np.array([-0.07]*18) * 2 * np.pi * shift_sq
mp_sq = -0.75 -0.075j
mul_sq = 9
p_hand_sq = np.array([mp_sq]*mul_sq + [np.real(mp_sq)-np.imag(mp_sq)*1j]*mul_sq)*2 * 2 * np.pi * shift_sq
p_hand_sq = np.append(p_hand_sq, [-250]*2 +[-600]*1 + [-5000]*0)
# k_hand_sq = 9e39
k_hand_sq = 3e32

##making steeper roll off
mp_sq = mp_sq * 2.25
p_hand_sq = np.append(p_hand_sq, np.array([mp_sq]*8 +[np.real(mp_sq)-np.imag(mp_sq)*1j]*8+ [-1]*8)* 2 * np.pi * shift_sq)
z_hand_sq = np.append(z_hand_sq, np.array([-1]*24)* 2 * np.pi * shift_sq)

#k_hand_sq = k_hand_sq *1e9

#plt.loglog(F_bug, Div_PSD**0.5, label = "Full FOM ASD")
plt.loglog(F_FOM_dwn, FOM_ASD, label = "Downsampled FOM ASD")
#plt.loglog(F_Hz, abs(TF_all), label = 'ALL ZPK Fit Bary. (order {})'.format(results.order))
plt.loglog(F_Hz, np.abs(iir_results.fitter.xfer_eval(F_Hz)), label = "data2filter Fit FOM")
fq_plot = np.logspace(-2, 5, 10000)


hand_sys = SISO.zpk(z_hand_sq, p_hand_sq, k_hand_sq, convention='IIRrational')
#fr_hand = hand_sys.fresponse(f=fq_plot)
fr_hand = hand_sys.fresponse(f=F_bug)

plt.loglog(-np.real(p_hand_sq), np.array([1e27]*len(p_hand_sq)), color='black', linestyle='', marker='o')
plt.loglog(-np.real(z_hand_sq), np.array([1e27]*len(z_hand_sq)), color='r', linestyle='', marker='x')
plt.loglog(*fr_hand.fplot_mag, label = 'Hand fit to DARM**2 ZPK')
plt.xlabel("Frequency [Hz]")
plt.ylabel("ASD")

plt.grid()
plt.legend()

#plt.savefig('FOM_BNS_fit.pdf', bbox_inches='tight')
<matplotlib.legend.Legend at 0x3308a9360>
../../_images/d3ac68be55ad9e2981736cf006bcacf6e506aba7b4b53164ed83326232a4b98d.png

Next we calculate what this spectrum will look like from this FOM

LLO_O4_DARM = np.loadtxt('O4_data/L1_darm_measured.txt').T
LLO_O4_DARM_known = np.loadtxt('O4_data/L1_darm_known_measured.txt').T
LLO_O4_SUS_damping = np.loadtxt('O4_data/L1_sus_damping.txt').T

L_arm = 3995

det_PSD_from_FOM = (BNS_PSD/fr_hand.mag**2)**0.5
plt.loglog(F_bug, trace.psd**0.5*L_arm , label = 'Detector Spectrum from GWINC', color='green')
# plt.loglog(LLO_O4_DARM[0], LLO_O4_DARM[1]*L_arm , label = 'Measured DARM LLO O4', color='red')
# plt.loglog(LLO_O4_SUS_damping[0], LLO_O4_SUS_damping[1]*L_arm , label = 'SUS Damping', color='orange')
plt.loglog(LLO_O4_DARM_known[0], LLO_O4_DARM_known[1]*L_arm , label = 'Measured Known noises LLO O4', color='black')
plt.loglog(F_bug, det_PSD_from_FOM**0.5*L_arm , label = 'Detector Spectrum from FOM', color='dodgerblue')
plt.xlabel("Frequency [Hz]")
plt.ylabel("DARM [m/rtHz]")

# plt.xlim([6, 10000])
# plt.ylim([1e-20, 1e-15])


plt.grid()
plt.legend()
<matplotlib.legend.Legend at 0x337fc1690>
../../_images/6e8101cc6318859e5fc50462a147f2e7c648d97f45c59f38bbefe0a0e70fc3f1.png

Now we will save our result so it can be loaded by the make full system example file

out_p = iir_results.fitter.poles.fullplane
out_z = iir_results.fitter.zeros.fullplane
out_k = iir_results.fitter.gain

# out_z = z_hand_sq
# out_p = p_hand_sq
# out_k = k_hand_sq

pre_save_sys = SISO.zpk(out_z, out_p, out_k, convention='IIRrational')
from wield.utilities.mpl import mplfigB
plotting_omega = np.logspace(-2, 4, 1000)
plt.close()
axB = mplfigB(Nrows = 2)
fr = pre_save_sys.fresponse(f=plotting_omega)
axB.ax0.loglog(*fr.fplot_mag, label='test')
axB.ax1.semilogx(*fr.fplot_deg225, label='test')


sys_save = SISO.zpk(out_z, out_p, out_k, convention='IIRrational')
zpk_dict = {
            'z' : [str(zero) for zero in np.asarray(tuple(sys_save.z)).tolist()],
            'p' : [str(pole) for pole in np.asarray(tuple(sys_save.p)).tolist()],
            'k' : float(sys_save.k)
            }

save('BNS_FOM.yml', zpk_dict)
#save('BNS_FOM_from_DARM.yml', zpk_dict)
../../_images/80a16b6fcc808daa7ead8003c213e73706ac8089bf680774e5a28717194f0a9d.png

Now it is saved. To open it use the following code

with open('BNS_FOM.yml', 'r') as file:
    filt_dict = yaml.safe_load(file)
p = np.array(filt_dict['p'], dtype=np.complex128)
z = np.array(filt_dict['z'], dtype=np.complex128)
k = np.float64(filt_dict['k'])
print(type(k))
print(len(z))
print(len(p))

print(k)
print(z)
print(p)
<class 'numpy.float64'>
49
52
-4.251712073985204e+34
[-1.71933238e+04+0.00000000e+00j -1.63099239e+04+0.00000000e+00j
 -1.29728162e+01+0.00000000e+00j -1.21499286e+01+0.00000000e+00j
 -7.55661313e-02+0.00000000e+00j -2.45521103e-02+0.00000000e+00j
 -2.48600198e+00+0.00000000e+00j -7.56379407e-02+0.00000000e+00j
 -2.48612066e+00+0.00000000e+00j -3.05765107e-01+6.60016416e-03j
 -3.05765107e-01-6.60016416e-03j -2.48486669e+00+9.73721622e-02j
 -2.48486669e+00-9.73721622e-02j -8.18777546e+01+8.99816240e+01j
 -8.18777546e+01-8.99816240e+01j -8.18777546e+01+8.99816240e+01j
 -8.18777546e+01-8.99816240e+01j -1.70409949e+00+1.71472024e+01j
 -1.70409949e+00-1.71472024e+01j -1.70409949e+00+1.71472024e+01j
 -1.70409949e+00-1.71472024e+01j -6.93961076e-02+2.22637834e+00j
 -6.93961076e-02-2.22637834e+00j -4.93047846e-01+1.67948889e+00j
 -4.93047846e-01-1.67948889e+00j -4.93047846e-01+1.67948889e+00j
 -4.93047846e-01-1.67948889e+00j -6.17306356e-01+8.52973777e+00j
 -6.17306356e-01-8.52973777e+00j -1.73319977e+01+5.90518075e+01j
 -1.73319977e+01-5.90518075e+01j -5.80500799e+02+3.59639192e+02j
 -5.80500799e+02-3.59639192e+02j -5.80500799e+02+3.59639192e+02j
 -5.80500799e+02-3.59639192e+02j -2.48486669e+00+9.73721622e-02j
 -2.48486669e+00-9.73721622e-02j -2.48486669e+00+9.73721622e-02j
 -2.48486669e+00-9.73721622e-02j -2.48486669e+00+9.73721622e-02j
 -2.48486669e+00-9.73721622e-02j -2.48486669e+00+9.73721622e-02j
 -2.48486669e+00-9.73721622e-02j -2.48486669e+00+9.73721622e-02j
 -2.48486669e+00-9.73721622e-02j -2.48486669e+00+9.73721622e-02j
 -2.48486669e+00-9.73721622e-02j -2.48486669e+00+9.73721622e-02j
 -2.48486669e+00-9.73721622e-02j]
[-1.26170405e-01  +0.j         -2.45290808e-02  +0.j
 -1.25665297e+04  +0.j         -1.26168860e-01  +0.j
 -8.03659898e+03+194.91460286j -8.03659898e+03-194.91460286j
 -2.37959049e+02  +6.39812641j -2.37959049e+02  -6.39812641j
 -9.60236922e+01  +2.28395724j -9.60236922e+01  -2.28395724j
 -1.25627968e+03 +33.14285309j -1.25627968e+03 -33.14285309j
 -5.32893506e+01  +5.46624111j -5.32893506e+01  -5.46624111j
 -6.49816520e+00 +38.83046579j -6.49816520e+00 -38.83046579j
 -6.49816520e+00 +38.83046579j -6.49816520e+00 -38.83046579j
 -3.00420896e-03  +3.93445773j -3.00420896e-03  -3.93445773j
 -1.63422797e-02  +3.57437107j -1.63422797e-02  -3.57437107j
 -2.46292757e-01  +0.50549057j -2.46292757e-01  -0.50549057j
 -2.46292757e-01  +0.50549057j -2.46292757e-01  -0.50549057j
 -5.61460029e-02  +1.7916299j  -5.61460029e-02  -1.7916299j
 -5.58553553e-01  +7.73696394j -5.58553553e-01  -7.73696394j
 -1.79345808e+01 +61.07755352j -1.79345808e+01 -61.07755352j
 -5.42617896e+02+121.0808547j  -5.42617896e+02-121.0808547j
 -5.42617896e+02+121.08085476j -5.42617896e+02-121.08085476j
 -5.32893506e+01  +5.46624111j -5.32893506e+01  -5.46624111j
 -5.32893506e+01  +5.46624111j -5.32893506e+01  -5.46624111j
 -5.32893506e+01  +5.46624111j -5.32893506e+01  -5.46624111j
 -5.32893506e+01  +5.46624111j -5.32893506e+01  -5.46624111j
 -5.32893506e+01  +5.46624111j -5.32893506e+01  -5.46624111j
 -5.32893506e+01  +5.46624111j -5.32893506e+01  -5.46624111j
 -5.32893506e+01  +5.46624111j -5.32893506e+01  -5.46624111j
 -5.32893506e+01  +5.46624111j -5.32893506e+01  -5.46624111j]
"""
This File has the sole purpose of creating the ASC DHARD Y Model that is then exported to the buzz repository.
"""

import os
import contextlib

import numpy as np
import matplotlib.pyplot as plt
import control

import pytest
import scipy
from scipy.signal import cheby2
from scipy import signal

from wield.bunch import Bunch
#from wield.control import MIMO
from wield.control import SISO
from wield.control.ss_bare.ss import BareStateSpace as RawStateSpace
from wield.control.ss_bare.ssprint import print_dense_nonzero
from wield.utilities.file_io import load, load_ls, save
from wield.utilities.mpl import mplfigB

from wield.pytest.fixtures import ( 
    tpath_join,
    plot,
    dprint,
    tpath,
    tpath_preclear,
    fpath_join,
    test_trigger,
)

from icecream import ic
from buzz import ssutil

import scipy.optimize
from wield.control import SISO


def bode(
    sys,
    axB=None,
    F_Hz=None,
    omega_limits=None,
    include_zp = True,
    label=None,
    **kwargs
):
    """
    sys should be a wield.control.SISO object
    """
    if axB is None:
        axB = mplfigB(Nrows = 2)

    if include_zp:
        z, p = sys._zp
        z = z[z.imag > 0]
        p = p[p.imag > 0]
        F_include = np.sort(np.concatenate(
            [
                z.imag,
                p.imag,
                z.imag - abs(z.real) - 1e-6,
                z.imag + abs(z.real) + 1e-6,
                p.imag - abs(p.real) - 1e-6,
                p.imag + abs(p.real) + 1e-6,
            ]
        )) / (2 * np.pi)
        F_include = F_include[F_include > 0]

        if omega_limits is None:
            omega_limits = (
                (F_include[0] + 1e-6) / 3,
                F_include[-1] * 3
            )

    if F_Hz is None:
        F_Hz = np.geomspace(omega_limits[0], omega_limits[1], 1000)

    if include_zp:
        print("F_include", F_include)
        F_Hz = np.sort(np.concatenate([F_Hz, F_include]))

    fr = sys.fresponse(f=F_Hz)
    axB.ax0.loglog(*fr.fplot_mag, label=label, **kwargs)
    if hasattr(axB, 'ax1'):
        axB.ax1.semilogx(*fr.fplot_deg225, label=label, **kwargs)
    return axB
FBNS_mod = SISO.zpk(z,p,k, convention='IIRrational')#.asSS
# FBNS_iod = {'F1.in':0, 'F1.out':0}
# FBNS = ssutil.makeSys(FBNS_mod, FBNS_iod)
# omega_lim = [min(F_Hz), max(F_Hz)]

#control.bode(FBNS.mod, Hz=True, wrap_phase=True, omega_limits=omega_lim)

from wield.utilities.mpl import mplfigB
plotting_omega = np.logspace(-2, 4, 1000)
plt.close()
axB = mplfigB(Nrows = 2)
fr = FBNS_mod.fresponse(f=plotting_omega)
axB.ax0.loglog(*fr.fplot_mag, label='test')
axB.ax1.semilogx(*fr.fplot_deg225, label='test')

# tol_list = range(10, 16)
# for tol in tol_list:
#     FBNS_tmp = ssutil.balance_sys_gain(FBNS, nr=tol)
#     nstates_tmp = FBNS.A.shape[0]
#     K_zpk_tmp = SISO.SISOStateSpace(BareStateSpace(FBNS_tmp.A, FBNS_tmp.B, FBNS_tmp.C, FBNS_tmp.D, None)).asZPK
#     control.bode(FBNS.mod, Hz=True, wrap_phase=True, omega_limits=omega_lim, label='sts: {}, tol: {}'.format(nstates_tmp, tol))

plt.legend(bbox_to_anchor=(1.05, 1), loc='upper left')
plt.savefig('postsave.png', dpi=300, bbox_inches='tight')
plt.show()
plt.close()
../../_images/f76f9d94ef53f4e2b6df38e1e9ee7b4aeb7f3e92c32d9b832ea74b04c64a7270.png