Skip to content
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
8 changes: 5 additions & 3 deletions PQEnalyzer/apps/tui.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
PLOT_FEATURES_BY_KEY,
enabled_feature_labels,
)
from ..plots.labels import parameter_label
from ..plots.options import PlotOptions
from ..plots.terminal_chart import build_terminal_chart
from .file_watcher import FileChangeWatcher
Expand Down Expand Up @@ -620,7 +621,7 @@ def render_table(self) -> None:
summary = self.summaries[parameter]
table.add_row(
parameter,
summary.unit,
summary.unit or "n/a",
str(summary.rows),
format_value(summary.latest),
format_value(summary.mean),
Expand All @@ -642,10 +643,11 @@ def update_detail(self, parameter) -> None:
return

summary = self.summaries[parameter]
unit = summary.unit or "n/a"
title = Text.assemble(
(parameter, "bold #58a6ff"),
(" Unit ", "#8b949e"),
(summary.unit, "bold #c9d1d9"),
(unit, "bold #c9d1d9"),
(" Rows ", "#8b949e"),
(str(summary.rows), "bold #c9d1d9"),
)
Expand Down Expand Up @@ -709,7 +711,7 @@ def render_chart(self) -> None:
summary = self.summaries[parameter]
self.query_one("#chart-title", Static).update(
Text.assemble(
(f"{parameter} / {summary.unit}", "bold #58a6ff"),
(parameter_label(parameter, summary.unit), "bold #58a6ff"),
f" rows {summary.rows}",
f" latest {format_value(summary.latest)}",
))
Expand Down
6 changes: 4 additions & 2 deletions PQEnalyzer/energy_access.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,9 +125,11 @@ def parameter_unit(energy, info_parameter: str) -> str:
attribute = PARAMETER_ATTRIBUTES.get(info_parameter)
unit_attribute = f"{attribute}_unit"
if attribute is not None and hasattr(energy, unit_attribute):
return getattr(energy, unit_attribute)
unit = getattr(energy, unit_attribute)
else:
unit = energy.units[info_parameter]

return energy.units[info_parameter]
return unit or ""


def parameter_unit_for_energies(energies: list, info_parameter: str) -> str:
Expand Down
11 changes: 11 additions & 0 deletions PQEnalyzer/plots/labels.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,17 @@ def unique_path_labels(filenames):
return labels


def parameter_label(parameter, unit):
"""
Return a parameter label without inventing a missing unit.
"""

if not unit:
return parameter

return f"{parameter} / {unit}"


def _unique_group_labels(members):
max_depth = max(len(parts) for _, parts in members)

Expand Down
5 changes: 3 additions & 2 deletions PQEnalyzer/plots/plot.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@
import matplotlib.pyplot as plt
import matplotlib.animation as animation

from ..energy_access import parameter_unit_for_energies
from .._logging import get_logger
from ..energy_access import parameter_unit_for_energies
from .features import PLOT_FEATURES
from .labels import parameter_label
from .options import PlotOptions
from .theme import apply_figure_theme, apply_matplotlib_theme

Expand Down Expand Up @@ -279,7 +280,7 @@ def parameter_axis_label(self, info_parameter: str) -> str:
self.reader.energies,
info_parameter,
)
return f"{info_parameter} / {unit}"
return parameter_label(info_parameter, unit)

