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
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ jobs:
python-version: ${{ matrix.python-version }}
cache: pip
- name: Install dependencies
run: pip install -e ".[dev,export]"
run: pip install -e ".[dev,export,notebook]"
- name: Prepare test cases
run: cd tests/cases && for f in *.tgz; do tar -xzvf "$f"; done
- name: Run tests
Expand Down
25 changes: 15 additions & 10 deletions fdsreader/explorer/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,12 +123,18 @@ def render_slice(fields, state, height, width, ramp):
def save_figure(fields, state, series, path, height, width):
"""Write the current view to an image or an animation, using matplotlib."""
try:
import matplotlib

matplotlib.use("Agg")
import matplotlib.pyplot as plt
# A figure with its own Agg canvas, rather than pyplot, leaves the backend of the process alone, e.g. when
# this is called from a notebook
from matplotlib.backends.backend_agg import FigureCanvasAgg
from matplotlib.figure import Figure
except ModuleNotFoundError:
raise SystemExit(f'{PROG}: --save needs matplotlib: pip install "fdsreader[notebook]"')

def new_figure(**kwargs):
fig = Figure(**kwargs)
FigureCanvasAgg(fig)
return fig

from .plots import plot_series, plot_slice

field = state.field(fields)
Expand All @@ -140,7 +146,8 @@ def save_figure(fields, state, series, path, height, width):
from matplotlib import animation

