GlitchGAN Evaluation¶
Evaluates the GlitchGAN generator (TensorFlow/Keras, epoch 210) on LIGO gravitational-wave glitch data.
Sections:
Waveform visualisation — real vs generated signals
UMAP 3D embedding (correlation metric, 2 000 samples per class)
GravitySpy classification — inject generated glitches and classify with the O3 CNN
Prerequisites:
Data files in
data/(see README for download instructions)GravitySpy installed; set
PATH_TO_REPObelow to your local clone
Dependencies¶
Install the eval extras:
pip install "glitchgan[eval]"
[3]:
import os
import sys
from pathlib import Path
PROJECT_ROOT = Path(".").resolve().parent
DATA_DIR = PROJECT_ROOT / "data"
PLOTS_DIR = PROJECT_ROOT / "evaluation_plots"
os.makedirs(PLOTS_DIR, exist_ok=True)
SEED = 56
os.environ["PYTHONHASHSEED"] = str(SEED)
sys.path.insert(0, str(PROJECT_ROOT / "src"))
from glitchgan.tf.model_components import ArgmaxLayer, ReduceSumDotLayer
_co = {"ArgmaxLayer": ArgmaxLayer, "ReduceSumDotLayer": ReduceSumDotLayer}
# ── Generator ─────────────────────────────────────────────────────────────────
GENERATOR_PATH = PROJECT_ROOT / "weights" / "tensorflow" / "generator_210_keras3.keras"
GENERATOR_EPOCH = 210
NOISE_DIM = 100
NUM_CLASSES = 7
SAMPLES_PER_CLASS = 100
LABEL_ORDER = [
"Blip", "Fast_Scattering", "Koi_Fish",
"Low_Frequency_Burst", "Scattered_Light", "Tomte", "Whistle",
]
# ── GravitySpy ────────────────────────────────────────────────────────────────
IFO = "H1"
SRATE = 4096
GW_START = 1262540000
GW_END = GW_START + 40
CHANNEL = f"{IFO}:GDS-CALIB_STRAIN"
PATH_TO_MODEL = PROJECT_ROOT / "models" / "sidd-cqg-paper-O3-model.h5"
# Update to your local GravitySpy repository clone:
PATH_TO_REPO = "/path/to/GravitySpy"
NUM_CLASSIFY = 10
SNR_TARGET = 50
INIT_TIME = -20
EVENT_TIME = 0
print("Project root :", PROJECT_ROOT)
print("Generator :", GENERATOR_PATH.name)
print("Model exists :", GENERATOR_PATH.exists())
Project root : /Users/tomdooney/Documents/Work/Projects/glitchgan
Generator : generator_210_keras3.keras
Model exists : True
[9]:
import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
import umap.umap_ as umap
try:
import scienceplots
plt.style.use(["science", "no-latex"])
# plt.style.use(["science"])
except ImportError:
pass # scienceplots requires LaTeX; fall back to matplotlib defaults
import random
import tensorflow as tf
# random.seed(SEED)
# np.random.seed(SEED)
# tf.random.set_seed(SEED)
1. Load Data¶
[10]:
X_real = np.load(DATA_DIR / "glitch_GAN_samples_scaled_balanced.npy")
y_onehot = np.load(DATA_DIR / "glitch_GAN_labels_balanced.npy")
y_real = np.array(LABEL_ORDER)[np.argmax(y_onehot, axis=1)]
print(f"Real signals : {X_real.shape}")
print(f"Classes : {LABEL_ORDER}")
Real signals : (35000, 8192)
Classes : ['Blip', 'Fast_Scattering', 'Koi_Fish', 'Low_Frequency_Burst', 'Scattered_Light', 'Tomte', 'Whistle']
2. Load Generator¶
[11]:
import keras
from glitchgan.tf import GlitchGAN
gan = GlitchGAN()
gan.generator = keras.models.load_model(str(GENERATOR_PATH), compile=False)
print(f"Loaded GlitchGAN generator from: {GENERATOR_PATH.name}")
Loaded GlitchGAN generator from: generator_210_keras3.keras
3. Training Loss History¶
[12]:
import json
import numpy as np
import matplotlib.pyplot as plt
HISTORY_PATH = PROJECT_ROOT / "history.json"
with open(HISTORY_PATH) as f:
history = json.load(f)
epochs = np.arange(1, len(history["d_loss"]) + 1)
SMOOTH = 10
def smooth(x, w):
return np.convolve(x, np.ones(w) / w, mode="valid")
epochs_s = epochs[SMOOTH - 1:]
series = [
("d_loss", "Discriminator", "C0"),
("d2d_loss", "Derivative discriminator", "C1"),
("g_loss", "Generator", "C2"),
("g_loss2d", "Generator (derivative)", "C3"),
("g_loss_combined", "Generator (combined)", "C4"),
]
fig, ax = plt.subplots(figsize=(9, 5))
for key, label, color in series:
# ax.plot(epochs, history[key], color=color, alpha=0.15, lw=0.6)
ax.plot(epochs, history[key], color=color, alpha=1, lw=0.6, label=label)
# ax.plot(epochs_s, smooth(history[key], SMOOTH), color=color, lw=1.5, label=label)
ax.axhline(0, color="k", lw=0.6, ls="--", alpha=0.4)
ax.set_xlabel("Epoch", fontsize=20)
ax.set_ylabel("Wasserstein Loss", fontsize=20)
ax.set_xlim(1, 500)
ax.legend(fontsize=14, frameon=True, loc="lower left")
plt.tight_layout()
save_path = PLOTS_DIR / "training_loss_history.pdf"
plt.savefig(save_path, bbox_inches="tight")
print(f"Saved: {save_path}")
plt.show()
Saved: /Users/tomdooney/Documents/Work/Projects/glitchgan/evaluation_plots/training_loss_history.pdf
4. Generate Signals¶
[13]:
from glitchgan.tf.utils import generate_examples
X_fake, class_vecs = generate_examples(
gan,
noise_dim=NOISE_DIM,
num_classes=NUM_CLASSES,
num_signals=NUM_CLASSES * SAMPLES_PER_CLASS,
sampling="vertex",
)
y_fake = np.array(LABEL_ORDER)[np.argmax(class_vecs, axis=1)]
print(f"Generated : {X_fake.shape} ({SAMPLES_PER_CLASS} per class)")
Generated : (700, 8192) (100 per class)
4. Waveform Visualisation¶
[14]:
from gwpy.timeseries import TimeSeries
import numpy as np
import matplotlib.pyplot as plt
_bg_cache = PROJECT_ROOT / "evaluation_plots" / f"strain_bg_{GW_START}_{GW_END}.hdf5"
try:
strain_bg = TimeSeries.read(_bg_cache, format="hdf5")
print(f"Loaded from cache: {_bg_cache}")
except Exception:
strain_bg = TimeSeries.fetch_open_data("H1", GW_START, GW_END)
strain_bg = strain_bg.resample(SRATE)
strain_bg.write(_bg_cache, format="hdf5", overwrite=True)
print(f"Fetched and cached: {_bg_cache}")
# Match reference exactly: use gwpy's to_pycbc(), keep whitened result as pycbc TimeSeries
noise = strain_bg.to_pycbc()
white_noise_pycbc, psd = noise.whiten(
len(noise) / (2 * SRATE),
len(noise) / (4 * SRATE),
remove_corrupted=False,
return_psd=True,
)
# numpy copy for plotting / Q-scan visualisation only
white_noise = np.asarray(white_noise_pycbc)
print(f"white_noise: {white_noise.shape} type: {type(white_noise)}")
plt.figure(figsize=(10, 3))
plt.plot(white_noise[SRATE * 4 : -SRATE * 4])
plt.xlabel("Samples"); plt.ylabel("Amplitude")
plt.title("Whitened background noise")
plt.tight_layout(); plt.show()
/opt/homebrew/Caskroom/miniforge/base/envs/glitchgan_test/lib/python3.11/site-packages/gwpy/time/_ligotimegps.py:42: UserWarning: Wswiglal-redir-stdio:
SWIGLAL standard output/error redirection is enabled in IPython.
This may lead to performance penalties. To disable locally, use:
with lal.no_swig_redirect_standard_output_error():
...
To disable globally, use:
lal.swig_redirect_standard_output_error(False)
Note however that this will likely lead to error messages from
LAL functions being either misdirected or lost when called from
Jupyter notebooks.
To suppress this warning, use:
import warnings
warnings.filterwarnings("ignore", "Wswiglal-redir-stdio")
import lal
from lal import LIGOTimeGPS
Loaded from cache: /Users/tomdooney/Documents/Work/Projects/glitchgan/evaluation_plots/strain_bg_1262540000_1262540040.hdf5
PyCBC.libutils: pkg-config call failed, setting NO_PKGCONFIG=1
white_noise: (163840,) type: <class 'numpy.ndarray'>
[15]:
from matplotlib.lines import Line2D
from glitchgan.utils import plot_q_transform, whitened_snr_scaling
try:
plt.style.use(["science", "no-latex"]) # remove "no-latex" if latex is on PATH
except Exception:
pass
def _inject(glitch, white_noise):
len_g = len(glitch)
id_start = len(white_noise) // 2 - len_g // 2
injected = white_noise.copy()
injected[id_start : id_start + len_g] += glitch
return injected
def plot_comparison_grid(sources, label_order, n_examples=1, save_name=None,
white_noise=None, snr_default=50, snr_overrides=None):
# Local RNG so sample selection is reproducible regardless of upstream state
rng = np.random.default_rng(SEED)
show_qscans = white_noise is not None
n_sources = len(sources)
n_classes = len(label_order)
rows_per_src = 2 if show_qscans else 1
n_rows = n_sources * rows_per_src
ts_height = 1.4
qs_height = 1.8
row_heights = [ts_height, qs_height] * n_sources if show_qscans else [ts_height] * n_sources
fig, axes = plt.subplots(
n_rows, n_classes * n_examples,
figsize=(2.4 * n_classes * n_examples, sum(row_heights)),
gridspec_kw={"hspace": 0.08, "wspace": 0.08,
"height_ratios": row_heights},
)
if n_rows == 1:
axes = axes[np.newaxis, :]
legend_handles = []
for src_idx, (X, y, src_label, color) in enumerate(sources):
legend_handles.append(Line2D([0], [0], color=color, lw=1.5, label=src_label))
ts_row = src_idx * rows_per_src
qs_row = ts_row + 1 if show_qscans else None
for ci, lbl in enumerate(label_order):
idx = np.where(y == lbl)[0]
chosen = rng.choice(idx, min(n_examples, len(idx)), replace=False)
snr = (snr_overrides or {}).get(lbl, snr_default)
for j, sample_idx in enumerate(chosen):
col = ci * n_examples + j
ax_ts = axes[ts_row, col]
ax_ts.plot(X[sample_idx], lw=0.6, color=color)
ax_ts.set_xticks([]); ax_ts.set_yticks([])
for spine in ax_ts.spines.values():
spine.set_linewidth(0.4)
if src_idx == 0 and j == 0:
ax_ts.set_title(lbl.replace("_", " "), fontsize=15, pad=3)
if show_qscans:
ax_qs = axes[qs_row, col]
glitch = whitened_snr_scaling(X[sample_idx], snr, srate=SRATE)
injected = _inject(glitch, white_noise)
plot_q_transform(injected, srate=SRATE, crop=(20, 2),
whiten=False, ax=ax_qs, colourbar=False)
ax_qs.set_xlabel(""); ax_qs.set_ylabel("")
ax_qs.set_xticks([]); ax_qs.set_yticks([])
for spine in ax_qs.spines.values():
spine.set_linewidth(0.4)
plt.tight_layout(rect=[0, 0, 1, 0.99])
# Extra gap between source pairs (does not affect within-pair spacing)
if show_qscans and n_sources > 1:
for src_idx in range(1, n_sources):
for row_offset in range(rows_per_src):
row = src_idx * rows_per_src + row_offset
for col in range(n_classes * n_examples):
pos = axes[row, col].get_position()
axes[row, col].set_position(
[pos.x0, pos.y0 - 0.08, pos.width, pos.height])
fig.legend(handles=legend_handles, loc="upper center", ncol=n_sources,
fontsize=15, frameon=False, bbox_to_anchor=(0.5, 0.99))
if save_name:
plt.savefig(PLOTS_DIR / f"{save_name}.pdf", bbox_inches="tight")
plt.show()
plot_comparison_grid(
sources=[
(X_real, y_real, "Real", "C0"),
(X_fake, y_fake, "GlitchGAN", "C1"),
],
label_order=LABEL_ORDER,
n_examples=1,
white_noise=white_noise,
snr_default=50,
snr_overrides={"Koi_Fish": 150, "Whistle": 150},
save_name=f"waveform_comparison_ep{GENERATOR_EPOCH}",
)
/var/folders/gv/z_2s63x116vbz6s2hmnl7mkc0000gn/T/ipykernel_9029/4281143109.py:73: UserWarning: This figure includes Axes that are not compatible with tight_layout, so results might be incorrect.
plt.tight_layout(rect=[0, 0, 1, 0.99])
---------------------------------------------------------------------------
FileNotFoundError Traceback (most recent call last)
File /opt/homebrew/Caskroom/miniforge/base/envs/glitchgan_test/lib/python3.11/site-packages/matplotlib/texmanager.py:250, in TexManager._run_checked_subprocess(cls, command, tex, cwd)
249 try:
--> 250 report = subprocess.check_output(
251 command, cwd=cwd if cwd is not None else cls._texcache,
252 stderr=subprocess.STDOUT)
253 except FileNotFoundError as exc:
File /opt/homebrew/Caskroom/miniforge/base/envs/glitchgan_test/lib/python3.11/subprocess.py:466, in check_output(timeout, *popenargs, **kwargs)
464 kwargs['input'] = empty
--> 466 return run(*popenargs, stdout=PIPE, timeout=timeout, check=True,
467 **kwargs).stdout
File /opt/homebrew/Caskroom/miniforge/base/envs/glitchgan_test/lib/python3.11/subprocess.py:548, in run(input, capture_output, timeout, check, *popenargs, **kwargs)
546 kwargs['stderr'] = PIPE
--> 548 with Popen(*popenargs, **kwargs) as process:
549 try:
File /opt/homebrew/Caskroom/miniforge/base/envs/glitchgan_test/lib/python3.11/subprocess.py:1026, in Popen.__init__(self, args, bufsize, executable, stdin, stdout, stderr, preexec_fn, close_fds, shell, cwd, env, universal_newlines, startupinfo, creationflags, restore_signals, start_new_session, pass_fds, user, group, extra_groups, encoding, errors, text, umask, pipesize, process_group)
1023 self.stderr = io.TextIOWrapper(self.stderr,
1024 encoding=encoding, errors=errors)
-> 1026 self._execute_child(args, executable, preexec_fn, close_fds,
1027 pass_fds, cwd, env,
1028 startupinfo, creationflags, shell,
1029 p2cread, p2cwrite,
1030 c2pread, c2pwrite,
1031 errread, errwrite,
1032 restore_signals,
1033 gid, gids, uid, umask,
1034 start_new_session, process_group)
1035 except:
1036 # Cleanup if the child failed starting.
File /opt/homebrew/Caskroom/miniforge/base/envs/glitchgan_test/lib/python3.11/subprocess.py:1955, in Popen._execute_child(self, args, executable, preexec_fn, close_fds, pass_fds, cwd, env, startupinfo, creationflags, shell, p2cread, p2cwrite, c2pread, c2pwrite, errread, errwrite, restore_signals, gid, gids, uid, umask, start_new_session, process_group)
1954 if err_filename is not None:
-> 1955 raise child_exception_type(errno_num, err_msg, err_filename)
1956 else:
FileNotFoundError: [Errno 2] No such file or directory: 'latex'
The above exception was the direct cause of the following exception:
RuntimeError Traceback (most recent call last)
Cell In[15], line 92
88 plt.savefig(PLOTS_DIR / f"{save_name}.pdf", bbox_inches="tight")
89 plt.show()
90
91
---> 92 plot_comparison_grid(
93 sources=[
94 (X_real, y_real, "Real", "C0"),
95 (X_fake, y_fake, "GlitchGAN", "C1"),
Cell In[15], line 88, in plot_comparison_grid(sources, label_order, n_examples, save_name, white_noise, snr_default, snr_overrides)
84
85 fig.legend(handles=legend_handles, loc="upper center", ncol=n_sources,
86 fontsize=15, frameon=False, bbox_to_anchor=(0.5, 0.99))
87 if save_name:
---> 88 plt.savefig(PLOTS_DIR / f"{save_name}.pdf", bbox_inches="tight")
89 plt.show()
File /opt/homebrew/Caskroom/miniforge/base/envs/glitchgan_test/lib/python3.11/site-packages/matplotlib/pyplot.py:1250, in savefig(*args, **kwargs)
1247 fig = gcf()
1248 # savefig default implementation has no return, so mypy is unhappy
1249 # presumably this is here because subclasses can return?
-> 1250 res = fig.savefig(*args, **kwargs) # type: ignore[func-returns-value]
1251 fig.canvas.draw_idle() # Need this if 'transparent=True', to reset colors.
1252 return res
File /opt/homebrew/Caskroom/miniforge/base/envs/glitchgan_test/lib/python3.11/site-packages/matplotlib/figure.py:3497, in Figure.savefig(self, fname, transparent, **kwargs)
3495 for ax in self.axes:
3496 _recursively_make_axes_transparent(stack, ax)
-> 3497 self.canvas.print_figure(fname, **kwargs)
File /opt/homebrew/Caskroom/miniforge/base/envs/glitchgan_test/lib/python3.11/site-packages/matplotlib/backend_bases.py:2157, in FigureCanvasBase.print_figure(self, filename, dpi, facecolor, edgecolor, orientation, format, bbox_inches, pad_inches, bbox_extra_artists, backend, **kwargs)
2154 # we do this instead of `self.figure.draw_without_rendering`
2155 # so that we can inject the orientation
2156 with getattr(renderer, "_draw_disabled", nullcontext)():
-> 2157 self.figure.draw(renderer)
2158 if bbox_inches:
2159 if bbox_inches == "tight":
File /opt/homebrew/Caskroom/miniforge/base/envs/glitchgan_test/lib/python3.11/site-packages/matplotlib/artist.py:94, in _finalize_rasterization.<locals>.draw_wrapper(artist, renderer, *args, **kwargs)
92 @wraps(draw)
93 def draw_wrapper(artist, renderer, *args, **kwargs):
---> 94 result = draw(artist, renderer, *args, **kwargs)
95 if renderer._rasterizing:
96 renderer.stop_rasterizing()
File /opt/homebrew/Caskroom/miniforge/base/envs/glitchgan_test/lib/python3.11/site-packages/matplotlib/artist.py:71, in allow_rasterization.<locals>.draw_wrapper(artist, renderer)
68 if artist.get_agg_filter() is not None:
69 renderer.start_filter()
---> 71 return draw(artist, renderer)
72 finally:
73 if artist.get_agg_filter() is not None:
File /opt/homebrew/Caskroom/miniforge/base/envs/glitchgan_test/lib/python3.11/site-packages/matplotlib/figure.py:3264, in Figure.draw(self, renderer)
3261 # ValueError can occur when resizing a window.
3263 self.patch.draw(renderer)
-> 3264 mimage._draw_list_compositing_images(
3265 renderer, self, artists, self.suppressComposite)
3267 renderer.close_group('figure')
3268 finally:
File /opt/homebrew/Caskroom/miniforge/base/envs/glitchgan_test/lib/python3.11/site-packages/matplotlib/image.py:134, in _draw_list_compositing_images(renderer, parent, artists, suppress_composite)
132 if not_composite or not has_images:
133 for a in artists:
--> 134 a.draw(renderer)
135 else:
136 # Composite any adjacent images together
137 image_group = []
File /opt/homebrew/Caskroom/miniforge/base/envs/glitchgan_test/lib/python3.11/site-packages/matplotlib/artist.py:71, in allow_rasterization.<locals>.draw_wrapper(artist, renderer)
68 if artist.get_agg_filter() is not None:
69 renderer.start_filter()
---> 71 return draw(artist, renderer)
72 finally:
73 if artist.get_agg_filter() is not None:
File /opt/homebrew/Caskroom/miniforge/base/envs/glitchgan_test/lib/python3.11/site-packages/gwpy/plot/axes.py:201, in Axes.draw(self, renderer)
198 ax.set_label_text(f"Time [{unit}] from {utc} UTC ({epoch!r})")
200 try:
--> 201 super().draw(renderer)
202 finally:
203 for ax in labels: # reset labels
File /opt/homebrew/Caskroom/miniforge/base/envs/glitchgan_test/lib/python3.11/site-packages/matplotlib/artist.py:71, in allow_rasterization.<locals>.draw_wrapper(artist, renderer)
68 if artist.get_agg_filter() is not None:
69 renderer.start_filter()
---> 71 return draw(artist, renderer)
72 finally:
73 if artist.get_agg_filter() is not None:
File /opt/homebrew/Caskroom/miniforge/base/envs/glitchgan_test/lib/python3.11/site-packages/matplotlib/axes/_base.py:3190, in _AxesBase.draw(self, renderer)
3187 for spine in self.spines.values():
3188 artists.remove(spine)
-> 3190 self._update_title_position(renderer)
3192 if not self.axison:
3193 for _axis in self._axis_map.values():
File /opt/homebrew/Caskroom/miniforge/base/envs/glitchgan_test/lib/python3.11/site-packages/matplotlib/axes/_base.py:3144, in _AxesBase._update_title_position(self, renderer)
3142 _log.debug('top of Axes not in the figure, so title not moved')
3143 return
-> 3144 if title.get_window_extent(renderer).ymin < top:
3145 _, y = self.transAxes.inverted().transform((0, top))
3146 title.set_position((x, y))
File /opt/homebrew/Caskroom/miniforge/base/envs/glitchgan_test/lib/python3.11/site-packages/matplotlib/text.py:969, in Text.get_window_extent(self, renderer, dpi)
964 raise RuntimeError(
965 "Cannot get window extent of text w/o renderer. You likely "
966 "want to call 'figure.draw_without_rendering()' first.")
968 with cbook._setattr_cm(fig, dpi=dpi):
--> 969 bbox, info, descent = self._get_layout(self._renderer)
970 x, y = self.get_unitless_position()
971 x, y = self.get_transform().transform((x, y))
File /opt/homebrew/Caskroom/miniforge/base/envs/glitchgan_test/lib/python3.11/site-packages/matplotlib/text.py:373, in Text._get_layout(self, renderer)
370 ys = []
372 # Full vertical extent of font, including ascenders and descenders:
--> 373 _, lp_h, lp_d = _get_text_metrics_with_cache(
374 renderer, "lp", self._fontproperties,
375 ismath="TeX" if self.get_usetex() else False,
376 dpi=self.get_figure(root=True).dpi)
377 min_dy = (lp_h - lp_d) * self._linespacing
379 for i, line in enumerate(lines):
File /opt/homebrew/Caskroom/miniforge/base/envs/glitchgan_test/lib/python3.11/site-packages/matplotlib/text.py:69, in _get_text_metrics_with_cache(renderer, text, fontprop, ismath, dpi)
66 """Call ``renderer.get_text_width_height_descent``, caching the results."""
67 # Cached based on a copy of fontprop so that later in-place mutations of
68 # the passed-in argument do not mess up the cache.
---> 69 return _get_text_metrics_with_cache_impl(
70 weakref.ref(renderer), text, fontprop.copy(), ismath, dpi)
File /opt/homebrew/Caskroom/miniforge/base/envs/glitchgan_test/lib/python3.11/site-packages/matplotlib/text.py:77, in _get_text_metrics_with_cache_impl(renderer_ref, text, fontprop, ismath, dpi)
73 @functools.lru_cache(4096)
74 def _get_text_metrics_with_cache_impl(
75 renderer_ref, text, fontprop, ismath, dpi):
76 # dpi is unused, but participates in cache invalidation (via the renderer).
---> 77 return renderer_ref().get_text_width_height_descent(text, fontprop, ismath)
File /opt/homebrew/Caskroom/miniforge/base/envs/glitchgan_test/lib/python3.11/site-packages/matplotlib/backends/_backend_pdf_ps.py:150, in RendererPDFPSBase.get_text_width_height_descent(self, s, prop, ismath)
147 def get_text_width_height_descent(self, s, prop, ismath):
148 # docstring inherited
149 if ismath == "TeX":
--> 150 return super().get_text_width_height_descent(s, prop, ismath)
151 elif ismath:
152 parse = self._text2path.mathtext_parser.parse(s, 72, prop)
File /opt/homebrew/Caskroom/miniforge/base/envs/glitchgan_test/lib/python3.11/site-packages/matplotlib/backend_bases.py:566, in RendererBase.get_text_width_height_descent(self, s, prop, ismath)
562 fontsize = prop.get_size_in_points()
564 if ismath == 'TeX':
565 # todo: handle properties
--> 566 return self.get_texmanager().get_text_width_height_descent(
567 s, fontsize, renderer=self)
569 dpi = self.points_to_pixels(72)
570 if ismath:
File /opt/homebrew/Caskroom/miniforge/base/envs/glitchgan_test/lib/python3.11/site-packages/matplotlib/texmanager.py:364, in TexManager.get_text_width_height_descent(cls, tex, fontsize, renderer)
362 if tex.strip() == '':
363 return 0, 0, 0
--> 364 dvifile = cls.make_dvi(tex, fontsize)
365 dpi_fraction = renderer.points_to_pixels(1.) if renderer else 1
366 with dviread.Dvi(dvifile, 72 * dpi_fraction) as dvi:
File /opt/homebrew/Caskroom/miniforge/base/envs/glitchgan_test/lib/python3.11/site-packages/matplotlib/texmanager.py:293, in TexManager.make_dvi(cls, tex, fontsize)
290 with TemporaryDirectory(dir=dvifile.parent) as tmpdir:
291 Path(tmpdir, "file.tex").write_text(
292 cls._get_tex_source(tex, fontsize), encoding='utf-8')
--> 293 cls._run_checked_subprocess(
294 ["latex", "-interaction=nonstopmode", "-halt-on-error",
295 "-no-shell-escape", "file.tex"], tex, cwd=tmpdir)
296 Path(tmpdir, "file.dvi").replace(dvifile)
297 # Also move the tex source to the main cache directory, but
298 # only for backcompat.
File /opt/homebrew/Caskroom/miniforge/base/envs/glitchgan_test/lib/python3.11/site-packages/matplotlib/texmanager.py:254, in TexManager._run_checked_subprocess(cls, command, tex, cwd)
250 report = subprocess.check_output(
251 command, cwd=cwd if cwd is not None else cls._texcache,
252 stderr=subprocess.STDOUT)
253 except FileNotFoundError as exc:
--> 254 raise RuntimeError(
255 f'Failed to process string with tex because {command[0]} '
256 'could not be found') from exc
257 except subprocess.CalledProcessError as exc:
258 raise RuntimeError(
259 '{prog} was not able to process the following string:\n'
260 '{tex!r}\n\n'
(...) 267 exc=exc.output.decode('utf-8', 'backslashreplace'))
268 ) from None
RuntimeError: Failed to process string with tex because latex could not be found
Error in callback <function _draw_all_if_interactive at 0x10ef984a0> (for post_execute), with arguments args (),kwargs {}:
---------------------------------------------------------------------------
FileNotFoundError Traceback (most recent call last)
File /opt/homebrew/Caskroom/miniforge/base/envs/glitchgan_test/lib/python3.11/site-packages/matplotlib/texmanager.py:250, in TexManager._run_checked_subprocess(cls, command, tex, cwd)
249 try:
--> 250 report = subprocess.check_output(
251 command, cwd=cwd if cwd is not None else cls._texcache,
252 stderr=subprocess.STDOUT)
253 except FileNotFoundError as exc:
File /opt/homebrew/Caskroom/miniforge/base/envs/glitchgan_test/lib/python3.11/subprocess.py:466, in check_output(timeout, *popenargs, **kwargs)
464 kwargs['input'] = empty
--> 466 return run(*popenargs, stdout=PIPE, timeout=timeout, check=True,
467 **kwargs).stdout
File /opt/homebrew/Caskroom/miniforge/base/envs/glitchgan_test/lib/python3.11/subprocess.py:548, in run(input, capture_output, timeout, check, *popenargs, **kwargs)
546 kwargs['stderr'] = PIPE
--> 548 with Popen(*popenargs, **kwargs) as process:
549 try:
File /opt/homebrew/Caskroom/miniforge/base/envs/glitchgan_test/lib/python3.11/subprocess.py:1026, in Popen.__init__(self, args, bufsize, executable, stdin, stdout, stderr, preexec_fn, close_fds, shell, cwd, env, universal_newlines, startupinfo, creationflags, restore_signals, start_new_session, pass_fds, user, group, extra_groups, encoding, errors, text, umask, pipesize, process_group)
1023 self.stderr = io.TextIOWrapper(self.stderr,
1024 encoding=encoding, errors=errors)
-> 1026 self._execute_child(args, executable, preexec_fn, close_fds,
1027 pass_fds, cwd, env,
1028 startupinfo, creationflags, shell,
1029 p2cread, p2cwrite,
1030 c2pread, c2pwrite,
1031 errread, errwrite,
1032 restore_signals,
1033 gid, gids, uid, umask,
1034 start_new_session, process_group)
1035 except:
1036 # Cleanup if the child failed starting.
File /opt/homebrew/Caskroom/miniforge/base/envs/glitchgan_test/lib/python3.11/subprocess.py:1955, in Popen._execute_child(self, args, executable, preexec_fn, close_fds, pass_fds, cwd, env, startupinfo, creationflags, shell, p2cread, p2cwrite, c2pread, c2pwrite, errread, errwrite, restore_signals, gid, gids, uid, umask, start_new_session, process_group)
1954 if err_filename is not None:
-> 1955 raise child_exception_type(errno_num, err_msg, err_filename)
1956 else:
FileNotFoundError: [Errno 2] No such file or directory: 'latex'
The above exception was the direct cause of the following exception:
RuntimeError Traceback (most recent call last)
File /opt/homebrew/Caskroom/miniforge/base/envs/glitchgan_test/lib/python3.11/site-packages/matplotlib/pyplot.py:278, in _draw_all_if_interactive()
276 def _draw_all_if_interactive() -> None:
277 if matplotlib.is_interactive():
--> 278 draw_all()
File /opt/homebrew/Caskroom/miniforge/base/envs/glitchgan_test/lib/python3.11/site-packages/matplotlib/_pylab_helpers.py:131, in Gcf.draw_all(cls, force)
129 for manager in cls.get_all_fig_managers():
130 if force or manager.canvas.figure.stale:
--> 131 manager.canvas.draw_idle()
File /opt/homebrew/Caskroom/miniforge/base/envs/glitchgan_test/lib/python3.11/site-packages/matplotlib/backend_bases.py:1893, in FigureCanvasBase.draw_idle(self, *args, **kwargs)
1891 if not self._is_idle_drawing:
1892 with self._idle_draw_cntx():
-> 1893 self.draw(*args, **kwargs)
File /opt/homebrew/Caskroom/miniforge/base/envs/glitchgan_test/lib/python3.11/site-packages/matplotlib/backends/backend_agg.py:382, in FigureCanvasAgg.draw(self)
379 # Acquire a lock on the shared font cache.
380 with (self.toolbar._wait_cursor_for_draw_cm() if self.toolbar
381 else nullcontext()):
--> 382 self.figure.draw(self.renderer)
383 # A GUI class may be need to update a window using this draw, so
384 # don't forget to call the superclass.
385 super().draw()
File /opt/homebrew/Caskroom/miniforge/base/envs/glitchgan_test/lib/python3.11/site-packages/matplotlib/artist.py:94, in _finalize_rasterization.<locals>.draw_wrapper(artist, renderer, *args, **kwargs)
92 @wraps(draw)
93 def draw_wrapper(artist, renderer, *args, **kwargs):
---> 94 result = draw(artist, renderer, *args, **kwargs)
95 if renderer._rasterizing:
96 renderer.stop_rasterizing()
File /opt/homebrew/Caskroom/miniforge/base/envs/glitchgan_test/lib/python3.11/site-packages/matplotlib/artist.py:71, in allow_rasterization.<locals>.draw_wrapper(artist, renderer)
68 if artist.get_agg_filter() is not None:
69 renderer.start_filter()
---> 71 return draw(artist, renderer)
72 finally:
73 if artist.get_agg_filter() is not None:
File /opt/homebrew/Caskroom/miniforge/base/envs/glitchgan_test/lib/python3.11/site-packages/matplotlib/figure.py:3264, in Figure.draw(self, renderer)
3261 # ValueError can occur when resizing a window.
3263 self.patch.draw(renderer)
-> 3264 mimage._draw_list_compositing_images(
3265 renderer, self, artists, self.suppressComposite)
3267 renderer.close_group('figure')
3268 finally:
File /opt/homebrew/Caskroom/miniforge/base/envs/glitchgan_test/lib/python3.11/site-packages/matplotlib/image.py:134, in _draw_list_compositing_images(renderer, parent, artists, suppress_composite)
132 if not_composite or not has_images:
133 for a in artists:
--> 134 a.draw(renderer)
135 else:
136 # Composite any adjacent images together
137 image_group = []
File /opt/homebrew/Caskroom/miniforge/base/envs/glitchgan_test/lib/python3.11/site-packages/matplotlib/artist.py:71, in allow_rasterization.<locals>.draw_wrapper(artist, renderer)
68 if artist.get_agg_filter() is not None:
69 renderer.start_filter()
---> 71 return draw(artist, renderer)
72 finally:
73 if artist.get_agg_filter() is not None:
File /opt/homebrew/Caskroom/miniforge/base/envs/glitchgan_test/lib/python3.11/site-packages/gwpy/plot/axes.py:201, in Axes.draw(self, renderer)
198 ax.set_label_text(f"Time [{unit}] from {utc} UTC ({epoch!r})")
200 try:
--> 201 super().draw(renderer)
202 finally:
203 for ax in labels: # reset labels
File /opt/homebrew/Caskroom/miniforge/base/envs/glitchgan_test/lib/python3.11/site-packages/matplotlib/artist.py:71, in allow_rasterization.<locals>.draw_wrapper(artist, renderer)
68 if artist.get_agg_filter() is not None:
69 renderer.start_filter()
---> 71 return draw(artist, renderer)
72 finally:
73 if artist.get_agg_filter() is not None:
File /opt/homebrew/Caskroom/miniforge/base/envs/glitchgan_test/lib/python3.11/site-packages/matplotlib/axes/_base.py:3190, in _AxesBase.draw(self, renderer)
3187 for spine in self.spines.values():
3188 artists.remove(spine)
-> 3190 self._update_title_position(renderer)
3192 if not self.axison:
3193 for _axis in self._axis_map.values():
File /opt/homebrew/Caskroom/miniforge/base/envs/glitchgan_test/lib/python3.11/site-packages/matplotlib/axes/_base.py:3144, in _AxesBase._update_title_position(self, renderer)
3142 _log.debug('top of Axes not in the figure, so title not moved')
3143 return
-> 3144 if title.get_window_extent(renderer).ymin < top:
3145 _, y = self.transAxes.inverted().transform((0, top))
3146 title.set_position((x, y))
File /opt/homebrew/Caskroom/miniforge/base/envs/glitchgan_test/lib/python3.11/site-packages/matplotlib/text.py:969, in Text.get_window_extent(self, renderer, dpi)
964 raise RuntimeError(
965 "Cannot get window extent of text w/o renderer. You likely "
966 "want to call 'figure.draw_without_rendering()' first.")
968 with cbook._setattr_cm(fig, dpi=dpi):
--> 969 bbox, info, descent = self._get_layout(self._renderer)
970 x, y = self.get_unitless_position()
971 x, y = self.get_transform().transform((x, y))
File /opt/homebrew/Caskroom/miniforge/base/envs/glitchgan_test/lib/python3.11/site-packages/matplotlib/text.py:373, in Text._get_layout(self, renderer)
370 ys = []
372 # Full vertical extent of font, including ascenders and descenders:
--> 373 _, lp_h, lp_d = _get_text_metrics_with_cache(
374 renderer, "lp", self._fontproperties,
375 ismath="TeX" if self.get_usetex() else False,
376 dpi=self.get_figure(root=True).dpi)
377 min_dy = (lp_h - lp_d) * self._linespacing
379 for i, line in enumerate(lines):
File /opt/homebrew/Caskroom/miniforge/base/envs/glitchgan_test/lib/python3.11/site-packages/matplotlib/text.py:69, in _get_text_metrics_with_cache(renderer, text, fontprop, ismath, dpi)
66 """Call ``renderer.get_text_width_height_descent``, caching the results."""
67 # Cached based on a copy of fontprop so that later in-place mutations of
68 # the passed-in argument do not mess up the cache.
---> 69 return _get_text_metrics_with_cache_impl(
70 weakref.ref(renderer), text, fontprop.copy(), ismath, dpi)
File /opt/homebrew/Caskroom/miniforge/base/envs/glitchgan_test/lib/python3.11/site-packages/matplotlib/text.py:77, in _get_text_metrics_with_cache_impl(renderer_ref, text, fontprop, ismath, dpi)
73 @functools.lru_cache(4096)
74 def _get_text_metrics_with_cache_impl(
75 renderer_ref, text, fontprop, ismath, dpi):
76 # dpi is unused, but participates in cache invalidation (via the renderer).
---> 77 return renderer_ref().get_text_width_height_descent(text, fontprop, ismath)
File /opt/homebrew/Caskroom/miniforge/base/envs/glitchgan_test/lib/python3.11/site-packages/matplotlib/backends/backend_agg.py:211, in RendererAgg.get_text_width_height_descent(self, s, prop, ismath)
209 _api.check_in_list(["TeX", True, False], ismath=ismath)
210 if ismath == "TeX":
--> 211 return super().get_text_width_height_descent(s, prop, ismath)
213 if ismath:
214 ox, oy, width, height, descent, font_image = \
215 self.mathtext_parser.parse(s, self.dpi, prop)
File /opt/homebrew/Caskroom/miniforge/base/envs/glitchgan_test/lib/python3.11/site-packages/matplotlib/backend_bases.py:566, in RendererBase.get_text_width_height_descent(self, s, prop, ismath)
562 fontsize = prop.get_size_in_points()
564 if ismath == 'TeX':
565 # todo: handle properties
--> 566 return self.get_texmanager().get_text_width_height_descent(
567 s, fontsize, renderer=self)
569 dpi = self.points_to_pixels(72)
570 if ismath:
File /opt/homebrew/Caskroom/miniforge/base/envs/glitchgan_test/lib/python3.11/site-packages/matplotlib/texmanager.py:364, in TexManager.get_text_width_height_descent(cls, tex, fontsize, renderer)
362 if tex.strip() == '':
363 return 0, 0, 0
--> 364 dvifile = cls.make_dvi(tex, fontsize)
365 dpi_fraction = renderer.points_to_pixels(1.) if renderer else 1
366 with dviread.Dvi(dvifile, 72 * dpi_fraction) as dvi:
File /opt/homebrew/Caskroom/miniforge/base/envs/glitchgan_test/lib/python3.11/site-packages/matplotlib/texmanager.py:293, in TexManager.make_dvi(cls, tex, fontsize)
290 with TemporaryDirectory(dir=dvifile.parent) as tmpdir:
291 Path(tmpdir, "file.tex").write_text(
292 cls._get_tex_source(tex, fontsize), encoding='utf-8')
--> 293 cls._run_checked_subprocess(
294 ["latex", "-interaction=nonstopmode", "-halt-on-error",
295 "-no-shell-escape", "file.tex"], tex, cwd=tmpdir)
296 Path(tmpdir, "file.dvi").replace(dvifile)
297 # Also move the tex source to the main cache directory, but
298 # only for backcompat.
File /opt/homebrew/Caskroom/miniforge/base/envs/glitchgan_test/lib/python3.11/site-packages/matplotlib/texmanager.py:254, in TexManager._run_checked_subprocess(cls, command, tex, cwd)
250 report = subprocess.check_output(
251 command, cwd=cwd if cwd is not None else cls._texcache,
252 stderr=subprocess.STDOUT)
253 except FileNotFoundError as exc:
--> 254 raise RuntimeError(
255 f'Failed to process string with tex because {command[0]} '
256 'could not be found') from exc
257 except subprocess.CalledProcessError as exc:
258 raise RuntimeError(
259 '{prog} was not able to process the following string:\n'
260 '{tex!r}\n\n'
(...) 267 exc=exc.output.decode('utf-8', 'backslashreplace'))
268 ) from None
RuntimeError: Failed to process string with tex because latex could not be found
---------------------------------------------------------------------------
FileNotFoundError Traceback (most recent call last)
File /opt/homebrew/Caskroom/miniforge/base/envs/glitchgan_test/lib/python3.11/site-packages/matplotlib/texmanager.py:250, in TexManager._run_checked_subprocess(cls, command, tex, cwd)
249 try:
--> 250 report = subprocess.check_output(
251 command, cwd=cwd if cwd is not None else cls._texcache,
252 stderr=subprocess.STDOUT)
253 except FileNotFoundError as exc:
File /opt/homebrew/Caskroom/miniforge/base/envs/glitchgan_test/lib/python3.11/subprocess.py:466, in check_output(timeout, *popenargs, **kwargs)
464 kwargs['input'] = empty
--> 466 return run(*popenargs, stdout=PIPE, timeout=timeout, check=True,
467 **kwargs).stdout
File /opt/homebrew/Caskroom/miniforge/base/envs/glitchgan_test/lib/python3.11/subprocess.py:548, in run(input, capture_output, timeout, check, *popenargs, **kwargs)
546 kwargs['stderr'] = PIPE
--> 548 with Popen(*popenargs, **kwargs) as process:
549 try:
File /opt/homebrew/Caskroom/miniforge/base/envs/glitchgan_test/lib/python3.11/subprocess.py:1026, in Popen.__init__(self, args, bufsize, executable, stdin, stdout, stderr, preexec_fn, close_fds, shell, cwd, env, universal_newlines, startupinfo, creationflags, restore_signals, start_new_session, pass_fds, user, group, extra_groups, encoding, errors, text, umask, pipesize, process_group)
1023 self.stderr = io.TextIOWrapper(self.stderr,
1024 encoding=encoding, errors=errors)
-> 1026 self._execute_child(args, executable, preexec_fn, close_fds,
1027 pass_fds, cwd, env,
1028 startupinfo, creationflags, shell,
1029 p2cread, p2cwrite,
1030 c2pread, c2pwrite,
1031 errread, errwrite,
1032 restore_signals,
1033 gid, gids, uid, umask,
1034 start_new_session, process_group)
1035 except:
1036 # Cleanup if the child failed starting.
File /opt/homebrew/Caskroom/miniforge/base/envs/glitchgan_test/lib/python3.11/subprocess.py:1955, in Popen._execute_child(self, args, executable, preexec_fn, close_fds, pass_fds, cwd, env, startupinfo, creationflags, shell, p2cread, p2cwrite, c2pread, c2pwrite, errread, errwrite, restore_signals, gid, gids, uid, umask, start_new_session, process_group)
1954 if err_filename is not None:
-> 1955 raise child_exception_type(errno_num, err_msg, err_filename)
1956 else:
FileNotFoundError: [Errno 2] No such file or directory: 'latex'
The above exception was the direct cause of the following exception:
RuntimeError Traceback (most recent call last)
File /opt/homebrew/Caskroom/miniforge/base/envs/glitchgan_test/lib/python3.11/site-packages/matplotlib/backend_bases.py:2157, in FigureCanvasBase.print_figure(self, filename, dpi, facecolor, edgecolor, orientation, format, bbox_inches, pad_inches, bbox_extra_artists, backend, **kwargs)
2154 # we do this instead of `self.figure.draw_without_rendering`
2155 # so that we can inject the orientation
2156 with getattr(renderer, "_draw_disabled", nullcontext)():
-> 2157 self.figure.draw(renderer)
2158 if bbox_inches:
2159 if bbox_inches == "tight":
File /opt/homebrew/Caskroom/miniforge/base/envs/glitchgan_test/lib/python3.11/site-packages/matplotlib/artist.py:94, in _finalize_rasterization.<locals>.draw_wrapper(artist, renderer, *args, **kwargs)
92 @wraps(draw)
93 def draw_wrapper(artist, renderer, *args, **kwargs):
---> 94 result = draw(artist, renderer, *args, **kwargs)
95 if renderer._rasterizing:
96 renderer.stop_rasterizing()
File /opt/homebrew/Caskroom/miniforge/base/envs/glitchgan_test/lib/python3.11/site-packages/matplotlib/artist.py:71, in allow_rasterization.<locals>.draw_wrapper(artist, renderer)
68 if artist.get_agg_filter() is not None:
69 renderer.start_filter()
---> 71 return draw(artist, renderer)
72 finally:
73 if artist.get_agg_filter() is not None:
File /opt/homebrew/Caskroom/miniforge/base/envs/glitchgan_test/lib/python3.11/site-packages/matplotlib/figure.py:3264, in Figure.draw(self, renderer)
3261 # ValueError can occur when resizing a window.
3263 self.patch.draw(renderer)
-> 3264 mimage._draw_list_compositing_images(
3265 renderer, self, artists, self.suppressComposite)
3267 renderer.close_group('figure')
3268 finally:
File /opt/homebrew/Caskroom/miniforge/base/envs/glitchgan_test/lib/python3.11/site-packages/matplotlib/image.py:134, in _draw_list_compositing_images(renderer, parent, artists, suppress_composite)
132 if not_composite or not has_images:
133 for a in artists:
--> 134 a.draw(renderer)
135 else:
136 # Composite any adjacent images together
137 image_group = []
File /opt/homebrew/Caskroom/miniforge/base/envs/glitchgan_test/lib/python3.11/site-packages/matplotlib/artist.py:71, in allow_rasterization.<locals>.draw_wrapper(artist, renderer)
68 if artist.get_agg_filter() is not None:
69 renderer.start_filter()
---> 71 return draw(artist, renderer)
72 finally:
73 if artist.get_agg_filter() is not None:
File /opt/homebrew/Caskroom/miniforge/base/envs/glitchgan_test/lib/python3.11/site-packages/gwpy/plot/axes.py:201, in Axes.draw(self, renderer)
198 ax.set_label_text(f"Time [{unit}] from {utc} UTC ({epoch!r})")
200 try:
--> 201 super().draw(renderer)
202 finally:
203 for ax in labels: # reset labels
File /opt/homebrew/Caskroom/miniforge/base/envs/glitchgan_test/lib/python3.11/site-packages/matplotlib/artist.py:71, in allow_rasterization.<locals>.draw_wrapper(artist, renderer)
68 if artist.get_agg_filter() is not None:
69 renderer.start_filter()
---> 71 return draw(artist, renderer)
72 finally:
73 if artist.get_agg_filter() is not None:
File /opt/homebrew/Caskroom/miniforge/base/envs/glitchgan_test/lib/python3.11/site-packages/matplotlib/axes/_base.py:3190, in _AxesBase.draw(self, renderer)
3187 for spine in self.spines.values():
3188 artists.remove(spine)
-> 3190 self._update_title_position(renderer)
3192 if not self.axison:
3193 for _axis in self._axis_map.values():
File /opt/homebrew/Caskroom/miniforge/base/envs/glitchgan_test/lib/python3.11/site-packages/matplotlib/axes/_base.py:3144, in _AxesBase._update_title_position(self, renderer)
3142 _log.debug('top of Axes not in the figure, so title not moved')
3143 return
-> 3144 if title.get_window_extent(renderer).ymin < top:
3145 _, y = self.transAxes.inverted().transform((0, top))
3146 title.set_position((x, y))
File /opt/homebrew/Caskroom/miniforge/base/envs/glitchgan_test/lib/python3.11/site-packages/matplotlib/text.py:969, in Text.get_window_extent(self, renderer, dpi)
964 raise RuntimeError(
965 "Cannot get window extent of text w/o renderer. You likely "
966 "want to call 'figure.draw_without_rendering()' first.")
968 with cbook._setattr_cm(fig, dpi=dpi):
--> 969 bbox, info, descent = self._get_layout(self._renderer)
970 x, y = self.get_unitless_position()
971 x, y = self.get_transform().transform((x, y))
File /opt/homebrew/Caskroom/miniforge/base/envs/glitchgan_test/lib/python3.11/site-packages/matplotlib/text.py:373, in Text._get_layout(self, renderer)
370 ys = []
372 # Full vertical extent of font, including ascenders and descenders:
--> 373 _, lp_h, lp_d = _get_text_metrics_with_cache(
374 renderer, "lp", self._fontproperties,
375 ismath="TeX" if self.get_usetex() else False,
376 dpi=self.get_figure(root=True).dpi)
377 min_dy = (lp_h - lp_d) * self._linespacing
379 for i, line in enumerate(lines):
File /opt/homebrew/Caskroom/miniforge/base/envs/glitchgan_test/lib/python3.11/site-packages/matplotlib/text.py:69, in _get_text_metrics_with_cache(renderer, text, fontprop, ismath, dpi)
66 """Call ``renderer.get_text_width_height_descent``, caching the results."""
67 # Cached based on a copy of fontprop so that later in-place mutations of
68 # the passed-in argument do not mess up the cache.
---> 69 return _get_text_metrics_with_cache_impl(
70 weakref.ref(renderer), text, fontprop.copy(), ismath, dpi)
File /opt/homebrew/Caskroom/miniforge/base/envs/glitchgan_test/lib/python3.11/site-packages/matplotlib/text.py:77, in _get_text_metrics_with_cache_impl(renderer_ref, text, fontprop, ismath, dpi)
73 @functools.lru_cache(4096)
74 def _get_text_metrics_with_cache_impl(
75 renderer_ref, text, fontprop, ismath, dpi):
76 # dpi is unused, but participates in cache invalidation (via the renderer).
---> 77 return renderer_ref().get_text_width_height_descent(text, fontprop, ismath)
File /opt/homebrew/Caskroom/miniforge/base/envs/glitchgan_test/lib/python3.11/site-packages/matplotlib/backends/backend_agg.py:211, in RendererAgg.get_text_width_height_descent(self, s, prop, ismath)
209 _api.check_in_list(["TeX", True, False], ismath=ismath)
210 if ismath == "TeX":
--> 211 return super().get_text_width_height_descent(s, prop, ismath)
213 if ismath:
214 ox, oy, width, height, descent, font_image = \
215 self.mathtext_parser.parse(s, self.dpi, prop)
File /opt/homebrew/Caskroom/miniforge/base/envs/glitchgan_test/lib/python3.11/site-packages/matplotlib/backend_bases.py:566, in RendererBase.get_text_width_height_descent(self, s, prop, ismath)
562 fontsize = prop.get_size_in_points()
564 if ismath == 'TeX':
565 # todo: handle properties
--> 566 return self.get_texmanager().get_text_width_height_descent(
567 s, fontsize, renderer=self)
569 dpi = self.points_to_pixels(72)
570 if ismath:
File /opt/homebrew/Caskroom/miniforge/base/envs/glitchgan_test/lib/python3.11/site-packages/matplotlib/texmanager.py:364, in TexManager.get_text_width_height_descent(cls, tex, fontsize, renderer)
362 if tex.strip() == '':
363 return 0, 0, 0
--> 364 dvifile = cls.make_dvi(tex, fontsize)
365 dpi_fraction = renderer.points_to_pixels(1.) if renderer else 1
366 with dviread.Dvi(dvifile, 72 * dpi_fraction) as dvi:
File /opt/homebrew/Caskroom/miniforge/base/envs/glitchgan_test/lib/python3.11/site-packages/matplotlib/texmanager.py:293, in TexManager.make_dvi(cls, tex, fontsize)
290 with TemporaryDirectory(dir=dvifile.parent) as tmpdir:
291 Path(tmpdir, "file.tex").write_text(
292 cls._get_tex_source(tex, fontsize), encoding='utf-8')
--> 293 cls._run_checked_subprocess(
294 ["latex", "-interaction=nonstopmode", "-halt-on-error",
295 "-no-shell-escape", "file.tex"], tex, cwd=tmpdir)
296 Path(tmpdir, "file.dvi").replace(dvifile)
297 # Also move the tex source to the main cache directory, but
298 # only for backcompat.
File /opt/homebrew/Caskroom/miniforge/base/envs/glitchgan_test/lib/python3.11/site-packages/matplotlib/texmanager.py:254, in TexManager._run_checked_subprocess(cls, command, tex, cwd)
250 report = subprocess.check_output(
251 command, cwd=cwd if cwd is not None else cls._texcache,
252 stderr=subprocess.STDOUT)
253 except FileNotFoundError as exc:
--> 254 raise RuntimeError(
255 f'Failed to process string with tex because {command[0]} '
256 'could not be found') from exc
257 except subprocess.CalledProcessError as exc:
258 raise RuntimeError(
259 '{prog} was not able to process the following string:\n'
260 '{tex!r}\n\n'
(...) 267 exc=exc.output.decode('utf-8', 'backslashreplace'))
268 ) from None
RuntimeError: Failed to process string with tex because latex could not be found
<Figure size 1680x640 with 28 Axes>
UMAP Embedding¶
3D UMAP embedding (correlation metric) of 2 000 real + 2 000 fake samples per class. Reproduces Figures 5 & 6 from the paper.
[ ]:
import tensorflow as tf
N_FULL = 2000
# Subsample real data
rng_full = np.random.default_rng(42)
real_idx = []
for lbl in LABEL_ORDER:
cls_idx = np.where(y_real == lbl)[0]
real_idx.extend(rng_full.choice(cls_idx, size=min(N_FULL, len(cls_idx)), replace=False))
X_real_full = X_real[real_idx]
y_real_full = y_real[real_idx]
print(f"Real subsampled : {X_real_full.shape}")
# Generate fake data in chunks (avoids OOM on large batch)
n_fake_full = NUM_CLASSES * N_FULL
noise_full = np.random.randn(n_fake_full, NOISE_DIM).astype("float32")
class_full = np.repeat(np.eye(NUM_CLASSES, dtype="float32"), N_FULL, axis=0)
CHUNK = 2048
chunks_fake = []
for i in range(0, n_fake_full, CHUNK):
out = gan.generator([noise_full[i:i+CHUNK], class_full[i:i+CHUNK]], training=False)
chunks_fake.append(out.numpy())
X_fake_full = np.concatenate(chunks_fake, axis=0)
y_fake_full = np.repeat(LABEL_ORDER, N_FULL)
print(f"Fake generated : {X_fake_full.shape}")
X_all_full = np.concatenate([X_real_full, X_fake_full], axis=0)
y_all_full = np.concatenate([y_real_full, y_fake_full], axis=0)
domain_labels_full = np.array(["Real"] * len(X_real_full) + ["Fake"] * len(X_fake_full))
print(f"UMAP input : {X_all_full.shape} ({X_all_full.nbytes / 1e9:.2f} GB)")
Real subsampled : (14000, 8192)
Fake generated : (14000, 8192)
UMAP input : (28000, 8192) (1.84 GB)
[ ]:
reducer_full = umap.UMAP(
n_components=3,
n_neighbors=15,
min_dist=0.6,
metric="correlation",
random_state=SEED,
low_memory=True,
)
embedding_full_corr = reducer_full.fit_transform(X_all_full)
print(f"Embedding shape : {embedding_full_corr.shape}")
/opt/homebrew/Caskroom/miniforge/base/envs/glitchgan_test/lib/python3.11/site-packages/umap/umap_.py:1952: UserWarning: n_jobs value 1 overridden to 1 by setting random_state. Use no seed for parallelism.
warn(
Embedding shape : (28000, 3)
[ ]:
low, high = 1, 99
x1, x2, x3 = embedding_full_corr[:, 0], embedding_full_corr[:, 1], embedding_full_corr[:, 2]
mask_clean = (
(x1 >= np.percentile(x1, low)) & (x1 <= np.percentile(x1, high)) &
(x2 >= np.percentile(x2, low)) & (x2 <= np.percentile(x2, high)) &
(x3 >= np.percentile(x3, low)) & (x3 <= np.percentile(x3, high))
)
print(f"Removed {np.sum(~mask_clean)} outliers ({np.sum(~mask_clean)/len(mask_clean)*100:.2f}%)")
embedding_full_clean = embedding_full_corr[mask_clean]
y_all_clean = y_all_full[mask_clean]
domain_labels_clean = domain_labels_full[mask_clean]
Removed 1680 outliers (6.00%)
[ ]:
# ── Figure 5: two-view 3D UMAP ────────────────────────────────────────────────
from matplotlib.lines import Line2D
n_per_class = 800
plot_indices = []
for lbl in LABEL_ORDER:
idx_real = np.where((y_all_clean == lbl) & (domain_labels_clean == "Real"))[0]
idx_fake = np.where((y_all_clean == lbl) & (domain_labels_clean == "Fake"))[0]
plot_indices.extend(np.random.choice(idx_real, min(len(idx_real), n_per_class), replace=False))
plot_indices.extend(np.random.choice(idx_fake, min(len(idx_fake), n_per_class), replace=False))
plot_indices = np.array(plot_indices)
embedding_plot = embedding_full_clean[plot_indices]
y_plot = y_all_clean[plot_indices]
domain_plot = domain_labels_clean[plot_indices]
unique_classes = np.array(LABEL_ORDER)
colors = plt.cm.tab10(np.linspace(0, 1, len(unique_classes)))
fig = plt.figure(figsize=(14, 6))
ax1 = fig.add_subplot(1, 2, 1, projection="3d")
ax2 = fig.add_subplot(1, 2, 2, projection="3d")
for lbl, color in zip(unique_classes, colors):
mask_real = (y_plot == lbl) & (domain_plot == "Real")
mask_fake = (y_plot == lbl) & (domain_plot == "Fake")
ax1.scatter(embedding_plot[mask_real, 0], embedding_plot[mask_real, 1], embedding_plot[mask_real, 2],
c=[color], s=1, alpha=0.4, label=lbl.replace("_", " "))
ax1.scatter(embedding_plot[mask_fake, 0], embedding_plot[mask_fake, 1], embedding_plot[mask_fake, 2],
c=[color], s=1, alpha=0.4)
ax2.scatter(embedding_plot[mask_real, 1], embedding_plot[mask_real, 0], embedding_plot[mask_real, 2],
c=[color], s=1, alpha=0.4)
ax2.scatter(embedding_plot[mask_fake, 1], embedding_plot[mask_fake, 0], embedding_plot[mask_fake, 2],
c=[color], s=1, alpha=0.4)
ax1.set_xlabel("UMAP-1", fontsize=18); ax1.set_ylabel("UMAP-2", fontsize=18); ax1.set_zlabel("UMAP-3", fontsize=18)
ax2.set_xlabel("UMAP-2", fontsize=18); ax2.set_ylabel("UMAP-1", fontsize=18); ax2.set_zlabel("UMAP-3", fontsize=18)
handles, labels_leg = ax1.get_legend_handles_labels()
fig.legend(handles, labels_leg, loc="lower center", ncol=len(unique_classes) // 2,
fontsize=16, title="Glitch Classes", title_fontsize=18,
frameon=False, bbox_to_anchor=(0.45, 0.02), markerscale=10)
plt.tight_layout(rect=[0, 0.12, 0.83, 1.3])
plt.subplots_adjust(wspace=0.25)
save_path = PLOTS_DIR / "umap_two_views_corr_2k.pdf"
plt.savefig(save_path, bbox_inches="tight")
print(f"Saved: {save_path}")
plt.show()
plt.close()
Saved: /Users/tomdooney/Documents/Work/Projects/glitchgan/evaluation_plots/umap_two_views_corr_2k.pdf
[ ]:
# ── Figure 6: per-class 3D UMAP ──────────────────────────────────────────────
fig = plt.figure(figsize=(4 * len(unique_classes), 5))
for i, lbl in enumerate(unique_classes):
ax = fig.add_subplot(1, len(unique_classes), i + 1, projection="3d")
mask_real = (y_plot == lbl) & (domain_plot == "Real")
mask_fake = (y_plot == lbl) & (domain_plot == "Fake")
ax.scatter(embedding_plot[mask_real, 0], embedding_plot[mask_real, 1], embedding_plot[mask_real, 2],
c="C0", label="Real", s=3, alpha=0.6)
ax.scatter(embedding_plot[mask_fake, 0], embedding_plot[mask_fake, 1], embedding_plot[mask_fake, 2],
c="C1", label="Fake", s=3, alpha=0.6)
ax.view_init(elev=25, azim=45)
ax.set_xlabel("UMAP-1", fontsize=17, labelpad=2)
ax.set_ylabel("UMAP-2", fontsize=17, labelpad=2)
ax.set_zlabel("UMAP-3", fontsize=17, labelpad=2)
ax.set_title(lbl.replace("_", " "), fontsize=30, pad=10)
ax.set_xticks([]); ax.set_yticks([]); ax.set_zticks([])
# if i == len(unique_classes) - 1:
# ax.legend(fontsize=25, loc="upper right", markerscale=1.5, frameon=False)
if i == 0:
ax.legend(fontsize=25, loc="lower left", markerscale=5, frameon=False)
plt.tight_layout(rect=[0.01, 0.01, 0.99, 1.1])
plt.subplots_adjust(wspace=0.25)
save_path = PLOTS_DIR / "umap_per_class_corr_2k.pdf"
plt.savefig(save_path, bbox_inches="tight")
print(f"Saved: {save_path}")
plt.show()
plt.close()
Saved: /Users/tomdooney/Documents/Work/Projects/glitchgan/evaluation_plots/umap_per_class_corr_2k.pdf
[ ]:
# --- 5a. Grid visualisation — 9 examples of each sampling mode ---------------
SAMPLES_GRID = 9
sigs_sp, cvecs_sp = generate_examples(
gan, noise_dim=NOISE_DIM, num_classes=NUM_CLASSES,
num_signals=SAMPLES_GRID, sampling="simplex")
sigs_un, cvecs_un = generate_examples(
gan, noise_dim=NOISE_DIM, num_classes=NUM_CLASSES,
num_signals=SAMPLES_GRID, sampling="uniform")
def _mixed_grid(signals, class_vecs, title):
fig, axes = plt.subplots(3, 3, figsize=(9, 6), sharex=True)
for ax, sig, cvec in zip(axes.flatten(), signals, class_vecs):
ax.plot(sig, lw=0.8, color="C0")
ax.set_xticks([])
ax.set_yticks([])
dominant = LABEL_ORDER[np.argmax(cvec)]
vec_str = ", ".join(f"{v:.2f}" for v in cvec)
ax.text(0.5, 1.04, f"{dominant}\n[{vec_str}]",
transform=ax.transAxes, fontsize=6.5, ha="center", va="bottom")
fig.suptitle(title, fontsize=12)
plt.tight_layout(pad=1.0, h_pad=2.2)
safe = title.lower().replace(" ", "_").replace("—", "").replace("(", "").replace(")", "").replace("=", "eq").strip("_")
plt.savefig(PLOTS_DIR / f"{safe}.pdf", bbox_inches="tight")
plt.show()
_mixed_grid(sigs_sp, cvecs_sp, "Generated Glitches — Simplex Mixing (sum=1)")
_mixed_grid(sigs_un, cvecs_un, "Generated Glitches — Uniform Mixing ([0,1])")
[ ]:
# --- 5b. Generate vertex / simplex / uniform for UMAP embedding --------------
SAMPLES_PER_TYPE = 1500
SAMPLES_VERTEX_TOTAL = (SAMPLES_PER_TYPE // NUM_CLASSES) * NUM_CLASSES # divisible
sigs_simplex, cvecs_simplex = generate_examples(
gan, noise_dim=NOISE_DIM, num_classes=NUM_CLASSES,
num_signals=SAMPLES_PER_TYPE, sampling="simplex")
sigs_uniform, cvecs_uniform = generate_examples(
gan, noise_dim=NOISE_DIM, num_classes=NUM_CLASSES,
num_signals=SAMPLES_PER_TYPE, sampling="uniform")
sigs_vertex, cvecs_vertex = generate_examples(
gan, noise_dim=NOISE_DIM, num_classes=NUM_CLASSES,
num_signals=SAMPLES_VERTEX_TOTAL, sampling="vertex")
y_vertex = np.array(LABEL_ORDER)[np.argmax(cvecs_vertex, axis=1)]
X_mix = np.concatenate([sigs_vertex, sigs_simplex, sigs_uniform], axis=0)
domain_labels = np.concatenate([
np.full(len(sigs_vertex), "Vertex"),
np.full(SAMPLES_PER_TYPE, "Simplex"),
np.full(SAMPLES_PER_TYPE, "Uniform"),
])
y_mix = np.concatenate([
y_vertex,
np.full(SAMPLES_PER_TYPE, "Simplex"),
np.full(SAMPLES_PER_TYPE, "Uniform"),
])
print(f"Total: {len(X_mix)} "
f"({len(sigs_vertex)} vertex, {SAMPLES_PER_TYPE} simplex, {SAMPLES_PER_TYPE} uniform)")
reducer_mix = umap.UMAP(n_components=3, random_state=SEED,
n_neighbors=30, min_dist=0.2, metric="euclidean")
embedding_mix = reducer_mix.fit_transform(X_mix)
print("UMAP done.")
Total: 4498 (1498 vertex, 1500 simplex, 1500 uniform)
/opt/homebrew/Caskroom/miniforge/base/envs/glitchgan_test/lib/python3.11/site-packages/umap/umap_.py:1952: UserWarning: n_jobs value 1 overridden to 1 by setting random_state. Use no seed for parallelism.
warn(
UMAP done.
[ ]:
# --- 5c. 3D UMAP: vertex colored by class, simplex/uniform in black ----------
_class_colors_mix = dict(zip(LABEL_ORDER,
plt.cm.tab10(np.linspace(0, 1, len(LABEL_ORDER)))))
fig = plt.figure(figsize=(10, 7))
ax = fig.add_subplot(111, projection="3d")
m_sp = domain_labels == "Simplex"
ax.scatter(embedding_mix[m_sp, 0], embedding_mix[m_sp, 1], embedding_mix[m_sp, 2],
c="k", marker="^", s=10, alpha=0.35, label="Simplex mix")
m_un = domain_labels == "Uniform"
ax.scatter(embedding_mix[m_un, 0], embedding_mix[m_un, 1], embedding_mix[m_un, 2],
c="k", marker="x", s=10, alpha=0.35, label="Uniform mix")
for lbl in LABEL_ORDER:
m = (domain_labels == "Vertex") & (y_mix == lbl)
ax.scatter(embedding_mix[m, 0], embedding_mix[m, 1], embedding_mix[m, 2],
c=[_class_colors_mix[lbl]], s=15, alpha=0.7, label=lbl)
ax.set_xlabel("UMAP-1", fontsize=12)
ax.set_ylabel("UMAP-2", fontsize=12)
ax.set_zlabel("UMAP-3", fontsize=12)
ax.legend(bbox_to_anchor=(1.05, 1), loc="upper left", fontsize=9)
plt.tight_layout()
plt.savefig(PLOTS_DIR / "simplex_uniform_umap.pdf", bbox_inches="tight")
plt.show()
[ ]:
# --- 5d. Save UMAP embedding for later reuse ---------------------------------
np.savez_compressed(
PLOTS_DIR / "simplex_uniform_umap_data.npz",
embedding=embedding_mix,
y_all=y_mix,
domain_labels=domain_labels,
label_order=np.array(LABEL_ORDER),
)
print(f"Saved → {PLOTS_DIR / 'simplex_uniform_umap_data.npz'}")
Saved → /Users/tomdooney/Documents/Work/Projects/glitchgan/evaluation_plots/simplex_uniform_umap_data.npz