Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add basic latex display support if unicodeit is installed #858

Merged
merged 3 commits into from
Apr 5, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
# 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