Skip to content

Commit

Permalink
add basic latex display support if unicodeit is installed (#858)
Browse files Browse the repository at this point in the history
If the external package
[unicodeit](https://github.com/svenkreiss/unicodeit) is installed,
translate variable names which contain simple LaTeX into unicode that
mimics the correct LaTeX rendering. This only works for very basic LaTeX
like `$\alpha$`.
  • Loading branch information
HDembinski authored Apr 5, 2023
1 parent d83dc28 commit 6f057af
Show file tree
Hide file tree
Showing 13 changed files with 151 additions and 33 deletions.
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,11 @@ cov: build/done build/testdep
JUPYTER_PLATFORM_DIRS=1 coverage run -m pytest
python -m pip uninstall --yes numba ipykernel ipywidgets
coverage run --append -m pytest
python -m pip uninstall --yes scipy matplotlib
python -m pip uninstall --yes scipy matplotlib unicodeit
coverage run --append -m pytest
pip install numba ipykernel ipywidgets scipy matplotlib
coverage html -d htmlcov
coverage report -m
python .ci/install_deps.py test
@echo htmlcov/index.html

doc: build/done build/html/done
Expand Down
8 changes: 6 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -49,19 +49,23 @@ documentation = "https://iminuit.readthedocs.io"
test = [
"coverage",
"cython",
"ipywidgets",
# ipywidgets 8.0.5 and 8.0.6 are broken

This comment has been minimized.

Copy link
@mgorny

mgorny Jun 23, 2023

I'm confused by this comment. The linked bug report says that the ipykernel dependency was intentionally removed, and you list ipykernel explicitly, so I find it hard to understand why prevent upgrading.

# see https://github.com/jupyter-widgets/ipywidgets/issues/3731
"ipywidgets<8.0.5",
"ipykernel", # needed by ipywidgets 8.0.6
"joblib",
"jacobi",
"matplotlib",
# numba currently requires numpy<1.24
"numpy<1.24",
"numba",
"numba-stats",
"pytest",
"scipy",
"tabulate",
"boost_histogram",
"resample>=1.5"
"resample",
"unicodeit"
]
doc = [
"sphinx~=5.3.0",
Expand Down
18 changes: 18 additions & 0 deletions src/iminuit/_optional_dependencies.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import contextlib
import warnings
from iminuit.warnings import OptionalDependencyWarning


@contextlib.contextmanager
def optional_module_for(functionality, *, replace=None, stacklevel=3):
try:
yield
except ModuleNotFoundError as e:
package = e.name.split(".")[0]
if replace:
package = replace.get(package, package)
msg = (
f"{functionality} requires optional package {package!r}. "
f"Install {package!r} manually to enable this functionality."
)
warnings.warn(msg, OptionalDependencyWarning, stacklevel=stacklevel)
34 changes: 30 additions & 4 deletions src/iminuit/_repr_html.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from ._repr_text import pdg_format, matrix_format, fmin_fields
from ._repr_text import pdg_format, matrix_format, fmin_fields, _parse_latex

good_style = "background-color:#92CCA6;color:black"
bad_style = "background-color:#c15ef7;color:black"
Expand Down Expand Up @@ -201,7 +201,7 @@ def params(mps):
rows.append(
tr(
th(str(i)),
td(mp.name),
td(_parse_latex(mp.name)),
td(v),
td(e),
td(mem),
Expand Down Expand Up @@ -243,7 +243,7 @@ def merrors(mes):
for me in mes:
header.append(
th(
me.name,
_parse_latex(me.name),
colspan=2,
title="Parameter name",
style="text-align:center",
Expand Down Expand Up @@ -279,7 +279,7 @@ def merrors(mes):


def matrix(arr):
names = tuple(arr._var2pos)
names = [_parse_latex(x) for x in arr._var2pos]

n = len(names)

Expand Down Expand Up @@ -316,3 +316,29 @@ def matrix(arr):
rows.append(tr(*cols))

return to_str(table(tr(td(), *[th(v) for v in names]), *rows))


# def _parse_latex(s):
# if s.startswith("$") and s.endswith("$"):
# with optional_module_for("displaying LaTeX"):
# from matplotlib.backends.backend_svg import RendererSVG
# from matplotlib.font_manager import FontProperties
# from io import StringIO

# fp = FontProperties(size=9)
# # get bounding box of rendered text
# r = RendererSVG(0, 0, StringIO())
# w, h, _ = r.get_text_width_height_descent(s, fp, ismath=True)

# with StringIO() as f:
# # render LaTeX as path
# r = RendererSVG(w, h, f)
# r.draw_text(r.new_gc(), 0, h, s, fp, angle=0, ismath=True)
# f.seek(0)
# svg = f.read()

# # strip preamble
# svg = svg[svg.index("<svg") + 4 :]
# return f'<svg role="img" style="fill:#f0f0f0" {svg}'

# return s
19 changes: 15 additions & 4 deletions src/iminuit/_repr_text.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from .pdg_format import _round, _strip
from iminuit._optional_dependencies import optional_module_for
import re
import numpy as np

Expand Down Expand Up @@ -90,7 +91,8 @@ def fmin(fm):


def params(mps):
vnames = (mp.name for mp in mps)
vnames = [_parse_latex(mp.name) for mp in mps]

name_width = max([4] + [len(x) for x in vnames])
num_width = max(2, len(f"{len(mps) - 1}"))

Expand Down Expand Up @@ -123,7 +125,7 @@ def params(mps):
format_row(
ws,
str(i),
mp.name,
vnames[i],
val,
err,
mel,
Expand All @@ -143,7 +145,7 @@ def merrors(mes):
n = len(mes)
ws = [10] + [23] * n
l1 = format_line(ws, "┌" + "┬" * n + "┐")
header = format_row(ws, "", *(m.name for m in mes))
header = format_row(ws, "", *(_parse_latex(m.name) for m in mes))
ws = [10] + [11] * (2 * n)
l2 = format_line(ws, "├" + "┼┬" * n + "┤")
l3 = format_line(ws, "└" + "┴" * n * 2 + "┘")
Expand Down Expand Up @@ -177,7 +179,7 @@ def merrors(mes):


def matrix(arr):
names = tuple(arr._var2pos)
names = [_parse_latex(x) for x in arr._var2pos]

n = len(arr)
nums = matrix_format(arr)
Expand Down Expand Up @@ -219,3 +221,12 @@ def matrix_format(matrix):
x = pdg_format(matrix[i, j], matrix[i, i], matrix[j, j])[0]
r.append(x)
return r


def _parse_latex(s):
if s.startswith("$") and s.endswith("$"):
with optional_module_for("rendering simple LaTeX"):
import unicodeit

return unicodeit.replace(s[1:-1])
return s
9 changes: 2 additions & 7 deletions src/iminuit/minuit.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import numpy as np
import typing as _tp
from iminuit.typing import UserBound
from iminuit._optional_dependencies import optional_module_for

# Better use numpy.typing.ArrayLike in the future, but this
# requires dropping Python-3.6 support
Expand Down Expand Up @@ -1898,18 +1899,12 @@ def mncontour(
pts = np.append(pts, pts[:1], axis=0)

if interpolated > size:
try:
with optional_module_for("interpolation"):
from scipy.interpolate import CubicSpline

xg = np.linspace(0, 1, len(pts))
spl = CubicSpline(xg, pts, bc_type="periodic")
pts = spl(np.linspace(0, 1, interpolated))

except ModuleNotFoundError:
warnings.warn(
"Interpolation requires scipy. Please install scipy.",
mutil.IMinuitWarning,
)
return pts

def draw_mncontour(
Expand Down
13 changes: 1 addition & 12 deletions src/iminuit/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from argparse import Namespace
from iminuit import _repr_html, _repr_text, _deprecated
from iminuit.typing import Key, UserBound
from iminuit.warnings import IMinuitWarning, HesseFailedWarning, PerformanceWarning
import numpy as np
from numpy.typing import NDArray
from typing import (
Expand Down Expand Up @@ -54,18 +55,6 @@
)


class IMinuitWarning(RuntimeWarning):
"""Generic iminuit warning."""


class HesseFailedWarning(IMinuitWarning):
"""HESSE failed warning."""


class PerformanceWarning(UserWarning):
"""Warning about performance issues."""


class BasicView(abc.ABC):
"""
Array-like view of parameter state.
Expand Down
17 changes: 17 additions & 0 deletions src/iminuit/warnings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
"""Warnings used by iminuit."""


class IMinuitWarning(RuntimeWarning):
"""Generic iminuit warning."""


class OptionalDependencyWarning(IMinuitWarning):
"""Feature requires an optional external package."""


class HesseFailedWarning(IMinuitWarning):
"""HESSE failed warning."""


class PerformanceWarning(UserWarning):
"""Warning about performance issues."""
5 changes: 5 additions & 0 deletions tests/params_latex_1.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
┌───┬──────┬───────────┬───────────┬────────────┬────────────┬─────────┬─────────┬───────┐
│ │ Name │ Value │ Hesse Err │ Minos Err- │ Minos Err+ │ Limit- │ Limit+ │ Fixed │
├───┼──────┼───────────┼───────────┼────────────┼────────────┼─────────┼─────────┼───────┤
│ 0 │ α │ 1.00 │ 0.01 │ │ │ │ │ │
└───┴──────┴───────────┴───────────┴────────────┴────────────┴─────────┴─────────┴───────┘
5 changes: 5 additions & 0 deletions tests/params_latex_2.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
┌───┬──────────┬───────────┬───────────┬────────────┬────────────┬─────────┬─────────┬───────┐
│ │ Name │ Value │ Hesse Err │ Minos Err- │ Minos Err+ │ Limit- │ Limit+ │ Fixed │
├───┼──────────┼───────────┼───────────┼────────────┼────────────┼─────────┼─────────┼───────┤
│ 0 │ $\alpha$ │ 1.00 │ 0.01 │ │ │ │ │ │
└───┴──────────┴───────────┴───────────┴────────────┴────────────┴─────────┴─────────┴───────┘
8 changes: 6 additions & 2 deletions tests/test_minuit.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
import numpy as np
from numpy.testing import assert_allclose, assert_equal
from iminuit import Minuit
from iminuit.util import Param, IMinuitWarning, make_func_code
from iminuit.util import Param, make_func_code
from iminuit.warnings import IMinuitWarning, OptionalDependencyWarning
from iminuit.typing import Annotated
from pytest import approx
from argparse import Namespace
Expand Down Expand Up @@ -185,7 +186,10 @@ def test_mncontour_interpolated():
assert len(pts) == 21

if not scipy_available:
with pytest.warns(IMinuitWarning, match="Interpolation requires scipy"):
with pytest.warns(
OptionalDependencyWarning,
match="interpolation requires optional package 'scipy'",
):
pts = m.mncontour("x", "y", size=20, interpolated=200)
assert len(pts) == 21
else:
Expand Down
18 changes: 18 additions & 0 deletions tests/test_repr.py
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,24 @@ def test_text_params(minuit):
assert _repr_text.params(minuit.params) == ref("params.txt")


def test_text_params_with_latex_names():
m = Minuit(lambda x: x**2, 1, name=[r"$\alpha$"])

try:
import unicodeit # noqa

assert _repr_text.params(m.params) == ref("params_latex_1.txt")

except ModuleNotFoundError:
from iminuit.warnings import OptionalDependencyWarning

with pytest.warns(
OptionalDependencyWarning,
match="rendering simple LaTeX requires optional package 'unicodeit'",
):
assert _repr_text.params(m.params) == ref("params_latex_2.txt")


def test_text_params_with_long_names():
mps = [
Param(
Expand Down
26 changes: 26 additions & 0 deletions tests/test_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from numpy.testing import assert_equal, assert_allclose
import numpy as np
from iminuit._core import MnUserParameterState
from iminuit._optional_dependencies import optional_module_for
import pickle


Expand Down Expand Up @@ -784,3 +785,28 @@ def test_smart_sampling_1(fn_expected):
def test_smart_sampling_2():
with pytest.warns(RuntimeWarning):
util._smart_sampling(np.log, 1e-10, 1, tol=1e-10)


def test_optional_module_for_1():
with optional_module_for("foo"):
import iminuit # noqa


def test_optional_module_for_2():
from iminuit.warnings import OptionalDependencyWarning

with pytest.warns(
OptionalDependencyWarning, match="foo requires optional package 'foobarbaz'"
):
with optional_module_for("foo"):
import foobarbaz # noqa


def test_optional_module_for_3():
from iminuit.warnings import OptionalDependencyWarning

with pytest.warns(
OptionalDependencyWarning, match="foo requires optional package 'foo'"
):
with optional_module_for("foo", replace={"foobarbaz": "foo"}):
import foobarbaz # noqa

0 comments on commit 6f057af

Please sign in to comment.