Skip to content

Comments

feat: add LONs for discrete problems#6

Draft
WojtAcht wants to merge 4 commits intomainfrom
feature/add-discrete-lons
Draft

feat: add LONs for discrete problems#6
WojtAcht wants to merge 4 commits intomainfrom
feature/add-discrete-lons

Conversation

@WojtAcht
Copy link
Member

@WojtAcht WojtAcht commented Jan 6, 2026

No description provided.

Copilot AI review requested due to automatic review settings January 6, 2026 21:11
@gemini-code-assist
Copy link

Summary of Changes

Hello @WojtAcht, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly expands the lonpy library's capabilities by introducing comprehensive support for discrete optimization problems. Previously focused on continuous optimization, the library can now construct, analyze, and visualize Local Optima Networks for combinatorial problems such as OneMax, Knapsack, and Number Partitioning. This is achieved through the implementation of Iterated Local Search (ILS) as a new sampling method, alongside a refactored module structure and comprehensive documentation updates to guide users through both continuous and discrete problem workflows. The changes also include a new Makefile for streamlined development and an enhanced visualization module with new edge coloring options.

Highlights

  • Discrete Optimization Support: The library now fully supports discrete optimization problems, allowing for the construction, analysis, and visualization of Local Optima Networks (LONs) for combinatorial problems.
  • New Sampling Algorithm: Introduced Iterated Local Search (ILS) as a sampling method specifically designed for discrete optimization problems, complementing the existing Basin-Hopping for continuous problems.
  • Built-in Discrete Problems: Added implementations for common discrete optimization problems including OneMax, Knapsack, Number Partitioning, and NK Landscape, making it easier to get started with discrete LON analysis.
  • Module Refactoring and New Data Structures: The codebase has been reorganized to clearly separate continuous and discrete optimization components, introducing new Solution and Neighborhood classes for discrete problem handling, and a ProblemInstance base class.
  • Enhanced LON Analysis and Visualization: A new MLON (Monotonic Local Optima Network) representation has been added, and the visualization module now supports coloring edges by type (improving, equal, worsening) for better landscape insights.
  • Improved Developer Experience: A Makefile has been added to streamline common development tasks such as installing dependencies, running tests (with coverage), checking code quality, and building/serving documentation.
  • Comprehensive Documentation Updates: All relevant documentation files (README, API reference, getting started guides, user guides) have been extensively updated to reflect the new discrete optimization features, providing clear examples and explanations.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This is an excellent and substantial pull request that adds support for discrete optimization problems to the lonpy library. The changes are well-structured, with a clear separation between continuous and discrete modules. The introduction of a ProblemInstance abstract base class is a great design choice that will make the library more extensible. The documentation and tests have been thoroughly updated to cover the new features. I have two suggestions for improvement: one regarding the handling of infeasible solutions in the Knapsack problem to improve search performance, and another to document a small breaking change in the configuration options.

n_iterations=1000, # Iterations per run
step_size=0.05, # Perturbation size
step_mode="per", # "per" (percentage) or "fix" (fixed)
step_mode="percentage", # "percentage" (of domain) or "fixed"

Choose a reason for hiding this comment

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

medium

The change of step_mode's value from "per" to "percentage" is a good improvement for clarity. However, this is a breaking change for users of the previous version. It would be beneficial to mention this in the pull request description or in a release notes/changelog section to ensure users are aware they need to update their configurations.

Comment on lines +113 to +114
if total_weight > self.capacity:
return 0.0

Choose a reason for hiding this comment

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

medium

Returning 0.0 for infeasible solutions can be problematic. If a feasible solution (like an empty knapsack) also has a fitness of 0.0, the search cannot distinguish between them. This creates a large plateau of infeasible solutions with no gradient to guide the search back to feasibility.

Consider applying a penalty proportional to the degree of infeasibility. For this maximization problem, you could return a negative value based on how much the capacity is exceeded. This provides a gradient that can help the search algorithm escape the infeasible region.

Suggested change
if total_weight > self.capacity:
return 0.0
if total_weight > self.capacity:
return -1.0 * (total_weight - self.capacity)

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR adds comprehensive support for discrete optimization problems to lonpy, significantly expanding the library's capabilities beyond continuous optimization.

Purpose: Add Local Optima Network (LON) construction and analysis for combinatorial/discrete optimization problems using Iterated Local Search (ILS) sampling.

Key changes:

  • Implements discrete solution representations (bitstrings and permutations) with neighborhood operators (flip and swap)
  • Adds ILS sampler for discrete LON construction with configurable hill climbing strategies
  • Introduces built-in discrete problems: OneMax, Knapsack, NumberPartitioning, and NKLandscape
  • Adds MLON (Monotonic LON) as an intermediate representation between LON and CMLON
  • Extends visualization to support MLON and edge coloring by type (improving/equal/worsening)
  • Refactors continuous sampling to use private methods and improved RNG handling

Reviewed changes

Copilot reviewed 26 out of 30 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
src/lonpy/discrete/solution.py Solution class for discrete problems with bitstring/permutation support
src/lonpy/discrete/neighborhoods.py FlipNeighborhood and SwapNeighborhood operators for local search
src/lonpy/discrete/local_search.py Hill climbing implementation with first/best improvement strategies
src/lonpy/discrete/sampling.py ILSSampler for discrete LON construction via Iterated Local Search
src/lonpy/problems/base.py Abstract ProblemInstance base class for optimization problems
src/lonpy/problems/discrete.py Built-in discrete problems (OneMax, Knapsack, NumberPartitioning, NKLandscape)
src/lonpy/lon.py Adds MLON class and classify_edges method for edge type classification
src/lonpy/visualization.py Extends visualization to support MLON and edge coloring by type
src/lonpy/continuous/sampling.py Refactors to use private methods and improved RNG handling
src/lonpy/__init__.py Updates exports to include discrete optimization components
tests/test_solution.py Comprehensive tests for Solution class
tests/test_neighborhoods.py Tests for neighborhood operators
tests/test_lon.py Tests for LON/MLON/CMLON construction and metrics
tests/test_discrete_problems.py Tests for discrete problem implementations
docs/ Documentation updates for discrete optimization throughout user guide and API reference
uv.lock Moves ioh dependency from dev-dependencies to optional dev extras

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +317 to +319

