示例#1
0
    def structure_data(self):
        ra = Unit.hms_to_rad(self._array["ra_h"], self._array["ra_m"], self._array["ra_s"])
        dec = Unit.dms_to_rad(self._array["dec_deg"], self._array["dec_m"], self._array["dec_s"])

        src_type = dict(vcs=True, non_vcs=False, defining=False, special=False, undefined=False)
        self.data = {
            src["iers_name"]: dict(icrf_name=src["icrf_name"], ra=ra[i], dec=dec[i], **src_type)
            for i, src in enumerate(self._array)
        }
示例#2
0
def partial_vectors(dset, estimator_config_key):
    """Call all partials specified in the configuration and set up the corresponding state vector

    The list of partials to calculate is taken from the config file of the given technique. Each partial calculator is
    passed a :class:`~where.data.dataset.Dataset` with data for the modelrun and should return a tuple with the partial
    vectors and their names.

    Args:
        dset (Dataset):                 A Dataset containing model run data.
        estimator_config_key (String):  Key in config file with the name of the estimator.

    Returns:
        Dict: List of names of the partial derivatives for each partial config key.
    """
    partial_vectors = dict()
    prefix = dset.vars["pipeline"]

    for config_key in estimators.partial_config_keys(estimator_config_key):
        partial_vectors[config_key] = list()
        partials = config.tech[config_key].list
        partial_data = plugins.call_all(package_name=__name__,
                                        plugins=partials,
                                        prefix=prefix,
                                        dset=dset)

        for param, (data, names, data_unit) in partial_data.items():
            param_unit_cfg = config.tech[param].unit
            if not param_unit_cfg.str:
                log.fatal(
                    f"No unit given for parameter {param!r} in {param_unit_cfg.source}"
                )

            display_unit = config.tech[param].display_unit.str
            display_unit = param_unit_cfg.str if not display_unit else display_unit
            partial_unit_str = f"{dset.unit('calc')[0]} / ({param_unit_cfg.str})"
            partial_unit = str(Unit(partial_unit_str).u)
            factor = Unit(data_unit, partial_unit)
            for values, name in zip(data.T, names):
                partial_name = f"{param}-{name}" if name else f"{param}"
                partial_vectors[config_key].append(partial_name)

                field_name = f"partial.{partial_name}"
                if field_name in dset.fields:
                    dset[field_name][:] = values * factor
                else:
                    dset.add_float(field_name,
                                   val=values * factor,
                                   unit=partial_unit,
                                   write_level="operational")
                    dset.meta.add(partial_name,
                                  display_unit,
                                  section="display_units")

    return partial_vectors
示例#3
0
def partial_vectors(dset, estimator_config_key):
    """Call all partials specified in the configuration and set up the corresponding state vector

    The list of partials to calculate is taken from the config file of the given technique. Each partial calculator is
    passed a :class:`~where.data.dataset.Dataset` with data for the modelrun and should return a tuple with the partial
    vectors and their names.

    Args:
        dset (Dataset):                 A Dataset containing model run data.
        estimator_config_key (String):  Key in config file with the name of the estimator.

    Returns:
        Dict: List of names of the partial derivatives for each partial config key.
    """
    partial_vectors = dict()
    prefix = config.analysis.get("analysis", default="").str

    for config_key in estimators.partial_config_keys(estimator_config_key):
        partial_vectors[config_key] = list()
        partial_data = plugins.call_all(package_name=__name__,
                                        config_key=config_key,
                                        prefix=prefix,
                                        dset=dset)

        for param, (data, names, data_unit) in partial_data.items():
            param_unit_cfg = config.tech[param].unit
            if not param_unit_cfg.str:
                log.fatal(
                    f"No unit given for parameter {param!r} in {param_unit_cfg.source}"
                )

            display_unit = config.tech[param].display_unit.str
            display_unit = param_unit_cfg.str if not display_unit else display_unit
            partial_unit = str(
                Unit("{} / ({})".format(dset.unit("calc"),
                                        param_unit_cfg.str)).u)
            factor = Unit(data_unit, partial_unit)
            for values, name in zip(data.T, names):
                partial_name = "{}-{}".format(param, name)
                dset.add_float(
                    "partial_" + partial_name,
                    table="partial",
                    val=values * factor,
                    unit=partial_unit,
                    write_level="operational",
                )
                dset.add_to_meta("display_units", partial_name, display_unit)
                partial_vectors[config_key].append(partial_name)

    return partial_vectors
