Skip to content

Commit d08e712

Browse files
committed
Add LaTeX reader
1 parent 6156e3a commit d08e712

4 files changed

Lines changed: 178 additions & 18 deletions

File tree

‎astropy/cosmology/_src/io/builtin/latex.py‎

Lines changed: 83 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@
4747
FlatLambdaCDM & Planck18 & 67.66 & 0.30966 & 2.7255 & 3.046 & 0.0 .. 0.06 & 0.04897 \\
4848
\end{tabular}
4949
\end{table}
50-
<BLANKLINE>
50+
<BLANKLINE>
5151
5252
.. testcleanup::
5353
@@ -57,13 +57,16 @@
5757
from typing import Any, TypeVar
5858

5959
import astropy.units as u
60+
import astropy.cosmology.units as cu
6061
from astropy.cosmology._src.core import Cosmology
6162
from astropy.cosmology._src.io.connect import readwrite_registry
6263
from astropy.cosmology._src.parameter import Parameter
64+
from astropy.cosmology._src.typing import _CosmoT
6365
from astropy.io.typing import PathLike, WriteableFileLike
6466
from astropy.table import QTable, Table
67+
from astropy.io.typing import PathLike, ReadableFileLike, WriteableFileLike #not sure if i should place it here but let's see if it all works...!
6568

66-
from .table import to_table
69+
from .table import from_table, to_table
6770

6871
_TableT = TypeVar("_TableT", bound=Table)
6972

@@ -83,6 +86,83 @@
8386
}
8487

8588

