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 a-matrix inspection function #124

Merged
merged 12 commits into from
Jan 2, 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
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ repos:
rev: v1.4.0
hooks:
- id: yesqa
additional_dependencies: [flake8-docstrings]
additional_dependencies: [flake8-docstrings, darglint==1.8.0]

- repo: https://github.com/asottile/setup-cfg-fmt
rev: v2.2.0
Expand Down
4 changes: 4 additions & 0 deletions pyglotaran_extras/inspect/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
"""Module with analysis inspection functionality."""
from pyglotaran_extras.inspect.a_matrix import show_a_matrixes

__all__ = ["show_a_matrixes"]
165 changes: 165 additions & 0 deletions pyglotaran_extras/inspect/a_matrix.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
"""Module containing a-matrix render functionality."""

from __future__ import annotations

import numpy as np
import xarray as xr
from glotaran.utils.ipython import MarkdownStr
from tabulate import tabulate

from pyglotaran_extras.inspect.utils import pretty_format_numerical
from pyglotaran_extras.inspect.utils import pretty_format_numerical_iterable
from pyglotaran_extras.inspect.utils import wrap_in_details_tag
from pyglotaran_extras.io.utils import result_dataset_mapping
from pyglotaran_extras.types import ResultLike


def a_matrix_to_html_table(
a_matrix: xr.DataArray,
megacomplex_suffix: str,
*,
normalize_initial_concentration: bool = False,
decimal_places: int = 3,
) -> str:
"""Create HTML multi header table from a-matrix.

Parameters
----------
a_matrix: xr.DataArray
DataArray containing the a-matrix values and coordinates.
megacomplex_suffix: str
Megacomplex suffix used for the a-matrix data variable and coordinate names.
normalize_initial_concentration: bool
Whether or not to normalize the initial concentration. Defaults to False.
decimal_places: int
Decimal places to display. Defaults to 3.

Returns
-------
str
Multi header HTML table representing the a-matrix.
"""
species = a_matrix.coords[f"species_{megacomplex_suffix}"].values
# Crete a copy so normalization does not mutate the original values
initial_concentration = np.array(
a_matrix.coords[f"initial_concentration_{megacomplex_suffix}"].values
)
lifetime = a_matrix.coords[f"lifetime_{megacomplex_suffix}"].values

if normalize_initial_concentration is True:
initial_concentration /= initial_concentration.sum()

header = (
["species<br>initial concentration<br>lifetime↓"]
+ [
f"{sp}<br>{pretty_format_numerical(ic,decimal_places)}<br>&nbsp;"
for sp, ic in zip(species, initial_concentration)
]
+ ["Sum"]
)

data = [
pretty_format_numerical_iterable(
(lifetime, *amps, amps.sum()), decimal_places=decimal_places
)
for lifetime, amps in zip(lifetime, a_matrix.values)
]
data.append(
pretty_format_numerical_iterable(
("Sum", *a_matrix.values.sum(axis=0), a_matrix.values.sum()),
decimal_places=decimal_places,
)
)

return (
tabulate(
data, headers=header, showindex=False, tablefmt="unsafehtml", disable_numparse=True
)
.replace(" 0 ", " ")
.replace(" 0<", " <")
.replace(">0 ", "> ")
)


def show_a_matrixes(
result: ResultLike,
*,
normalize_initial_concentration: bool = False,
decimal_places: int = 3,
a_matrix_min_size: int | None = None,
expanded_datasets: tuple[str, ...] = (),
heading_offset: int = 2,
) -> MarkdownStr:
"""Show all a-matrixes of a result grouped by dataset and megacomplex name.

Each dataset is wrapped in a HTML details tag which is by default collapsed.
s-weigand marked this conversation as resolved.
Show resolved Hide resolved

Parameters
----------
result: ResultLike
Result or result dataset.
normalize_initial_concentration: bool
Whether or not to normalize the initial concentration. Defaults to False.
decimal_places: int
Decimal places to display. Defaults to 3.
a_matrix_min_size: int
Defaults to None.
expanded_datasets: tuple[str, ...]
Names of dataset to expand the details view for. Defaults to empty tuple () which means no
dataset is expanded.
heading_offset: int
Number of heading level to offset the headings. Defaults to 2 which means that the
first/top most heading is h3.

Returns
-------
MarkdownStr
Markdown representation of the a-matrixes used in the optimization.
"""
heading_prefix = heading_offset * "#"
output_str = f"#{heading_prefix} A-Matrixes\n"

