Skip to content
Open
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
3 changes: 3 additions & 0 deletions moal/cli.py
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if use_cdn is False what happens here?
Are you wanting to save both everytime?

Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,9 @@ def simulate(config: Path, output_dir: Path | None, verbose: bool) -> None:
html_path = out_dir / "dashboard_animation.html"
dashboard.save_html(html_path)

html_cdn_path = out_dir / "dashboard_animation_cdn.html"
dashboard.save_html(html_cdn_path, use_cdn=True)

gif_path = out_dir / "dashboard_animation.gif"
dashboard.save_gif(gif_path)
dashboard.close()
Expand Down
13 changes: 9 additions & 4 deletions moal/dashboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -487,7 +487,7 @@ def save_gif(
except Exception as exc:
logger.warning("Could not save dashboard GIF: %s", exc)

def save_html(self, path: str | Path) -> None:
def save_html(self, path: str | Path, *, use_cdn: bool = False) -> None:
"""Export the animated figure as a standalone HTML file.

The exported file embeds an iteration slider and play/pause buttons so
Expand All @@ -498,12 +498,17 @@ def save_html(self, path: str | Path) -> None:
----------
path : str or Path
Destination file path (should end in ``.html``).
use_cdn : bool, optional
When ``True``, the Plotly JS bundle is loaded from the Plotly CDN
instead of being embedded in the file. This reduces the file size
from ~3 MB to a few KB but requires an internet connection to view.
Default is ``False`` (fully self-contained file).
"""
animated_fig = self._build_animated_figure()
animated_fig.update_layout(width=self._export_width, height=self._export_height)
animated_fig.write_html(
str(path),
include_plotlyjs=True,
include_plotlyjs="cdn" if use_cdn else True,
post_script=_PLAY_PAUSE_SCRIPT,
auto_play=False,
)
Expand Down Expand Up @@ -1151,10 +1156,10 @@ def _build_animated_figure(self) -> go.Figure:
"args": [
[str(i + 1)],
{
"frame": {"duration": 350, "redraw": False},
"frame": {"duration": 700, "redraw": False},
"mode": "immediate",
"transition": {
"duration": 220,
"duration": 300,
"easing": _EASING,
"ordering": "layout first",
},
Expand Down
2 changes: 1 addition & 1 deletion moal/preprocessing.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ def canonicalize(self, smiles: str) -> str | None:
if mol is None or mol.GetNumAtoms() == 0:
logger.warning("SMILES reduced to empty molecule after salt stripping: %s", smiles)
return None
return Chem.MolToSmiles(mol, isomericSmiles=True)
return str(Chem.MolToSmiles(mol, isomericSmiles=True))
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MolToSmiles returns a str, no?


def process_batch(self, smiles_list: list[str]) -> tuple[list[str], list[str]]:
"""Canonicalize a batch of SMILES strings.
Expand Down
38 changes: 38 additions & 0 deletions tests/test_dashboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -554,6 +554,44 @@ def test_html_disables_plotly_builtin_autoplay(self, tmp_path):
assert re.search(r"Plotly\.animate\('[^']+', null\);", content) is None
assert "redraw: false" in content

def test_save_html_cdn_omits_embedded_plotlyjs(self, tmp_path):
"""save_html(use_cdn=True) must load Plotly from CDN instead of embedding it."""
db = LiveDashboard(n_iterations=2, n_compounds=20)
records = _make_records(4)
for _ in range(2):
db.update(records, activity_threshold=7.0, iter_drc_cost=5.0, iter_ps_cost=1.0)

cdn_path = tmp_path / "dashboard_cdn.html"
embedded_path = tmp_path / "dashboard_embedded.html"
db.save_html(cdn_path, use_cdn=True)
db.save_html(embedded_path, use_cdn=False)
db.close()

cdn_content = cdn_path.read_text()

# CDN version must reference the Plotly CDN URL
assert "cdn.plot.ly" in cdn_content
# CDN version must be substantially smaller (no embedded JS bundle)
assert cdn_path.stat().st_size < embedded_path.stat().st_size

def test_slider_and_playback_use_same_frame_duration(self, tmp_path):
"""Slider step args and the injected play script must use the same frame/transition durations."""
db = LiveDashboard(n_iterations=2, n_compounds=20)
records = _make_records(4)
for _ in range(2):
db.update(records, activity_threshold=7.0, iter_drc_cost=5.0, iter_ps_cost=1.0)

html_path = tmp_path / "dashboard.html"
db.save_html(html_path)
db.close()

content = html_path.read_text()
# Slider step JSON uses compact form; injected play script uses spaced form
assert '"duration":700' in content # slider step frame duration
assert "duration: 700" in content # injected JS play animation frame duration
assert '"duration":300' in content # slider step transition duration
assert "duration: 300" in content # injected JS play animation transition duration

def test_animated_frames_include_per_iteration_axis_layout(self):
"""Animated HTML frames must carry per-iteration axis updates, not just trace data."""
db = LiveDashboard(n_iterations=3, n_compounds=20)
Expand Down