From 0488736ac1b6610659c17528a9c198cee56d3cf2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Assaoka?= Date: Mon, 2 Mar 2026 13:15:35 -0300 Subject: [PATCH 1/6] feat: Mkdocs Init --- docs/index.md | 31 ++++++++++++++++++++++ docs/reference/environment.md | 3 +++ docs/reference/logstrategy.md | 3 +++ docs/reference/plots.md | 3 +++ docs/reference/rko.md | 3 +++ mkdocs.yml | 48 +++++++++++++++++++++++++++++++++++ requirements_docs.txt | 4 +++ 7 files changed, 95 insertions(+) create mode 100644 docs/index.md create mode 100644 docs/reference/environment.md create mode 100644 docs/reference/logstrategy.md create mode 100644 docs/reference/plots.md create mode 100644 docs/reference/rko.md create mode 100644 mkdocs.yml create mode 100644 requirements_docs.txt diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..3fa1587 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,31 @@ +# RKO - Random-Key Optimizer (Python Framework) + +Welcome to the documentation for the **RKO (Random-Key Optimizer)**. + +The Random-Key Optimizer is a versatile and efficient metaheuristic framework designed for a wide range of combinatorial optimization problems. Its core paradigm is the encoding of solutions as vectors of random keys—real numbers uniformly distributed in the interval [0, 1). This representation maps the discrete, and often complex, search space of a combinatorial problem to a continuous n-dimensional unit hypercube. + +## Installation + +You can download the package using pip: +```bash +pip install rko +``` + +## Getting Started + +To learn how to use RKO, please refer to the Github repository's [README](https://github.com/RKO-solver/RKO_Python) which details how to create your own `Environment` and run the `RKO` solver. + +## API Reference + +Explore the detailed API Reference using the navigation menu: + +- **[RKO](reference/rko.md)**: The core solver class, encapsulating all search operators and metaheuristics. +- **[Environment](reference/environment.md)**: The abstract base class (`RKOEnvAbstract`) enabling the integration of problem-specific logic. +- **[LogStrategy](reference/logstrategy.md)**: Mechanisms for logging search progress. +- **[Plots](reference/plots.md)**: Visualization utilities for convergence and other metrics. + +--- + +### Maintainers +- Felipe Silvestre Cardoso Roberto - [Linkedin](https://www.linkedin.com/in/felipesilvestrecr/) +- João Victor Assaoka Ribeiro - [Linkedin](https://www.linkedin.com/in/assaoka/) diff --git a/docs/reference/environment.md b/docs/reference/environment.md new file mode 100644 index 0000000..688b875 --- /dev/null +++ b/docs/reference/environment.md @@ -0,0 +1,3 @@ +# Environment + +::: rko.Environment diff --git a/docs/reference/logstrategy.md b/docs/reference/logstrategy.md new file mode 100644 index 0000000..b3d0437 --- /dev/null +++ b/docs/reference/logstrategy.md @@ -0,0 +1,3 @@ +# LogStrategy + +::: rko.LogStrategy diff --git a/docs/reference/plots.md b/docs/reference/plots.md new file mode 100644 index 0000000..4720e98 --- /dev/null +++ b/docs/reference/plots.md @@ -0,0 +1,3 @@ +# Plots + +::: rko.Plots diff --git a/docs/reference/rko.md b/docs/reference/rko.md new file mode 100644 index 0000000..8e35962 --- /dev/null +++ b/docs/reference/rko.md @@ -0,0 +1,3 @@ +# RKO + +::: rko.RKO diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000..951e36f --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,48 @@ +site_name: RKO - Random-Key Optimizer +site_description: A Python library for Random-Key Optimization (RKO). +site_author: Felipe Silvestre Cardoso Roberto & João Assaoka +repo_url: https://github.com/RKO-solver/RKO_Python +repo_name: RKO-solver/RKO_Python + +theme: + name: material + features: + - navigation.tabs + - navigation.sections + - navigation.top + - search.suggest + - search.highlight + - content.code.copy + palette: + - scheme: default + primary: indigo + accent: indigo + toggle: + icon: material/brightness-7 + name: Switch to dark mode + - scheme: slate + primary: indigo + accent: indigo + toggle: + icon: material/brightness-4 + name: Switch to light mode + +plugins: + - search + - mkdocstrings: + default_handler: python + handlers: + python: + paths: [src] + options: + docstring_style: google + show_root_heading: true + show_source: true + +nav: + - Home: index.md + - API Reference: + - RKO: reference/rko.md + - Environment: reference/environment.md + - LogStrategy: reference/logstrategy.md + - Plots: reference/plots.md diff --git a/requirements_docs.txt b/requirements_docs.txt new file mode 100644 index 0000000..923e8d5 --- /dev/null +++ b/requirements_docs.txt @@ -0,0 +1,4 @@ +mkdocs +mkdocs-material +pymdown-extensions +mkdocstrings[python] From d6558a3444001e49ce57c5e52b5a34c0c2de8106 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Assaoka?= Date: Mon, 2 Mar 2026 13:58:57 -0300 Subject: [PATCH 2/6] feat: Visual Improvements in Documentation --- docs/assets/favicon.png | Bin 0 -> 756 bytes docs/assets/logo.jpg | Bin 0 -> 30650 bytes docs/stylesheets/extra.css | 29 +++++++++++++++++++++++++++++ mkdocs.yml | 22 ++++++++++++++++++---- 4 files changed, 47 insertions(+), 4 deletions(-) create mode 100644 docs/assets/favicon.png create mode 100644 docs/assets/logo.jpg create mode 100644 docs/stylesheets/extra.css diff --git a/docs/assets/favicon.png b/docs/assets/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..3e90d0edd270b0e1fd84e3f71c110215c447d5ec GIT binary patch literal 756 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 B66^o~ literal 0 HcmV?d00001 diff --git a/docs/stylesheets/extra.css b/docs/stylesheets/extra.css new file mode 100644 index 0000000..a180940 --- /dev/null +++ b/docs/stylesheets/extra.css @@ -0,0 +1,29 @@ +:root { + /* Cores principais baseadas no verde da UNIFESP (#225a37) */ + --md-primary-fg-color: #225a37; + --md-primary-fg-color--light: #347d4e; + --md-primary-fg-color--dark: #13331f; + --md-primary-bg-color: #ffffff; + --md-primary-bg-color--light: #ffffffcc; + + /* Cores de destaque (accent) usando o mesmo verde */ + --md-accent-fg-color: #225a37; + --md-accent-fg-color--transparent: #225a37f2; + --md-accent-bg-color: #ffffff; + --md-accent-bg-color--light: #ffffffcc; +} + +[data-md-color-scheme="slate"] { + /* Header verde no modo esculo com texto branco */ + --md-primary-fg-color: #225a37; + --md-primary-fg-color--light: #347d4e; + --md-primary-fg-color--dark: #184529; + --md-primary-bg-color: #ffffff; + --md-primary-bg-color--light: #ffffffcc; + + /* Cor de destaque mais clara no escuro para melhor contraste */ + --md-accent-fg-color: #4ea86e; + --md-accent-fg-color--transparent: #4ea86e1a; + --md-accent-bg-color: #000000; + --md-accent-bg-color--light: #000000cc; +} diff --git a/mkdocs.yml b/mkdocs.yml index 951e36f..3df893c 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -6,6 +6,8 @@ repo_name: RKO-solver/RKO_Python theme: name: material + logo: assets/logo.jpg + favicon: assets/favicon.png features: - navigation.tabs - navigation.sections @@ -15,18 +17,21 @@ theme: - content.code.copy palette: - scheme: default - primary: indigo - accent: indigo + primary: custom + accent: custom toggle: icon: material/brightness-7 name: Switch to dark mode - scheme: slate - primary: indigo - accent: indigo + primary: custom + accent: custom toggle: icon: material/brightness-4 name: Switch to light mode +extra_css: + - stylesheets/extra.css + plugins: - search - mkdocstrings: @@ -46,3 +51,12 @@ nav: - Environment: reference/environment.md - LogStrategy: reference/logstrategy.md - Plots: reference/plots.md + +markdown_extensions: + - pymdownx.highlight: + anchor_linenums: true + line_spans: __span + pygments_lang_class: true + - pymdownx.inlinehilite + - pymdownx.snippets + - pymdownx.superfences From 4bbf65d85d2efc3d23a7c0228d429afb79011f87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Assaoka?= Date: Mon, 2 Mar 2026 14:10:46 -0300 Subject: [PATCH 3/6] refactor: Updating the `LogStrategy` Docstrings --- src/rko/LogStrategy.py | 123 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 118 insertions(+), 5 deletions(-) diff --git a/src/rko/LogStrategy.py b/src/rko/LogStrategy.py index ff36761..b4ee4f3 100644 --- a/src/rko/LogStrategy.py +++ b/src/rko/LogStrategy.py @@ -6,48 +6,128 @@ # === Interface Strategy === class LogStrategy(ABC): + """ + Abstract Base Class for logging strategies. + + Defines the standard interface for any logging strategy used in the framework. + """ + @abstractmethod def log(self, *args: list[any], **kwargs: dict[str, any]): + """ + Logs the provided arguments. + + Args: + *args (list[any]): Variable length argument list to be logged. + **kwargs (dict[str, any]): Arbitrary keyword arguments (e.g., end='\\n'). + """ pass # === Estratégias Concretas === class TerminalLogger(LogStrategy): - """Escreve apenas no terminal.""" + """ + Logging strategy that writes output exclusively to the terminal. + + Useful for interactive monitoring of the optimization process. + """ + def log(self, *args: list[any], **kwargs: dict[str, any]): - # flush=True é vital em multiprocessing para não bufferizar a saída + """ + Prints the provided arguments to the standard output. + + Args: + *args (list[any]): Variable length argument list to be printed. + **kwargs (dict[str, any]): Arbitrary keyword arguments. + """ + # flush=True is vital in multiprocessing to prevent output buffering print(*args, **kwargs, flush=True) class FileLogger(LogStrategy): - """Escreve apenas em arquivo.""" + """ + Logging strategy that writes output exclusively to a specified file. + + Useful for keeping persistent records of the optimization runs. + """ + def __init__(self, filepath: str, reset: bool = False): + """ + Initializes the file logger. + + Args: + filepath (str): The path to the file where logs should be written. + reset (bool, optional): If True, overwrites the existing file. + If False, appends to the existing file. Defaults to False. + """ self.filepath = filepath if reset: with open(self.filepath, 'w') as f: f.write(f"--- Log Iniciado em {datetime.now()} ---\n") def log(self, *args: list[any], **kwargs: dict[str, any]): + """ + Appends the provided arguments to the configured log file. + + Args: + *args (list[any]): Variable length argument list to be written. + **kwargs (dict[str, any]): Arbitrary keyword arguments. + """ with open(self.filepath, 'a') as f: print(*args, **kwargs, file=f) class DualLogger(LogStrategy): - """Escreve no Terminal E no Arquivo ao mesmo tempo (Composite Pattern).""" + """ + Composite logging strategy that writes to both the terminal and a file simultaneously. + """ + def __init__(self, filepath: str, reset: bool = False): + """ + Initializes the dual logger. + + Args: + filepath (str): The path to the file where logs should be written. + reset (bool, optional): If True, overwrites the existing log file. + Defaults to False. + """ self.terminal = TerminalLogger() self.file = FileLogger(filepath, reset) def log(self, *args: list[any], **kwargs: dict[str, any]): + """ + Logs the provided arguments to both the terminal and the file. + + Args: + *args (list[any]): Variable length argument list to be logged. + **kwargs (dict[str, any]): Arbitrary keyword arguments. + """ self.terminal.log(*args, **kwargs) self.file.log(*args, **kwargs) # === Gerenciador de Processos === class ParallelLogManager: + """ + Manages logging asynchronously in a multiprocessing environment. + + Creates a dedicated listener process that receives log messages from + different workers via a thread-safe Queue, preventing race conditions. + """ + def __init__(self, strategy: LogStrategy): + """ + Initializes the parallel log manager. + + Args: + strategy (LogStrategy): The concrete logging strategy to be used + (e.g., TerminalLogger, FileLogger, DualLogger). + """ self.strategy = strategy self.queue = multiprocessing.Manager().Queue() self.stop_event = multiprocessing.Event() self.listener_process = None def start(self): + """ + Starts the dedicated listener daemon process for handling logs. + """ self.listener_process = multiprocessing.Process( target=self._listener_worker, args=(self.queue, self.strategy, self.stop_event) @@ -55,16 +135,32 @@ def start(self): self.listener_process.start() def stop(self): + """ + Signals the listener process to terminate and waits for it to finish gracefully. + """ self.stop_event.set() if self.listener_process: self.listener_process.join() def get_logger(self): - """Retorna o objeto que será passado para os workers.""" + """ + Produces a lightweight proxy logger instance to be passed to parallel workers. + + Returns: + WorkerLogger: A logger proxy that sends messages to the central queue. + """ return WorkerLogger(self.queue) @staticmethod def _listener_worker(queue, strategy, stop_event): + """ + Background worker method that consumes log messages from the queue. + + Args: + queue (multiprocessing.Queue): The queue holding incoming log payloads. + strategy (LogStrategy): The strategy responsible for executing the logging action. + stop_event (multiprocessing.Event): Event to signal termination. + """ while not stop_event.is_set() or not queue.empty(): while not queue.empty(): try: @@ -77,8 +173,25 @@ def _listener_worker(queue, strategy, stop_event): # === Adaptador para os Workers === class WorkerLogger: + """ + Proxy logger passed to worker processes to offload log actions to the main queue. + """ + def __init__(self, queue): + """ + Initializes the worker logger. + + Args: + queue (multiprocessing.Queue): The shared queue connected to the LogManager. + """ self.queue = queue def log(self, *args, **kwargs): + """ + Puts the log payload onto the queue for execution by the central listener. + + Args: + *args: Variable length argument list to be logged. + **kwargs: Arbitrary keyword arguments. + """ self.queue.put((args, kwargs)) From 92bbb70bbc19eb66a359f8ba04cee6b6dd7a2855 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Assaoka?= Date: Mon, 2 Mar 2026 14:23:00 -0300 Subject: [PATCH 4/6] feat: GitHub Actions workflow for MkDocs documentation deployment to GitHub Pages. --- .github/workflows/deploy.yml | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 938927a..88e5a26 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -36,3 +36,26 @@ jobs: - name: Publish to PyPI uses: pypa/gh-action-pypi-publish@release/v1 + + deploy-docs: + runs-on: ubuntu-latest + needs: build-and-publish + permissions: + contents: write + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.10" + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements_docs.txt + + - name: Deploy MkDocs to GitHub Pages + run: mkdocs gh-deploy --force From c66701f7954e231265a15fc4537df459da8fc5a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Assaoka?= Date: Mon, 2 Mar 2026 14:36:51 -0300 Subject: [PATCH 5/6] feat: New tab to put tutorials. --- docs/tutorials/knapsack.md | 126 +++++++++++++++++++++++++++++++++++ docs/tutorials/tsp.md | 131 +++++++++++++++++++++++++++++++++++++ mkdocs.yml | 3 + 3 files changed, 260 insertions(+) create mode 100644 docs/tutorials/knapsack.md create mode 100644 docs/tutorials/tsp.md diff --git a/docs/tutorials/knapsack.md b/docs/tutorials/knapsack.md new file mode 100644 index 0000000..6cddc17 --- /dev/null +++ b/docs/tutorials/knapsack.md @@ -0,0 +1,126 @@ +# Knapsack Problem (KP) Tutorial + +This tutorial demonstrates how to solve the classic 0/1 Knapsack Problem using the **RKO** framework. The goal of this problem is to pack a set of items, with given weights and profits, into a knapsack with limited capacity to maximize the total profit. + +The environment requires reading a problem instance from a file, creating a binary solution based on threshold decoding, and computing the total profit with a penalty if the capacity is exceeded. + +## 1. Defining the Environment + +Below is the complete implementation of the `KnapsackProblem` class, which extends the `RKOEnvAbstract` base class. + +```python +import numpy as np +import os +import sys + +from rko import RKO, RKOEnvAbstract, FileLogger, HistoryPlotter + +class KnapsackProblem(RKOEnvAbstract): + """ + An implementation of the Knapsack Problem environment for the RKO solver. + """ + def __init__(self, instance_path: str): + super().__init__() # Initialize the abstract base class + print(f"Loading Knapsack Problem instance from: {instance_path}") + + self.instance_name = instance_path.split('/')[-1] + self.LS_type: str = 'Best' # Options: 'Best' or 'First' + self.dict_best: dict = {"Best": [149]} + + # Internal loading method + self._load_data(instance_path) + + # Solution size represents the number of randomly generated keys + self.tam_solution = self.n_items + + self.save_q_learning_report = False + + # Metaheuristics hyperparameters + self.BRKGA_parameters = {'p': [100, 50], 'pe': [0.20, 0.15], 'pm': [0.05], 'rhoe': [0.70]} + self.SA_parameters = {'SAmax': [10, 5], 'alphaSA': [0.5, 0.7], 'betaMin': [0.01, 0.03], 'betaMax': [0.05, 0.1], 'T0': [10]} + self.ILS_parameters = {'betaMin': [0.10, 0.5], 'betaMax': [0.20, 0.15]} + self.VNS_parameters = {'kMax': [5, 3], 'betaMin': [0.05, 0.1]} + self.PSO_parameters = {'PSize': [100, 50], 'c1': [2.05], 'c2': [2.05], 'w': [0.73]} + self.GA_parameters = {'sizePop': [100, 50], 'probCros': [0.98], 'probMut': [0.005, 0.01]} + self.LNS_parameters = {'betaMin': [0.10], 'betaMax': [0.30], 'TO': [100], 'alphaLNS': [0.95, 0.9]} + + def _load_data(self, instance_path: str): + """ + Loads the knapsack problem data from a text file. + The file has format: + + + + ... + """ + with open(instance_path, 'r') as f: + lines = f.readlines() + self.n_items, self.capacity = map(int, lines[0].strip().split()) + + self.profits = [] + self.weights = [] + + for line in lines[1:]: + if line.strip(): + p, w = map(int, line.strip().split()) + self.profits.append(p) + self.weights.append(w) + + def decoder(self, keys: np.ndarray) -> list[int]: + """ + Decodes a random-key vector into a knapsack solution. + An item is included if its corresponding key is > 0.5. + """ + # A solution is a binary list where 1 means the item is in the knapsack + solution = [1 if key > 0.5 else 0 for key in keys] + return solution + + def cost(self, solution: list[int], final_solution: bool = False) -> float: + """ + Calculates the cost of the knapsack solution. + Since this is a maximization problem, the cost is the negative of the total profit. + A penalty is applied for exceeding the knapsack's capacity. + """ + total_profit = 0 + total_weight = 0 + for i, item_included in enumerate(solution): + if item_included: + total_profit += self.profits[i] + total_weight += self.weights[i] + + # Apply a heavy penalty for infeasible solutions (exceeding capacity) + if total_weight > self.capacity: + penalty = 100000 * (total_weight - self.capacity) + total_profit -= penalty + + # The RKO framework assumes a minimization problem by default, + # so we return the negative of the profit. + return -total_profit +``` + +### Problem Constraint Penalty +Often, the search process requires exploring the infeasible domain space to find the optimal global structure. Penalties like `100000 * (total_weight - self.capacity)` severely lower the score, deterring solvers while giving feedback on how close the solution was to feasibility. Notice that the score function returns `-total_profit`, since RKO always minimizes globally. + +## 2. Setting Up and Optimizing + +With the Environment mapped, simply set the logger up, start the search methods, and visualize the output graph when finished. + +```python +if __name__ == "__main__": + current_directory = os.path.dirname(os.path.abspath(__file__)) + + # 1. Instantiate the Instance Environment + env = KnapsackProblem(os.path.join(current_directory, 'kp50.txt')) + + # 2. Add Logger configuration + logger = FileLogger(os.path.join(current_directory, 'results.txt'), reset=True) + + # 3. Solver Initiation + solver = RKO(env, logger=logger) + + # 4. Solves with all heuristics combined over 30s limit overall + solver.solve(time_total=30, brkga=1, lns=1, vns=1, ils=1, sa=1, pso=1, ga=1, runs=2) + + # 5. Output Graphics + HistoryPlotter.plot_convergence(os.path.join(current_directory, 'results.txt'), run_number=1).show() +``` diff --git a/docs/tutorials/tsp.md b/docs/tutorials/tsp.md new file mode 100644 index 0000000..fb82e70 --- /dev/null +++ b/docs/tutorials/tsp.md @@ -0,0 +1,131 @@ +# Travelling Salesperson Problem (TSP) Tutorial + +This tutorial demonstrates how to solve the Travelling Salesperson Problem (TSP) using the **RKO** framework. TSP is a classic combinatorial optimization problem where the goal is to find the shortest possible route that visits every city exactly once and returns to the origin city. + +In our implementation, the environment handles generating a random TSP instance, calculating distances, and decoding random keys into a valid tour. + +## 1. Creating the Environment + +Below is the complete implementation of the `TSPProblem` environment. It extends the `RKOEnvAbstract` class, defining the problem's specifics, such as city generation, random key decoding, and the cost function. + +```python +import numpy as np +import os +import random +import matplotlib.pyplot as plt +from rko import RKO, RKOEnvAbstract, check_env, FileLogger, HistoryPlotter + +class TSPProblem(RKOEnvAbstract): + """ + An implementation of the Traveling Salesperson Problem (TSP) environment for the RKO solver. + This class generates a random instance upon initialization. + """ + def __init__(self, num_cities: int = 20): + super().__init__() + print(f"Generating a random TSP instance with {num_cities} cities.") + + self.num_cities = num_cities + self.instance_name = f"TSP_{num_cities}_cities" + self.LS_type: str = 'Best' + self.dict_best: dict = {} + + self.save_q_learning_report = False + + # Generate city coordinates and the distance matrix + self.cities = self._generate_cities(num_cities) + self.distance_matrix = self._calculate_distance_matrix() + + # Solution size represents the number of randomly generated keys + self.tam_solution = self.num_cities + + # Customizing parameters for metaheuristics (example) + self.BRKGA_parameters = {'p': [100, 50], 'pe': [0.20, 0.15], 'pm': [0.05], 'rhoe': [0.70]} + self.SA_parameters = {'SAmax': [10, 5], 'alphaSA': [0.5, 0.7], 'betaMin': [0.01, 0.03], 'betaMax': [0.05, 0.1], 'T0': [10]} + self.ILS_parameters = {'betaMin': [0.10, 0.5], 'betaMax': [0.20, 0.15]} + self.VNS_parameters = {'kMax': [5, 3], 'betaMin': [0.05, 0.1]} + self.PSO_parameters = {'PSize': [100, 50], 'c1': [2.05], 'c2': [2.05], 'w': [0.73]} + self.GA_parameters = {'sizePop': [100, 50], 'probCros': [0.98], 'probMut': [0.005, 0.01]} + self.LNS_parameters = {'betaMin': [0.10], 'betaMax': [0.30], 'TO': [100], 'alphaLNS': [0.95, 0.9]} + + def _generate_cities(self, num_cities: int) -> np.ndarray: + """Generates random (x, y) coordinates for each city in a 100x100 grid.""" + return np.random.rand(num_cities, 2) * 100 + + def _calculate_distance_matrix(self) -> np.ndarray: + """Computes the Euclidean distance between every pair of cities.""" + num_cities = len(self.cities) + dist_matrix = np.zeros((num_cities, num_cities)) + for i in range(num_cities): + for j in range(i, num_cities): + dist = np.linalg.norm(self.cities[i] - self.cities[j]) + dist_matrix[i, j] = dist_matrix[j, i] = dist + return dist_matrix + + def decoder(self, keys: np.ndarray) -> list[int]: + """ + Decodes a random-key vector into a TSP tour. + The tour is determined by the sorted order of the keys. + """ + tour = np.argsort(keys) + return tour.tolist() + + def cost(self, solution: list[int], final_solution: bool = False) -> float: + """ + Calculates the total distance of a given TSP tour. + The RKO framework will minimize this value. + """ + total_distance = 0 + num_cities_in_tour = len(solution) + for i in range(num_cities_in_tour): + from_city = solution[i] + to_city = solution[(i + 1) % num_cities_in_tour] + total_distance += self.distance_matrix[from_city, to_city] + + return total_distance +``` + +### Decoding Strategy +In the `decoder` method, `np.argsort()` takes the randomly generated variables (in `[0, 1)`) and returns the indices representing their sorted order. This creates a valid permutation of cities automatically. + +## 2. Running the RKO Solver + +Once the environment is set up, you can instantiate the `RKO` solver and initiate the search. This setup uses multiple metaheuristics concurrently. + +```python +if __name__ == "__main__": + current_directory = os.path.dirname(os.path.abspath(__file__)) + + # 1. Instantiate the problem environment (50 cities). + env = TSPProblem(num_cities=50) + check_env(env) # Verify the environment implementation is valid! + + # 2. Setup the logger + logger = FileLogger(os.path.join(current_directory, 'results.txt'), reset=True) + + # 3. Instantiate the RKO solver. + solver = RKO(env=env, logger=logger) + + # 4. Run the solver for 10 seconds with selected metaheuristics + final_cost, final_solution, time_to_best = solver.solve( + time_total=10, + runs=1, + vns=1, + ils=1, + sa=1 + ) + + solution = env.decoder(final_solution) + + # 5. Output and logging + HistoryPlotter.plot_convergence(os.path.join(current_directory, 'results.txt'), run_number=1, title="TSP Convergence").show() + + print("\n" + "="*30) + print(" FINAL RESULTS ") + print("="*30) + print(f"Instance Name: {env.instance_name}") + print(f"Best Tour Cost Found: {final_cost:.4f}") + print(f"Time to Find Best Solution: {time_to_best}s") + print(f"Best Tour (City Sequence): {solution}") +``` + +This will run VNS, ILS and SA in parallel, sharing solutions between all of them. The `HistoryPlotter` can be used to generate beautiful convergence graphs visualizing the search trajectory for the best value. diff --git a/mkdocs.yml b/mkdocs.yml index 3df893c..98c6f73 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -51,6 +51,9 @@ nav: - Environment: reference/environment.md - LogStrategy: reference/logstrategy.md - Plots: reference/plots.md + - Tutorials: + - Knapsack Problem: tutorials/knapsack.md + - Travelling Salesperson Problem: tutorials/tsp.md markdown_extensions: - pymdownx.highlight: From 946a2af2be6655db2b1af6ed9b13f8a32c870cf4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Assaoka?= Date: Wed, 4 Mar 2026 18:17:33 -0300 Subject: [PATCH 6/6] docs: Add the mkdocs version --- pyproject.toml | 2 +- requirements_docs.txt | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index f3e11bf..4b5af35 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "rko" -version = "0.1.2" +version = "0.1.3" description = "A Python library for Random-Key Optimization (RKO)." readme = "README.md" requires-python = ">=3.8" diff --git a/requirements_docs.txt b/requirements_docs.txt index 923e8d5..93361b0 100644 --- a/requirements_docs.txt +++ b/requirements_docs.txt @@ -1,4 +1,4 @@ -mkdocs -mkdocs-material -pymdown-extensions -mkdocstrings[python] +mkdocs==1.6.1 +mkdocs-material==9.7.3 +pymdown-extensions==10.21 +mkdocstrings[python]==1.0.3