Example #1
0
class oem_retrieval():
    """
    AUTHOR:
      Hayden Smotherman
    DESCRIPTION:
      This class is meant to run an OEM retrieval on data from the radiometers at MPI Solar System.
      The hope is that it simplifies the process of using the typhon OEM retreival for the specific
      use-case of Paul Hartogh's group at MPI Solar System.

      It can load in the data, attempt to fit and subtract-out sine curves from the noise,
      and filter high-frequency oscillations that are present in some data.

      It can also run a retrieval using the typhon OEM retrieval and plot the results,
      along with relevant statistical metrics.
    """
    def __init__(self):
        """
        AUTHOR:
          Hayden Smotherman
        DESCRIPTION:
          This function initializes the oem_retrieval class
        INPUTS:
          NONE
        OUTPUTS:
          NONE
        """

        self.signal = np.array(
            [])  # This array will hold the brightness temperature data
        self.noise = np.array(
            [])  # This array will hold the noise for the radiometer
        self.freq = np.array(
            [])  # This array will hold the frequencies of "signal" and "noise"

    def load_data(self,
                  data_type='radiometer',
                  use_data=0,
                  filename='',
                  signal=[],
                  noise=[],
                  freq=[]):
        """
        AUTHOR:
          Hayden Smotherman
        DESCRIPTION:
          This function loads in radiometer data to the oem_retrival class.
          It accepts data in a number of different formats.
        INPUTS:
          data_type - A string denoting which format the input data will be in.
            VALUES:   'numpy'  - Regular numpy arrays passed in to "signal", "noise", and "freq"
                        REQUIRES: signal, noise, freq
                      'radiometer' - A .raw file generated by a radiometer at MPI Solar System.
                        REQUIRES: filename
          use_data - Integer value denoting how much data to use when running a mean OEM retrieval.
                     This value should be zero (use all data) or negative (use the last x signals)
          signal - An array of the brightness temperatures of an O3-666 line.
                   ONLY USED WITH data_type='numpy'
          noise  - An array of the noise of the radiometer
                   ONLY USED WITH data_type='numpy'
          freq   - An array of the central frequency of each datapoint in "signal" and "noise"
                   ONLY USED WITH data_type='numpy'
        OUTPUTS:
        """

        eform = '>i16f7500f100f28f28f'

        dform = '>i16f4092f100f28f28f'

        aform = '>i16f16384f128f28f28f'

        xform = '>i16f1019f128f28f28f'

        if data_type == 'radiometer':
            # Load data from a .raw file from the 210MHz radiometer
            # Load in the raw data
            if filename[0] == 'a':
                data_format = aform
            elif filename[0] == 'd':
                data_format = dform
            elif filename[0] == 'e':
                data_format = eform
            elif filename[0] == 'x':
                data_format = xform
            else:
                raise ImportError(
                    'Could not understand the data format based on the file name.'
                )

            Calibrated = calibration(format=data_format)
            Calibrated.load(filename)
            Calibrated.calibrate()

            # Pre-allocate signal and noise arrays
            self.signal = np.array(Calibrated._signal[use_data:])
            self.noise = np.array(Calibrated._noise[use_data:])

            # Generate the frequency array based on the data format
            if filename[0] == 'a':
                # First we split the data, since two signals are embedded in the ._signal array
                # NOTE: Currently this only keeps the first half of the data. It should keep all of it.
                self.signal = Calibrated._signal[use_data:]
                self.noise = Calibrated._noise[use_data:]
                Half_Length = int(len(Calibrated._noise[0]) /
                                  2)  # Data is set as one long array
                for i in range(len(self.signal)):
                    self.signal[i] = np.copy(
                        Calibrated._signal[i][0:Half_Length])
                    self.noise[i] = np.copy(
                        Calibrated._noise[i][0:Half_Length])
                self.signal = np.array(self.signal)
                self.noise = np.array(self.noise)

                # Width of data is 1.5GHz
                # Central frequency is data number 14 in _raw array, requested frequency
                Min_Freq = Calibrated._raw[0][14] - 1.50 / 2
                Max_Freq = Calibrated._raw[0][14] + 1.50 / 2
                self.freq = np.linspace(Min_Freq, Max_Freq, Half_Length)
            elif filename[0] == 'd':
                # Width of data is 40MHz
                # Central frequency is data number 14 in _raw array, requested frequency
                Min_Freq = Calibrated._raw[0][14] - .040 / 2
                Max_Freq = Calibrated._raw[0][14] + .040 / 2
                self.freq = np.linspace(Min_Freq, Max_Freq,
                                        len(Calibrated._noise[0]))
            elif filename[0] == 'e':
                # Width of data is 210MHz
                # Central frequency is data number 14 in _raw array, requested frequency
                Min_Freq = Calibrated._raw[0][14] - .210 / 2
                Max_Freq = Calibrated._raw[0][14] + .210 / 2
                self.freq = np.linspace(Min_Freq, Max_Freq,
                                        len(Calibrated._noise[0]))
            elif filename[0] == 'x':
                # Width of data is 4.4GHz
                # Central frequency is data number 14 in _raw array, requested frequency
                Min_Freq = Calibrated._raw[0][14] - 4.4 / 2
                Max_Freq = Calibrated._raw[0][14] + 4.4 / 2
                self.freq = np.linspace(Min_Freq, Max_Freq,
                                        len(Calibrated._noise[0]))
            else:
                raise ImportError(
                    'Could not generate frequency array. Unknown file format.')

            Calibrated = None

        elif data_type == 'numpy':
            # Load in arbitrary data that is already in a numpy array
            self.signal = np.array(signal)
            self.noise = np.array(noise)
            self.freq = np.array(freq)

        # Define fit_ values here in case subtract_sinewave is never called
        self.fit_freq = np.copy(self.freq)
        self.fit_signal = np.copy(self.signal)
        self.fit_noise = np.copy(self.noise)

    def process_data(self,
                     lims=[21, 23],
                     shift_freq=False,
                     fit_sine=False,
                     plot_example=True,
                     initial_guess=[3, 1 / 1000, 0],
                     central_freq=1.42175037e+2):
        """
        AUTHOR:
          Hayden Smotherman
        DESCRIPTION:
          This function clips data from the front and back of the dataset based on front_lim and
          back_lim. If "fit_sine=True", then it also subtracts out sine waves from the data.
        INPUTS:
          lims - Optional frequency limits (in GHz) that determine the range of data to use.
          shift_freq - Boolean that determines whether or not to shift the frequency such that the
                       max of the averaged data aligns with the peak of the O3 signal.
          fit_sine      - Boolean that determines whether or not to fit a sine wave to the noise then
                          subtract it out of the signal.
          plot_example  - Boolean that determines whether or not to plot an example fit. Only used if
                          fit_sine=True
          initial_guess - This is the initial guess for the sine parameters used by lsqfit. Only used
                          if fit_sine=True
          central_freq  - This is the frequency center of the measurements.  Only used if
                          shift_freq=True
        OUTPUTS:
          NONE
        NOTES:
          - This function works best for data in dform and eform. Datasets with longer frequency
            baselines are not fit as well and may give bad results.
          - By default, "process_data" clips the first 25 data points regardless of what is set in "lims".
            This is done because the first ~20 data points are often severe outliers.
        """

        # Calculate the index corresponding to the minimum frequency limit
        if shift_freq:
            if len(self.signal.shape) > 1:
                Average_Signal = np.mean(self.signal, axis=0)
            else:
                Average_Signal = np.copy(self.signal)

            Freq_Peak = self.freq[Average_Signal == np.max(Average_Signal)]
            Freq_Difference = Freq_Peak - central_freq
            self.freq -= Freq_Difference

        front_lim = np.where(
            self.freq == np.min(self.freq[self.freq > lims[0]]))
        front_lim = int(front_lim[0])
        if (front_lim < 25):
            # Set front_lim minimum to 25, since the first ~25 data points are very often bad
            front_lim = 25
        # Calculate the index corresponding to the maximum frequency limit
        back_lim = np.where(
            self.freq == np.max(self.freq[self.freq < lims[1]]))
        back_lim = int(back_lim[0])

        Signal_Length = self.signal.shape[1]  # Length of the signal array
        Signal_Number = self.signal.shape[
            0]  # Number of signals in the data file

        X_Data = np.linspace(
            0, Signal_Length - 1,
            Signal_Length)  # Array of indicies for the fitting
        X_Data = X_Data[front_lim:back_lim]

        # Preallocate the memory for the fitted values of signal and noise
        self.fit_signal = np.zeros([
            self.signal.shape[0],
            np.size(self.signal[0][front_lim:back_lim])
        ])
        self.fit_noise = np.zeros(
            [self.noise.shape[0],
             np.size(self.noise[0][front_lim:back_lim])])
        self.fit_freq = np.copy(self.freq[front_lim:back_lim])

        if fit_sine:
            for i in range(Signal_Number):
                # Iterate over all signals and subtract out the best-fit sine curve

                # Load in noise locally for ease of use
                Noise_0 = self.noise[i]

                # Limit the data based on front_lim and back_lim and subtract the mean
                Noise_0 = Noise_0[front_lim:back_lim]
                Noise_Median = np.median(Noise_0)
                Base_Noise = Noise_0 - Noise_Median  # Subtract out the noise mean for fitting

                Sine_Params = initial_guess  # Amplitude, freq, phase

                # Sine function
                optimize_func = lambda x: x[0] * np.sin(x[1] * X_Data + x[2]
                                                        ) - Base_Noise
                # Perform least square fit to the sine function
                est_amp, est_freq, est_phase = leastsq(optimize_func,
                                                       Sine_Params)[0]
                # Save the fit
                Fit = est_amp * np.sin(est_freq * X_Data + est_phase)

                # Load in signal locally
                Signal_0 = self.signal[i]
                Signal_0 = Signal_0[front_lim:back_lim]

                # Subtract the fit from the signal and the noise and save it in new variables
                #print(np.size(Signal_0), np.size(Fit))
                self.fit_signal[i] = Signal_0 - Fit
                self.fit_noise[i] = Noise_0 - Fit

            if plot_example:
                # Plot the last "noise" value along with the fit sine curve
                plt.figure(figsize=[12, 8])
                plt.plot(self.fit_freq, Noise_0)
                plt.plot(self.fit_freq, Fit + Noise_Median)
                plt.legend(['Unfitted Noise', 'Best Fit Sine Curve'],
                           fontsize=20)
                plt.xlabel('Frequency [GHz]', fontsize=20)
                plt.ylabel('Brightness Temperature [K]', fontsize=20)
                plt.title('Raw Noise and Best Fit Curve', fontsize=24)

                # Plot the fitted noise over the raw noise
                plt.figure(figsize=[12, 8])
                plt.plot(self.fit_freq, self.noise[-1][front_lim:back_lim])
                plt.plot(self.fit_freq, self.fit_noise[-1])
                plt.legend(['Unfitted Noise', 'Fitted Noise'], fontsize=20)
                plt.xlabel('Frequency [GHz]', fontsize=20)
                plt.ylabel('Brightness Temperature [K]', fontsize=20)
                plt.title('Unfitted Noise and Fitted Noise', fontsize=24)

                # Plot the fitted signal over the raw signal
                plt.figure(figsize=[12, 8])
                plt.plot(self.fit_freq, self.signal[-1][front_lim:back_lim])
                plt.plot(self.fit_freq, self.fit_signal[-1])
                plt.legend(['Unfitted Signal', 'Fitted Signal'], fontsize=20)
                plt.xlabel('Frequency [GHz]', fontsize=20)
                plt.ylabel('Brightness Temperature [K]', fontsize=20)
                plt.title('Unfitted Signal and Fitted Signal', fontsize=24)
        else:
            for i in range(Signal_Number):
                self.fit_signal[i] = self.signal[i][front_lim:back_lim]
                self.fit_noise[i] = self.noise[i][front_lim:back_lim]

    def mean_retrieval(self,
                       use_data=0,
                       filtered=False,
                       shift_freq=False,
                       sigma=None,
                       central_freq=1.42175037e+2):
        """
        AUTHOR:
          Hayden Smotherman
        DESCRIPTION:
          This function uses the Typhon OEM retrieval package to recover atmospheric O3 levels
          given the brightness temperature signal of the O3-666 line. If self.signal is a 2D
          matrix, then this function will run the retrieval on the mean of this data.
        INPUTS:
          filtered - Boolean that determines whether or not to use the scipy.signal.filtfilt
                     function on the mean signal value in order to potentially improve the signal
          shift_freq - Boolean that determines whether or not to shift the frequency such that the
                       max of the averaged data aligns with the peak of the O3 signal.
          central_freq  - This is the frequency center of the measurements.  Only used if
                          shift_freq=True
        OUTPUTS:
          NONE
        """

        if len(self.signal.shape) > 1:
            self.average_signal = np.mean(self.fit_signal, axis=0)
            self.average_noise = np.mean(self.fit_noise, axis=0)
        else:
            self.average_signal = np.copy(self.fit_signal)
            self.average_noise = np.copy(self.fit_noise)

        if filtered:
            n = 4  # the larger n is, the smoother the curve will be
            b = [1.0 / n] * n
            a = 1

            self.average_signal = sg.filtfilt(b, a, self.average_signal)
            self.average_noise = sg.filtfilt(b, a, self.average_noise)

        if shift_freq:
            Freq_Peak = self.fit_freq[self.average_signal == np.max(
                self.average_signal)]
            Freq_Difference = Freq_Peak - central_freq
            self.fit_freq -= Freq_Difference

        self._initialize_arts_workspace()

        if sigma is None:
            self.sigma = np.sqrt(
                np.sum(
                    np.abs(self.average_noise - np.mean(self.average_noise))) /
                len(self.average_noise))
        else:
            self.sigma = sigma

        self._initialize_covmat()

        self._run_retrieval()

    def plot_oem(self, basename='', plot_results=True, plot_statistics=True):
        """
        AUTHOR:
          Hayden Smotherman
        DESCRIPTION:
          This function uses the Typhon OEM retrieval package to recover atmospheric O3 levels
          given the brightness temperature signal of the O3-666 line. If self.signal is a 2D
          matrix, then this function will run the retrieval on the mean of this data.
        INPUTS:
          plot_results - Boolean that determines whether or not to plot the results of the retrieval.
          plot_statistics - Boolean that determines whether or not to plot relevant statistics
                            from the retrieval.
        OUTPUTS:
          NONE
        """

        if plot_results:
            # Plot the retrieved signal and the retrieval values
            # First plot the averaged signal and the retrieved signal
            plt.figure(1, figsize=[12, 8])
            plt.clf()
            plt.plot(self.fit_freq, self.average_signal)
            plt.plot(self.arts.f_grid.value / 1e9, self.arts.yf.value, 'r')
            plt.xlabel('Frequency [GHz]', fontsize=20)
            plt.ylabel('Brightness Temperature [K]', fontsize=20)
            plt.legend(['Data', 'Retrieval'], fontsize=20)
            plt.title('Average Signal and Retrieved Signal', fontsize=24)
            plt.savefig(basename + 'fig1.png')

            # Now plot the actual retrieved Ozone VMR
            Altitude = self.arts.z_field.value.flatten()
            plt.figure(2, figsize=[8, 12])
            plt.clf()
            plt.plot(10**self.arts.xa.value[:-1] * 1e6, Altitude)
            plt.plot(10**self.arts.x.value[:-1] * 1e6, Altitude)
            plt.legend(['Prior', 'OEM Retrieval'], fontsize=20)
            plt.ylabel('Altitude [m]', fontsize=20)
            plt.xlabel('O3 [ppmv]', fontsize=20)
            plt.title('Altitude vs. O3 VMR', fontsize=24)
            plt.savefig(basename + 'fig2.png')

            plt.figure(1, figsize=[12, 8])
            plt.clf()
            plt.plot(self.fit_freq, self.average_signal - self.arts.yf.value,
                     'r')
            plt.xlabel('Frequency [GHz]', fontsize=20)
            plt.ylabel('Residual [K]', fontsize=20)
            plt.title('Residual Signal', fontsize=24)
            plt.savefig(basename + 'fig5.png')

        if plot_statistics:
            # Plot some relevant statistics of the OEM retrieval
            Altitude = self.arts.z_field.value.flatten()
            averaging_kernel = self.arts.dxdy.value @ self.arts.jacobian.value
            measurement_response = averaging_kernel @ np.ones(
                averaging_kernel.shape[1])

            plt.figure(3, figsize=[12, 8])
            plt.clf()
            [plt.plot(kernel[:-1], Altitude) for kernel in averaging_kernel.T]
            plt.title('Averaging kernel for the OEM retrieval', fontsize=24)
            plt.ylabel('Altitude [m]', fontsize=20)
            plt.savefig(basename + 'fig3.png')

            plt.figure(4, figsize=[12, 8])
            plt.clf()
            plt.plot(measurement_response[:-1], Altitude)
            plt.title('Measurement response for the OEM retrieval',
                      fontsize=24)
            plt.ylabel('Alititude [m]', fontsize=20)
            plt.savefig(basename + 'fig4.png')

    def _initialize_arts_workspace(self):
        """
        AUTHOR:
          Richard Larsson, Hayden Smotherman
        DESCRIPTION:
          This function is meant to be run interally to this class. It initializes the arts workspace.
        INPUTS:
          NONE
        OUTPUTS:
          NONE
        """

        # -*- coding: utf-8 -*-
        """
        Created on Thu Jul  5 15:53:02 2018

        @author: larsson
        """

        xmls = typhon.environ.get('ARTS_DATA_PATH')

        self.arts = Workspace(0)

        @arts_agenda
        def water_psat_agenda(ws):
            ws.water_p_eq_fieldMK05()

        @arts_agenda
        def propmat_clearsky_agenda_zeeman(ws):
            ws.propmat_clearskyInit()
            ws.propmat_clearskyAddOnTheFly()
            ws.Ignore(ws.rtp_mag)
            ws.Ignore(ws.rtp_los)

        @arts_agenda
        def ppath_agenda_step_by_step(ws):
            ws.Ignore(ws.rte_pos2)
            ws.ppathStepByStep()

        @arts_agenda
        def iy_main_agenda_emission(ws):
            ws.Ignore(ws.iy_id)
            ws.ppathCalc()
            ws.iyEmissionStandard()

        @arts_agenda
        def iy_space_agenda_cosmic_background(ws):
            ws.Ignore(ws.rtp_pos)
            ws.Ignore(ws.rtp_los)
            ws.MatrixCBR(ws.iy, ws.stokes_dim, ws.f_grid)

        @arts_agenda
        def iy_surface_agenda(ws):
            ws.SurfaceDummy()
            ws.iySurfaceRtpropAgenda()

        @arts_agenda
        def ppath_step_agenda_geometric(ws):
            ws.Ignore(ws.t_field)
            ws.Ignore(ws.vmr_field)
            ws.Ignore(ws.f_grid)
            ws.Ignore(ws.ppath_lraytrace)
            ws.ppath_stepGeometric()

        @arts_agenda
        def abs_xsec_agenda_lines(ws):
            ws.abs_xsec_per_speciesInit()
            ws.abs_xsec_per_speciesAddLines2()
            ws.abs_xsec_per_speciesAddConts()

        @arts_agenda
        def surface_rtprop_agenda(ws):
            ws.InterpSurfaceFieldToPosition(out=ws.surface_skin_t,
                                            field=ws.t_surface)
            ws.surfaceBlackbody()

        @arts_agenda
        def geo_pos_agenda(ws):
            ws.Ignore(ws.ppath)
            ws.VectorSet(ws.geo_pos, np.array([]))

        @arts_agenda
        def sensor_response_agenda(ws):
            ws.AntennaOff()
            ws.sensorOff()
            ws.Ignore(ws.f_backend)

        # Set some agendas
        self.arts.Copy(self.arts.surface_rtprop_agenda, surface_rtprop_agenda)
        self.arts.Copy(self.arts.abs_xsec_agenda, abs_xsec_agenda_lines)
        self.arts.Copy(self.arts.ppath_step_agenda,
                       ppath_step_agenda_geometric)
        self.arts.Copy(self.arts.propmat_clearsky_agenda,
                       propmat_clearsky_agenda_zeeman)
        self.arts.Copy(self.arts.iy_main_agenda, iy_main_agenda_emission)
        self.arts.Copy(self.arts.iy_space_agenda,
                       iy_space_agenda_cosmic_background)
        self.arts.Copy(self.arts.ppath_agenda, ppath_agenda_step_by_step)
        self.arts.Copy(self.arts.iy_surface_agenda, iy_surface_agenda)
        self.arts.Copy(self.arts.geo_pos_agenda, geo_pos_agenda)
        self.arts.Copy(self.arts.water_p_eq_agenda, water_psat_agenda)
        self.arts.Copy(self.arts.sensor_response_agenda,
                       sensor_response_agenda)

        # Set some quantities that are unused because you do not need them (for now)
        self.arts.Touch(self.arts.surface_props_data)
        self.arts.Touch(self.arts.surface_props_names)
        self.arts.Touch(self.arts.mag_u_field)
        self.arts.Touch(self.arts.mag_v_field)
        self.arts.Touch(self.arts.mag_w_field)
        self.arts.Touch(self.arts.wind_u_field)
        self.arts.Touch(self.arts.wind_v_field)
        self.arts.Touch(self.arts.wind_w_field)
        self.arts.Touch(self.arts.transmitter_pos)
        self.arts.Touch(self.arts.iy_aux_vars)
        self.arts.Touch(self.arts.mblock_dlos_grid)
        self.arts.Touch(self.arts.scat_species)

        # Ozone line and continua
        self.arts.abs_cont_descriptionInit()
        self.arts.abs_cont_descriptionAppend(tagname="H2O-PWR98",
                                             model="Rosenkranz")
        self.arts.abs_cont_descriptionAppend(tagname="O2-PWR98",
                                             model="Rosenkranz")
        self.arts.abs_cont_descriptionAppend(tagname="O2-PWR98",
                                             model="Rosenkranz")
        self.arts.abs_cont_descriptionAppend(tagname="N2-CIArotCKDMT252",
                                             model="CKDMT252")
        self.arts.abs_cont_descriptionAppend(tagname="N2-CIAfunCKDMT252",
                                             model="CKDMT252")
        self.arts.abs_speciesSet(species=[
            'O2-PWR98', 'H2O-PWR98', 'N2-CIAfunCKDMT252, N2-CIArotCKDMT252'
        ])
        self.arts.abs_linesReadFromSplitArtscat(
            basename=os.path.join(xmls, 'spectroscopy/Perrin/'),
            fmin=np.min(self.fit_freq) * 1e9 - 1e8,
            fmax=np.max(self.fit_freq) * 1e9 + 1e8)
        self.arts.abs_lines_per_speciesCreateFromLines()

        # Set builtin Earth-viable isotopologue values and partition functions
        self.arts.isotopologue_ratiosInitFromBuiltin()
        self.arts.partition_functionsInitFromBuiltin()

        self.arts.nlteOff()  # LTE
        self.arts.stokes_dim = 1  # No polarization
        self.arts.xsec_speedup_switch = 0  # No speedup (experimental feature)
        self.arts.rte_alonglos_v = 0.  # No movement of satellite or rotation of planet
        self.arts.lm_p_lim = 0.  # Just do line mixing if available (it is not)
        self.arts.abs_f_interp_order = 1  # Interpolation in frequency if you add a sensor
        self.arts.ppath_lmax = 10000.  # Maximum path length (Original)
        self.arts.ppath_lraytrace = 10000.  # Maximum path trace (Original)
        self.arts.refellipsoidEarth(model="Sphere")  # Europa average radius
        self.arts.iy_unit = "PlanckBT"  # Output results in Planck Brightess Temperature

        #  Set the size of the problem (change to your own numbers)
        NP = 201  # Number of pressure levels
        NF = len(self.fit_freq)
        self.arts.lon_true = np.array([0])
        self.arts.lat_true = np.array([0])
        self.arts.AtmosphereSet1D()
        self.arts.p_grid = np.logspace(5.04, -1.2, NP)
        self.arts.z_surface = np.zeros((1, 1))
        self.arts.t_surface = np.full((1, 1), 295.)
        self.arts.f_grid = np.linspace(
            np.min(self.fit_freq) * 1e9,
            np.max(self.fit_freq) * 1e9, NF)
        self.arts.sensorOff()  # No sensor simulations

        # Read the atmosphere... folder should contain:
        # "H2O.xml"
        # "t.xml"
        # "z.xml"
        # The files can be in binary format
        self.arts.AtmRawRead(
            basename=xmls +
            'planets/Earth/Fascod/subarctic-summer/subarctic-summer')
        self.arts.AtmFieldsCalc()

        # Set observation geometry... You can make more positions and los
        self.arts.sensor_pos = np.array([[10000]
                                         ])  # [[ALT, LAT, LON]] (Original)

        self.arts.sensor_los = np.array([[63]])  # [[ZENITH, AZIMUTH]]

        # Temperature and Ozone VMR Jacobian
        self.arts.jacobianInit()
        self.arts.jacobianAddTemperature(g1=self.arts.p_grid.value,
                                         g2=np.array([]),
                                         g3=np.array([]))
        self.arts.jacobianAddAbsSpecies(g1=self.arts.p_grid.value,
                                        g2=np.array([]),
                                        g3=np.array([]),
                                        species='H2O-PWR98')
        self.arts.jacobianClose()
        self.arts.cloudboxOff()  # No Clouds

        # Check that the input looks OK
        self.arts.atmgeom_checkedCalc()
        self.arts.atmfields_checkedCalc()
        self.arts.cloudbox_checkedCalc()
        self.arts.sensor_checkedCalc()
        self.arts.propmat_clearsky_agenda_checkedCalc()
        self.arts.abs_xsec_agenda_checkedCalc()

        # Perform the calculations!
        self.arts.yCalc()

    def _initialize_covmat(self):
        """
        AUTHOR:
          Simon Pfreundschuh, Hayden Smotherman
        DESCRIPTION:
          This function is meant to be run interally to this class. It initializes the covariance matricies.
        INPUTS:
          NONE
        OUTPUTS:
          NONE
        """
        z_grid = self.arts.z_field.value.flatten()

        n_p = self.arts.p_grid.value.size

        self.arts.retrievalDefInit()
        # Kernel panic if 'grid_1' and 'sigma_1' are not the same size
        self.arts.covmat1D(
            self.arts.covmat_block,
            grid_1=z_grid,
            sigma_1=1e-7 * np.ones(n_p),  # Relative uncertainty
            cls_1=1.0e3 * np.ones(n_p),  # Correlation length [m]
            fname="exp")
        self.arts.retrievalAddAbsSpecies(species="H2O-PWR98",
                                         unit="vmr",
                                         atmosphere_dim=1,
                                         g1=self.arts.p_grid,
                                         g2=np.array([]),
                                         g3=np.array([]))
        self.arts.jacobianSetFuncTransformation(transformation_func="none")

        self.arts.covmatDiagonal(out=self.arts.covmat_block,
                                 out_inverse=self.arts.covmat_inv_block,
                                 vars=100.0 * np.ones(1))
        self.arts.retrievalAddPolyfit(poly_order=0)
        #        self.arts.covmatDiagonal(out = self.arts.covmat_block,
        #                            out_inverse = self.arts.covmat_inv_block,
        #                            vars = 100.0  * np.ones(8*2))
        #        self.arts.retrievalAddSinefit(period_lengths = np.array([10e3, 20e3, 1e6, 2e6, 5e6, 10e6, 20e6, 1e9]))
        self.arts.retrievalDefClose()

        # More uncertainty measurements
        self.arts.covmatDiagonal(self.arts.covmat_block,
                                 self.arts.covmat_inv_block,
                                 vars=self.sigma**2 *
                                 np.ones(self.arts.y.value.shape))
        self.arts.covmat_seSet(self.arts.covmat_block)
        self.arts.jacobianAdjustAndTransform

        # Kernel panic if 'arts.Ignore(arts.inversion_iteration_counter)' is not included

        @arts_agenda
        def inversion_iterate_agenda(arts):
            arts.Ignore(arts.inversion_iteration_counter)
            arts.x2artsAtmAndSurf()
            arts.Copy(arts.f_backend, arts.f_grid)
            arts.x2artsSensor()
            arts.atmfields_checkedCalc(
                negative_vmr_ok=1
            )  # negative_vmr_ok added to avoid error with negative vmr values
            arts.atmgeom_checkedCalc()
            arts.yCalc()
            arts.jacobianAdjustAndTransform()
            arts.VectorAddVector(arts.yf, arts.y, arts.y_baseline)

        self.arts.Copy(self.arts.inversion_iterate_agenda,
                       inversion_iterate_agenda)

    def _run_retrieval(self):
        """
        AUTHOR:
          Simon Pfreundschuh, Hayden Smotherman
        DESCRIPTION:
          This function is meant to be run interally to this class. It runs the actual retrieval.
        INPUTS:
          NONE
        OUTPUTS:
          NONE
        """

        # Run the OEM retrieval

        self.arts.Touch(self.arts.particle_bulkprop_field)
        self.arts.Touch(self.arts.particle_bulkprop_names)
        self.arts.VectorSetConstant(self.arts.sensor_time, 1, 0.)
        self.arts.xaStandard()
        self.arts.x = np.zeros(0)
        self.arts.y.value[:] = self.average_signal
        self.arts.jacobian = np.zeros((0, 0))

        self.arts.OEM(method="li",
                      max_iter=20,
                      display_progress=1,
                      lm_ga_settings=np.array(
                          [100.0, 5.0, 2.0, 10.0, 1.0, 1.0]))
        self.arts.avkCalc()