示例#4
0
    def __init__(self, text="Elapsed time:", unit=None, logger=log.time):
        """Set up a new timer

        The text to be shown when logging the timer can be customized. Typically, the value of the timer will be added
        at the end of the string (e.g. 'Elapsed time: 0.1234 seconds'). However, this can be customized by adding a
        '{}' to the text. For example `text='Used {} to run the code'` will produce something like 'Used 0.1234 seconds
        to run the code'.

        Args:
            text (String):      Text used when logging the timer (see above).
            unit (String):      Unit used for logging the timer (Default is seconds).
            logger (Function):  Function used to do the logging.
        """
        super().__init__()
        self._start = None
        self._end = None
        self.text = text if "{}" in text else (text + " {}").strip()
        self.unit_name = "seconds" if unit is None else unit
        self.unit_factor = 1 if unit is None else Unit("seconds", unit)
        self.logger = logger

        # Use midgard instead
        caller = sys._getframe(1)
        func_name = caller.f_code.co_name
        file_name = caller.f_code.co_filename
        line_num = caller.f_lineno
        log.dev(
            f"{file_name} ({line_num}) {func_name}: where.lib.timer is deprecated, use midgard.dev.timer instead"
        )
示例#5
0
def saastamoinen_zenith_wet_delay(latitude, height, temperature, e):
    r"""Calculates zenith wet delay based Saastamoinen model

    The total tropospheric delay for a given zenith distance :math:`z` is determined after Equation (19a) in
    Saastamoinen :cite:`saastamoinen1972`:

    .. math::
       \Delta T = 0.002277 \cdot \sec z \cdot (p + (1255/T + 0.05) \cdot e) - 1.16 \cdot \tan^2 z

    The zenith tropospheric delay is determined with :math:`z = 0`:

    .. math::
       \Delta T^z = \Delta T_h^z + \Delta T_w^z = zhd + zwd

    with the zenith hydrostatic delay :math:`zhd = 0.002277 \cdot p` and zenith wet delay
    :math:`zwd = 0.002277 \cdot (1255/T + 0.05) \cdot e`.

    Args:
        latitude (numpy.ndarray):     Geodetic latitude for each observation in [rad]
        height (numpy.ndarray):       Orthometric height for each observation in [m]
        temperature (numpy.ndarray):  Temperature for each observation in [Celsius]
        e (numpy.ndarray):            Water vapor pressure for each observation in [hPa]

    Returns:
        numpy.ndarray:     Zenith wet delay for each observation in [m]
    """
    # Zenith wet delay based on Eq. (19a) in Saastamoinen :cite:`saastamoinen1972`
    return 0.002_276_8 * (1255 / Unit.celsius_to_kelvin(temperature) +
                          0.05) * e
示例#6
0
    def parse_radio_source(self, line, _):
        """Read station position

        Reads the station position from the NGS file.

        Args:
            line:  Input data from NGS file
        """
        src_name = line["name"]
        self.data[src_name] = dict()
        self.data[src_name]["ra"] = Unit.hms_to_rad(float(line["ra_hrs"]),
                                                    int(line["ra_mins"]),
                                                    float(line["ra_secs"]))
        self.data[src_name]["dec"] = Unit.dms_to_rad(
            float(line["dec_degs"].replace(" ", "")), int(line["dec_mins"]),
            float(line["dec_secs"]))
示例#7
0
    def __init__(self, name, num_obs, dataset):
        """Direction-init doc?

            dataset:    A reference to the master Dataset. This is needed to lookup _time_obj and _other_obj.
        """
        super().__init__(name, num_obs, dataset)

        # Add units for DirectionTable-properties (set by @Unit.register)
        self._prop_units = Unit.units_dict(__name__)
示例#8
0
    def __init__(self, name, num_obs, dataset):
        super().__init__(name, num_obs, dataset)

        # Add units for PosVelTable-properties (set by @Unit.register)
        self._prop_units.update(Unit.units_dict(__name__))

        # Add columns for velocity
        self._itrs = np.full((self.num_obs, 6), np.nan, dtype=float)
        self._gcrs = np.full((self.num_obs, 6), np.nan, dtype=float)
示例#9
0
    def parse_obs(self, unit_in="meter", except_fields=()):
        """Read information about an observation

        Stores the information in the temporary cache-dict, which will be transfered to self.data when all information
        about this observation is parsed. If `unit_in` is given, all values will be converted to meter unless the field
        is listed in the `except_fields`-list.

        Args:
            unit_in (String):      Name of unit of values to be parsed.
            except_fields (Tuple): Names of fields where values should not be converted to meters.
        """
        # Find scale factor for converting to meter
        if unit_in == "meter":
            scale_factor = 1
        else:
            quantity = Unit(unit_in)
            try:
                scale_factor = quantity.to("meter").magnitude
            except Unit.DimensionalityError:
                # Try to convert between time and length by multiplying by the speed of light
                scale_factor = (
                    quantity * constant.c *
                    Unit("meters per second")).to("meter").magnitude
        obs = {}

        # Define the function doing the actual parsing
        def parse_func(line, cache):
            for field in line:
                if field.startswith("flag_"):  # Flags are currently ignored
                    continue
                try:
                    obs[field] = float(line[field])
                except ValueError:
                    obs[field] = 0
                    log.debug(
                        f"Could not convert {line['field']} to a number for {field}. Value set to 0.0"
                    )
                if field not in except_fields:
                    obs[field] *= scale_factor
            for field, value in obs.items():
                self.data.setdefault(field, list()).append(value)

        return parse_func