result_map = result_dataset_mapping(result)

for dataset_name in result_map:

a_matrix_names = list(
filter(
lambda var_name: var_name.startswith("a_matrix_"),
result_map[dataset_name].data_vars,
)
)

if not a_matrix_names:
continue

details_content = ""

for a_matrix_name in a_matrix_names:

mc_suffix = a_matrix_name.replace("a_matrix_", "")

a_matrix = result_map[dataset_name][a_matrix_name]

if a_matrix_min_size is not None and max(a_matrix.shape) < a_matrix_min_size:
continue

details_content += f"###{heading_prefix} {mc_suffix}:\n\n"

details_content += a_matrix_to_html_table(
a_matrix,
mc_suffix,
normalize_initial_concentration=normalize_initial_concentration,
decimal_places=decimal_places,
)

if details_content != "":

output_str += wrap_in_details_tag(
details_content,
summary_content=dataset_name,
summary_heading_level=2 + heading_offset,
is_open=dataset_name in expanded_datasets,
)

return MarkdownStr(output_str)
115 changes: 115 additions & 0 deletions pyglotaran_extras/inspect/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
"""Inspection utility module."""

from __future__ import annotations

from collections.abc import Generator
from collections.abc import Iterable

import numpy as np


def wrap_in_details_tag(
details_content: str,
*,
summary_content: str | None = None,
summary_heading_level: int | None = None,
is_open: bool = False,
) -> str:
"""Wrap ``details_content`` in a html details tag and add summary if ``summary_content`` set.

Parameters
----------
details_content: str
Markdown string that should be displayed when the details are expanded.
summary_content: str | None
Summary test that should be displayed. Defaults to None so the summary is ``Details``.
summary_heading_level: int | None
Level of the heading wrapping the ``summary`` if it is not None. Defaults to None.
is_open: bool
Whether or not the details tag should be initially opened. Defaults to False.

Returns
-------
str
"""
out_str = f'\n<details {"open" if is_open else ""}>\n'
if summary_content is not None:
out_str += "<summary>\n"
if summary_heading_level is None:
out_str += f"{summary_content}\n"
else:
# Ref.:
# https://css-tricks.com/two-issues-styling-the-details-element-and-how-to-solve-them/
out_str += f'<h{summary_heading_level} style="display:inline;">\n'
out_str += f"{summary_content}\n"
out_str += f"</h{summary_heading_level}>\n"

out_str += "</summary>\n"

out_str += f"\n{details_content}\n<br>\n</details>"
return out_str


def pretty_format_numerical(value: float | int, decimal_places: int = 1) -> str:
"""Format value with with at most ``decimal_places`` decimal places.

Used to format values like the t-value.

TODO: remove after raise pyglotaran dependency to 0.7.0
Forward port of https://github.com/glotaran/pyglotaran/pull/1192

Parameters
----------
value: float | int
Numerical value to format.
decimal_places: int
Decimal places to display. Defaults to 1.

Returns
-------
str
Pretty formatted version of the value.
"""
# Bool returned by numpy do not support the ``is`` comparison (not same singleton as in python)
# Ref: https://stackoverflow.com/a/37744300/3990615
if not np.isfinite(value):
return str(value)
if abs(value - int(value)) <= np.finfo(np.float64).eps:
return str(int(value))
abs_value = abs(value)
if abs_value < 10 ** (-decimal_places):
format_instruction = f".{decimal_places}e"
elif abs_value < 10 ** (decimal_places):
format_instruction = f".{decimal_places}f"
else:
format_instruction = ".0f"
return f"{value:{format_instruction}}"


def pretty_format_numerical_iterable(
input_values: Iterable[str | float], decimal_places: int | None = 3
) -> Generator[str | float, None, None]:
"""Pretty format numerical values in an iterable of numerical values or strings.

Parameters
----------
input_values: Iterable[str | float]
Values that should be formatted.
decimal_places: int | None
Number of decimal places a value should have, if None the original value will be used.
Defaults to 3.

See Also
--------
pretty_format_numerical

Yields
------
str | float
Formatted string or initial value if ``decimal_places`` is None.
"""
for val in input_values:
if decimal_places is None or isinstance(val, str):
yield val
else:
yield pretty_format_numerical(val, decimal_places=decimal_places)
4 changes: 4 additions & 0 deletions requirements_dev.txt
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
# Runtime dependencies

cycler==0.11.0
matplotlib==3.6.2
numpy==1.22.4
pyglotaran==0.6.0
tabulate==0.9.0
xarray==2022.11.0

# Documentation dependencies
-r docs/requirements.txt
Expand Down
5 changes: 4 additions & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,11 @@ project_urls =
[options]
packages = find:
install_requires =
cycler>=0.10
matplotlib>=3.3.0
pyglotaran>=0.5.1
numpy>=1.21.2,<1.24
pyglotaran>=0.6
tabulate>=0.8.9
xarray>=2022.3.0
python_requires = >=3.8,<3.11
zip_safe = True
Expand Down
3 changes: 3 additions & 0 deletions tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from pathlib import Path

TEST_DATA = Path(__file__).parent / "data"
21 changes: 21 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,27 @@
from dataclasses import replace

import pytest
from glotaran.optimization.optimize import optimize
from glotaran.testing.simulated_data.parallel_spectral_decay import SCHEME as scheme_par
from glotaran.testing.simulated_data.sequential_spectral_decay import SCHEME as scheme_seq

from pyglotaran_extras.io.setup_case_study import get_script_dir


def wrapped_get_script_dir():
"""Testfunction for calls to get_script_dir used inside of other functions."""
return get_script_dir(nesting=1)


@pytest.fixture(scope="session")
def result_parallel_spectral_decay():
"""Test result from ``glotaran.testing.simulated_data.parallel_spectral_decay``."""
scheme = replace(scheme_par, maximum_number_function_evaluations=1)
yield optimize(scheme)


@pytest.fixture(scope="session")
def result_sequential_spectral_decay():
"""Test result from ``glotaran.testing.simulated_data.sequential_spectral_decay``."""
scheme = replace(scheme_seq, maximum_number_function_evaluations=1)
yield optimize(scheme)
11 changes: 11 additions & 0 deletions tests/data/a_matrix/a_matrix_to_html_table_decimal_2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<table>
<thead>
<tr><th>species<br>initial concentration<br>lifetime↓ </th><th>species_1<br>1<br>&nbsp; </th><th>species_2<br>1<br>&nbsp; </th><th>species_3<br>1<br>&nbsp; </th><th>Sum </th></tr>
</thead>
<tbody>
<tr><td>2 </td><td>0.33 </td><td> </td><td> </td><td>0.33 </td></tr>
<tr><td>3.33 </td><td> </td><td>0.33 </td><td> </td><td>0.33 </td></tr>
<tr><td>10 </td><td> </td><td> </td><td>0.33 </td><td>0.33 </td></tr>
<tr><td>Sum </td><td>0.33 </td><td>0.33 </td><td>0.33 </td><td>1 </td></tr>
</tbody>
</table>
11 changes: 11 additions & 0 deletions tests/data/a_matrix/a_matrix_to_html_table_default.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<table>
<thead>
<tr><th>species<br>initial concentration<br>lifetime↓ </th><th>species_1<br>1<br>&nbsp; </th><th>species_2<br>1<br>&nbsp; </th><th>species_3<br>1<br>&nbsp; </th><th>Sum </th></tr>
</thead>
<tbody>
<tr><td>2 </td><td>0.333 </td><td> </td><td> </td><td>0.333</td></tr>
<tr><td>3.333 </td><td> </td><td>0.333 </td><td> </td><td>0.333</td></tr>
<tr><td>10 </td><td> </td><td> </td><td>0.333 </td><td>0.333</td></tr>
<tr><td>Sum </td><td>0.333 </td><td>0.333 </td><td>0.333 </td><td>1 </td></tr>
</tbody>
</table>
11 changes: 11 additions & 0 deletions tests/data/a_matrix/a_matrix_to_html_table_normalized.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<table>
<thead>
<tr><th>species<br>initial concentration<br>lifetime↓ </th><th>species_1<br>0.333<br>&nbsp; </th><th>species_2<br>0.333<br>&nbsp; </th><th>species_3<br>0.333<br>&nbsp; </th><th>Sum </th></tr>
</thead>
<tbody>
<tr><td>2 </td><td>0.333 </td><td> </td><td> </td><td>0.333</td></tr>
<tr><td>3.333 </td><td> </td><td>0.333 </td><td> </td><td>0.333</td></tr>
<tr><td>10 </td><td> </td><td> </td><td>0.333 </td><td>0.333</td></tr>
<tr><td>Sum </td><td>0.333 </td><td>0.333 </td><td>0.333 </td><td>1 </td></tr>
</tbody>
</table>
Loading