def style_single_plot(
self,
Expand Down
9 changes: 7 additions & 2 deletions PQEnalyzer/plots/plot_dashboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
parameter_unit_for_energies,
series,
)
from .labels import unique_path_labels
from .labels import parameter_label, unique_path_labels
from .theme import (
apply_figure_theme,
apply_matplotlib_theme,
Expand Down Expand Up @@ -183,7 +183,12 @@ def __label_axis(self, ax, parameter, index):
unit = parameter_unit_for_energies(self.reader.energies, parameter)
palette = palette_for_appearance_mode(
getattr(self.app, "appearance_mode", None))
ax.set_title(f"{parameter} / {unit}", fontsize=9, loc="left", pad=6)
ax.set_title(
parameter_label(parameter, unit),
fontsize=9,
loc="left",
pad=6,
)
ax.set_title(
self.__latest_value_title(parameter),
fontsize=8,
Expand Down
7 changes: 4 additions & 3 deletions PQEnalyzer/plots/terminal_chart.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
series,
)
from .features import iter_time_series_overlays
from .labels import unique_path_labels
from .labels import parameter_label, unique_path_labels
from .theme import series_rgb


Expand Down Expand Up @@ -57,9 +57,10 @@ def build_terminal_chart(reader, info_parameter, width=88, height=22,
)

unit = parameter_unit_for_energies(reader.energies, info_parameter)
plt.title(f"{info_parameter} / {unit}")
label = parameter_label(info_parameter, unit)
plt.title(label)
plt.xlabel(axis_label(reader.energies[0]))
plt.ylabel(f"{info_parameter} / {unit}")
plt.ylabel(label)

chart = plt.build()
plt.clear_figure()
Expand Down
35 changes: 35 additions & 0 deletions tests/apps/test_tui.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,18 @@ def read_last(self):
self.energies = [FakeEnergy([1.0, 3.0, 9.0])]


class FakeUnitlessReader:

def __init__(self):
energy = FakeEnergy([300.0, 301.0, 302.0])
energy.units["PARAMETER"] = None
self.filenames = ["qmcfc.en"]
self.energies = [energy]

def read_last(self):
return None


class FakeMultiParameterEnergy:

info = {
Expand Down Expand Up @@ -188,6 +200,29 @@ async def run_scenario():
asyncio.run(run_scenario())


def test_tui_app_marks_missing_qmcfc_unit_without_leaking_none():
app = TuiApp(FakeUnitlessReader(), watch=False)

async def run_scenario():
async with app.run_test(size=(100, 30)) as pilot:
await pilot.pause()

table = app.query_one("#parameters", DataTable)
detail = app.query_one("#detail-title", Static)
assert table.get_row("PARAMETER")[1] == "n/a"
assert "Unit n/a" in str(detail.content)

await pilot.press("enter")
await pilot.pause()

chart_title = app.query_one("#chart-title", Static)
chart = app.query_one("#chart-canvas", Static)
assert "PARAMETER /" not in str(chart_title.content)
assert "None" not in str(chart.content)

asyncio.run(run_scenario())


def test_tui_escape_restores_table_focus_and_vim_navigation():
app = TuiApp(FakeMultiParameterReader(), watch=False)

Expand Down
3 changes: 3 additions & 0 deletions tests/data/qmcfc-output.en
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
1 2 300 1.0 -10 0 20 0 0 0 0 0 0 0 0 0 0 0 1000 0.500 0 1.0
2 2 301 1.1 -11 0 21 0 0 0 0 0 0 0 0 0 0 0 1001 0.501 0 1.1
3 2 302 1.2 -12 0 22 0 0 0 0 0 0 0 0 0 0 0 1002 0.502 0 1.2
16 changes: 16 additions & 0 deletions tests/data/qmcfc-output.info
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
-------------------------------------------------------------------------------
| QMCFC output information |
-------------------------------------------------------------------------------
| SIMULATION TIME 3.000 QM_MOLECULES 2.000 |
| TEMPERATURE 302.000 PRESSURE 1.200 |
| E(QM) -12.000 E(MM) 0.000 |
| E(KIN) 22.000 E(INTRA) 0.000 |
| E(BOND) 0.000 E(ANGLE) 0.000 |
| E(DIHEDRAL) 0.000 E(IMPROPER) 0.000 |
| E(COULOMB) 0.000 E(NONCOULOMB) 0.000 |
| E(CF) 0.000 E(CF_RF) 0.000 |
| E(RF) 0.000 E(THREEBODY) 0.000 |
| VOLUME 1002.000 DENSITY 0.502 |
| MOMENTUM 0.000 LOOPTIME 1.200 |
-------------------------------------------------------------------------------

30 changes: 28 additions & 2 deletions tests/plots/test_gui_plots.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,9 @@ def get(self):

class FakeEnergy:

def __init__(self, values):
def __init__(self, values, unit="unit"):
self.info = {"PARAMETER": "PARAMETER"}
self.units = {"PARAMETER": "unit"}
self.units = {"PARAMETER": unit}
self.data = {"PARAMETER": np.array(values, dtype=float)}
self.simulation_time = np.arange(1, len(values) + 1)

Expand Down Expand Up @@ -284,6 +284,19 @@ def test_time_labels_use_custom_independent_axis_label():
assert plot.ax.get_xlabel() == "Optimization Step"


def test_time_plot_omits_missing_qmcfc_unit_from_labels():
app = FakeApp([FakeEnergy([300, 301, 302], unit=None)])
plot = PlotTime(app)

plot.main_data("PARAMETER")
plot.labels("PARAMETER")

assert plot.ax.get_ylabel() == "PARAMETER"
assert plot.ax.get_legend().get_texts()[0].get_text() == (
"series-0.en (302)"
)


def test_time_main_data_disambiguates_duplicate_filenames():
app = FakeApp(
[FakeEnergy([1, 2, 3, 4]), FakeEnergy([2, 3, 4, 5])],
Expand Down Expand Up @@ -486,6 +499,19 @@ def test_dashboard_uses_custom_independent_axis_label():
assert plot.axes[-1].get_xlabel() == "Optimization Step"


def test_dashboard_omits_missing_qmcfc_units_from_titles():
energy = FakeDashboardEnergy()
energy.units["TEMPERATURE"] = None
energy.units["PRESSURE"] = None
app = FakeApp([energy])
plot = PlotDashboard(app)

plot.redraw()

assert plot.axes[0].get_title(loc="left") == "TEMPERATURE"
assert plot.axes[1].get_title(loc="left") == "PRESSURE"


def test_dashboard_uses_compact_latest_titles_for_multiple_files():
first = FakeDashboardEnergy()
second = FakeDashboardEnergy()
Expand Down
7 changes: 6 additions & 1 deletion tests/plots/test_labels.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
from PQEnalyzer.plots.labels import unique_path_labels
from PQEnalyzer.plots.labels import parameter_label, unique_path_labels


def test_parameter_label_includes_only_available_units():
assert parameter_label("TEMPERATURE", "K") == "TEMPERATURE / K"
assert parameter_label("TEMPERATURE", "") == "TEMPERATURE"


def test_unique_path_labels_keep_unique_basenames():
Expand Down
13 changes: 11 additions & 2 deletions tests/plots/test_terminal_chart.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@

class FakeEnergy:

def __init__(self, values, time=None):
def __init__(self, values, time=None, unit="unit"):
if time is None:
time = [1, 2, 3]
self.info = {"PARAMETER": "PARAMETER"}
self.units = {"PARAMETER": "unit"}
self.units = {"PARAMETER": unit}
self.data = {"PARAMETER": np.array(values)}
self.simulation_time = np.array(time)

Expand Down Expand Up @@ -76,6 +76,15 @@ def test_terminal_chart_uses_custom_independent_axis_label():
assert "Optimization Step" in chart


def test_terminal_chart_omits_missing_qmcfc_unit():
reader = FakeReader([FakeEnergy([300.0, 301.0, 302.0], unit=None)])

chart = build_terminal_chart(reader, "PARAMETER", width=48, height=12)

assert "PARAMETER" in chart
assert "None" not in chart


def test_terminal_chart_can_render_statistic_overlays():
reader = FakeReader([FakeEnergy([1.0, 2.0, 4.0, 8.0, 16.0])])
options = PlotOptions(
Expand Down
11 changes: 11 additions & 0 deletions tests/test_energy_access.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,17 @@ def test_parameter_access_keeps_custom_parameter_fallback():
np.array([1.0, 2.0, 3.0]))


def test_parameter_access_normalizes_missing_qmcfc_units():
energy = Reader(
["tests/data/qmcfc-output.en"],
MDEngineFormat.QMCFC,
).energies[0]

assert parameter_unit(energy, "TEMPERATURE") == ""
assert parameter_unit(energy, "QM_MOLECULES") == ""
assert series(energy, "TEMPERATURE").unit == ""


def test_concatenate_helpers_join_series_from_multiple_energy_files():
energies = [
read_energy("tests/data/md-01.en"),
Expand Down
Loading