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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

### Bug Fixes

- `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).
- Runs without accepted transitions are no longer dropped from the LON and its performance metrics. `BasinHoppingSampler` and `ILSSampler` now record such a run as a single trace row from its initial local optimum to itself. `LON.from_trace_data()` drops self-loops, so the optimum becomes a vertex without edges and the run counts towards `success` and `deviation`. Basin-Hopping runs with a non-finite initial minimum are still skipped. Seeded traces and LONs change only when some run had no accepted transitions. Set `preserve_runs_without_transitions=False` in either sampler config to restore the earlier behavior, excluding these runs from both the graph and performance metrics (#39).
Expand Down
11 changes: 7 additions & 4 deletions src/lonkit/visualization.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,10 +148,13 @@ def get_layout(self, graph, seed: int | None = None) -> np.ndarray:
"""Get 2D layout coordinates, with shape ``(0, 2)`` for an empty graph."""
if graph.vcount() == 0:
return np.empty((0, 2))
if seed is not None:
np.random.seed(seed)
ig.set_random_number_generator(random.Random(seed))
layout = graph.layout_auto()
if seed is None:
return np.array(graph.layout_auto().coords)
ig.set_random_number_generator(random.Random(seed))
try:
layout = graph.layout_auto()
finally:
ig.set_random_number_generator(random)
return np.array(layout.coords)

def plot_2d(
Expand Down
32 changes: 32 additions & 0 deletions tests/test_visualization.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import random
from pathlib import Path

import igraph as ig
import matplotlib.pyplot as plt
import numpy as np
import pytest
Expand Down Expand Up @@ -28,6 +30,36 @@ def test_layout_different_seeds_give_different_coordinates(
coords2 = visualizer.get_layout(rastrigin_lon.graph, seed=SEED + 1)
assert not np.array_equal(coords1, coords2)

def test_layout_does_not_change_global_numpy_rng(
self, rastrigin_lon: LON, visualizer: LONVisualizer
) -> None:
np.random.seed(123)
expected = np.random.rand(5)

np.random.seed(123)
visualizer.get_layout(rastrigin_lon.graph, seed=SEED)
np.testing.assert_array_equal(np.random.rand(5), expected)

def test_layout_restores_igraph_rng(
self, rastrigin_lon: LON, visualizer: LONVisualizer
) -> None:
ig.set_random_number_generator(random)
random.seed(123)
expected = ig.Graph.Erdos_Renyi(n=50, m=100).get_edgelist()

random.seed(123)
visualizer.get_layout(rastrigin_lon.graph, seed=SEED)
assert ig.Graph.Erdos_Renyi(n=50, m=100).get_edgelist() == expected

def test_layout_same_seed_ignores_global_numpy_state(
self, rastrigin_lon: LON, visualizer: LONVisualizer
) -> None:
np.random.seed(1)
coords1 = visualizer.get_layout(rastrigin_lon.graph, seed=SEED)
np.random.seed(2)
coords2 = visualizer.get_layout(rastrigin_lon.graph, seed=SEED)
np.testing.assert_array_equal(coords1, coords2)


class TestPlot2DReproducibility:
def test_plot_2d_same_seed_same_figure(
Expand Down
Loading