Show the code
from IPython.display import Markdown
from tabulate import tabulate
from astropy.table import Table
from types import MethodType
from format_multiple_errors import format_multiple_errors
from numbers import Real
import numpy as np
import math
def format_cell(x,
exponential=False,
digits=2):
"""
Render a single table cell in latex depending on the type of x:
- scalar -> single value
- 1-tuple/list/np.array (value,) -> treat like scalar
- 2-tuple/list/np.array (value, err) -> value ± err
- 3-tuple/list/np.array (value, err_minus, err_plus) -> value_{-err_minus}^{+err_plus}
Formatted as exponential if exponential = True, with the indicated number of digits.
Format overrides may also be specified in a dictionary at the end of the tuple.
Non-numerical values are returned as string unchanged, if allowed.
"""
# TODO: improve automatic choice of exponential and digits value when unspecified
# Missing value
if x is None:
return "—"
list_formats = (list, tuple, np.ndarray)
numeric_formats = (Real, np.floating, np.integer)
# Scalar number
fmt = f".{digits}{'e' if exponential else 'f'}" # Format
if isinstance(x, np.ndarray) and x.ndim == 0: # Regularize potential ndarray 0D (e.g. np.array(value)) to python scalars
x = x.item()
if isinstance(x, numeric_formats): # Plain scalar value
return fr"${x:{fmt}}$"
if isinstance(x, list_formats) and len(x) == 1: # Or 1-tuple -> treat like scalar
value = x[0]
if isinstance(value, numeric_formats):
return fr"${value:{fmt}}$"
# Significant figures given digits
def sigfig(digits, value):
e = 0 if value == 0 else int(math.floor(math.log10(abs(value))))
return max(1, digits + e + 1) if not exponential else digits + 1
# 2-tuple symmetric uncertainty
if isinstance(x, list_formats) and len(x) == 2 and all(isinstance(v, numeric_formats) for v in x):
value, err = x
return rf"${format_multiple_errors(value, err, exponential=exponential, significant_figures=sigfig(digits, value), length_control='central', latex=True)}$"
# 3-tuple asymmetric uncertainty
if isinstance(x, list_formats) and len(x) == 3 and all(isinstance(v, numeric_formats) for v in x):
value, err_lower, err_upper, = x
return rf"${format_multiple_errors(value, (err_lower, err_upper), exponential=exponential, significant_figures=sigfig(digits, value), length_control='central', latex=True)}$"
# Special case: entry includes explicit format overrides as a dictionary in the last element of the tuple
if isinstance(x, list_formats) and isinstance(x[-1], dict):
*values, fmt_overrides = x
note = fmt_overrides.pop("note", "") # Return value associated to the key and remove entry from dictionary (otherwise format_cell() produces an error)
fmt_merged = {
"exponential": exponential,
"digits": digits,
**fmt_overrides
} # Shallow merge: if duplicate key in dictionary, take the last one, effectively overriding the specified format parameters
values_unpacked = values[0] if len(values) == 1 else tuple(values)
return format_cell(values_unpacked, **fmt_merged) + note
# Fallback: try to convert to string, or raise exception
try:
return str(x)
except:
raise ValueError(f"Unrecognized cell format for entry {x}")
def table_to_markdown(tbl,
exponential=False,
digits=2,
col_fmt_overrides=None,
headers=None,
**kwargs):
"""
Convert an Astropy Table into a Markdown table, formatting every cell through the format_cell() function.
Formatting instructions passed to format_cell() can be overridden for specific columns through the col_fmt_overrides field
"""
default_fmt = {
"exponential": exponential,
"digits": digits
}
rows = []
for r in tbl:
row = []
for c in tbl.colnames:
if col_fmt_overrides is None:
fmt = default_fmt # Default format
else:
fmt = {**default_fmt, **col_fmt_overrides.get(c, {})} # Shallow merge: if duplicate key, take the last one, overriding the specified format parameters for current column
row.append(format_cell(r[c], **fmt))
rows.append(row)
if headers is None:
headers = tbl.colnames
md = tabulate(
rows,
headers,
**kwargs
)
return md
# ---------------- Example usage ----------------
# Map column name --> (header, units); TODO: units using astropy.units and converting when formatting header
colmap = {
"ID": ("Name", None),
"redshift": ("Redshift", None),
"magnification": ("Lensed", None),
"MUV": (r"$M_{\rm UV}$", None),
"beta": (r"$\beta$-slope", None),
"Mstar": (r"$M_\star$", r"M$_\odot$"),
"MIII": (r"$M_\mathrm{III}$", r"M$_\odot$"),
"SFR": ("SFR", r"M$_\odot$yr$^{-1}$"),
"Lya_flux": (r"Ly$\alpha$ flux", r"erg s$^{-1}$ cm$^{-2}$"),
"Lya_EW": (r"Ly$\alpha$ EW", "Å"),
"Lya_FWHM": (r"Ly$\alpha$ FWHM", r"km s$^{-1}$"),
"Ha_flux": (r"H$\alpha$ flux", r"erg s$^{-1}$ cm$^{-2}$"),
"Ha_EW": (r"H$\alpha$ EW", "Å"),
"Ha_FWHM": (r"H$\alpha$ FWHM", r"km s$^{-1}$"),
"Hb_flux": (r"H$\beta$ flux", r"erg s$^{-1}$ cm$^{-2}$"),
"Hb_EW": (r"H$\beta$ EW", "Å"),
"Hb_FWHM": (r"H$\beta$ FWHM", r"km s$^{-1}$"),
"HeII1640_flux": ("HeII1640 flux", r"erg s$^{-1}$ cm$^{-2}$"),
"HeII1640_EW": ("HeII1640 EW", "Å"),
"HeII1640_FWHM": ("HeII1640 FWHM", r"km s$^{-1}$"),
"HeII4686_flux": ("HeII4686 flux", r"erg s$^{-1}$ cm$^{-2}$"),
"HeII4686_EW": ("HeII4686 EW", "Å"),
"HeII4686_FWMH": ("HeII4686 FWHM", r"km s$^{-1}$"),
"OIII5007_flux": ("[OIII]5007 flux", r"erg s$^{-1}$ cm$^{-2}$"),
"OIII5007_EW": ("[OIII]5007 EW", "Å"),
"OIII5007_FWHM": ("[OIII]5007 FWHM", r"km s$^{-1}$"),
"OIIItoHb": (r"[OIII]/H$\beta$", None),
"OtoH": ("12 + log(O/H)", None),
"metallicity": ("Metallicity", r"Z$_\odot$"),
"reference": ("Reference", None),
}
names = tuple(colmap.keys())
headers = tuple(
(fr"{label}<br>[{unit}]" if unit else f"{label}<br> ")
for (label, unit) in colmap.values()
)
# Setup table
table = Table(
names=names,
dtype=("object",)*len(names),
masked=True
)
# Minor patch of astropy table add_row method, so that missing values are set to None by default instead of 0
defaults = {c: None for c in table.colnames}
def add_row_with_defaults(self, row=None, *args, **kwargs):
if isinstance(row, dict):
row = {**defaults, **row}
return Table.add_row(self, row, *args, **kwargs)
table.add_row = MethodType(add_row_with_defaults, table)
# Column format overrides
col_fmt_overrides = {
"Mstar": {"exponential": True},
"MIII": {"exponential": True},
"Lya_flux": {"exponential": True},
"Ha_flux": {"exponential": True},
"Hb_flux": {"exponential": True},
"HeII1640_flux": {"exponential": True},
"HeII4686_flux": {"exponential": True},
"OIII5007_flux": {"exponential": True},
}
# Notes style
def note(text, color="red"):
return rf"$^{{\textcolor{{{color}}}{{({text})}}}}$"
# Fill table
table.add_row(dict(
ID="GNHeII J1236+6215",
redshift=(2.9803, 0.0010, {"digits": 4}),
magnification="No",
MUV=(-22.09, 0.02, {"digits": 2}),
beta=(-2.18, 0.06, {"digits": 2}),
Lya_flux=(23.0e-18, 5.4e-18, {"digits": 2}),
Lya_EW=(19.2),
Lya_FWHM=(758, 90, {"digits": 0}), # Note that values for the FWHMs are also given in A in Tab. 4
Ha_flux=(16.46e-18, 0.32e-18, {"digits": 3}),
Ha_EW=166.5,
Ha_FWHM=(268, 41, {"digits": 0}),
Hb_flux=(5.44e-18, 0.37e-18, {"digits": 2}),
Hb_EW=(26.6),
Hb_FWHM=(320, 137, {"digits": 0}),
HeII1640_flux=(8.8e-18, 1.8e-18),
HeII1640_EW=8.3,
HeII1640_FWHM=(573, 191, {"digits": 0}),
Mstar=(7.8e8, 3.1e8),
SFR=(12.2, 2.0, {"note": note("a")}), # From SED, i.e. the value reported in the abstract
OIII5007_flux=(59.6e-18, 2.2e-18, {"digits": 2}),
OIII5007_EW=248.8,
OIIItoHb=(5.45, 0.32, {"digits": 2}), # They also give HeII/Hbeta = 1.96 \pm 0.30, and HeII/Halpha = 0.69, 0.10, reported to closely match with the candidate from Wang+24
OtoH=(7.85, 0.22, {"digits": 2}),
metallicity=(0.003, 0.002, {"digits": 3}),
reference="@mondal2025"
))
table.add_row(dict(
ID="MPG-CR3",
redshift=(3.193, 0.016, {"digits": 3}),
magnification="No",
Lya_flux=(5.8e-17, 0.7e-17),
Lya_EW=(822, 101, {"digits": 0}),
Ha_flux=(4.2e-18, 0.6e-18, {"note": note("b")}),
Ha_EW=(2814, 327, {"digits": 0}),
Hb_flux=(6.3e-19, 0.7e-19),
HeII1640_flux=(None, {"note": note("c")}),
MIII=6.1e5,
OIII5007_flux=r"$< 5.6 \times 10^{-19}$", # At 2sigma
OtoH=r"$< 6.52$",
metallicity=r"$< 8 \times 10^{-3}$",
reference="@cai2025"
))
table.add_row(dict(
ID="AMORE6",
redshift=(5.7253, 0.00005, {"digits": 4}),
magnification=("Yes", {"note": note("d")}), # $\mu = 39.32_{-3.48}^{+3.73}$ for AMORE6-A, vs $\mu = 77.69_{-5.92}^{+8.37}$ for AMORE6-B
MUV=(-14.52, 0.08, 0.07, {"digits": 2, "note": note("e")}),
beta=(-2.77, 0.09, 0.07, {"digits": 2}),
Lya_flux= (4.95e-19, 0.92e-19, {"digits": 2}), # From Lyalpha-to-Hbeta ratio = 10.01 \pm 1.4, propagating uncertainties
Hb_flux=(0.49e-19, 0.06e-19),
Hb_EW=(1594.7, 206.9),
Mstar=(4.37e5, 0.73e5, 2.24e5, {"digits": 2}),
SFR=(0.35, 0.06, {"digits": 2, "note": note("f")}), # From Hbeta, converting from log to linear
OIII5007_flux=r"$< 1.1 \times 10^{-20}$", # Upper limits at 2sigma
OtoH=r"$< 5.78$",
metallicity=r"$< 0.0012$",
reference="@morishita2025"
))
table.add_row(dict(
ID="JOF-21739",
redshift=(6.17, 0.19, 0.06, {"digits": 2}),
magnification="Yes", # Value of magnification not indicated
MUV=(-17.62, 0.15, 0.17, {"digits": 2}),
beta=(-2.79, 0.05, {"digits": 2}),
Ha_EW=(3600, 430, {"digits": 0}),
MIII=r"$\sim 10^5 - 10^6$",
OIIItoHb=r"$< 0.32$",
OtoH=r"$< 6.2$",
metallicity=r"$< 0.003$",
reference="@fujimoto2025"
))
'''
# Rejected: detected [OIII] in the spectrum
table.add_row(dict(
ID="GLIMPSE-1604",
redshift=(6.50, 0.24, 0.03, {"digits": 2}),
magnification="Yes", # mu = (2.9, 0.2, 0.1),
MUV=(-15.89, 0.14, 0.12, {"digits": 2}),
beta=(-2.34, 0.36, {"digits": 2}),
Ha_EW=(2810, 550, {"digits": 0}),
MIII=r"$\sim 10^5$",
OIIItoHb=r"$< 0.44$",
OtoH=r"$< 6.4$",
metallicity=r"$< 0.005$",
reference="@fujimoto2025"
))
'''
table.add_row(dict(
ID="LAP1",
redshift=(6.639, 0.004, {"digits": 3}),
magnification="Yes", # mu_tot(median) = (120, 9), mu_tang(median) = (55, 6, 2), mu_tot > 500 for images A1,A2, mu_tot=98,99 for B1,B2
MUV=r"$> -11.2$",
Mstar=r"$\lesssim 10^4$",
Lya_flux=(369.2e-20, 29.3e-20, {"digits": 3}),
Lya_EW=r"$> 370$",
Ha_flux=(69.4e-20, 5.5e-20, {"digits": 2}),
Ha_EW=r"$> 2020$",
Hb_flux=(26.3e-20, 2.7e-20, {"digits": 2}),
Hb_EW=r"$> 420$",
HeII1640_flux=(79.6e-20, 20.7e-20, {"digits": 2, "note": note("g")}),
OIII5007_flux=(14.5e-20, 3.4e-20, {"digits": 2}),
OIII5007_EW=r"$> 246$",
OIIItoHb=(0.55, 0.14, 0.15, {"digits": 2}),
OtoH=r"$< 6.3$",
metallicity=r"$< 0.004$",
reference="@vanzella2023"
))
table.add_row(dict(
ID="RX J2129-z8HeII",
redshift=(8.1623, 0.0007, {"digits": 4}),
magnification="Yes", # mu = (2.26, 0.14),
MUV=(-19.58, 0.02, 0.03, {"digits": 2}),
beta=(-2.53, 0.07, 0.06, {"digits": 2}),
Mstar=(5.6e7, 0.7e7, 0.8e7), # Tot. stellar mass of the system LogM*/Msun = 7.75 \pm 0.06 from Tab. 1
MIII=(7.8e5, 1.4e5), # Mass of the putative Pop III component estimated from the HeII luminosity
SFR=(9.56, 1.70, 4.51, {"digits": 2}),
Hb_flux=(71e-20, 10e-20, {"note": note("h")}),
Hb_EW=(202, 34, {"digits": 0}),
HeII1640_flux=(120e-20, 22e-20, {"digits": 2}),
HeII1640_EW=(21, 4, {"digits": 0}),
HeII4686_flux=r"$< 1.6 \times 10^{-19}$", # 2sigma upper limits
HeII4686_EW=r"$< 49$",
OIII5007_flux=(390e-20, 10e-20, {"digits": 2}),
OIII5007_EW=(1015, 83, {"digits": 0}),
OIIItoHb=(5.5, 0.8),
OtoH=(7.63, 0.09, 0.14, {"digits": 2}),
metallicity=r"$\sim 0.1$", # Log(Zstar/Zsun) ~ -0.9
reference="@wang2024"
))
table.add_row(dict(
ID="EXCELS-63107",
redshift=(8.271, {"digits": 3}),
magnification="No",
MUV=(-19.9, 0.1),
beta=(-3.3, 0.3),
Mstar=(3.72e8, 3.37e8, 4.05e8, {"digits": 2, "note": note("i")}), # Assuming extended SF + recent burst model and converting from log to linear --> LogMstar/Msun = 8.57 -1.03 +0.32 to linear, with mass of the burst LogMburst/Msun = 7.35 -1.30 +0.22 (see Sec. 3.3 and Tab. 3)
SFR=(7.8, 0.6), # From Hbeta
Hb_flux=(10.69e-19, 0.84e-19, {"digits": 3}),
OIII5007_flux=(38.54e-19, 1.20e-19, {"digits": 3}),
OIIItoHb=(3.61, 0.30, {"digits": 2}),
OtoH=(6.89, 0.21, 0.26, {"digits": 2, "note": note("j")}),
metallicity=(0.016, {"digits": 3}),
reference="@cullen2025"
))
table.add_row(dict(
ID="GN-z11 HeII clump",
redshift=(10.6034, 0.0013, {"digits": 4, "note": note("k")}),
magnification="No",
MIII=r"$\sim (2 - 2.5) \times 10^5$",
HeII1640_flux=(1.8e-19, 0.34e-19, {"note": note("l")}), # Fluxes and EWs from Tab. 2 (HeII clump small aperture), converting log to linear
HeII1640_EW=(62, 27, 25, {"digits": 0, "note": note("m")}),
reference="@maiolino2024"
))
# Markdown table
md = table_to_markdown(table, tablefmt="github", headers=headers, exponential=False, digits=1, col_fmt_overrides=col_fmt_overrides)
Markdown(md)| Name |
Redshift |
Lensed |
\(M_{\rm UV}\) |
\(\beta\)-slope |
\(M_\star\) [M\(_\odot\)] |
\(M_\mathrm{III}\) [M\(_\odot\)] |
SFR [M\(_\odot\)yr\(^{-1}\)] |
Ly\(\alpha\) flux [erg s\(^{-1}\) cm\(^{-2}\)] |
Ly\(\alpha\) EW [Å] |
Ly\(\alpha\) FWHM [km s\(^{-1}\)] |
H\(\alpha\) flux [erg s\(^{-1}\) cm\(^{-2}\)] |
H\(\alpha\) EW [Å] |
H\(\alpha\) FWHM [km s\(^{-1}\)] |
H\(\beta\) flux [erg s\(^{-1}\) cm\(^{-2}\)] |
H\(\beta\) EW [Å] |
H\(\beta\) FWHM [km s\(^{-1}\)] |
HeII1640 flux [erg s\(^{-1}\) cm\(^{-2}\)] |
HeII1640 EW [Å] |
HeII1640 FWHM [km s\(^{-1}\)] |
HeII4686 flux [erg s\(^{-1}\) cm\(^{-2}\)] |
HeII4686 EW [Å] |
HeII4686 FWHM [km s\(^{-1}\)] |
[OIII]5007 flux [erg s\(^{-1}\) cm\(^{-2}\)] |
[OIII]5007 EW [Å] |
[OIII]5007 FWHM [km s\(^{-1}\)] |
[OIII]/H\(\beta\) |
12 + log(O/H) |
Metallicity [Z\(_\odot\)] |
Reference |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| GNHeII J1236+6215 | \(2.9803 \pm 0.0010\) | No | \(-22.09 \pm 0.02\) | \(-2.18 \pm 0.06\) | \((7.8 \pm 3.1) \times 10^{8}\) | — | \(12.2 \pm 2.0\)\(^{\textcolor{red}{(a)}}\) | \((2.30 \pm 0.54) \times 10^{-17}\) | \(19.2\) | \(758 \pm 90\) | \((1.646 \pm 0.032) \times 10^{-17}\) | \(166.5\) | \(268 \pm 41\) | \((5.44 \pm 0.37) \times 10^{-18}\) | \(26.6\) | \(320 \pm 137\) | \((8.8 \pm 1.8) \times 10^{-18}\) | \(8.3\) | \(573 \pm 191\) | — | — | — | \((5.96 \pm 0.22) \times 10^{-17}\) | \(248.8\) | — | \(5.45 \pm 0.32\) | \(7.85 \pm 0.22\) | \(0.003 \pm 0.002\) | Mondal et al. (2025) |
| MPG-CR3 | \(3.193 \pm 0.016\) | No | — | — | — | \(6.1e+05\) | — | \((5.8 \pm 0.7) \times 10^{-17}\) | \(822 \pm 101\) | — | \((4.2 \pm 0.6) \times 10^{-18}\)\(^{\textcolor{red}{(b)}}\) | \(2814 \pm 327\) | — | \((6.3 \pm 0.7) \times 10^{-19}\) | — | — | —\(^{\textcolor{red}{(c)}}\) | — | — | — | — | — | \(< 5.6 \times 10^{-19}\) | — | — | — | \(< 6.52\) | \(< 8 \times 10^{-3}\) | Cai et al. (2025) |
| AMORE6 | \(5.7253 \pm 0.0001\) | Yes\(^{\textcolor{red}{(d)}}\) | \(-14.52 {}^{+0.08}_{-0.07}\)\(^{\textcolor{red}{(e)}}\) | \(-2.77 {}^{+0.09}_{-0.07}\) | \((4.37 {}^{+0.73}_{-2.24}) \times 10^{5}\) | — | \(0.35 \pm 0.06\)\(^{\textcolor{red}{(f)}}\) | \((4.95 \pm 0.92) \times 10^{-19}\) | — | — | — | — | — | \((4.9 \pm 0.6) \times 10^{-20}\) | \(1594.7 \pm 206.9\) | — | — | — | — | — | — | — | \(< 1.1 \times 10^{-20}\) | — | — | — | \(< 5.78\) | \(< 0.0012\) | Morishita et al. (2025) |
| JOF-21739 | \(6.17 {}^{+0.19}_{-0.06}\) | Yes | \(-17.62 {}^{+0.15}_{-0.17}\) | \(-2.79 \pm 0.05\) | — | \(\sim 10^5 - 10^6\) | — | — | — | — | — | \(3600 \pm 430\) | — | — | — | — | — | — | — | — | — | — | — | — | — | \(< 0.32\) | \(< 6.2\) | \(< 0.003\) | Fujimoto et al. (2025) |
| LAP1 | \(6.639 \pm 0.004\) | Yes | \(> -11.2\) | — | \(\lesssim 10^4\) | — | — | \((3.692 \pm 0.293) \times 10^{-18}\) | \(> 370\) | — | \((6.94 \pm 0.55) \times 10^{-19}\) | \(> 2020\) | — | \((2.63 \pm 0.27) \times 10^{-19}\) | \(> 420\) | — | \((7.96 \pm 2.07) \times 10^{-19}\)\(^{\textcolor{red}{(g)}}\) | — | — | — | — | — | \((1.45 \pm 0.34) \times 10^{-19}\) | \(> 246\) | — | \(0.55 {}^{+0.14}_{-0.15}\) | \(< 6.3\) | \(< 0.004\) | Vanzella et al. (2023) |
| RX J2129-z8HeII | \(8.1623 \pm 0.0007\) | Yes | \(-19.58 {}^{+0.02}_{-0.03}\) | \(-2.53 {}^{+0.07}_{-0.06}\) | \((5.6 {}^{+0.7}_{-0.8}) \times 10^{7}\) | \((7.8 \pm 1.4) \times 10^{5}\) | \(9.56 {}^{+1.70}_{-4.51}\) | — | — | — | — | — | — | \((7.1 \pm 1.0) \times 10^{-19}\)\(^{\textcolor{red}{(h)}}\) | \(202 \pm 34\) | — | \((1.20 \pm 0.22) \times 10^{-18}\) | \(21 \pm 4\) | — | \(< 1.6 \times 10^{-19}\) | \(< 49\) | — | \((3.90 \pm 0.10) \times 10^{-18}\) | \(1015 \pm 83\) | — | \(5.5 \pm 0.8\) | \(7.63 {}^{+0.09}_{-0.14}\) | \(\sim 0.1\) | Wang et al. (2024) |
| EXCELS-63107 | \(8.271\) | No | \(-19.9 \pm 0.1\) | \(-3.3 \pm 0.3\) | \((3.72 {}^{+3.37}_{-4.05}) \times 10^{8}\)\(^{\textcolor{red}{(i)}}\) | — | \(7.8 \pm 0.6\) | — | — | — | — | — | — | \((1.069 \pm 0.084) \times 10^{-18}\) | — | — | — | — | — | — | — | — | \((3.854 \pm 0.120) \times 10^{-18}\) | — | — | \(3.61 \pm 0.30\) | \(6.89 {}^{+0.21}_{-0.26}\)\(^{\textcolor{red}{(j)}}\) | \(0.016\) | Cullen et al. (2025) |
| GN-z11 HeII clump | \(10.6034 \pm 0.0013\)\(^{\textcolor{red}{(k)}}\) | No | — | — | — | \(\sim (2 - 2.5) \times 10^5\) | — | — | — | — | — | — | — | — | — | — | \((1.8 \pm 0.3) \times 10^{-19}\)\(^{\textcolor{red}{(l)}}\) | \(62 {}^{+27}_{-25}\)\(^{\textcolor{red}{(m)}}\) | — | — | — | — | — | — | — | — | — | — | Maiolino et al. (2024) |