示例#10
0
    def _convert_dms2rad(self, field):
        """Convert DMS (degrees, minutes, seconds) to radians

        Args:
            field (String):   Original field with degrees, minutes, seconds separated by whitespace.

        Returns:
            Float:  Field converted to radians.
        """
        degrees, minutes, seconds = [float(f) for f in field.split()]
        return Unit.dms_to_rad(degrees, minutes, seconds)
示例#11
0
    def site_id(self):
        """Mandatory block.

        Content:
        *Code PT Domes____ T Station description___ Approx_lon_ Approx_lat_ App_h__
        """
        self.fid.write("+SITE/ID\n")
        self.fid.write(
            "*Code PT Domes____ T Station description___ Approx_lon_ Approx_lat_ App_h__\n"
        )
        for sta in self.dset.unique("station"):
            site_id = self.dset.meta[sta]["site_id"]
            domes = self.dset.meta[sta]["domes"]
            marker = self.dset.meta[sta]["marker"]
            height = self.dset.meta[sta]["height"]
            description = self.dset.meta[sta]["description"][0:22]
            long_deg, long_min, long_sec = Unit.rad_to_dms(
                self.dset.meta[sta]["longitude"])
            lat_deg, lat_min, lat_sec = Unit.rad_to_dms(
                self.dset.meta[sta]["latitude"])

            self.fid.write(
                " {} {:>2} {:5}{:4} {:1} {:<22} {:>3.0f} {:>2.0f} {:4.1f} {:>3.0f} {:>2.0f} {:4.1f} {:7.1f}"
                "\n".format(
                    site_id,
                    "A",
                    domes,
                    marker,
                    _TECH[self.dset.meta["tech"]],
                    description,
                    (long_deg + 360) % 360,
                    long_min,
                    long_sec,
                    lat_deg,
                    lat_min,
                    lat_sec,
                    height,
                ))
        self.fid.write("-SITE/ID\n")
示例#12
0
    def __init__(self, name, num_obs, dataset):
        """Position-init doc?

            dataset:    A reference to the master Dataset. This is needed to lookup _time_obj and _other_obj.
        """
        super().__init__(name, num_obs, dataset)

        # Add units for PositionTable-properties (set by @Unit.register)
        self._prop_units = Unit.units_dict(__name__)

        # Organize data in attributes instead of a data-dict
        self._itrs = np.full((self.num_obs, 3), np.nan, dtype=float)
        self._gcrs = np.full((self.num_obs, 3), np.nan, dtype=float)
        self._time_obj = None
        self._other_obj = None
        self._dependent_on_me = list()
示例#13
0
def davis_zenith_wet_delay(latitude, height, temperature, e):
    r"""Calculates zenith wet delay based on Saastamoinen/Davis model

    The total tropospheric delay for a given zenith distance :math:`z` is determined after Equation (19a) in
    Saastamoinen :cite:`saastamoinen1972`:

    .. math::
       \Delta T = 0.002277 \cdot \sec z \cdot (p + (1255/T + 0.05) \cdot e) - 1.16 \cdot \tan^2 z

    The zenith tropospheric delay is determined with :math:`z = 0`:

    .. math::
       \Delta T^z = \Delta T_h^z + \Delta T_w^z = zhd + zwd

    with the zenith hydrostatic delay :math:`zhd = 0.002277 \cdot p` and zenith wet delay :math:`zwd = 0.002277 \cdot
    (1255/T + 0.05) \cdot e`.

    A Fortran routine written by Davis corrects also for the gravity effect due to height :math:`H` and latitude
    :math:`\phi` (see http://acc.igs.org/tropo/wetsaas.f) and uses the constant :math:`0.0022768` instead of
    :math:`0.002277`, which leads to

    .. math::
       zwd = 0.0022768 \cdot (1255/T + 0.05) \cdot e / (1 - 0.00266 \cos 2 \phi - 0.00000028 H) .

    The difference between the orthometric height and geodetic height is called geoid undulation and can reach up to
    100 m due to Boehm et al. :cite:`boehm2007`. The influence of height difference is not significant in this
    equation.  The geodetic (ellipsoidal) height is therefore often used instead of the orthometric height.

    Args:
        latitude (numpy.ndarray):     Geodetic latitude for each observation in [rad]
        height (numpy.ndarray):       Orthometric height for each observation in [m]
        temperature (numpy.ndarray):  Temperature for each observation in [Celsius]
        e (numpy.ndarray):            Water vapor pressure for each observation in [hPa]

    Returns:
        numpy.ndarray:    Zenith wet delay for each observation in [m]
    """
    gravity_corr = saastamoinen_gravity_correction(latitude, height)

    # Zenith wet delay based on Eq. (19a) in Saastamoinen :cite:`saastamoinen1972` with additional gravity
    # correction
    zwd = 0.002_276_8 * (1255 / Unit.celsius_to_kelvin(temperature) +
                         0.05) * e / gravity_corr

    return zwd
示例#14
0
class Eop:
    """A class that can calculate EOP corrections.

    One instance of the `Eop`-class calculates corrections for a given set of time epochs (specified when the instance
    is created). However, all instances share a cache of results from the various functions calculating corrections.
    """

    _correction_cache = dict()

    def __init__(self,
                 eop_data,
                 time,
                 models=None,
                 pole_model=None,
                 window=4,
                 sources=None):
        """Create an Eop-instance that calculates EOP corrections for the given time epochs

        The interpolation window is based on https://hpiers.obspm.fr/iers/models/interp.f which uses 4 days.

        Args:
            eop_data (Dict): Dictionary of tabular EOP data, typically read from file.
            time (Time):     Time epochs for which to calculate EOPs.
            models (Tuple):  Optional tuple of EOP correction models. If not given, the config setting is used.
            window (Int):    Number of days to use as interpolation window.
        """
        self.window = window
        self.time = time
        self.sources = sources
        self.data = self.pick_data(eop_data, self.time, self.window, sources)
        self.calculate_leap_second_offset()

        # Figure out which correction models to use
        self.models = config.tech.get("eop_models", value=models,
                                      default="").list

        if "rg_zont2" in self.models:
            self.remove_low_frequency_tides()

        # Figure out which pole model to use:
        self.pole_model = config.tech.get("eop_pole_model",
                                          value=pole_model,
                                          default=None).str
        if self.pole_model == "mean_2015":
            # Read the tabulated data needed for the model
            data = parsers.parse_key("eop_mean_pole_2015").as_dict()
            self.mean_pole_last_idx = len(data["year"]) - 1
            self.mean_pole_years = interpolate.interp1d(
                data["year"],
                data["year"],
                kind="previous",
                fill_value="extrapolate")
            self.mean_pole_idx = interpolate.interp1d(data["year"],
                                                      range(len(data["year"])),
                                                      kind="previous",
                                                      fill_value=np.nan,
                                                      bounds_error=False)
            self.mean_pole_x = interpolate.interp1d(range(len(data["x"])),
                                                    data["x"],
                                                    kind="previous",
                                                    fill_value="extrapolate")
            self.mean_pole_y = interpolate.interp1d(range(len(data["y"])),
                                                    data["y"],
                                                    kind="previous",
                                                    fill_value="extrapolate")

    @staticmethod
    def pick_data(eop_data, time, window, sources):
        """Pick out subset of eop_data relevant for the given time epochs and interpolation window

        Args:
            eop_data (Dict):   Dictionary of EOP data indexed by MJD dates.
            time (Time):       Time epochs for which to calculate EOPs.
            window (Int):      Interpolation window [days].

        Returns:
            Dict: EOP data subset to the time period needed.
        """
        if time.isscalar:
            start_time = np.floor(time.utc.mjd) - window // 2
            end_time = np.ceil(time.utc.mjd) + window // 2
        else:
            start_time = np.floor(time.min().utc.mjd) - window // 2
            end_time = np.ceil(time.max().utc.mjd) + window // 2

        sources = config.tech.get("eop_sources", value=sources).list
        for source in sources:
            try:
                return {
                    d: eop_data[source][d].copy()
                    for d in np.arange(start_time, end_time + 1)
                }
            except KeyError:
                pass

        # No data found if we reached this point
        paths = [str(files.path(f"eop_{k}")) for k in sources]
        raise MissingDataError(
            "Not all days in the time period {:.0f} - {:.0f} MJD were found in EOP-files {}"
            "".format(start_time, end_time, ", ".join(paths)))

    def calculate_leap_second_offset(self):
        """Calculate leap second offsets for each day

        Use the difference between UTC and TAI as a proxy for the leap second offset. The leap second offset is
        calculated and stored to the EOP data-dictionary. This is used to correct for the leap second jumps when
        interpolating the UT1 - UTC values.
        """
        days = Time(np.array(list(self.data.keys())),
                    format="mjd",
                    scale="utc")
        leap_offset = np.round(
            (days.utc.mjd - days.tai.mjd) * Unit.day2seconds)
        daily_offset = {int(d): lo for d, lo in zip(days.mjd, leap_offset)}

        for d, lo in daily_offset.items():
            self.data[d]["leap_offset"] = lo

    def remove_low_frequency_tides(self):
        """Remove the effect of low frequency tides.

        Tidal variations in the Earth's rotation with periods from 5 days to 18.6 years is present in the UT1-UTC time
        series as described in the IERS Conventions 2010 chapter 8.1. To improve the interpolation of the UT1-UTC time
        series this effect can be removed. In that case the effect needs to be added again to the final interpolated
        values.
        """
        for mjd in self.data.keys():
            # Julian centuries since J2000
            t = Time(mjd, format="mjd")
            t_julian_centuries = (t.tt.jd - 2_451_545.0) / 36525
            dut1_corr = iers.rg_zont2(t_julian_centuries)[0]
            self.data[mjd]["ut1_utc"] -= dut1_corr

    @cache.property
    @Unit.register("arcseconds")
    def x(self):
        """X-motion of the Celestial Intermediate Pole

        See section 5.5.1 in IERS Conventions, :cite:`iers2010`.

        Returns:
            Array: X-motion of the CIP, one value for each time epoch [arcseconds].
        """
        values = self._interpolate_table("x")
        values += self._corrections(("ortho_eop", iers.ortho_eop, 0, 1e-6),
                                    ("pmsdnut2", iers.pmsdnut2, 0, 1e-6))
        return values

    @cache.property
    @Unit.register("arcseconds per day")
    def x_rate(self):
        """X-motion of the Celestial Intermediate Pole

        See section 5.5.1 in IERS Conventions, :cite:`iers2010`.

        Returns:
            Array: X-motion of the CIP, one value for each time epoch [arcseconds].
        """
        values = self._interpolate_table("x", derivative_order=1)
        # values += self._corrections(('ortho_eop', iers.ortho_eop, 0, 1e-6),
        #                            ('pmsdnut2', iers.pmsdnut2, 0, 1e-6))
        return values

    @cache.property
    @Unit.register("arcseconds")
    def x_pole(self):
        return getattr(self, f"x_{self.pole_model}")()

    #
    # x-pole models
    #
    def x_secular(self):
        """Returns the x-coordinate of the secular pole

        See chapter 7 in IERS Conventions, ::cite:`iers2010`:

        Returns:
            Array: Secular X-motion of the CIP, one value for each time epoch [arcseconds].
        """
        # IERS conventions 2010 v.1.2.0 (chapter 7, equation 21)
        return (55.0 + 1.677 *
                (self.time.jyear - 2000)) * Unit.milliarcsec2arcsec

    def x_mean_2015(self):
        """x-coordindate of Conventional mean pole model version 2015

        Reimplementation of IERS Conventions 2010 Software function IERS_CMP_2015.F
        (ftp://maia.usno.navy.mil/conventions/2010/2010_update/chapter7/software/IERS_CMP_2015.F)

        Units: Arcseconds
        """
        epochs = self.time.jyear
        mean_pole_idx = self.mean_pole_idx(epochs)
        # Inside of tabulated data range
        in_range_idx = ~np.isnan(mean_pole_idx)
        dt = epochs[in_range_idx] - self.mean_pole_years(epochs[in_range_idx])
        idx = mean_pole_idx[in_range_idx]
        x = np.full(len(epochs), fill_value=np.nan)
        x[in_range_idx] = self.mean_pole_x(
            idx) + dt * (self.mean_pole_x(idx + 1) - self.mean_pole_x(idx))

        # Extrapolate outside of tabulated data range
        dt = epochs[~in_range_idx] - self.mean_pole_years(
            epochs[~in_range_idx])
        x[~in_range_idx] = self.mean_pole_x(self.mean_pole_last_idx) + dt * (
            self.mean_pole_x(self.mean_pole_last_idx) -
            self.mean_pole_x(self.mean_pole_last_idx - 1))
        return x

    def x_mean_2010(self):
        """x-coordindate of Conventional mean pole model version 2010

        Reimplementation of IERS Conventions 2010 Software function IERS_CMP_2015.F
        (ftp://maia.usno.navy.mil/conventions/2010/2010_update/chapter7/software/IERS_CMP_2015.F)

        Units: Arcseconds
        """
        epochs = self.time.jyear
        dt = epochs - 2000.0
        idx = dt < 10
        x = np.zeros(len(epochs))
        x[idx] = 0.055_974 + 0.001_824_3 * dt[idx] + 0.000_184_13 * dt[
            idx]**2 + 0.000_007_024 * dt[idx]**3
        x[~idx] = 0.23513 + 0.007_614_1 * dt[~idx]
        return x

    def x_mean_2003(self):
        """x-coordindate of Conventional mean pole model version 2003

        Reimplementation of IERS Conventions 2010 Software function IERS_CMP_2015.F
        (ftp://maia.usno.navy.mil/conventions/2010/2010_update/chapter7/software/IERS_CMP_2015.F)

        Units: Arcseconds
        """
        return 0.054 + 0.00083 * (self.time.jyear - 2000.0)

    @cache.property
    @Unit.register("arcseconds")
    def y_pole(self):
        return getattr(self, f"y_{self.pole_model}")()

    #
    # y-pole models
    #
    def y_secular(self):
        """Returns the x-coordinate of the secular pole

        See chapter 7 in IERS Conventions, ::cite:`iers2010`:

        Returns:
            Array: Mean X-motion of the CIP, one value for each time epoch [arcseconds].
        """
        # IERS conventions 2010 v.1.2.0 (chapter 7, equation 21)
        return (320.5 + 3.460 *
                (self.time.jyear - 2000)) * Unit.milliarcsec2arcsec

    def y_mean_2015(self):
        """y-coordindate of Conventional mean pole model version 2015

        Reimplementation of IERS Conventions 2010 Software function IERS_CMP_2015.F
        (ftp://maia.usno.navy.mil/conventions/2010/2010_update/chapter7/software/IERS_CMP_2015.F)

        Units: Arcseconds
        """
        epochs = self.time.jyear
        mean_pole_idx = self.mean_pole_idx(epochs)
        # Inside of tabulated data range
        in_range_idx = ~np.isnan(mean_pole_idx)
        dt = epochs[in_range_idx] - self.mean_pole_years(epochs[in_range_idx])
        idx = mean_pole_idx[in_range_idx]
        y = np.full(len(epochs), fill_value=np.nan)
        y[in_range_idx] = self.mean_pole_y(
            idx) + dt * (self.mean_pole_y(idx + 1) - self.mean_pole_y(idx))

        # Extrapolate outside of tabulated data range
        dt = epochs[~in_range_idx] - self.mean_pole_years(
            epochs[~in_range_idx])
        y[~in_range_idx] = self.mean_pole_y(self.mean_pole_last_idx) + dt * (
            self.mean_pole_y(self.mean_pole_last_idx) -
            self.mean_pole_y(self.mean_pole_last_idx - 1))
        return y

    def y_mean_2010(self):
        """y-coordindate of Conventional mean pole model version 2010

        Reimplementation of IERS Conventions 2010 Software function IERS_CMP_2015.F
        (ftp://maia.usno.navy.mil/conventions/2010/2010_update/chapter7/software/IERS_CMP_2015.F)

        Units: Arcseconds
        """
        epochs = self.time.jyear
        dt = epochs - 2000.0
        idx = dt < 10
        y = np.zeros(len(epochs))
        y[idx] = 0.346_346 + 0.001_789_6 * dt[idx] - 0.000_107_29 * dt[
            idx]**2 - 0.000_000_908 * dt[idx]**3
        y[~idx] = 0.358_891 - 0.000_628_7 * dt[~idx]
        return y

    def y_mean_2003(self):
        """y-coordindate of Conventional mean pole model version 2003

        Reimplementation of IERS Conventions 2010 Software function IERS_CMP_2015.F
        (ftp://maia.usno.navy.mil/conventions/2010/2010_update/chapter7/software/IERS_CMP_2015.F)

        Units: Arcseconds
        """
        return 0.357 + 0.00395 * (self.time.jyear - 2000.0)

    @cache.property
    @Unit.register("arcseconds")
    def y(self):
        """Y-motion of the Celestial Intermediate Pole

        See section 5.5.1 in IERS Conventions, :cite:`iers2010`.

        Returns:
            Array: Y-motion of the CIP, one value for each time epoch [arcseconds].
        """
        values = self._interpolate_table("y")
        values += self._corrections(("ortho_eop", iers.ortho_eop, 1, 1e-6),
                                    ("pmsdnut2", iers.pmsdnut2, 1, 1e-6))
        return values

    @cache.property
    @Unit.register("arcseconds per day")
    def y_rate(self):
        """X-motion of the Celestial Intermediate Pole

        See section 5.5.1 in IERS Conventions, :cite:`iers2010`.

        Returns:
            Array: X-motion of the CIP, one value for each time epoch [arcseconds].
        """
        values = self._interpolate_table("y", derivative_order=1)
        # values += self._corrections(('ortho_eop', iers.ortho_eop, 0, 1e-6),
        #                            ('pmsdnut2', iers.pmsdnut2, 0, 1e-6))
        return values

    @cache.property
    @Unit.register("seconds")
    def ut1_utc(self):
        """Delta between UT1 and UTC

        See section 5.5.3 in IERS Conventions, :cite:`iers2010`. Does correction for leap second jumps before
        interpolation.

        Reapplies low frequency tides if these were removed before interpolation.

        Returns:
            Array: UT1 - UTC, one value for each time epoch [seconds].
        """
        values = self._interpolate_table("ut1_utc",
                                         leap_second_correction=True)
        values += self._corrections(("ortho_eop", iers.ortho_eop, 2, 1e-6),
                                    ("utlibr", iers.utlibr, 0, 1e-6))

        # low frequency tides
        if "rg_zont2" in self.models:
            correction_cache = self._correction_cache.setdefault(
                "rg_zont2", dict())
            # Julian centuries since J2000
            t_julian_centuries = (self.time.tt.jd - 2_451_545.0) / 36525

            if self.time.isscalar:
                mjd = self.time.tt.mjd
                if mjd not in correction_cache:
                    correction_cache[mjd] = iers.rg_zont2(
                        t_julian_centuries)[0]
                dut1_corr = correction_cache[mjd]
            else:
                dut1_corr = list()
                for t in self.time.tt:
                    if t.mjd not in correction_cache:
                        t_julian_centuries = (t.tt.jd - 2_451_545.0) / 36525
                        correction_cache[t.mjd] = iers.rg_zont2(
                            t_julian_centuries)[0]
                    dut1_corr.append(correction_cache[t.mjd])

            values += dut1_corr
        return values

    @cache.property
    @Unit.register("seconds per day")
    def ut1_utc_rate(self):
        """Delta between UT1 and UTC

        See section 5.5.3 in IERS Conventions, :cite:`iers2010`. Does correction for leap second jumps before
        interpolation.

        Reapplies low frequency tides if these were removed before interpolation.

        TODO: apply models based on eop.models
        Only works if eop.models = ()

        Returns:
            Array: UT1 - UTC, one value for each time epoch [seconds].
        """
        values = self._interpolate_table("ut1_utc",
                                         leap_second_correction=True,
                                         derivative_order=1)
        # values += self._corrections(("ortho_eop", iers.ortho_eop, 2, 1e-6), ("utlibr", iers.utlibr, 0, 1e-6))

        # Low frequency tides
        #         if "rg_zont2" in self.models:
        #             correction_cache = self._correction_cache.setdefault("rg_zont2", dict())
        #             # Julian centuries since J2000
        #             t_julian_centuries = (self.time.tt.jd - 2451545.0) / 36525
        #
        #             if self.time.isscalar:
        #                 mjd = self.time.tt.mjd
        #                 if mjd not in correction_cache:
        #                     correction_cache[mjd] = iers.rg_zont2(t_julian_centuries)[0]
        #                 dut1_corr = correction_cache[mjd]
        #             else:
        #                 dut1_corr = list()
        #                 for t in self.time.tt:
        #                     if t.mjd not in correction_cache:
        #                         t_julian_centuries = (t.tt.jd - 2451545.0) / 36525
        #                         correction_cache[t.mjd] = iers.rg_zont2(t_julian_centuries)[0]
        #                     dut1_corr.append(correction_cache[t.mjd])
        #
        #             values += dut1_corr
        #         return values
        return values

    @cache.property
    @Unit.register("seconds")
    def lod(self):
        """Length of day

        See section 5.5.3 in IERS Conventions, :cite:`iers2010`.

        TODO: How should this be implemented? Previous implementation simply used nearest value. Is this in the
        conventions?

        Returns:
            Array: Length of day, one value for each time epoch [seconds].
        """
        values = self._interpolate_table("lod")
        return values

    @cache.property
    @Unit.register("arcseconds")
    def dx(self):
        """X-offset of the Celestial Intermediate Pole

        See section 5.5.? in IERS Conventions, :cite:`iers2010`.

        Returns:
            Array: X-offset of the CIP, one value for each time epoch [arcseconds].
        """
        values = self._interpolate_table("dx")
        return values

    @cache.property
    @Unit.register("arcseconds")
    def dy(self):
        """Y-offset of the Celestial Intermediate Pole

        See section 5.5.? in IERS Conventions, :cite:`iers2010`.

        Returns:
            Array: Y-offset of the CIP, one value for each time epoch [arcseconds].
        """
        values = self._interpolate_table("dy")

        return values

    def _interpolate_table(self,
                           key,
                           leap_second_correction=False,
                           derivative_order=0):
        """Interpolate daily values to the given time epochs

        Uses Lagrange interpolation with the given interpolation window.

        We have observed that the Lagrange interpolation introduces instabilities when the EOP data are constant (as
        for instance in the VASCC-data). In this case, we force the Lagrange polynomial to be constant.

        Args:
            key (String):                   Name of data to be interpolated, key in `self.data`.
            leap_second_correction (Bool):  Whether data should be corrected for leap seconds before interpolation.

        Returns:
            Array: Interpolated values, one value for each time epoch.
        """
        days = np.unique(self.time.utc.mjd_int)
        offsets = range(-math.ceil(self.window / 2) + 1,
                        math.floor(self.window / 2) + 1)

        if leap_second_correction:
            leap = {
                d: np.array([
                    self.data[d + o].get("leap_offset", np.nan) -
                    self.data[d]["leap_offset"] for o in offsets
                ])
                for d in days
            }
            for lo in leap.values():
                lo[np.isnan(lo)] = 0
        else:
            leap = {d: 0 for d in days}

        table_values = {
            d: np.array([self.data[d + o][key] for o in offsets]) + leap[d]
            for d in days
        }
        interpolators = {
            d: interpolate.lagrange(offsets, v)
            for d, v in table_values.items()
        }
        for poly in interpolators.values():
            poly.c[
                np.abs(poly.c) <
                1e-15] = 0  # Avoid numerical instabilities for constant values

        if derivative_order:
            interp_values = {
                d: np.polyder(ip, derivative_order)(self.time.utc.mjd_frac)
                for d, ip in interpolators.items()
            }
        else:
            interp_values = {
                d: ip(self.time.utc.mjd_frac)
                for d, ip in interpolators.items()
            }

        if self.time.isscalar:
            return interp_values[self.time.utc.mjd_int]

        values = np.empty(self.time.size)
        for day in days:
            idx = self.time.utc.mjd_int == day
            values[idx] = interp_values[day][idx]

        return values

    def _corrections(self, *correction_models):
        """Calculate corrections to tabular values

        The correction models are specified as tuples with name, function, output column and scale factor. Calls to the
        correction functions are cached since some correction functions are used by several EOP-values.

        Args:
            correction_models (Tuple): Specification of correction models (see above)

        Returns:
            Array: Corrections to tabular values, one value for each time epoch.
        """
        corrections = 0 if self.time.isscalar else np.zeros(self.time.size)
        for name, correction_func, out_idx, factor in correction_models:
            if name not in self.models:
                continue

            correction_cache = self._correction_cache.setdefault(name, dict())
            if self.time.isscalar:
                mjd = self.time.tt.mjd
                if mjd not in correction_cache:
                    correction_cache[mjd] = correction_func(mjd)
                corrections += factor * correction_cache[mjd][out_idx]
            else:
                for idx, mjd in enumerate(self.time.tt.mjd):
                    if mjd not in correction_cache:
                        correction_cache[mjd] = correction_func(mjd)

                    corrections[idx] += factor * correction_cache[mjd][out_idx]

        return corrections

    # Add methods to deal with units for Eop-properties (set by @Unit.register)
    convert_to = Unit.convert_factory(__name__)
    unit_factor = staticmethod(Unit.factor_factory(__name__))
    unit = staticmethod(Unit.unit_factory(__name__))
