diff --git a/CLAUDE.md b/CLAUDE.md index f575b721..b9f3d639 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -9,21 +9,31 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co pip install -e ".[dev]" ``` -### Run Tests -```bash -# All tests -python -m pytest test_autoarray/ +### Run Tests +```bash +# All tests +python -m pytest test_autoarray/ # Single test file python -m pytest test_autoarray/structures/test_arrays.py -# With output -python -m pytest test_autoarray/structures/test_arrays.py -s -``` - -### Formatting -```bash -black autoarray/ +# With output +python -m pytest test_autoarray/structures/test_arrays.py -s +``` + +### Codex / sandboxed runs + +When running Python from Codex or any restricted environment, set writable cache directories so `numba` and `matplotlib` do not fail on unwritable home or source-tree paths: + +```bash +NUMBA_CACHE_DIR=/tmp/numba_cache MPLCONFIGDIR=/tmp/matplotlib python -m pytest test_autoarray/ +``` + +This workspace is often imported from `/mnt/c/...` and Codex may not be able to write to module `__pycache__` directories or `/home/jammy/.cache`, which can cause import-time `numba` caching failures without this override. + +### Formatting +```bash +black autoarray/ ``` ## Architecture diff --git a/autoarray/dataset/plot/interferometer_plots.py b/autoarray/dataset/plot/interferometer_plots.py index df872e9c..d4b20382 100644 --- a/autoarray/dataset/plot/interferometer_plots.py +++ b/autoarray/dataset/plot/interferometer_plots.py @@ -42,7 +42,7 @@ def subplot_interferometer_dataset( fig, axes = plt.subplots(2, 3, figsize=conf_subplot_figsize(2, 3)) axes = axes.flatten() - plot_grid(dataset.data.in_grid, ax=axes[0], title="Visibilities") + plot_grid(dataset.data.in_grid, ax=axes[0], title="Visibilities", xlabel="", ylabel="") plot_grid( Grid2DIrregular.from_yx_1d( y=dataset.uv_wavelengths[:, 1] / 10**3.0, @@ -50,14 +50,16 @@ def subplot_interferometer_dataset( ), ax=axes[1], title="UV-Wavelengths", + xlabel="", + ylabel="", ) plot_yx( dataset.amplitudes, dataset.uv_distances / 10**3.0, ax=axes[2], title="Amplitudes vs UV-distances", - ylabel="Jy", - xlabel="k$\\lambda$", + xtick_suffix='"', + ytick_suffix="Jy", plot_axis_type="scatter", ) plot_yx( @@ -65,8 +67,8 @@ def subplot_interferometer_dataset( dataset.uv_distances / 10**3.0, ax=axes[3], title="Phases vs UV-distances", - ylabel="deg", - xlabel="k$\\lambda$", + xtick_suffix='"', + ytick_suffix="deg", plot_axis_type="scatter", ) plot_array( diff --git a/autoarray/plot/utils.py b/autoarray/plot/utils.py index 9e72d2b0..a3a8da78 100644 --- a/autoarray/plot/utils.py +++ b/autoarray/plot/utils.py @@ -533,15 +533,52 @@ def _colorbar_tick_values(norm) -> Optional[List[float]]: return [lo, mid, hi] +_SUPERSCRIPT_DIGITS = str.maketrans("0123456789-", "⁰¹²³⁴⁵⁶⁷⁸⁹⁻") + + +def _to_scientific(v: float) -> Optional[str]: + """Convert *v* to Unicode scientific notation (e.g. ``4.3×10⁴``). + + Returns ``None`` when ``f"{v:.2g}"`` does not produce an exponent (unusual + edge case for certain values near the g-format threshold). + """ + s = f"{v:.2g}" + if "e" not in s: + return None + mantissa, exp = s.split("e") + sign = "-" if exp.startswith("-") else "" + exp_num = exp.lstrip("+-").lstrip("0") or "0" + superscript = f"{sign}{exp_num}".translate(_SUPERSCRIPT_DIGITS) + return f"{mantissa}×10{superscript}" + + def _fmt_tick(v: float) -> str: - """Format a single tick value to 2 decimal places without scientific notation.""" + """Format a single tick value compactly. + + Values with 5 or more digits (abs(v) >= 10000) or very small values + (abs(v) < 0.001) are rendered as compact scientific notation using + Unicode superscripts, e.g. ``4.3×10⁴`` or ``1.2×10⁻⁵``. This avoids + LaTeX expansion that would overflow the colorbar width. Values in + between are rendered with ``:.2f``. + """ + abs_v = abs(v) + if abs_v != 0 and (abs_v >= 10000 or abs_v < 0.001): + sci = _to_scientific(v) + return sci if sci is not None else f"{v:.2g}" return f"{v:.2f}" def _colorbar_tick_labels(tick_values: List[float], cb_unit: Optional[str] = None) -> List[str]: - """Format tick values without scientific notation, appending *cb_unit* to the middle label. + """Format tick values, appending *cb_unit* to the middle label. + + All three labels use a consistent notation style: if any tick is rendered + in scientific notation (``×10ⁿ``), every non-zero tick is forced through + the same format. This prevents the central tick from showing e.g. + ``-5000.00`` when the outer ticks show ``-2×10⁴`` / ``1.5×10⁴`` because + the midpoint happens to fall below the per-value threshold. - If *cb_unit* is ``None`` the unit is read from config; pass ``""`` for unitless panels. + If *cb_unit* is ``None`` the unit is read from config; pass ``""`` for + unitless panels. """ if cb_unit is None: try: @@ -551,6 +588,18 @@ def _colorbar_tick_labels(tick_values: List[float], cb_unit: Optional[str] = Non cb_unit = "" labels = [_fmt_tick(v) for v in tick_values] mid = len(labels) // 2 + + # Enforce consistent notation: if any label uses ×10, convert all others. + if any("×10" in lbl for lbl in labels): + for i, (lbl, v) in enumerate(zip(labels, tick_values)): + if "×10" not in lbl: + if v == 0: + labels[i] = "0" + else: + sci = _to_scientific(v) + if sci is not None: + labels[i] = sci + labels[mid] = f"{labels[mid]}{cb_unit}" return labels @@ -569,8 +618,8 @@ def _apply_colorbar( Override the unit string on the middle tick. Pass ``""`` for unitless panels. ``None`` reads the unit from config. is_subplot - When ``True`` uses ``labelsize_subplot`` from config (default 22) instead of - the single-figure ``labelsize`` (default 22). + When ``True`` uses ``labelsize_subplot`` from config (default 18) instead of + the single-figure ``labelsize`` (default 18). """ tick_values = _colorbar_tick_values(getattr(mappable, "norm", None)) @@ -582,7 +631,7 @@ def _apply_colorbar( ticks=tick_values, ) labelsize_key = "labelsize_subplot" if is_subplot else "labelsize" - labelsize = float(_conf_colorbar(labelsize_key, 22)) + labelsize = float(_conf_colorbar(labelsize_key, 18)) labelrotation = float(_conf_colorbar("labelrotation", 90)) if tick_values is not None: cb.ax.set_yticklabels( diff --git a/autoarray/plot/yx.py b/autoarray/plot/yx.py index fc26ad7b..b3cf3fcc 100644 --- a/autoarray/plot/yx.py +++ b/autoarray/plot/yx.py @@ -25,8 +25,10 @@ def plot_yx( title: str = "", xlabel: str = "", ylabel: str = "", + xtick_suffix: str = "", + ytick_suffix: str = "", label: Optional[str] = None, - color: str = "b", + color: str = "k", linestyle: str = "-", plot_axis_type: str = "linear", # --- figure control (used only when ax is None) ----------------------------- @@ -129,7 +131,21 @@ def plot_yx( ax.fill_between(x, y1, y2, alpha=0.3) # --- labels ---------------------------------------------------------------- - apply_labels(ax, title=title, xlabel=xlabel, ylabel=ylabel) + apply_labels(ax, title=title, xlabel=xlabel, ylabel=ylabel, is_subplot=not owns_figure) + + # --- 3-point ticks with optional unit suffixes ---------------------------- + from autoarray.plot.utils import _inward_ticks, _round_ticks, _conf_ticks + + factor = _conf_ticks("extent_factor_2d", 0.75) + + xlo, xhi = ax.get_xlim() + ylo, yhi = ax.get_ylim() + xticks = _round_ticks(_inward_ticks(xlo, xhi, factor, 3)) + yticks = _round_ticks(_inward_ticks(ylo, yhi, factor, 3)) + ax.set_xticks(xticks) + ax.set_xticklabels([f"{v:g}{xtick_suffix}" for v in xticks]) + ax.set_yticks(yticks) + ax.set_yticklabels([f"{v:g}{ytick_suffix}" for v in yticks]) if label is not None: ax.legend(fontsize=12)