plt.tight_layout()
plt.show()

# A Priori State
# For the a priori state we assume zero wind in any direction. The a priori vector for the OEM is created by
# the `xaStandard` WSM, which computes $x_a$ from the current atmospheric state.
ws.wind_u_field.value[:] = 0.0
ws.wind_v_field.value[:] = 0.0
ws.xaStandard()

# The OEM Calculation
ws.x = np.zeros(0)
ws.jacobian = np.zeros((0, 0))
ws.y.value[:] = y
ws.OEM(method="lm",
       max_iter=20,
       display_progress=1,
       lm_ga_settings=np.array([100.0, 2.0, 2.0, 10.0, 1.0, 1.0]))
ws.x2artsStandard()

# Retrieval Results
f, axs = plt.subplots(1, 2, figsize=(10, 5))

ax = axs[0]
ax.axvline(u_wind, color="grey", ls="--", label="Truth")
ax.plot(ws.wind_u_field.value[:, 0, 0], z // 1e3, label="Retrieved")
ax.set_xlabel("$v_u$ [m/s]")
ax.set_ylabel("Altitude [km]")
ax.legend()

ax = axs[1]
ax.axvline(v_wind, color="grey", ls="--", label="Truth")
Example #3
0
def test_wind_3d_demo():
    ws = Workspace()

    ws.execute_controlfile("general/general.arts")
    ws.verbositySet(0, 0, 0, 0)
    ws.execute_controlfile("general/agendas.arts")
    ws.execute_controlfile("general/continua.arts")
    ws.execute_controlfile("general/planet_earth.arts")

    ws.Copy(ws.abs_xsec_agenda, ws.abs_xsec_agenda__noCIA)
    ws.Copy(ws.ppath_agenda, ws.ppath_agenda__FollowSensorLosPath)
    ws.Copy(ws.ppath_step_agenda, ws.ppath_step_agenda__GeometricPath)
    ws.Copy(ws.iy_space_agenda, ws.iy_space_agenda__CosmicBackground)
    ws.Copy(ws.iy_surface_agenda, ws.iy_surface_agenda__UseSurfaceRtprop)
    ws.Copy(ws.iy_main_agenda, ws.iy_main_agenda__Emission)
    ws.Copy(ws.propmat_clearsky_agenda, ws.propmat_clearsky_agenda__OnTheFly)

    # General Settings
    # For the wind retrievals, the forward model calculations are performed on a 3D atmosphere grid.
    # Radiation is assumed to be unpolarized.
    ws.atmosphere_dim = 3
    ws.stokes_dim = 1
    ws.iy_unit = "RJBT"

    # Absorption
    # We only consider absorption from ozone in this example. The lineshape data is available from
    # the ARTS testdata available in `controlfiles/testdata`.
    ws.abs_speciesSet(["O3", "H2O-PWR98"])
    ws.abs_lineshapeDefine("Voigt_Kuntz6", "VVH", 750e9)
    ws.ReadXML(ws.abs_lines, "testdata/ozone_line.xml")
    ws.abs_lines_per_speciesCreateFromLines()

    # Atmosphere (A Priori)
    # We create a pressure grid using the `PFromZSimple` function to create a grid of approximate pressure levels
    # corresponding to altitudes in the range
    # z = 0.0, 2000.0, ..., 94000.0
    z_toa = 95e3
    z_surf = 1e3
    z_grid = np.arange(z_surf - 1e3, z_toa, 2e3)
    ws.PFromZSimple(ws.p_grid, z_grid)
    ws.lat_grid = np.arange(-40.0, 1.0, 40.0)
    ws.lon_grid = np.arange(40.0, 61.0, 20.0)
    ws.z_surface = z_surf * np.ones(
        (np.asarray(ws.lat_grid).size, np.asarray(ws.lon_grid).size))

    # For the a priori state we read data from the Fascod climatology that is part of the ARTS xml data.
    ws.AtmRawRead(basename="planets/Earth/Fascod/tropical/tropical")
    ws.AtmFieldsCalcExpand1D()

    # Adding Wind
    # Wind in ARTS is represented by the `wind_u_field` and `wind_v_field` WSVs, which hold the horizontal components
    # of the wind at each grid point of the atmosphere model. For this example, a constant wind is assumed.
    u_wind = 60.0
    v_wind = -40.0
    ws.wind_u_field = u_wind * np.ones(
        (ws.p_grid.value.size, ws.lat_grid.value.size, ws.lon_grid.value.size))
    ws.wind_v_field = v_wind * np.ones(
        (ws.p_grid.value.size, ws.lat_grid.value.size, ws.lon_grid.value.size))
    ws.wind_w_field = np.zeros((0, 0, 0))

    # Frequency Grid and Sensor
    # The frequency grid for the simulation consists of 119 grid points between 110.516 and 111.156 GHz.
    # The frequencies are given by a degree-10 polynomial that has been obtained from a fit to the data from
    # the original `qpack` example. This is obscure but also kind of cool.
    coeffs = np.array([
        5.06312189e-08, -2.68851772e-05, 6.20655463e-03, -8.16344090e-01,
        6.75337174e+01, -3.66786505e+03, 1.32578167e+05, -3.14514304e+06,
        4.57491354e+07, 1.10516484e+11
    ])
    ws.f_grid = np.poly1d(coeffs)(np.arange(119))

    # For the sensor we assume a channel width and channel spacing of 50 kHz. We also call AntennaOff to compute
    # only one pencilbeam along the line of sight of the sensor.
    df = 50e3
    f_backend = np.arange(ws.f_grid.value.min() + 2.0 * df,
                          ws.f_grid.value.max() - 2.0 * df, df)
    ws.backend_channel_responseGaussian(np.array([df]), np.array([2.0]))
    ws.AntennaOff()

    ws.sensor_norm = 1
    ws.sensor_time = np.zeros(1)
    ws.sensor_responseInit()

    # Sensor Position and Viewing Geometry
    # 5 Measurements are performed, one straight up, and four with zenith angle  70∘70∘  in directions SW, NW, NE, SE.
    # In ARTS the measurement directions are given by a two-column matrix, where the first column contains the zenith
    # angle and the second column the azimuth angle.
    ws.sensor_los = np.array([[
        0.0,
        0.0,
    ], [70.0, -135.0], [70.0, -45.0], [70.0, 45.0], [70.0, 135.0]])
    ws.sensor_pos = np.array([[2000.0, -21.1, 55.6]] * 5)

    # Reference Measurement
    # Before we can calculate `y`, our setup needs to pass the following tests:
    ws.abs_f_interp_order = 3
    ws.propmat_clearsky_agenda_checkedCalc()
    ws.sensor_checkedCalc()
    ws.atmgeom_checkedCalc()
    ws.atmfields_checkedCalc()
    ws.abs_xsec_agenda_checkedCalc()
    ws.jacobianOff()
    ws.cloudboxOff()
    ws.cloudbox_checkedCalc()

    ws.yCalc()
    y = np.copy(ws.y.value)

    # Setting up the Retrieval
    # In this example, we retrieve ozone and the horizontal and vertical components of the wind velocities.
    # The state space covariance matrix in ARTS is represented by the **covmat_sa** WSV.
    # It belongs to the CovarianceMatrix group, which is used to represent block diagonal matrices.
    # For each retrieval quantity that is added to the retrieval, a corresponding block must be added to **covmat_sa**.
    # This is usually done by the corresponding **retrievalAdd...** call, which looks for this block
    # in the **covmat_block** WSV.
    # In short the general workflow for adding a retrieval quantity is as follows:
    #  - Create the covariance matrix for the retrieval quantity either calling one of the **covmat...** WSV or
    #    by loading your own matrix
    #  - Write the matrix block into **covmat_block**
    #  - Call the **retrievalAdd...** method to add the retrieval quantity and the covariance matrix block
    #    to **covmat_sa**
    lat_ret_grid = np.array([np.mean(ws.lat_grid)])
    lon_ret_grid = np.array([np.mean(ws.lon_grid)])
    n_p = ws.p_grid.value.size

    ws.retrievalDefInit()
    ws.covmat1D(
        ws.covmat_block,
        grid_1=z_grid,
        sigma_1=0.1 * np.ones(n_p),  # Relative uncertainty
        cls_1=10e3 * np.ones(n_p),  # 10km correlation length
        fname="lin")
    ws.retrievalAddAbsSpecies(species="O3",
                              unit="rel",
                              g1=ws.p_grid,
                              g2=lat_ret_grid,
                              g3=lon_ret_grid)
    # Wind u-component
    ws.covmat1D(
        ws.covmat_block,
        grid_1=z_grid[::2],
        sigma_1=100.0 * np.ones(n_p // 2),  # Relative uncertainty
        cls_1=10e3 * np.ones(n_p // 2),  # 10km correlation length
        fname="lin")
    ws.retrievalAddWind(g1=ws.p_grid.value[::2],
                        g2=np.array([np.mean(ws.lat_grid)]),
                        g3=np.array([np.mean(ws.lon_grid)]),
                        component="u")
    # Wind v-component
    ws.covmat1D(
        ws.covmat_block,
        grid_1=z_grid[::2],
        sigma_1=100.0 * np.ones(n_p // 2),  # Relative uncertainty
        cls_1=10e3 * np.ones(n_p // 2),  # 10km correlation length
        fname="lin")
    ws.retrievalAddWind(g1=ws.p_grid.value[::2],
                        g2=np.array([np.mean(ws.lat_grid)]),
                        g3=np.array([np.mean(ws.lon_grid)]),
                        component="v")
    ws.retrievalDefClose()
    ws.covmatDiagonal(ws.covmat_block,
                      ws.covmat_inv_block,
                      vars=0.0001 * np.ones(ws.y.value.shape))
    ws.covmat_seSet(ws.covmat_block)

    @arts_agenda
    def inversion_iterate_agenda(ws):
        ws.x2artsStandard()
        ws.atmfields_checkedCalc()
        ws.atmgeom_checkedCalc()
        ws.yCalc()
        ws.Print(ws.y)
        ws.Print(ws.jacobian)
        ws.VectorAddVector(ws.yf, ws.y, ws.y_baseline)
        ws.IndexAdd(ws.inversion_iteration_counter,
                    ws.inversion_iteration_counter, 1)

    ws.Copy(ws.inversion_iterate_agenda, inversion_iterate_agenda)

    # A Priori State
    # For the a priori state we assume zero wind in any direction. The a priori vector for the OEM is created by
    # the `xaStandard` WSM, which computes $x_a$ from the current atmospheric state.
    ws.wind_u_field.value[:] = 0.0
    ws.wind_v_field.value[:] = 0.0
    ws.xaStandard()

    # The OEM Calculation
    ws.x = np.zeros(0)
    ws.jacobian = np.zeros((0, 0))
    ws.y.value[:] = y
    ws.OEM(method="lm",
           max_iter=20,
           display_progress=1,
           lm_ga_settings=np.array([100.0, 2.0, 2.0, 10.0, 1.0, 1.0]))
    ws.x2artsStandard()

    z = ws.z_field.value[:, 0, 0].ravel()
    wind_u = ws.wind_u_field.value[z > 40e3, 0, 0]
    wind_v = ws.wind_v_field.value[z > 40e3, 0, 0]
    assert np.allclose(wind_u, u_wind, atol=1)
    assert np.allclose(wind_v, v_wind, atol=1)