# Or load from file
problem = Knapsack.from_file("instance.txt")
Copy link

Copilot AI Jan 6, 2026

Choose a reason for hiding this comment

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

The documentation mentions Knapsack.from_file("instance.txt") as a method to load knapsack instances from files, but this method doesn't exist in the Knapsack class implementation. Either implement the from_file class method, or remove this line from the documentation.

Suggested change
# Or load from file
problem = Knapsack.from_file("instance.txt")

Copilot uses AI. Check for mistakes.
Comment on lines +16 to +21
from lonpy.problems import (
Knapsack,
NumberPartitioning,
OneMax,
ProblemInstance,
)
Copy link

Copilot AI Jan 6, 2026

Choose a reason for hiding this comment

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

NKLandscape is implemented in discrete.py and exported in problems/init.py, but is not exported in the main lonpy/init.py. This makes it inaccessible via the public API (e.g., from lonpy import NKLandscape will fail). Either add NKLandscape to the exports in src/lonpy/init.py, or remove it from the problems module if it's not ready for public use.

Copilot uses AI. Check for mistakes.
Comment on lines +179 to +280
class NKLandscape(ProblemInstance):
"""
NK Landscape problem: tunable rugged fitness landscape.

The NK model is a problem-independent model for constructing multimodal
landscapes that can gradually be tuned from smooth to rugged:
- N: number of binary genes in the genotype (string length)
- K: number of genes that influence a particular gene (0 <= K <= N-1)

By increasing K from 0 to N-1, landscapes can be tuned from smooth to rugged.

This implementation uses the adjacent neighborhood model, where the K variables
forming the context of gene s_i are the K variables closest to s_i in a total
ordering (s_1, s_2, ..., s_n) using periodic boundaries.

The fitness is the average of N contribution functions, where each
contribution f_i depends on bit i and its K adjacent neighbors.

Attributes:
n: Length of the bitstring (number of genes).
k: Epistasis parameter (number of neighboring genes influencing each gene).
seed: Random seed for reproducible instance generation.
contributions: Lookup tables for each bit's contribution.

Example:
>>> problem = NKLandscape(n=18, k=4, seed=42)
>>> solution = [1, 0, 1, 1, 0, 0, 1, 0, 1, 0, 1, 1, 0, 0, 1, 0, 1, 0]
>>> fitness = problem.evaluate(solution)
"""

n: int = 18
k: int = 4
seed: int = 1
contributions: list[dict[tuple[int, ...], float]] = field(default_factory=list, repr=False)

def __post_init__(self) -> None:
"""Generate NK landscape instance if not already initialized."""
if self.k < 0 or self.k >= self.n:
raise ValueError(f"k must be in [0, n-1], got k={self.k}, n={self.n}")

if not self.contributions:
self._generate_instance()

def _generate_instance(self) -> None:
"""Generate contribution tables for adjacent neighborhood model."""
rng = np.random.default_rng(self.seed)

self.contributions = []

for _ in range(self.n):
# Create contribution lookup table
# Each entry maps (bit_i, neighbor_0, ..., neighbor_k-1) -> contribution
n_entries = 2 ** (self.k + 1) # 2^(K+1) possible combinations
contribution_table: dict[tuple[int, ...], float] = {}

for entry_idx in range(n_entries):
# Convert entry index to bit pattern
bits = tuple((entry_idx >> b) & 1 for b in range(self.k + 1))
contribution_table[bits] = float(rng.random())

self.contributions.append(contribution_table)

def _get_adjacent_neighbors(self, i: int) -> list[int]:
"""
Get K adjacent neighbors for gene i using periodic boundaries.

Args:
i: Index of the gene.

Returns:
List of K neighbor indices.
"""
return [(i + j + 1) % self.n for j in range(self.k)]

def maximize(self) -> bool:
"""NK Landscape is a maximization problem."""
return True

def evaluate(self, solution: list[int]) -> float:
"""
Evaluate fitness as average of all contribution functions.

Args:
solution: Bitstring as list of 0/1 integers.

Returns:
Average contribution (value in [0, 1]).
"""
total = 0.0

for i in range(self.n):
# Get the relevant bits: bit i and its K adjacent neighbors
neighbors = self._get_adjacent_neighbors(i)
key_bits = [solution[i]] + [solution[j] for j in neighbors]
key = tuple(key_bits)
total += self.contributions[i][key]

return total / self.n

def supports_delta_evaluation(self) -> bool:
"""NK Landscape does not support efficient delta evaluation."""
return False
Copy link

Copilot AI Jan 6, 2026

Choose a reason for hiding this comment

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

NKLandscape is not mentioned or documented anywhere else in the codebase (no tests, no examples in docs), but is defined and exported from the problems module. This inconsistency suggests it may not be ready for release. Consider either: (1) adding tests and documentation for NKLandscape, or (2) removing it from this PR to add in a future release when it's fully integrated.

Copilot uses AI. Check for mistakes.
@WojtAcht WojtAcht marked this pull request as draft February 24, 2026 22:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant