def _plot( dset1: "Dataset", dset2: "Dataset", ddiff: "Dataset", common_fields: Set[str], figure_dir: "pathlib.PosixPath" ) -> None: """Generate plots Args: dset1: First dataset containing the data. dset2: Second dataset containing the data. ddiff: Dataset containing the differences for each field between Dataset 'dset1' and Dataset 'dset2' common_fields: Set with fields common in both datasets figure_dir: Figure directory. """ dset1_name = config.where.gnss_compare_datasets.get("dset1_name", default="dset1").str + ":" dset2_name = config.where.gnss_compare_datasets.get("dset2_name", default="dset2").str + ":" for field in common_fields: ylabel = FIELDS[field].label if field in FIELDS.keys() else field.lower() title = FIELDS[field].title if field in FIELDS.keys() else "" unit = Unit(FIELDS[field].unit).units if field in FIELDS.keys() else Unit(dset1.unit(field)).units options = _set_plot_config(title=title) plot_scatter_subplots( x_array=dset1.time.gps.datetime, y_arrays=[dset1[field], dset2[field], ddiff[field]], xlabel="Time [GPS]", ylabels=[f"{dset1_name} {ylabel}", f"{dset2_name} {ylabel}", f"Difference: {ylabel}"], colors=["steelblue", "darkorange", "limegreen"], y_units=[f"{unit:~P}", f"{unit:~P}", f"{unit:~P}"], figure_path=figure_dir / f"plot_{field}.{FIGURE_FORMAT}", opt_args=options, )
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"] # Delete values from previous iterations if "partial" in dset.fields: del dset.partial 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}" 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
def get_field_by_attrs(dset: "Dataset", attrs: Tuple[str], unit: str) -> np.ndarray: """Get field values of a Dataset specified by the field attributes If necessary the unit of the data fields are corrected to the defined 'output' unit. Args: dset: Dataset, a dataset containing the data. attrs: Field attributes (e.g. for Time object: (<scale>, <time format>)). unit: Unit used for output. Returns: Array with Dataset field values """ f = dset for attr in attrs: f = getattr(f, attr) # Convert 'unit' if necessary if unit: field = f"{'.'.join(attrs)}" if dset.unit(field): field_unit = dset.unit(field)[0] try: log.debug( f"Convert dataset field {field} from unit {field_unit} to {unit}." ) f = f * Unit(field_unit).to(unit).m except exceptions.UnitError: log.warn(f"Cannot convert from '{field_unit}' to '{unit}'.") return f
def get_field(dset: "Dataset", field: str, attrs: Tuple[str], unit: str) -> np.ndarray: """Get field values of a Dataset specified by the field attributes If necessary the unit of the data fields are corrected to the defined 'output' unit. Args: dset: Dataset, a dataset containing the data. field: Field name. attrs: Field attributes (e.g. for Time object: (<scale>, <time format>)). unit: Unit used for output. Returns: Array with Dataset field values """ f = dset[field] for attr in attrs: f = getattr(f, attr) # Convert 'unit' if necessary if unit: field_attrs = field if len(attrs) == 0 else f"{field}.{'.'.join(attrs)}" try: field_unit = dset.unit(field_attrs)[0] except (exceptions.UnitError, TypeError) as e: log.debug(f"Skip unit conversion for field '{field_attrs}'.") return f # Skip unit conversion for text fields, which do not have a unit. try: log.debug(f"Convert dataset field {field} from unit {field_unit} to {unit}.") f = f * Unit(field_unit).to(unit).m except (exceptions.UnitError): log.warn(f"Cannot convert from '{field_unit}' to '{unit}' for field {field}.") return f
def _extend(self, other_field, memo) -> None: """Add observations from another field""" if other_field.data.ndim != self.data.ndim: raise ValueError( f"Field '{self.name}' cannot be extended. Dimensions must be equal. ({other_field.data.ndim} != {self.data.ndim})" ) try: factors = [ Unit(from_unit, to_unit) for from_unit, to_unit in zip(other_field._unit, self._unit) ] except exceptions.UnitError: raise exceptions.UnitError( f"Cannot extend field '{self.name}'. {other_field._unit} cannot be converted to {self._unit}" ) except TypeError: if self._unit == other_field._unit == None: factors = 1 else: raise exceptions.UnitError( f"Cannot extend field '{self.name}'. {other_field._unit} cannot be converted to {self._unit}" ) old_id = id(self.data) self.data = np.insert(self.data, self.num_obs, other_field.data * factors, axis=0) memo[old_id] = self.data
def _parse_observation(self, line: Dict[str, str], cache: Dict[str, Any]) -> None: """Parse observations of COST records """ # Skip slant sample lines if not line["hour"]: if float(line["minute"]) > 0: log.debug("Parsing of slant sample data is not implemented.") return if line["hour"][0].isalpha(): return # Save data in cache for key, value in line.items(): value = float(value) if value.replace('.', '').replace( '-', '').isnumeric() else value if key in UNIT_DEF.keys(): value = value * Unit(UNIT_DEF[key].from_, UNIT_DEF[key].to_) cache.setdefault(f"data_{key}", list()).append(value) cache.setdefault(f"data_time", list()).append( datetime( cache["date_data"].year, cache["date_data"].month, cache["date_data"].day, int(line["hour"]), int(line["minute"]), int(line["second"]), ))
def as_cartesian(self): """Get Euler pole of tectonic plate in cartesian coordinates (X,Y,Z) Returns: Euler pole in cartesian coordinates (X, Y, Z) in [milliarcsecond/yr] """ return np.array([self.pole.wx, self.pole.wy, self.pole.wz]) * Unit( self.pole.unit).to("milliarcsecond per year").m
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
def to_unit(self, unit: str) -> None: """Change rotation pole unit for all tectonic plates Args: unit: Define unit of rotation pole (e.g. 'radian per year', 'milliarcsecond per year') """ for plate, pole in self.poles.items(): for entry in ["wx", "wy", "wz", "dwx", "dwy", "dwz"]: setattr(pole, entry, getattr(pole, entry) * Unit(pole.unit).to(unit).m) pole.unit = unit self.poles[plate] = replace(pole) # Make a copy of RotationPole object
def as_spherical(self): """Get Euler pole of tectonic plate in spherical coordinates, that means location in latitude and longitude and magnitude of rotation https://geo.libretexts.org/Courses/University_of_California_Davis/GEL_056%3A_Introduction_to_Geophysics/Geophysics_is_everywhere_in_geology.../04%3A_Plate_Tectonics/4.07%3A_Plate_Motions_on_a_Sphere Returns: Euler pole in spherical coordinates (latitude [deg], longitude [deg], magnitude of rotation [degree per million years]) """ return self.to_spherical( np.array([self.pole.wx, self.pole.wy, self.pole.wz]) * Unit(self.pole.unit).to("milliarcsecond per year").m)
def test_as_cartesian(): """Test as_spherical() and to_cartesian() function """ pm = PlateMotion(plate="eura", model="itrf2014") crt = pm.as_cartesian() expected_crt = np.array([pm.pole.wx, pm.pole.wy, pm.pole.wz]) * Unit( pm.pole.unit).to("milliarcsecond per year").m print( f"DEBUG test_as_cartesian: crt({crt[0]:.3f}, {crt[1]:.3f}, {crt[2]:.3f}); " f"expected_crt({expected_crt[0]:.3f}, {expected_crt[1]:.3f}, {expected_crt[2]:.3f}))" ) np.testing.assert_allclose(crt, expected_crt, rtol=0, atol=1e-3)
def to_spherical(self, pole: np.ndarray) -> np.ndarray: """Convert Euler pole of tectonic plate from cartesian to spherical coordinates, that means location in latitude and longitude and magnitude of rotation https://geo.libretexts.org/Courses/University_of_California_Davis/GEL_056%3A_Introduction_to_Geophysics/Geophysics_is_everywhere_in_geology.../04%3A_Plate_Tectonics/4.07%3A_Plate_Motions_on_a_Sphere Args: pole: Euler pole array in cartesian coordinates (X,Y,Z) in [milliarcsecond per year]) Returns: Euler pole in spherical coordinates (latitude [deg], longitude [deg], magnitude of rotation [degree per million years]) """ wx = pole[0] * Unit.milliarcsec2radian wy = pole[1] * Unit.milliarcsec2radian wz = pole[2] * Unit.milliarcsec2radian latitude = np.arctan2(wz, np.sqrt(wx**2 + wy**2)) * Unit.radian2degree longitude = np.arctan2(wy, wx) * Unit.radian2degree omega = np.sqrt(wx**2 + wy**2 + wz**2) * Unit("radian per year").to( "degree per year").m * 1000000 return np.array([latitude, longitude, omega])
def to_cartesian(self, pole: np.ndarray) -> np.ndarray: """Convert Euler pole of tectonic plate from spherical to cartesian coordinates That means from location in latitude and longitude and magnitude of rotation to X, Y and Z coordinates. https://geo.libretexts.org/Courses/University_of_California_Davis/GEL_056%3A_Introduction_to_Geophysics/Geophysics_is_everywhere_in_geology.../04%3A_Plate_Tectonics/4.07%3A_Plate_Motions_on_a_Sphere Args: pole: Euler pole array in spherical coordinates (latitude [deg], longitude [deg], magnitude of rotation [degree per million years]) Returns: Euler pole in cartesian coordinates (X, Y, Z) in [milliarcsecond/yr] """ lat = pole[0] * Unit.degree2radian lon = pole[1] * Unit.degree2radian w = pole[2] * Unit("degree per year").to("radian per year").m / 1000000 wx = w * np.cos(lat) * np.cos(lon) * Unit.radian2milliarcsecond wy = w * np.cos(lat) * np.sin(lon) * Unit.radian2milliarcsecond wz = w * np.sin(lat) * Unit.radian2milliarcsecond return np.array([wx, wy, wz])
def get_pole(self, plate: str, unit: Union[None, str] = None ) -> "RotationPole": """Get rotation pole object for given tectonic plate Args: plate: Name of tectonic plate (e.g. eura) model: Plate motion model name unit: Define unit of rotation pole (e.g. 'radian per year', 'milliarcsecond per year') Returns: Instance of RotationPole dataclass for chosen tectonic plate """ try: pole = replace(self.poles[plate]) # Make a copy of RotationPole object except KeyError: plates = ", ".join(sorted(self.poles.keys())) raise exceptions.UnknownSystemError(f"Tectonic plate {plate!r} unknown in plate motion model {self.name}. Use one of {plates}") if unit: for entry in ["wx", "wy", "wz", "dwx", "dwy", "dwz"]: setattr(pole, entry, getattr(pole, entry) * Unit(pole.unit).to(unit).m) pole.unit = unit return pole
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. """ # Delete values from previous iterations if "state" in dset.fields: del dset.state if "estimate" in dset.fields: del dset.estimate 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]) # 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") # 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] 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")
def _difference(self, other, num_obs, self_idx, other_idx, copy_self_on_error=False, copy_other_on_error=False): """Perform the - operation for each field in self and other""" result = self.__class__() for fieldname, field in self._fields.items(): if fieldname in other._fields: try: factors = [Unit(_from, _to) for _to, _from in zip(field._unit, other._fields[fieldname]._unit)] except TypeError: factors = None except exceptions.UnitError as err: raise ValueError(f"Cannot compute difference for field `{fieldname}`: {err}") try: if factors: difference = self[fieldname][self_idx] - other[fieldname][other_idx] * np.array(factors) else: difference = self[fieldname][self_idx] - other[fieldname][other_idx] fieldtype = fieldtypes.fieldtype(difference) func = fieldtypes.function(fieldtype) field = func( num_obs=num_obs, name=fieldname, val=difference, unit=field._unit, write_level=field._write_level.name, ) result.add_field(fieldname, field) except IndexError as err: # fieldname is a collection collection = self[fieldname]._difference( other[fieldname], num_obs, self_idx, other_idx, copy_self_on_error=copy_self_on_error, copy_other_on_error=copy_other_on_error, ) fieldtype = fieldtypes.fieldtype(collection) func = fieldtypes.function(fieldtype) field = func( num_obs=num_obs, name=fieldname, val=collection, unit=field._unit, write_level=field._write_level.name, ) result.add_field(fieldname, field) except TypeError as err: # Fields that do not support the - operator if copy_self_on_error: index_data = self[fieldname][self_idx] fieldtype = fieldtypes.fieldtype(index_data) func = fieldtypes.function(fieldtype) self_fieldname = f"{fieldname}_self" field = func( num_obs=num_obs, name=self_fieldname, val=index_data, unit=field._unit, write_level=field._write_level.name, ) result.add_field(self_fieldname, field) if copy_other_on_error: index_data = other[fieldname][other_idx] fieldtype = fieldtypes.fieldtype(index_data) func = fieldtypes.function(fieldtype) other_fieldname = f"{fieldname}_other" field = func( num_obs=num_obs, name=other_fieldname, val=index_data, unit=other._fields[fieldname]._unit, write_level=other._fields[fieldname]._write_level.name, ) result.add_field(other_fieldname, field) return result