diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 49b1e9e..ac39996 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -47,5 +47,8 @@ jobs: - name: Install dependencies run: uv sync --all-extras + - name: Install Chrome for image export tests + run: uv run kaleido_get_chrome + - name: Run tests run: uv run pytest diff --git a/CHANGELOG.md b/CHANGELOG.md index 08fbfd5..4930ac1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### Bug Fixes +- Raised the minimum Plotly version to 6.1.1 and Kaleido to 1.0.0 to exclude incompatible image-export dependencies. Documented the Chrome/Chromium requirement and added real PNG and rotation GIF export tests, with Chrome installation in CI. `plot_3d()` now passes image dimensions explicitly to the exporter so the minimum supported versions preserve the requested size. - `LONVisualizer.get_layout()` with a `seed`, and therefore `plot_2d()`, `plot_3d()`, `create_rotation_gif()` and `visualize_all()`, no longer reseeds the global NumPy RNG or leaves a seeded RNG installed in igraph. The seed now applies only to the layout, and igraph's default RNG is restored afterwards. Seeded layouts are unchanged (#53). - Continuous sampling now handles failed local searches explicitly. A local search fails when `minimize` raises `ValueError` or returns a non-finite fitness. A failed initial local search fails its run, and a failed search after a perturbation is rejected and counts as a non-improving iteration. Previously, runs starting at a `nan` minimum silently vanished, and an `inf` minimum made `LON.from_trace_data()` fail after all sampling had finished. Results with `success=False` and a finite fitness are still used as local optima. Each failed local search emits a warning with its context and reason (#44). - `StepSizeEstimator` divides each sample's escape count by its number of evaluated perturbations instead of `n_perturbations`, so failed local searches no longer bias the escape rate down. Samples whose initial local search fails are skipped, and `RuntimeError` is raised if no sample can be evaluated, instead of returning `nan` (#44). diff --git a/README.md b/README.md index 85ff52f..00c5868 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,10 @@ lonkit is a Python library for constructing, analyzing, and visualizing Local Op pip install lonkit ``` +Exporting 3D plots to images or GIFs also requires Chrome/Chromium. If it is not +installed, run `kaleido_get_chrome` after installing lonkit. See the +[installation guide](https://helix-agh.github.io/lonkit/getting-started/installation/). + Or install from source: ```bash diff --git a/docs/contributing.md b/docs/contributing.md index 580cbf6..5323f17 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -27,6 +27,9 @@ pre-commit install ### Running Tests +The image export tests require Chrome/Chromium. If it is not installed, run +`uv run kaleido_get_chrome` once before running the suite. + ```bash pytest ``` diff --git a/docs/getting-started/installation.md b/docs/getting-started/installation.md index 0139c93..3823f0d 100644 --- a/docs/getting-started/installation.md +++ b/docs/getting-started/installation.md @@ -16,6 +16,25 @@ The simplest way to install lonkit is via pip: pip install lonkit ``` +### Chrome for 3D image and GIF export + +Saving 3D plots as images, creating rotation GIFs, and calling `visualize_all()` +require Chrome or Chromium. Kaleido 1.x uses an external browser; it does not +bundle one. If Chrome is already installed, Kaleido should find it automatically. +Otherwise, install a compatible browser with: + +```bash +kaleido_get_chrome +``` + +For a source checkout managed with uv, use `uv run kaleido_get_chrome`. +The download requires an internet connection. In CI, run this command before +the visualization tests. 2D plots and creating Plotly figures without image +export do not require Chrome. + +See the [Kaleido installation instructions](https://github.com/plotly/Kaleido#installation) +for details. + ## Install from Source For the latest development version: @@ -81,14 +100,17 @@ On some systems, you may need to install igraph system dependencies first: ### Plotly/Kaleido Issues -If you encounter issues with static image export: +lonkit requires Plotly >= 6.1.1 and Kaleido >= 1.0.0. If you encounter a version +compatibility error, upgrade both packages together: ```bash -pip install --upgrade kaleido +pip install --upgrade "plotly>=6.1.1" "kaleido>=1.0.0" ``` -On some systems, you may need: +If export reports that Chrome is missing, install it with: ```bash -pip install kaleido==0.2.1 +kaleido_get_chrome ``` + +Do not downgrade to Kaleido 0.2.1; it is outside lonkit's supported dependencies. diff --git a/docs/user-guide/visualization.md b/docs/user-guide/visualization.md index 34f72e0..3942ad7 100644 --- a/docs/user-guide/visualization.md +++ b/docs/user-guide/visualization.md @@ -6,6 +6,9 @@ description: 2D and 3D visualizations of Local Optima Networks. Export plots as This guide covers how to create visualizations of Local Optima Networks. +Exporting 3D images and rotation GIFs requires Chrome/Chromium. If needed, install +it with `kaleido_get_chrome`; see the [installation guide](../getting-started/installation.md#chrome-for-3d-image-and-gif-export). + ## Quick Start ```python diff --git a/pyproject.toml b/pyproject.toml index e3aefe1..ed4fe0d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,8 +40,8 @@ dependencies = [ "pandas>=2.0.0", "igraph>=0.11.0", "matplotlib>=3.7.0", - "plotly>=5.15.0", - "kaleido>=0.2.1", + "plotly>=6.1.1", + "kaleido>=1.0.0", "imageio>=2.31.0", "joblib>=1.4.0", "tqdm>=4.67.3", diff --git a/src/lonkit/visualization.py b/src/lonkit/visualization.py index ee6159f..1583d12 100644 --- a/src/lonkit/visualization.py +++ b/src/lonkit/visualization.py @@ -352,7 +352,7 @@ def plot_3d( ) if output_path: - fig.write_image(str(output_path)) + fig.write_image(str(output_path), width=width, height=height) return fig diff --git a/tests/test_visualization.py b/tests/test_visualization.py index 75a631b..8c3fedf 100644 --- a/tests/test_visualization.py +++ b/tests/test_visualization.py @@ -5,6 +5,7 @@ import matplotlib.pyplot as plt import numpy as np import pytest +from PIL import Image from lonkit import LON, LONVisualizer from tests.conftest import SEED @@ -15,6 +16,33 @@ def visualizer() -> LONVisualizer: return LONVisualizer() +@pytest.mark.parametrize("extension", ["png", "gif"]) +def test_3d_export(visualizer: LONVisualizer, tmp_path: Path, extension: str) -> None: + graph = ig.Graph(n=3, edges=[(0, 1), (1, 2)], directed=True) + graph.vs["Fitness"] = [3.0, 1.0, 0.0] + graph.vs["Count"] = [1, 1, 1] + graph.es["Count"] = [1, 1] + lon = LON(graph=graph, best_fitness=0.0) + path = tmp_path / f"plot.{extension}" + + if extension == "png": + visualizer.plot_3d(lon, output_path=path, width=200, height=200, seed=SEED) + else: + visualizer.create_rotation_gif( + lon, path, width=200, height=200, duration=1, fps=3, seed=SEED + ) + + with Image.open(path) as image: + assert image.format == extension.upper() + assert image.size == (200, 200) + assert image.n_frames == (1 if extension == "png" else 3) + pixels = np.array(image.convert("RGB")) + assert pixels.min() < pixels.max() + if extension == "gif": + image.seek(1) + assert not np.array_equal(pixels, np.array(image.convert("RGB"))) + + class TestLayoutReproducibility: def test_layout_same_seed_gives_same_coordinates( self, rastrigin_lon: LON, visualizer: LONVisualizer diff --git a/uv.lock b/uv.lock index 5e0948e..258f532 100644 --- a/uv.lock +++ b/uv.lock @@ -913,7 +913,7 @@ requires-dist = [ { name = "imageio", specifier = ">=2.31.0" }, { name = "ioh", marker = "extra == 'dev'", specifier = ">=0.3.22" }, { name = "joblib", specifier = ">=1.4.0" }, - { name = "kaleido", specifier = ">=0.2.1" }, + { name = "kaleido", specifier = ">=1.0.0" }, { name = "matplotlib", specifier = ">=3.7.0" }, { name = "mkdocs", marker = "extra == 'dev'", specifier = ">=1.6.1" }, { name = "mkdocs-material", marker = "extra == 'dev'", specifier = ">=9.7.0" }, @@ -922,7 +922,7 @@ requires-dist = [ { name = "mypy", marker = "extra == 'dev'", specifier = ">=1.13.0" }, { name = "numpy", specifier = ">=1.24.0" }, { name = "pandas", specifier = ">=2.0.0" }, - { name = "plotly", specifier = ">=5.15.0" }, + { name = "plotly", specifier = ">=6.1.1" }, { name = "pre-commit", marker = "extra == 'dev'", specifier = ">=4.0.0" }, { name = "pytest", marker = "extra == 'dev'", specifier = ">=7.0.0" }, { name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=4.0.0" },