示例#15
0
文件: _kalman.py 项目: uasau/where
    def _add_fields(self, dset, param_names):
        """Add fields to the given dataset

        Adds fields for state vectors and estimate vectors for each parameter. Parameters with names ending with an
        underscore, `_`, are not added to the dataset.

        Args:
            dset (Dataset):       The dataset.
            param_names (List):   Strings with names of parameters. Used to form field names.

        """
        for idx, param_name in enumerate(param_names):
            if param_name.endswith("_"):
                continue

            # State vectors
            fieldname = f"state.{param_name}"
            fieldname_sigma = fieldname + "_sigma"
            value = self.x_smooth[:dset.num_obs, idx, 0]
            value_sigma = np.sqrt(self.x_hat_ferr[:dset.num_obs, idx])

            if fieldname in dset.fields:
                dset[fieldname][:] = value * dset.meta["display_factors"][
                    param_name]
            else:
                # Convert values to the display unit. It corresponds to "meter per <unit of partial>"
                partial_unit = dset.unit("partial.{}".format(param_name))
                to_unit = dset.meta["display_units"][param_name]
                from_unit = f"meter/({partial_unit[0]})"
                factor = Unit(from_unit, to_unit)
                dset.meta.add(param_name, factor, section="display_factors")
                dset.add_float(fieldname,
                               val=value * factor,
                               unit=to_unit,
                               write_level="operational")

            if fieldname_sigma in dset.fields:
                dset[fieldname_sigma][:] = value_sigma * dset.meta[
                    "display_factors"][param_name]
            else:
                # Convert values to the display unit. It corresponds to "meter per <unit of partial>"
                partial_unit = dset.unit("partial.{}".format(param_name))
                to_unit = dset.meta["display_units"][param_name]
                from_unit = f"meter/({partial_unit[0]})"
                factor = Unit(from_unit, to_unit)
                dset.meta.add(param_name, factor, section="display_factors")
                dset.add_float(fieldname_sigma,
                               val=value_sigma * factor,
                               unit=to_unit,
                               write_level="operational")

            # Estimate vectors
            fieldname = f"estimate.{param_name}"
            value = self.h[:dset.num_obs, idx,
                           0] * self.x_smooth[:dset.num_obs, idx, 0]
            if fieldname in dset.fields:
                dset[fieldname][:] = value
            else:
                dset.add_float(fieldname,
                               val=value,
                               unit="meter",
                               write_level="analysis")

        value = (self.x_smooth.transpose(0, 2, 1) @ self.h)[:dset.num_obs, 0,
                                                            0]
        fieldname = "est"
        if fieldname in dset.fields:
            dset[fieldname][:] = value
        else:
            dset.add_float(fieldname,
                           val=value,
                           unit="meter",
                           write_level="operational")