diff --git a/.github/workflows/python-app.yaml b/.github/workflows/python-app.yaml index 0051bd59..d92b935b 100644 --- a/.github/workflows/python-app.yaml +++ b/.github/workflows/python-app.yaml @@ -23,6 +23,8 @@ jobs: - uses: actions/checkout@v2 - name: Set up Python 3 uses: actions/setup-python@v2 + with: + python-version: '3.13' - name: pip installation # test pip installation first (much faster than conda) @@ -53,5 +55,4 @@ jobs: #- name: conda installation # timeout-minutes: 60 # run: | - # bash tests/install_conda.bash - + # bash tests/install_conda.bash \ No newline at end of file diff --git a/mtuq/graphics/header.py b/mtuq/graphics/header.py index 6e6f5db0..ade03ff2 100644 --- a/mtuq/graphics/header.py +++ b/mtuq/graphics/header.py @@ -10,6 +10,10 @@ from matplotlib import pyplot from matplotlib.font_manager import FontProperties +try: + from matplotlib.textpath import TextPath +except ImportError: + TextPath = None from mtuq.graphics.beachball import plot_beachball, _plot_beachball_matplotlib from mtuq.graphics._pygmt import exists_pygmt, plot_force_pygmt from mtuq.graphics._matplotlib import plot_force_matplotlib @@ -38,6 +42,8 @@ class HeaderStyle: text_left_margin: float = HEADER_TEXT_LEFT_MARGIN font_size: int = HEADER_TEXT_FONT_SIZE text_vspace: float = HEADER_TEXT_VSPACE + text_right_margin: float = HEADER_TEXT_LEFT_MARGIN + min_font_size: int = 10 @dataclass @@ -98,12 +104,15 @@ class TextBlock(HeaderBlock): """A text block for displaying formatted text in headers.""" def __init__(self, text: str, fontsize=HEADER_TEXT_FONT_SIZE, bold=False, - italic=False, vspace=HEADER_TEXT_VSPACE, **kwargs): + italic=False, vspace=HEADER_TEXT_VSPACE, auto_shrink=True, + min_fontsize=None, **kwargs): self.text = text self.fontsize = fontsize self.bold = bold self.italic = italic self.vspace = vspace + self.auto_shrink = auto_shrink + self.min_fontsize = min_fontsize self.kwargs = kwargs def render(self, ax, info, px, py, *, height: float, width: float, @@ -118,9 +127,30 @@ def render(self, ax, info, px, py, *, height: float, width: float, fontsize = self.fontsize or style.font_size vspace = self.vspace or style.text_vspace + text_value = self.text.format(**info.__dict__) + + # Compute available width in axis units and shrink fonts if required + max_width = max(width - px - style.text_right_margin * height, 0.) + target_min = self.min_fontsize if self.min_fontsize is not None else style.min_font_size + + if self.auto_shrink and max_width > 0 and TextPath is not None: + font_for_metrics = font.copy() + font_for_metrics.set_size(fontsize) + try: + text_path = TextPath((0, 0), text_value, prop=font_for_metrics) + text_width = text_path.get_extents().width / 72.0 + except Exception: + text_width = 0.0 + + if text_width > max_width and text_width > 0.0: + scale = max_width / text_width + fontsize = max(target_min, fontsize * scale) + + font.set_size(fontsize) + # Use attribute access for info container - ax.text(px, py, self.text.format(**info.__dict__), fontproperties=font, - fontsize=fontsize, **self.kwargs) + ax.text(px, py, text_value, fontproperties=font, fontsize=fontsize, + **self.kwargs) return py - vspace @@ -543,8 +573,9 @@ def create_moment_tensor_header(process_bw, process_sw, misfit_bw, misfit_sw, best_misfit_bw, best_misfit_sw, model, solver, mt, lune_dict, origin, data_bw=None, data_sw=None, mt_grid=None, event_name=None, **kwargs): """Create a complete moment tensor header""" + process_sw_supp = kwargs.pop('process_sw_supp', None) header_info = prepare_moment_tensor_header_info( - origin, mt, lune_dict, process_bw, process_sw, kwargs.get('process_sw_supp', None), + origin, mt, lune_dict, process_bw, process_sw, process_sw_supp, misfit_bw, misfit_sw, best_misfit_bw, best_misfit_sw, model, solver, data_bw=data_bw, data_sw=data_sw, mt_grid=mt_grid, event_name=event_name, **kwargs) header = build_moment_tensor_header(header_info) @@ -555,8 +586,9 @@ def create_force_header(process_bw, process_sw, misfit_bw, misfit_sw, best_misfit_bw, best_misfit_sw, model, solver, force, force_dict, origin, data_bw=None, data_sw=None, force_grid=None, event_name=None, **kwargs): """Create a complete force header""" + process_sw_supp = kwargs.pop('process_sw_supp', None) header_info = prepare_force_header_info( - origin, force, force_dict, process_bw, process_sw, kwargs.get('process_sw_supp', None), + origin, force, force_dict, process_bw, process_sw, process_sw_supp, misfit_bw, misfit_sw, best_misfit_bw, best_misfit_sw, model, solver, data_bw=data_bw, data_sw=data_sw, force_grid=force_grid, event_name=event_name, **kwargs) header = build_force_header(header_info) diff --git a/mtuq/misfit/waveform/__init__.py b/mtuq/misfit/waveform/__init__.py index 62d0063d..fa6df8f2 100644 --- a/mtuq/misfit/waveform/__init__.py +++ b/mtuq/misfit/waveform/__init__.py @@ -233,7 +233,7 @@ def __call__(self, data, greens, sources, progress_handle=Null(), normalize=normalize, ext='Cython') - def collect_attributes(self, data, greens, source, normalize=False): + def collect_attributes(self, data, greens, source, normalize=None): """ Collects misfit, time shifts and other attributes corresponding to each trace """ @@ -250,6 +250,9 @@ def collect_attributes(self, data, greens, source, normalize=False): source, components=data.get_components(), stats=data.get_stats(), mode='map', inplace=True) + if normalize is None: + normalize = self.normalize + # attaches attributes to synthetics _ = level0.misfit( data, greens, iterable(source), self.norm, self.time_shift_groups, @@ -270,7 +273,7 @@ def collect_attributes(self, data, greens, source, normalize=False): return deepcopy(attrs) - def collect_synthetics(self, data, greens, source, normalize=False, mode=2): + def collect_synthetics(self, data, greens, source, normalize=None, mode=2): """ Collects synthetics with misfit, time shifts and other attributes attached """ @@ -310,11 +313,14 @@ def collect_synthetics(self, data, greens, source, normalize=False, mode=2): source, components=components, stats=data.get_stats(), mode='map', inplace=True) + if normalize is None: + normalize = self.normalize + # attaches attributes to synthetics _ = level0.misfit( data, greens, iterable(source), self.norm, self.time_shift_groups, self.time_shift_min, self.time_shift_max, msg_handle=Null(), - normalize=False, set_attributes=True) + normalize=normalize, set_attributes=True) return deepcopy(synthetics) diff --git a/mtuq/misfit/waveform/_stats.py b/mtuq/misfit/waveform/_stats.py index b31db475..88a81b2e 100644 --- a/mtuq/misfit/waveform/_stats.py +++ b/mtuq/misfit/waveform/_stats.py @@ -35,7 +35,8 @@ def calculate_norm_data(data, norm, components, apply_weights=True): dt = stream[0].stats.delta for _k in indices: - d = stream[_k].data + trace = stream[_k] + d = trace.data if norm=='L1': value = np.sum(np.abs(d))*dt @@ -44,14 +45,14 @@ def calculate_norm_data(data, norm, components, apply_weights=True): value = np.sum(d**2)*dt elif norm=='hybrid': - value = np.sqrt(np.sum(r**2))*dt + value = np.sqrt(np.sum(d**2))*dt # optionally, applies user-supplied weights attached during # process_data() if apply_weights: try: - value *= d[_k].weight - except: + value *= trace.weight + except Exception: pass norm_data += value diff --git a/mtuq/misfit/waveform/level0.py b/mtuq/misfit/waveform/level0.py index 2469fa2e..85855eb9 100644 --- a/mtuq/misfit/waveform/level0.py +++ b/mtuq/misfit/waveform/level0.py @@ -113,7 +113,13 @@ def misfit(data, greens, sources, norm, time_shift_groups, s[_k].attrs.norm = norm - s[_k].attrs.misfit = value + if normalize: + try: + s[_k].attrs.misfit = value / norm_data + except Exception: + s[_k].attrs.misfit = value + else: + s[_k].attrs.misfit = value s[_k].attrs.idx_start = idx_start s[_k].attrs.idx_stop = idx_stop diff --git a/pyproject.toml b/pyproject.toml index 6ba91dec..9743dd2a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,7 +14,7 @@ description = "moment tensor (mt) uncertainty quantification (uq)" authors = [ { name = "Ryan Modrak" } ] -requires-python = ">=3" +requires-python = ">=3,<3.14" keywords = ["seismology"] classifiers = [ "Development Status :: 4 - Beta",