steps = range(0, len(field.times), max(1, len(field.times) // 120))
fig, ax = plt.subplots()
fig = new_figure()
ax = fig.subplots()

def draw(step):
ax.clear()
Expand All @@ -164,12 +171,11 @@ def draw(step):
anim.save(str(path), writer=writer)
except Exception as exc:
raise SystemExit(f"{PROG}: could not write {path}: {exc}")
plt.close(fig)
return f"wrote {path} ({len(list(steps))} frames)"

panels = (1 if field is None else 1) + (1 if series else 0)
fig, axes = plt.subplots(1, max(1, panels), figsize=(7 * max(1, panels), 5.5))
axes = np.atleast_1d(axes)
panels = (0 if field is None else 1) + (1 if series else 0)
fig = new_figure(figsize=(7 * max(1, panels), 5.5))
axes = np.atleast_1d(fig.subplots(1, max(1, panels)))
at = 0
if field is not None:
timestep = state.clamp(fields)
Expand All @@ -191,7 +197,6 @@ def draw(step):
plot_series(series, ax=axes[at], cursor_time=state.time(fields) if field is not None else None)
fig.tight_layout()
fig.savefig(str(path), dpi=150)
plt.close(fig)
return f"wrote {path}"


Expand Down
7 changes: 4 additions & 3 deletions fdsreader/explorer/notebook.py
Original file line number Diff line number Diff line change
Expand Up @@ -667,9 +667,10 @@ def _on_simulation_loaded(self, sim):
self._setup_fields(sim)
return

self.summary.value = self._overview()
self._setup_fields(sim) # also positions the time bar
self._refresh_curves()
self._setup_fields(sim) # also positions the time bar, and draws the curves when there is a slice
self.summary.value = self._overview() # after the fields, so it can count the slices
if not self.fields:
self._refresh_curves()

def _overview(self):
"""Short HTML summary: what is in this simulation, at a glance."""
Expand Down
58 changes: 44 additions & 14 deletions tests/explorer/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,15 +55,12 @@ def __init__(self, nx=8, ny=16, n_t=3, quantity="TEMPERATURE", unit="C"):
self.unit = unit
self.times = np.linspace(0.0, 2.0, n_t)
self.axes = ("y", "z")
self.coords = {"x": np.array([0.0]),
"y": np.linspace(-2.0, 2.0, nx),
"z": np.linspace(0.0, 8.0, ny)}
self.coords = {"x": np.array([0.0]), "y": np.linspace(-2.0, 2.0, nx), "z": np.linspace(0.0, 8.0, ny)}
self.shape = (nx, ny)
# a blob that rises and grows, so frames differ both in where the heat is and
# in how hot it gets -- which is what makes a per-step colour scale visible
yy, zz = np.meshgrid(self.coords["y"], self.coords["z"], indexing="ij")
self._frames = [20 + (150 + 180 * k) * np.exp(-(yy ** 2) - (zz - 1 - 2 * k) ** 2)
for k in range(n_t)]
self._frames = [20 + (150 + 180 * k) * np.exp(-(yy**2) - (zz - 1 - 2 * k) ** 2) for k in range(n_t)]

def frame(self, timestep):
return self._frames[timestep]
Expand All @@ -90,12 +87,26 @@ def fake_field():
def fake_series():
times = np.linspace(0.0, 4.0, 21)
return [
{"label": "DEVC A — TEMPERATURE [C]", "name": "A", "times": times,
"values": 20 + 10 * times, "quantity": "TEMPERATURE", "unit": "C",
"source": "device", "device": None},
{"label": "DEVC B — TEMPERATURE [C]", "name": "B", "times": times,
"values": 20 + 4 * times ** 2, "quantity": "TEMPERATURE", "unit": "C",
"source": "device", "device": None},
{
"label": "DEVC A — TEMPERATURE [C]",
"name": "A",
"times": times,
"values": 20 + 10 * times,
"quantity": "TEMPERATURE",
"unit": "C",
"source": "device",
"device": None,
},
{
"label": "DEVC B — TEMPERATURE [C]",
"name": "B",
"times": times,
"values": 20 + 4 * times**2,
"quantity": "TEMPERATURE",
"unit": "C",
"source": "device",
"device": None,
},
]


Expand All @@ -110,9 +121,7 @@ def devc_without_data(request, tmp_path):
root = tmp_path / request.param
root.mkdir()
(root / "c.smv").write_text(
"CHID\n c\n\n"
"CSVF\n devc\n c_devc.csv\n\n"
"DEVICE\n TC % TEMPERATURE\n 1.0 0.0 1.0 0.0 0.0 1.0\n"
"CHID\n c\n\nCSVF\n devc\n c_devc.csv\n\nDEVICE\n TC % TEMPERATURE\n 1.0 0.0 1.0 0.0 0.0 1.0\n"
)
if request.param == "empty":
(root / "c_devc.csv").write_text("")
Expand Down Expand Up @@ -149,3 +158,24 @@ def devc_line_case(tmp_path):
(root / "c_devc.csv").write_text("s,C\nTime,TC\n0.0,20.0\n1.0,30.0\n2.0,45.0\n")
(root / "c_line.csv").write_text("m,C\nHeight,LINE\n0.5,100.0\n1.5,200.0\n")
return root


@pytest.fixture
def app(monkeypatch, fake_field, fake_series):
"""An interactive application over one field shown twice and two curves."""
from explorer_helpers import FakeSim

from fdsreader.explorer import tui

monkeypatch.setattr(tui, "fields_of", lambda sim: [fake_field, fake_field])
monkeypatch.setattr(tui, "build_series", lambda sim: fake_series)
return tui.Interactive(FakeSim())


@pytest.fixture
def agg():
"""Draw on the Agg backend, so no display is needed, and close every figure afterwards."""
plt = pytest.importorskip("matplotlib.pyplot")
plt.switch_backend("Agg")
yield plt
plt.close("all")
17 changes: 17 additions & 0 deletions tests/explorer/explorer_helpers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
"""Helpers shared by the explorer tests."""

from fdsreader.explorer.cli import main


class FakeSim:
"""Just enough of a simulation for the interactive front end when fields and series are patched in."""

chid = "fake"
root_path = "."


def run(capsys, *argv):
"""Run the CLI and return (exit status, stdout, stderr)."""
status = main([str(a) for a in argv])
captured = capsys.readouterr()
return status, captured.out, captured.err
15 changes: 3 additions & 12 deletions tests/explorer/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,7 @@
import json

import pytest

from fdsreader.explorer.cli import main


def run(capsys, *argv):
"""Run the CLI and return (exit status, stdout, stderr)."""
status = main([str(a) for a in argv])
captured = capsys.readouterr()
return status, captured.out, captured.err
from explorer_helpers import run


def test_overview(tiny_case, capsys):
Expand Down Expand Up @@ -40,7 +32,7 @@ def test_curve_is_drawn_with_axes_and_a_legend(tiny_case, capsys):
assert lines[0].startswith(" 100 |")
assert lines[0].rstrip().endswith("*")
assert lines[0].split("|", 1)[1].startswith(" ")
assert lines[-4].startswith(" 20 |*") # and the bottom row starts at t = 0
assert lines[-4].startswith(" 20 |*") # and the bottom row starts at t = 0
assert lines[-3].startswith(" +---") # then the axis rule
assert "t [s]" in lines[-2]
assert lines[-1] == " * DEVC TC_1 — TEMPERATURE [C]"
Expand All @@ -59,8 +51,7 @@ def test_mixed_units_are_flagged(tiny_case, capsys):

def test_cursor_is_drawn_at_the_requested_time(tiny_case, capsys):
_, without, _ = run(capsys, tiny_case, "--curve", "TC_1", "--width", "20")
_, with_cursor, _ = run(capsys, tiny_case, "--curve", "TC_1", "--width", "20",
"--time", "5")
_, with_cursor, _ = run(capsys, tiny_case, "--curve", "TC_1", "--width", "20", "--time", "5")
assert with_cursor.count("|") > without.count("|")


Expand Down
Loading
Loading