89+
def read_latex(
90+
filename: PathLike | ReadableFileLike[Table],
91+
index: int | str | None = None,
92+
*,
93+
move_to_meta: bool = False,
94+
cosmology: str | type[_CosmoT] | None = None,
95+
latex_names: bool = True,
96+
**kwargs: Any
97+
) -> _CosmoT:
98+
r"""Read a |Cosmology| from a LaTeX file.
99+
100+
Parameters
101+
----------
102+
filename : path-like or file-like
103+
From where to read the Cosmology.
104+
index : int or str or None, optional
105+
Needed to select the row in tables with multiple rows. ``index`` can be an
106+
integer for the row number or, if the table is indexed by a column, the value of
107+
that column. If the table is not indexed and ``index`` is a string, the "name"
108+
column is used as the indexing column.
109+
110+
move_to_meta : bool, optional keyword-only
111+
Whether to move keyword arguments that are not in the Cosmology class' signature
112+
to the Cosmology's metadata. This will only be applied if the Cosmology does NOT
113+
have a keyword-only argument (e.g. ``**kwargs``). Arguments moved to the
114+
metadata will be merged with existing metadata, preferring specified metadata in
115+
the case of a merge conflict (e.g. for ``Cosmology(meta={'key':10}, key=42)``,
116+
the ``Cosmology.meta`` will be ``{'key': 10}``).
117+
cosmology : str or |Cosmology| class or None, optional keyword-only
118+
The cosmology class (or string name thereof) to use when constructing the
119+
cosmology instance. The class also provides default parameter values, filling in
120+
any non-mandatory arguments missing in 'table'.
121+
latex_names : bool, optional keyword-only
122+
Whether the |Table| (might) have latex column names for the parameters that need
123+
to be mapped to the correct parameter name -- e.g. $H_0$ to 'H0'. This is
124+
`True` by default, but can be turned off (set to `False`) if there is a known
125+
name conflict (e.g. both an 'H0' and '$H_0$' column) as this will raise an
126+
error. In this case, the correct name ('H0') is preferred.
127+
**kwargs : Any
128+
Passed to ``QTable.read``. ``format`` is set to 'ascii.latex', regardless of
129+
input.
130+
131+
Returns
132+
-------
133+
|Cosmology| subclass instance
134+
135+
Raises
136+
------
137+
ValueError
138+
If the keyword argument 'format' is given and is not "ascii.latex".
139+
"""
140+
141+
# Check that the format is 'ascii.latex' (or not specified)
142+
fmt = kwargs.pop("format", "ascii.latex")
143+
if fmt != "ascii.latex":
144+
raise ValueError(f"format must be 'ascii.latex', not {fmt}")
145+
146+
# Reading is handled by `QTable`.
147+
with u.add_enabled_units(cu):
148+
table = QTable.read(filename, format="ascii.latex", **kwargs)
149+
150+
#No need of units of different cosmology parameters to support cosmology conversion
151+
del table[0]
152+
153+
if latex_names:
154+
table_columns = set(table.colnames)
155+
for name, latex in _FORMAT_TABLE.items():
156+
if latex in table_columns:
157+
table.rename_column(latex, name)
158+
159+
160+
return from_table(
161+
table, index, move_to_meta = move_to_meta, cosmology = cosmology, rename = None
162+
)
163+
164+
165+
86166
def write_latex(
87167
cosmology: Cosmology,
88168
file: PathLike | WriteableFileLike[_TableT],
@@ -210,5 +290,6 @@ def latex_identify(
210290
# ===================================================================
211291
# Register
212292

293+
readwrite_registry.register_reader("ascii.latex", Cosmology, read_latex)
213294
readwrite_registry.register_writer("ascii.latex", Cosmology, write_latex)
214295
readwrite_registry.register_identifier("ascii.latex", Cosmology, latex_identify)

‎astropy/cosmology/_src/tests/io/test_latex.py‎

Lines changed: 58 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,65 @@
22

33
import pytest
44

5-
from astropy.cosmology._src.io.builtin.latex import _FORMAT_TABLE, write_latex
5+
from astropy.cosmology._src.io.builtin.latex import (
6+
_FORMAT_TABLE,
7+
write_latex,
8+
read_latex
9+
)
610
from astropy.io.registry.base import IORegistryError
711
from astropy.table import QTable, Table
812

913
from .base import ReadWriteDirectTestBase, ReadWriteTestMixinBase
1014

1115

12-
class WriteLATEXTestMixin(ReadWriteTestMixinBase):
16+
class ReadWriteLATEXTestMixin(ReadWriteTestMixinBase):
1317
"""
14-
Tests for a Cosmology[Write] with ``format="latex"``.
18+
Tests for a Cosmology[Read/Write] with ``format="ascii.latex"``.
1519
This class will not be directly called by :mod:`pytest` since its name does
1620
not begin with ``Test``. To activate the contained tests this class must
1721
be inherited in a subclass. Subclasses must define a :func:`pytest.fixture`
1822
``cosmo`` that returns/yields an instance of a |Cosmology|.
1923
See ``TestCosmology`` for an example.
2024
"""
2125

26+
def test_to_latex_bad_index(self, read, write, tmp_path):
27+
"""Test if argument ``index`` is incorrect"""
28+
29+
fp = tmp_path / "test_to_latex_bad_index.tex"
30+
31+
write(fp, format="ascii.latex")
32+
33+
# single-row table and has a non-0/None index
34+
with pytest.raises(IndexError, match="index 2 out of range"):
35+
read(fp, index=2, format="ascii.latex")
36+
37+
# string index where doesn't match
38+
with pytest.raises(KeyError, match="No matches found for key"):
39+
read(fp, index="row 0", format="ascii.latex")
40+
41+
def test_read_latex_invalid_path(self, read):
42+
"""Test passing an invalid or non-existent path"""
43+
44+
#using "blabla" makes it platform independent
45+
invalid_fp = "blabla.tex"
46+
47+
with pytest.raises(FileNotFoundError, match="No such file or directory"):
48+
read(invalid_fp, format="ascii.latex")
49+
50+
def test_latex_column_mnu(self, read, write, tmp_path):
51+
"""Test for table column m_nu to have a numpy array, essential for proper cosmology conversion"""
52+
53+
fp = tmp_path / "test_rename_latex_columns.tex"
54+
55+
write(fp, latex_names = True)
56+
57+
tbl = QTable.read(fp)
58+
59+
import numpy as np
60+
assert isinstance(tbl["$m_{nu}$"].value, np.ndarray)
61+
62+
# -----------------------
63+
2264
def test_to_latex_failed_cls(self, write, tmp_path):
2365
"""Test failed table type."""
2466
fp = tmp_path / "test_to_latex_failed_cls.tex"
@@ -64,18 +106,18 @@ def test_write_latex_unsupported_format(self, write, tmp_path):
64106
write(fp, format=invalid_format)
65107

66108

67-
class TestReadWriteLaTex(ReadWriteDirectTestBase, WriteLATEXTestMixin):
109+
class TestReadWriteLaTex(ReadWriteDirectTestBase, ReadWriteLATEXTestMixin):
68110
"""
69-
Directly test ``write_latex``.
111+
Directly test ``read/write_latex``.
70112
These are not public API and are discouraged from use, in favor of
71-
``Cosmology.write(..., format="latex")``, but should be
113+
``Cosmology.read/write(..., format="latex")``, but should be
72114
tested regardless b/c they are used internally.
73115
"""
74116

75117
def setup_class(self):
76-
self.functions = {"write": write_latex}
118+
self.functions = {"write": write_latex, "read": read_latex}
77119

78-
def test_rename_direct_latex_columns(self, write, tmp_path):
120+
def test_rename_direct_latex_columns(self, read, write, tmp_path):
79121
"""Tests renaming columns"""
80122
fp = tmp_path / "test_rename_latex_columns.tex"
81123
write(fp, latex_names=True)
@@ -84,3 +126,11 @@ def test_rename_direct_latex_columns(self, write, tmp_path):
84126
for column_name in tbl.colnames[2:]:
85127
# for now, Cosmology as metadata and name is stored in first 2 slots
86128
assert column_name in _FORMAT_TABLE.values()
129+
130+
cosmo = read(fp, format="ascii.latex")
131+
converted_tbl = cosmo.to_format("astropy.table")
132+
133+
# asserts each column name has been reverted
134+
for column_name in converted_tbl.colnames[1:]:
135+
# for now now, metadata is still stored in first slot
136+
assert column_name in _FORMAT_TABLE.keys()

‎astropy/io/ascii/core.py‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -946,7 +946,13 @@ def str_vals(self):
946946
self._set_fill_values(self.cols)
947947
self._set_col_formats()
948948
for col in self.cols:
949-
col.str_vals = list(col.info.iter_str_vals())
949+
950+
#Necessary for storing array like str for m_nu in a latex file
951+
if col.info.name == "$m_{nu}$":
952+
col.str_vals = [str(val) for val in col.value]
953+
else:
954+
col.str_vals = list(col.info.iter_str_vals())
955+
950956
self._replace_vals(self.cols)
951957
return [col.str_vals for col in self.cols]
952958

‎astropy/units/quantity.py‎

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,11 @@ def get_sortable_arrays(self):
247247
return [self._parent]
248248

249249

250+
def _parse_floats_from_whitespace(data:str)->list[float]:
251+
data = data.strip("[]")
252+
return [float(a) for a in data.split()]
253+
254+
250255
class Quantity(np.ndarray):
251256
"""A `~astropy.units.Quantity` represents a number with some associated unit.
252257
@@ -455,17 +460,35 @@ def __new__(
455460
# the float function below and ensure things like 1.2.3deg
456461
# will not work.
457462
pattern = (
458-
r"\s*[+-]?"
459-
r"((\d+\.?\d*)|(\.\d+)|([nN][aA][nN])|"
460-
r"([iI][nN][fF]([iI][nN][iI][tT][yY]){0,1}))"
461-
r"([eE][+-]?\d+)?"
462-
r"[.+-]?"
463-
)
463+
r"\s*"
464+
r"(?:"
465+
# ---- Scalar Number ----
466+
r"[+-]?"
467+
r"((\d+\.?\d*)|(\.\d+)|([nN][aA][nN])|"
468+
r"([iI][nN][fF]([iI][nN][iI][tT][yY]){0,1}))"
469+
r"([eE][+-]?\d+)?"
470+
r"|"
471+
# ---- Vector like data [2.3 5.6 8.9] ----
472+
r"\[\s*"
473+
r"[+-]?"
474+
r"((\d+\.?\d*)|(\.\d+)|([nN][aA][nN])|"
475+
r"([iI][nN][fF]([iI][nN][iI][tT][yY]){0,1}))"
476+
r"([eE][+-]?\d+)?"
477+
r"(?:\s+"
478+
r"[+-]?"
479+
r"((\d+\.?\d*)|(\.\d+)|([nN][aA][nN])|"
480+
r"([iI][nN][fF]([iI][nN][iI][tT][yY]){0,1}))"
481+
r"([eE][+-]?\d+)?"
482+
r")*"
483+
r"\s*\]"
484+
r")"
485+
r"\s*"
486+
)
464487

465488
v = re.match(pattern, value)
466489
unit_string = None
467490
try:
468-
value = float(v.group())
491+
value = float(v.group()) if not ("[" in v.group() and "]" in v.group()) else _parse_floats_from_whitespace(v.group())
469492

470493
except Exception:
471494
raise TypeError(

0 commit comments

Comments
 (0)