diff --git a/README.md b/README.md index 58678aa..f19f938 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ lon = compute_lon( lower_bound=-5.12, upper_bound=5.12, n_runs=20, - n_iterations=500, + max_perturbations_without_improvement=500, seed=42 ) @@ -81,12 +81,13 @@ cmlon_metrics = cmlon.compute_metrics() from lonpy import BasinHoppingSampler, BasinHoppingSamplerConfig config = BasinHoppingSamplerConfig( - n_runs=50, # Number of independent runs - n_iterations=1000, # Iterations per run - step_size=0.05, # Perturbation size - step_mode="per", # "per" (percentage) or "fix" (fixed) - hash_digits=4, # Precision for identifying optima - seed=42 # For reproducibility + n_runs=50, # Number of independent runs + max_perturbations_without_improvement=1000, # Stop after this many consecutive non-improving perturbations + step_size=0.05, # Perturbation size + step_mode="percentage", # "percentage" or "fixed" + coordinate_precision=4, # Precision for identifying optima + fitness_precision=None, # Precision for fitness values (None = full double) + seed=42 # For reproducibility ) sampler = BasinHoppingSampler(config) diff --git a/docs/api/index.md b/docs/api/index.md index e89ba48..c9575c5 100644 --- a/docs/api/index.md +++ b/docs/api/index.md @@ -12,6 +12,7 @@ Data structures for Local Optima Networks. - [`LON`](lon.md#lonpy.lon.LON) - Local Optima Network representation - [`CMLON`](lon.md#lonpy.lon.CMLON) - Compressed Monotonic LON +- [`LONConfig`](lon.md#lonpy.lon.LONConfig) - Configuration for LON construction ### [Sampling Module](sampling.md) @@ -80,16 +81,3 @@ viz.create_rotation_gif(lon, output_path="lon.gif") # All visualizations viz.visualize_all(lon, output_folder="./output") ``` - -## Dependencies - -lonpy depends on: - -- `numpy` - Numerical computations -- `scipy` - Optimization -- `pandas` - Data handling -- `igraph` - Graph operations -- `matplotlib` - 2D plotting -- `plotly` - 3D plotting -- `imageio` - GIF creation -- `kaleido` - Static image export diff --git a/docs/api/lon.md b/docs/api/lon.md index 27ba578..96931f4 100644 --- a/docs/api/lon.md +++ b/docs/api/lon.md @@ -12,7 +12,6 @@ - vertex_fitness - vertex_count - get_sinks - - get_global_optima_indices - compute_metrics - to_cmlon @@ -30,3 +29,13 @@ - get_global_sinks - get_local_sinks - compute_metrics + +::: lonpy.lon.LONConfig + options: + show_root_heading: true + show_source: true + members: + - fitness_aggregation + - warn_on_duplicates + - max_fitness_deviation + - eq_atol diff --git a/docs/contributing.md b/docs/contributing.md index 45f1147..1c92ff2 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -89,17 +89,6 @@ mkdocs build 6. Push to your fork 7. Open a Pull Request -### Commit Messages - -Use clear, descriptive commit messages: - -``` -feat: add support for custom perturbation functions -fix: correct edge weight calculation in CMLON -docs: update visualization examples -test: add tests for edge cases in sampling -``` - ## Reporting Issues When reporting bugs, please include: diff --git a/docs/getting-started/concepts.md b/docs/getting-started/concepts.md index 97a2250..9bd00b5 100644 --- a/docs/getting-started/concepts.md +++ b/docs/getting-started/concepts.md @@ -67,13 +67,16 @@ lonpy constructs LONs by: ### Node Identification -Two solutions are considered the same local optimum if their coordinates match after rounding to `hash_digits` decimal places: +Two solutions are considered the same local optimum if their coordinates match after rounding to `coordinate_precision` decimal places: ```python -# With hash_digits=4: -# x1 = [1.23456, 2.34567] → "1.2346_2.3457" -# x2 = [1.23458, 2.34569] → "1.2346_2.3457" +# With coordinate_precision=5 (default): +# x1 = [1.234561, 2.345671] → "1.23456_2.34567" +# x2 = [1.234564, 2.345674] → "1.23456_2.34567" # Same node! + +# With coordinate_precision=None (full double precision): +# No rounding — only exact matches are the same node ``` ## LON Metrics @@ -86,20 +89,23 @@ lonpy computes several metrics to characterize fitness landscapes: | `n_funnels` | Number of sinks (no outgoing edges) | Distinct attraction basins | | `n_global_funnels` | Sinks at global optimum | How many paths lead to success | | `neutral` | Proportion of equal-fitness connections | Landscape flatness | -| `strength` | Incoming flow to global optima | Global optimum accessibility | +| `global_strength` | Incoming flow to global optima relative to all nodes | Global optimum accessibility | +| `sink_strength` | Incoming flow to global sinks relative to all sinks | Global sink dominance | +| `success` | Proportion of runs reaching global optimum | Search algorithm effectiveness | +| `deviation` | Mean absolute deviation from global optimum | Solution quality across runs | ### Interpreting Metrics **Easy landscape** (single funnel): - Few funnels (ideally 1) -- High strength (most flow reaches global) +- High global_strength and sink_strength (most flow reaches global) - All paths converge to global optimum **Hard landscape** (multiple funnels): - Many funnels competing for flow -- Low strength (flow diverted to local sinks) +- Low global_strength and sink_strength (flow diverted to local sinks) - Search easily gets trapped ## CMLON (Compressed Monotonic LON) diff --git a/docs/getting-started/installation.md b/docs/getting-started/installation.md index 41900e2..fa427b2 100644 --- a/docs/getting-started/installation.md +++ b/docs/getting-started/installation.md @@ -22,21 +22,6 @@ cd lonpy pip install -e . ``` -## Dependencies - -lonpy automatically installs the following dependencies: - -| Package | Version | Purpose | -|---------|---------|---------| -| numpy | >= 1.24.0 | Numerical computations | -| scipy | >= 1.10.0 | Optimization algorithms | -| pandas | >= 2.0.0 | Data manipulation | -| igraph | >= 0.11.0 | Graph operations | -| matplotlib | >= 3.7.0 | 2D plotting | -| plotly | >= 5.15.0 | Interactive 3D plots | -| kaleido | >= 0.2.1 | Static image export | -| imageio | >= 2.31.0 | GIF creation | - ## Development Installation To install lonpy with development dependencies for contributing: diff --git a/docs/getting-started/quickstart.md b/docs/getting-started/quickstart.md index d973110..7568c23 100644 --- a/docs/getting-started/quickstart.md +++ b/docs/getting-started/quickstart.md @@ -22,7 +22,7 @@ lon = compute_lon( lower_bound=-5.12, # Search space lower bound upper_bound=5.12, # Search space upper bound n_runs=20, # Number of Basin-Hopping runs - n_iterations=500, # Iterations per run + max_perturbations_without_improvement=500, # Stop after this many consecutive non-improving perturbations seed=42 # For reproducibility ) @@ -43,7 +43,8 @@ print(f"Number of optima: {metrics['n_optima']}") print(f"Number of funnels: {metrics['n_funnels']}") print(f"Global funnels: {metrics['n_global_funnels']}") print(f"Neutrality: {metrics['neutral']:.1%}") -print(f"Strength to global: {metrics['strength']:.1%}") +print(f"Global strength: {metrics['global_strength']:.1%}") +print(f"Sink strength: {metrics['sink_strength']:.1%}") ``` **What do these metrics mean?** @@ -54,7 +55,10 @@ print(f"Strength to global: {metrics['strength']:.1%}") | `n_funnels` | Number of sink nodes (basins of attraction) | | `n_global_funnels` | Funnels leading to the global optimum | | `neutral` | Proportion of nodes with equal-fitness neighbors | -| `strength` | Proportion of flow directed toward global optima | +| `global_strength` | Proportion of global optima incoming strength to total incoming strength | +| `sink_strength` | Proportion of global sinks incoming strength to all sinks incoming strength | +| `success` | Proportion of runs that reached the global optimum | +| `deviation` | Mean absolute deviation from the global optimum | ## Visualizing the Network @@ -136,7 +140,7 @@ lon = compute_lon( lower_bound=-5.12, upper_bound=5.12, n_runs=30, - n_iterations=500, + max_perturbations_without_improvement=500, seed=42 ) @@ -154,7 +158,6 @@ viz = LONVisualizer() outputs = viz.visualize_all( lon, output_folder="./output", - create_gifs=True, seed=42 ) diff --git a/docs/index.md b/docs/index.md index b63195b..0b42a6e 100644 --- a/docs/index.md +++ b/docs/index.md @@ -34,7 +34,7 @@ Local Optima Networks (LONs) are graph-based models that capture the global stru --- - Compute landscape metrics including funnel analysis, neutrality measures, and global optima strength + Compute landscape metrics including funnel analysis, neutrality measures, and strength metrics (global and sink) - **Beautiful Visualizations** diff --git a/docs/user-guide/analysis.md b/docs/user-guide/analysis.md index 27a1a43..459d461 100644 --- a/docs/user-guide/analysis.md +++ b/docs/user-guide/analysis.md @@ -36,7 +36,7 @@ print(f"Found {n_optima} local optima") - Higher values indicate more multimodal landscapes - Compare across functions to assess relative complexity -- Depends on sampling thoroughness and hash_digits precision +- Depends on sampling thoroughness and coordinate_precision ### n_funnels @@ -83,21 +83,85 @@ print(f"Neutrality: {neutral:.1%}") - High % = Many plateaus or degenerate optima - Affects how CMLON compression works -### strength +### global_strength -**Proportion of incoming edge weight to global optima.** +**Proportion of global optima incoming strength to total incoming strength of all nodes.** ```python -strength = lon_metrics['strength'] -print(f"Global strength: {strength:.1%}") +global_strength = lon_metrics['global_strength'] +print(f"Global strength: {global_strength:.1%}") ``` **Interpretation:** - 100% = All transitions flow toward global optimum -- Low % = Most flow diverted to suboptimal sinks +- Low % = Most flow diverted to suboptimal nodes - Key indicator of optimization difficulty +### sink_strength + +**Proportion of global sinks incoming strength to incoming strength of all sink nodes.** + +```python +sink_strength = lon_metrics['sink_strength'] +print(f"Sink strength: {sink_strength:.1%}") +``` + +**Interpretation:** + +- 100% = All sink-directed flow reaches global sinks +- Low % = Most sink flow goes to local (suboptimal) sinks +- Focuses specifically on competition between sinks + +## Performance Metrics + +In addition to network topology metrics, `compute_metrics()` returns performance metrics based on the sampling runs: + +### success + +**Proportion of sampling runs that reached the global optimum.** + +```python +success = lon_metrics['success'] +print(f"Success rate: {success:.1%}") +``` + +**Interpretation:** + +- 100% = Every run found the global optimum +- Low % = Many runs got trapped in local optima +- Useful for comparing algorithm effectiveness across landscapes + +### deviation + +**Mean absolute deviation from the global optimum across all runs.** + +```python +deviation = lon_metrics['deviation'] +print(f"Mean deviation: {deviation:.6f}") +``` + +**Interpretation:** + +- 0.0 = All runs converged to the global optimum +- Higher values indicate runs ending far from the global optimum +- Complements `success` by measuring "how far off" unsuccessful runs are + +### Separating Network and Performance Metrics + +You can also compute them separately: + +```python +# Only network topology metrics (n_optima, n_funnels, etc.) +network_metrics = lon.compute_network_metrics() + +# Only performance metrics (success, deviation) +performance_metrics = lon.compute_performance_metrics() + +# Both combined (equivalent to compute_metrics()) +all_metrics = lon.compute_metrics() +``` + ## CMLON-Specific Metrics CMLON computes additional metrics: @@ -165,10 +229,6 @@ names = lon.vertex_names sinks = lon.get_sinks() print(f"Sink indices: {sinks}") -# Global optima -global_idx = lon.get_global_optima_indices() -print(f"Global optimum indices: {global_idx}") - # For CMLON: separate global and local sinks global_sinks = cmlon.get_global_sinks() local_sinks = cmlon.get_local_sinks() @@ -233,7 +293,7 @@ def classify_landscape(lon): return "Easy: Single-funnel landscape" elif cmlon_metrics['global_funnel_proportion'] > 0.8: return "Moderate: Multiple funnels but well-connected" - elif metrics['strength'] > 0.5: + elif metrics['global_strength'] > 0.5: return "Moderate: Good flow to global optimum" else: return "Hard: Multiple competing funnels" diff --git a/docs/user-guide/examples.md b/docs/user-guide/examples.md index 2cd0743..7831bf6 100644 --- a/docs/user-guide/examples.md +++ b/docs/user-guide/examples.md @@ -20,7 +20,7 @@ lon = compute_lon( lower_bound=-5.12, upper_bound=5.12, n_runs=30, - n_iterations=500, + max_perturbations_without_improvement=500, seed=42 ) @@ -85,20 +85,21 @@ for name, (func, lb, ub, optimal) in functions.items(): lower_bound=lb, upper_bound=ub, n_runs=30, - n_iterations=500, + max_perturbations_without_improvement=500, seed=42 ) - metrics = lon.compute_metrics(known_best=optimal * 10**4) # scaled + metrics = lon.compute_metrics(known_best=optimal) cmlon = lon.to_cmlon() - cmlon_metrics = cmlon.compute_metrics(known_best=optimal * 10**4) + cmlon_metrics = cmlon.compute_metrics(known_best=optimal) results.append({ "Function": name, "Optima": lon.n_vertices, "Funnels": metrics['n_funnels'], "Global Funnels": metrics['n_global_funnels'], - "Strength": f"{metrics['strength']:.1%}", + "Global Strength": f"{metrics['global_strength']:.1%}", + "Sink Strength": f"{metrics['sink_strength']:.1%}", "Global Funnel %": f"{cmlon_metrics['global_funnel_proportion']:.1%}", }) @@ -144,7 +145,8 @@ print(f"Local sinks: {len(cmlon.get_local_sinks())}") cmlon_metrics = cmlon.compute_metrics() print("\n=== CMLON Metrics ===") print(f"Global funnel proportion: {cmlon_metrics['global_funnel_proportion']:.1%}") -print(f"Strength to global: {cmlon_metrics['strength']:.1%}") +print(f"Global strength: {cmlon_metrics['global_strength']:.1%}") +print(f"Sink strength: {cmlon_metrics['sink_strength']:.1%}") # Visualize viz = LONVisualizer() @@ -165,11 +167,11 @@ def schwefel(x): # Custom configuration for challenging function config = BasinHoppingSamplerConfig( - n_runs=100, # More runs for coverage - n_iterations=300, # Moderate depth + n_runs=100, # More runs for coverage + max_perturbations_without_improvement=300, # Moderate depth step_mode="percentage", - step_size=0.15, # Larger steps for this landscape - hash_digits=3, # Coarser grouping + step_size=0.15, # Larger steps for this landscape + coordinate_precision=3, # Coarser grouping bounded=True, minimizer_method="L-BFGS-B", minimizer_options={ @@ -196,7 +198,8 @@ print(f"Best fitness: {lon.best_fitness}") # Known optimum at x = (420.9687, ...) with f(x) ≈ 0 metrics = lon.compute_metrics() print(f"Funnels: {metrics['n_funnels']}") -print(f"Strength: {metrics['strength']:.1%}") +print(f"Global strength: {metrics['global_strength']:.1%}") +print(f"Sink strength: {metrics['sink_strength']:.1%}") ``` ## Accessing Raw Trace Data @@ -209,7 +212,7 @@ from lonpy import BasinHoppingSampler, BasinHoppingSamplerConfig def sphere(x): return np.sum(x**2) -config = BasinHoppingSamplerConfig(n_runs=5, n_iterations=100, seed=42) +config = BasinHoppingSamplerConfig(n_runs=5, max_perturbations_without_improvement=100, seed=42) sampler = BasinHoppingSampler(config) domain = [(-5.0, 5.0), (-5.0, 5.0)] @@ -290,7 +293,7 @@ def analyze_function(name, func, bounds, output_dir): lower_bound=bounds[0], upper_bound=bounds[1], n_runs=30, - n_iterations=500, + max_perturbations_without_improvement=500, seed=42 ) diff --git a/docs/user-guide/sampling.md b/docs/user-guide/sampling.md index 20b8b82..9d3e8ca 100644 --- a/docs/user-guide/sampling.md +++ b/docs/user-guide/sampling.md @@ -27,13 +27,13 @@ For more control, use `BasinHoppingSamplerConfig`: from lonpy import BasinHoppingSampler, BasinHoppingSamplerConfig config = BasinHoppingSamplerConfig( - n_runs=30, # Number of independent runs - n_iterations=500, # Iterations per run - step_mode="percentage", # "percentage" or "fixed" - step_size=0.1, # Perturbation magnitude - hash_digits=4, # Precision for node identification - opt_digits=-1, # Precision for optimization (-1 = full) - bounded=True, # Enforce domain bounds + n_runs=30, # Number of independent runs + max_perturbations_without_improvement=500, # Stop after this many consecutive non-improving perturbations + step_mode="percentage", # "percentage" or "fixed" + step_size=0.1, # Perturbation magnitude + coordinate_precision=4, # Precision for node identification (None = full) + fitness_precision=None, # Precision for fitness values (None = full) + bounded=True, # Enforce domain bounds minimizer_method="L-BFGS-B", seed=42 ) @@ -48,22 +48,21 @@ lon = sampler.sample_to_lon(my_objective, domain) | Parameter | Default | Description | |-----------|---------|-------------| -| `n_runs` | 10 | Number of independent Basin-Hopping runs | -| `n_iterations` | 1000 | Iterations per run | +| `n_runs` | 100 | Number of independent Basin-Hopping runs | +| `max_perturbations_without_improvement` | 1000 | Consecutive non-improving perturbations before stopping a run | | `seed` | None | Random seed for reproducibility | -**Choosing n_runs and n_iterations:** +**Choosing n_runs and max_perturbations_without_improvement:** - More runs = better coverage of the landscape -- More iterations = deeper exploitation from each starting point -- Trade-off: `n_runs × n_iterations` determines total evaluations +- Higher `max_perturbations_without_improvement` = deeper exploitation from each starting point (each run continues until this many consecutive perturbations fail to improve) ```python # Wide coverage (recommended for unknown landscapes) -config = BasinHoppingSamplerConfig(n_runs=50, n_iterations=200) +config = BasinHoppingSamplerConfig(n_runs=50, max_perturbations_without_improvement=200) # Deep exploration (for complex local structure) -config = BasinHoppingSamplerConfig(n_runs=10, n_iterations=1000) +config = BasinHoppingSamplerConfig(n_runs=10, max_perturbations_without_improvement=1000) ``` ### Perturbation Settings @@ -100,17 +99,30 @@ config = BasinHoppingSamplerConfig( | Parameter | Default | Description | |-----------|---------|-------------| -| `hash_digits` | 4 | Decimal places for node identification | -| `opt_digits` | -1 | Decimal places for optimization (-1 = full) | +| `coordinate_precision` | 5 | Decimal places for coordinate rounding and node identification (`None` = full double precision) | +| `fitness_precision` | None | Decimal places for fitness values (`None` = full double precision) | -**hash_digits** determines when two solutions are considered the same optimum: +**coordinate_precision** determines when two solutions are considered the same optimum: ```python # High precision: More distinct nodes -config = BasinHoppingSamplerConfig(hash_digits=6) +config = BasinHoppingSamplerConfig(coordinate_precision=6) # Low precision: More merging, fewer nodes -config = BasinHoppingSamplerConfig(hash_digits=2) +config = BasinHoppingSamplerConfig(coordinate_precision=2) + +# Full precision: No rounding +config = BasinHoppingSamplerConfig(coordinate_precision=None) +``` + +**fitness_precision** controls rounding of fitness values: + +```python +# Round fitness to 4 decimal places +config = BasinHoppingSamplerConfig(fitness_precision=4) + +# Full double precision (default) +config = BasinHoppingSamplerConfig(fitness_precision=None) ``` ### Local Minimizer Settings @@ -118,7 +130,7 @@ config = BasinHoppingSamplerConfig(hash_digits=2) | Parameter | Default | Description | |-----------|---------|-------------| | `minimizer_method` | "L-BFGS-B" | Scipy minimizer algorithm | -| `minimizer_options` | `{"ftol": 1e-07, "gtol": 1e-05}` | Minimizer options | +| `minimizer_options` | `{"ftol": 1e-07, "gtol": 0, "maxiter": 15000}` | Minimizer options | ```python # Custom minimizer settings @@ -132,6 +144,93 @@ config = BasinHoppingSamplerConfig( ) ``` +## LON Construction Configuration + +When constructing a LON from trace data, you can configure how duplicate nodes (nodes with multiple observed fitness values) are handled using `LONConfig`: + +```python +from lonpy import LONConfig, BasinHoppingSampler, BasinHoppingSamplerConfig + +lon_config = LONConfig( + fitness_aggregation="min", # How to resolve duplicate fitness values + warn_on_duplicates=True, # Warn when duplicates detected + max_fitness_deviation=None, # Error if deviation exceeds threshold +) + +config = BasinHoppingSamplerConfig(n_runs=30, seed=42) +sampler = BasinHoppingSampler(config) +lon = sampler.sample_to_lon(my_objective, domain, lon_config=lon_config) +``` + +### Fitness Aggregation Strategies + +| Strategy | Description | +|----------|-------------| +| `"min"` | Use minimum fitness (default) | +| `"max"` | Use maximum fitness | +| `"mean"` | Use average fitness | +| `"first"` | Use first occurrence | +| `"strict"` | Raise error if duplicates detected | + +### Data Quality Checks + +```python +# Strict mode: fail if any node has multiple fitness values +lon_config = LONConfig(fitness_aggregation="strict") + +# Set a maximum allowed deviation +lon_config = LONConfig(max_fitness_deviation=0.01) +``` + +You can also pass `lon_config` to `compute_lon()`: + +```python +from lonpy import compute_lon, LONConfig + +lon = compute_lon( + func=my_objective, + dim=2, + lower_bound=-5.0, + upper_bound=5.0, + lon_config=LONConfig(fitness_aggregation="mean"), +) +``` + +## Custom Initial Points + +By default, Basin-Hopping starts each run from a random point sampled uniformly from the domain. You can provide custom starting points via `initial_points`: + +```python +import numpy as np +from lonpy import compute_lon, BasinHoppingSampler, BasinHoppingSamplerConfig + +# Generate custom initial points (must have shape (n_runs, dim)) +n_runs = 30 +dim = 2 +initial_points = np.random.default_rng(0).uniform(-5.12, 5.12, size=(n_runs, dim)) + +# With compute_lon +lon = compute_lon( + func=my_objective, + dim=dim, + lower_bound=-5.12, + upper_bound=5.12, + n_runs=n_runs, + initial_points=initial_points, + seed=42 +) + +# Or with BasinHoppingSampler +config = BasinHoppingSamplerConfig(n_runs=n_runs, seed=42) +sampler = BasinHoppingSampler(config) +lon = sampler.sample_to_lon(my_objective, domain, initial_points=initial_points) +``` + +**Requirements:** + +- Shape must be `(n_runs, dim)` — one point per run +- When `bounded=True`, all points must lie within the domain bounds + ## Domain Specification The domain is specified as a list of (lower, upper) tuples: @@ -189,10 +288,10 @@ lon = sampler.sample_to_lon(func, domain, progress_callback=progress) # Rastrigin, Ackley, etc. with known bounds config = BasinHoppingSamplerConfig( n_runs=30, - n_iterations=500, + max_perturbations_without_improvement=500, step_mode="percentage", step_size=0.1, - hash_digits=4, + coordinate_precision=4, seed=42 ) ``` @@ -203,10 +302,10 @@ config = BasinHoppingSamplerConfig( # Start with wider exploration config = BasinHoppingSamplerConfig( n_runs=50, - n_iterations=200, + max_perturbations_without_improvement=200, step_mode="percentage", - step_size=0.15, # Larger steps initially - hash_digits=3, # Coarser grouping + step_size=0.15, # Larger steps initially + coordinate_precision=3, # Coarser grouping ) # Refine based on initial results @@ -218,7 +317,7 @@ config = BasinHoppingSamplerConfig( # More runs needed for coverage config = BasinHoppingSamplerConfig( n_runs=100, - n_iterations=500, + max_perturbations_without_improvement=500, step_mode="percentage", step_size=0.05, # Smaller relative steps ) diff --git a/mkdocs.yml b/mkdocs.yml index a124ffc..c58650c 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -48,6 +48,7 @@ plugins: heading_level: 2 members_order: source show_signature_annotations: true + separate_signature: true markdown_extensions: - pymdownx.highlight: diff --git a/src/lonpy/visualization.py b/src/lonpy/visualization.py index b744166..a8400f2 100644 --- a/src/lonpy/visualization.py +++ b/src/lonpy/visualization.py @@ -517,7 +517,6 @@ def visualize_all( Args: lon: LON instance. output_folder: Output directory path. - create_gifs: Whether to create rotation GIFs (slower). seed: Random seed for reproducible layouts. Returns: