pymdls 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,31 @@
1
+ # Byte-compiled / cache
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.nbi
5
+ *.nbc
6
+
7
+ # Environments
8
+ .venv/
9
+ venv/
10
+ env/
11
+
12
+ # Packaging and builds
13
+ build/
14
+ dist/
15
+ *.egg-info/
16
+ *.egg
17
+
18
+ # Tests and coverage
19
+ .pytest_cache/
20
+ .coverage
21
+ .coverage.*
22
+ htmlcov/
23
+
24
+ # Example output
25
+ front.csv
26
+ front.png
27
+
28
+ # Editors and OS
29
+ .DS_Store
30
+ .idea/
31
+ .vscode/
@@ -0,0 +1,32 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented in this file. The format
4
+ follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this
5
+ project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
+
7
+ ## [Unreleased]
8
+
9
+ ## [0.1.0] - 2026-11-10
10
+
11
+ ### Added
12
+
13
+ - `MDLS` engine with an archive of feasible non-dominated solutions, uniform
14
+ selection by default, and iteration, evaluation and wall-clock budgets.
15
+ - `@move(improves=k, attempts=n)` decorator for readable, configurable moves.
16
+ - `Archive` with relative-tolerance dominance, duplicate rejection, dynamic
17
+ growth, `knee()` and `extreme(k)` helpers.
18
+ - `Result` and `MoveStats` with per-move accounting, including dominated
19
+ candidates, and a text `summary()`.
20
+ - Extension points: `is_feasible`, `selection`, `copy_solution`, and the
21
+ `select`, `offer`, `copy` and `on_iteration` hooks.
22
+ - Logging-based progress output (`verbose=n`) instead of printing.
23
+ - Bi-objective knapsack example with `run` and `plot` commands.
24
+ - Test suite, user guide, packaging metadata and GitHub Actions workflows for
25
+ CI and PyPI trusted publishing.
26
+
27
+ The PyPI distribution is named `pymdls` because `mdls` is blocked by PyPI's
28
+ name-similarity rule (it collides with the existing project `mdis`). The
29
+ import package is still `mdls`.
30
+
31
+ [Unreleased]: https://github.com/nsushant/ORBITAL/compare/v0.1.0...MDLS
32
+ [0.1.0]: https://github.com/nsushant/ORBITAL/releases/tag/v0.1.0
@@ -0,0 +1,38 @@
1
+ cff-version: 1.2.0
2
+ message: "If you use this software, please cite it as below."
3
+ title: MDLS
4
+ abstract: "A decorator-based implementation of multi-directional local search for multi-objective optimisation, with an archive of feasible non-dominated solutions and user-supplied moves."
5
+ type: software
6
+ authors:
7
+ - family-names: Nigudkar
8
+ given-names: Sushanta
9
+ version: 0.1.0
10
+ date-released: 2026-11-10
11
+ license: MIT
12
+ repository-code: "https://github.com/nsushant/ORBITAL/tree/MDLS"
13
+ url: "https://github.com/nsushant/ORBITAL/tree/MDLS"
14
+ identifiers:
15
+ - type: pypi
16
+ value: pymdls
17
+ keywords:
18
+ - multi-objective optimisation
19
+ - multi-objective optimization
20
+ - local search
21
+ - pareto
22
+ - metaheuristics
23
+ preferred-citation:
24
+ type: article
25
+ authors:
26
+ - family-names: Tricoire
27
+ given-names: Fabien
28
+ - family-names: Graf
29
+ given-names: Alexandra
30
+ - family-names: Hartl
31
+ given-names: Richard F.
32
+ title: "The bi-objective stochastic covering tour problem"
33
+ year: 2012
34
+ journal: "Computers & Operations Research"
35
+ volume: 39
36
+ issue: 7
37
+ start: 1582
38
+ end: 1592
pymdls-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Sushanta Nigudkar
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
pymdls-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,154 @@
1
+ Metadata-Version: 2.5
2
+ Name: pymdls
3
+ Version: 0.1.0
4
+ Summary: Multi-directional local search with user-supplied moves
5
+ Project-URL: Homepage, https://github.com/nsushant/ORBITAL/tree/MDLS
6
+ Project-URL: Repository, https://github.com/nsushant/ORBITAL/tree/MDLS
7
+ Project-URL: Documentation, https://github.com/nsushant/ORBITAL/blob/MDLS/user_guide.md
8
+ Project-URL: Issues, https://github.com/nsushant/ORBITAL/issues
9
+ Project-URL: Changelog, https://github.com/nsushant/ORBITAL/blob/MDLS/CHANGELOG.md
10
+ Author: Sushanta Nigudkar
11
+ License: MIT
12
+ License-File: LICENSE
13
+ Keywords: local search,mdls,metaheuristics,multi-objective optimisation,multi-objective optimization,pareto
14
+ Classifier: Development Status :: 4 - Beta
15
+ Classifier: Intended Audience :: Science/Research
16
+ Classifier: License :: OSI Approved :: MIT License
17
+ Classifier: Operating System :: OS Independent
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3 :: Only
20
+ Classifier: Programming Language :: Python :: 3.10
21
+ Classifier: Programming Language :: Python :: 3.11
22
+ Classifier: Programming Language :: Python :: 3.12
23
+ Classifier: Topic :: Scientific/Engineering :: Mathematics
24
+ Requires-Python: >=3.10
25
+ Requires-Dist: numba>=0.57
26
+ Requires-Dist: numpy>=1.23
27
+ Provides-Extra: dev
28
+ Requires-Dist: build>=1.0; extra == 'dev'
29
+ Requires-Dist: pytest>=7.0; extra == 'dev'
30
+ Requires-Dist: twine>=5.0; extra == 'dev'
31
+ Provides-Extra: examples
32
+ Requires-Dist: matplotlib>=3.6; extra == 'examples'
33
+ Description-Content-Type: text/markdown
34
+
35
+ # mdls
36
+
37
+ `mdls` is a small, decorator-based implementation of **multi-directional local
38
+ search** (MDLS) for multi-objective optimisation. You supply an `evaluate`
39
+ function and one move per objective; the library owns the non-dominated
40
+ archive, selection, budgets and statistics.
41
+
42
+ The method follows Tricoire, Graf and Hartl (2012). This package is a
43
+ generalised, reusable version of the MDLS engine used in
44
+ [ORBITAL](https://github.com/nsushant/ORBITAL/tree/MDLS).
45
+
46
+ ## Install
47
+
48
+ ```bash
49
+ pip install pymdls
50
+ ```
51
+
52
+ The distribution is named `pymdls`; the import package is `mdls`, so the code
53
+ below always uses `from mdls import ...`.
54
+
55
+ Python 3.10 or newer is required. NumPy and Numba are installed automatically.
56
+
57
+ For the example's plotting command, install the extra:
58
+
59
+ ```bash
60
+ pip install "pymdls[examples]"
61
+ ```
62
+
63
+ ## Quick start
64
+
65
+ ```python
66
+ import numpy as np
67
+
68
+ from mdls import MDLS, move
69
+
70
+
71
+ def evaluate(x, inst):
72
+ """MDLS minimises; g <= 0 means feasible."""
73
+ return -(inst["obj"] @ x), [inst["weight"] @ x - inst["cap"]]
74
+
75
+
76
+ def _toggle(x, inst, rng, k, n_toggle):
77
+ """One attempt: flip items, keep the copy if objective k improves."""
78
+ y = x.copy()
79
+ flip = rng.choice(x.size, size=n_toggle, replace=False)
80
+ y[flip] = ~y[flip]
81
+ if inst["weight"] @ y > inst["cap"]:
82
+ return None
83
+ return y if inst["obj"][k] @ y > inst["obj"][k] @ x else None
84
+
85
+
86
+ @move(improves=0, attempts=8)
87
+ def toggle_0(x, inst, rng, n_toggle=2):
88
+ return _toggle(x, inst, rng, 0, n_toggle)
89
+
90
+
91
+ @move(improves=1, attempts=8)
92
+ def toggle_1(x, inst, rng, n_toggle=2):
93
+ return _toggle(x, inst, rng, 1, n_toggle)
94
+
95
+
96
+ rng = np.random.default_rng(0)
97
+ n = 20
98
+ inst = {"obj": rng.random((2, n)), "weight": rng.random(n), "cap": 0.3 * n}
99
+ moves = [toggle_0(n_toggle=2), toggle_1(n_toggle=2)]
100
+ search = MDLS(evaluate, moves=moves, instance=inst)
101
+
102
+ res = search.run([np.zeros(n, dtype=bool)], max_eval=500, seed=0)
103
+ print(res.summary())
104
+ print(res.objectives)
105
+ ```
106
+
107
+ ## What the library provides
108
+
109
+ - **Archive** of feasible, non-dominated solutions with a relative tolerance,
110
+ duplicate rejection and dominated-point eviction.
111
+ - **Move contract**: a move is one improving attempt, optionally retried up to
112
+ `attempts` times by the `@move` decorator.
113
+ - **Balanced search**: every iteration applies one sampled move per objective
114
+ from the same archived base solution.
115
+ - **Budgets**: stop on iterations (`max_iter`), evaluations (`max_eval`,
116
+ including initial solutions) or wall-clock time (`time_limit`).
117
+ - **Statistics**: per-move counts of calls, no-moves, non-descents, infeasible
118
+ results, dominated results, accepted results and time.
119
+ - **Extension points**: `is_feasible`, `selection`, `copy_solution`, and the
120
+ `select`, `offer`, `copy` and `on_iteration` hooks.
121
+
122
+ Moves are ordinary functions and stay readable; Numba is optional but is used
123
+ inside the library's dominance scan and can be used inside your own moves.
124
+
125
+ ## Examples
126
+
127
+ The repository includes a bi-objective 0/1 knapsack:
128
+
129
+ ```bash
130
+ python examples/knapsack.py run # one search, writes front.csv
131
+ python examples/knapsack.py plot # fronts for up to four seeds, writes front.png
132
+ ```
133
+
134
+ The full tutorial, including the move contract and every option, is in
135
+ [`user_guide.md`](https://github.com/nsushant/ORBITAL/blob/MDLS/user_guide.md).
136
+
137
+ ## Testing
138
+
139
+ ```bash
140
+ pip install -e ".[dev,examples]"
141
+ pytest
142
+ ```
143
+
144
+ ## License
145
+
146
+ MIT. See [LICENSE](LICENSE).
147
+
148
+ ## Citation
149
+
150
+ If you use this package in academic work, cite the MDLS paper and optionally
151
+ the software itself (see `CITATION.cff`):
152
+
153
+ > Tricoire, F., Graf, A., & Hartl, R. F. (2012). The bi-objective stochastic
154
+ > covering tour problem. *Computers & Operations Research*, 39(7), 1582–1592.
pymdls-0.1.0/README.md ADDED
@@ -0,0 +1,120 @@
1
+ # mdls
2
+
3
+ `mdls` is a small, decorator-based implementation of **multi-directional local
4
+ search** (MDLS) for multi-objective optimisation. You supply an `evaluate`
5
+ function and one move per objective; the library owns the non-dominated
6
+ archive, selection, budgets and statistics.
7
+
8
+ The method follows Tricoire, Graf and Hartl (2012). This package is a
9
+ generalised, reusable version of the MDLS engine used in
10
+ [ORBITAL](https://github.com/nsushant/ORBITAL/tree/MDLS).
11
+
12
+ ## Install
13
+
14
+ ```bash
15
+ pip install pymdls
16
+ ```
17
+
18
+ The distribution is named `pymdls`; the import package is `mdls`, so the code
19
+ below always uses `from mdls import ...`.
20
+
21
+ Python 3.10 or newer is required. NumPy and Numba are installed automatically.
22
+
23
+ For the example's plotting command, install the extra:
24
+
25
+ ```bash
26
+ pip install "pymdls[examples]"
27
+ ```
28
+
29
+ ## Quick start
30
+
31
+ ```python
32
+ import numpy as np
33
+
34
+ from mdls import MDLS, move
35
+
36
+
37
+ def evaluate(x, inst):
38
+ """MDLS minimises; g <= 0 means feasible."""
39
+ return -(inst["obj"] @ x), [inst["weight"] @ x - inst["cap"]]
40
+
41
+
42
+ def _toggle(x, inst, rng, k, n_toggle):
43
+ """One attempt: flip items, keep the copy if objective k improves."""
44
+ y = x.copy()
45
+ flip = rng.choice(x.size, size=n_toggle, replace=False)
46
+ y[flip] = ~y[flip]
47
+ if inst["weight"] @ y > inst["cap"]:
48
+ return None
49
+ return y if inst["obj"][k] @ y > inst["obj"][k] @ x else None
50
+
51
+
52
+ @move(improves=0, attempts=8)
53
+ def toggle_0(x, inst, rng, n_toggle=2):
54
+ return _toggle(x, inst, rng, 0, n_toggle)
55
+
56
+
57
+ @move(improves=1, attempts=8)
58
+ def toggle_1(x, inst, rng, n_toggle=2):
59
+ return _toggle(x, inst, rng, 1, n_toggle)
60
+
61
+
62
+ rng = np.random.default_rng(0)
63
+ n = 20
64
+ inst = {"obj": rng.random((2, n)), "weight": rng.random(n), "cap": 0.3 * n}
65
+ moves = [toggle_0(n_toggle=2), toggle_1(n_toggle=2)]
66
+ search = MDLS(evaluate, moves=moves, instance=inst)
67
+
68
+ res = search.run([np.zeros(n, dtype=bool)], max_eval=500, seed=0)
69
+ print(res.summary())
70
+ print(res.objectives)
71
+ ```
72
+
73
+ ## What the library provides
74
+
75
+ - **Archive** of feasible, non-dominated solutions with a relative tolerance,
76
+ duplicate rejection and dominated-point eviction.
77
+ - **Move contract**: a move is one improving attempt, optionally retried up to
78
+ `attempts` times by the `@move` decorator.
79
+ - **Balanced search**: every iteration applies one sampled move per objective
80
+ from the same archived base solution.
81
+ - **Budgets**: stop on iterations (`max_iter`), evaluations (`max_eval`,
82
+ including initial solutions) or wall-clock time (`time_limit`).
83
+ - **Statistics**: per-move counts of calls, no-moves, non-descents, infeasible
84
+ results, dominated results, accepted results and time.
85
+ - **Extension points**: `is_feasible`, `selection`, `copy_solution`, and the
86
+ `select`, `offer`, `copy` and `on_iteration` hooks.
87
+
88
+ Moves are ordinary functions and stay readable; Numba is optional but is used
89
+ inside the library's dominance scan and can be used inside your own moves.
90
+
91
+ ## Examples
92
+
93
+ The repository includes a bi-objective 0/1 knapsack:
94
+
95
+ ```bash
96
+ python examples/knapsack.py run # one search, writes front.csv
97
+ python examples/knapsack.py plot # fronts for up to four seeds, writes front.png
98
+ ```
99
+
100
+ The full tutorial, including the move contract and every option, is in
101
+ [`user_guide.md`](https://github.com/nsushant/ORBITAL/blob/MDLS/user_guide.md).
102
+
103
+ ## Testing
104
+
105
+ ```bash
106
+ pip install -e ".[dev,examples]"
107
+ pytest
108
+ ```
109
+
110
+ ## License
111
+
112
+ MIT. See [LICENSE](LICENSE).
113
+
114
+ ## Citation
115
+
116
+ If you use this package in academic work, cite the MDLS paper and optionally
117
+ the software itself (see `CITATION.cff`):
118
+
119
+ > Tricoire, F., Graf, A., & Hartl, R. F. (2012). The bi-objective stochastic
120
+ > covering tour problem. *Computers & Operations Research*, 39(7), 1582–1592.
@@ -0,0 +1,195 @@
1
+ """Bi-objective 0/1 knapsack solved with mdls.MDLS; run `python examples/knapsack.py -h`."""
2
+
3
+ import argparse
4
+ import json
5
+ import logging
6
+ from pathlib import Path
7
+
8
+ import numpy as np
9
+ from numba import njit
10
+
11
+ from mdls import MDLS, move
12
+
13
+ COLOURS = ["#1f77b4", "#d62728", "#2ca02c", "#9467bd"] # one per compared run; grey is reserved
14
+ GREY = "#9e9e9e"
15
+
16
+
17
+ def make_instance(n_items, cap_frac, seed):
18
+ """Random weights and two profit rows; capacity is a fraction of total weight."""
19
+ rng = np.random.default_rng(seed)
20
+ w = rng.integers(10, 100, n_items).astype(float)
21
+ p = rng.integers(10, 100, (2, n_items)).astype(float)
22
+ return dict(w=w, p=p, cap=cap_frac * w.sum())
23
+
24
+
25
+ def evaluate(x, inst):
26
+ """f = negated profits (MDLS minimises), g = [load - capacity] (feasible when <= 0)."""
27
+ return -(inst["p"] @ x), [inst["w"] @ x - inst["cap"]]
28
+
29
+
30
+ @njit
31
+ def greedy_fill(x, w, cap, order):
32
+ """Add the items of x in `order` while they fit; x is modified in place."""
33
+ load = 0.0
34
+ for i in range(x.size):
35
+ if x[i]:
36
+ load += w[i]
37
+ for t in range(order.size):
38
+ i = order[t]
39
+ if not x[i] and load + w[i] <= cap:
40
+ x[i] = True
41
+ load += w[i]
42
+ return x
43
+
44
+
45
+ def _drop_add(x, inst, rng, k, n_drop, noise):
46
+ """One attempt: drop the weakest held items on profit k, refill greedily.
47
+
48
+ Returns a copy that strictly improves profit k, or None; x is left untouched
49
+ so the decorator can retry from the same base.
50
+ """
51
+ p, w = inst["p"][k], inst["w"]
52
+ held = np.flatnonzero(x)
53
+ if held.size == 0:
54
+ return None
55
+ y = x.copy()
56
+ ratio = p[held] / w[held]
57
+ noisy = ratio * rng.uniform(1 - noise, 1 + noise, held.size) # avoids stalling
58
+ y[held[np.argsort(noisy)[:n_drop]]] = False
59
+ greedy_fill(y, w, inst["cap"], np.argsort(-p / w))
60
+ return y if p @ y > p @ x else None
61
+
62
+
63
+ @move(improves=0, attempts=4)
64
+ def drop_add_0(x, inst, rng, n_drop=3, noise=0.2):
65
+ """Move for profit 0: drop the weakest held items, refill greedily.
66
+
67
+ The drop is noisy, so the decorator retries up to four times.
68
+ """
69
+ return _drop_add(x, inst, rng, 0, n_drop, noise)
70
+
71
+
72
+ @move(improves=1, attempts=4)
73
+ def drop_add_1(x, inst, rng, n_drop=3, noise=0.2):
74
+ """Move for profit 1: drop the weakest held items, refill greedily.
75
+
76
+ The drop is noisy, so the decorator retries up to four times.
77
+ """
78
+ return _drop_add(x, inst, rng, 1, n_drop, noise)
79
+
80
+
81
+ @njit
82
+ def find_swap(w, p, slack, i_draw, j_draw):
83
+ """First drawn pair that raises profit 0 and still fits; returns (found, i, j)."""
84
+ for t in range(i_draw.size):
85
+ i, j = i_draw[t], j_draw[t]
86
+ if p[j] > p[i] and w[j] - w[i] <= slack:
87
+ return True, i, j
88
+ return False, 0, 0
89
+
90
+
91
+ @move(improves=0, attempts=1)
92
+ def swap(x, inst, rng, tries=50):
93
+ """Move for profit 0: first-improvement 1-1 swap over randomly drawn pairs.
94
+
95
+ `tries` is the batch size inside the numba kernel, so no decorator retries.
96
+ """
97
+ w, p = inst["w"], inst["p"][0]
98
+ held, free = np.flatnonzero(x), np.flatnonzero(~x)
99
+ if held.size == 0 or free.size == 0:
100
+ return None
101
+ slack = inst["cap"] - w @ x
102
+ i_draw = held[rng.integers(0, held.size, tries)]
103
+ j_draw = free[rng.integers(0, free.size, tries)]
104
+ found, i, j = find_swap(w, p, slack, i_draw, j_draw)
105
+ if not found:
106
+ return None
107
+ x[i], x[j] = False, True
108
+ return x
109
+
110
+
111
+ def build(cfg):
112
+ """Instance, MDLS object and init function from a config dict."""
113
+ inst = make_instance(cfg["n_items"], cfg["capacity_fraction"], cfg["instance_seed"])
114
+ moves = [drop_add_0(n_drop=cfg["n_drop"], noise=cfg["noise"]),
115
+ drop_add_1(n_drop=cfg["n_drop"], noise=cfg["noise"])]
116
+ if cfg["use_swap"]:
117
+ moves.append(swap(tries=cfg["swap_tries"]))
118
+ n = len(inst["w"])
119
+
120
+ def init(rng):
121
+ empty = np.zeros(n, dtype=bool)
122
+ starts = [greedy_fill(empty.copy(), inst["w"], inst["cap"],
123
+ np.argsort(-inst["p"][k] / inst["w"])) for k in range(2)]
124
+ return starts + [greedy_fill(empty.copy(), inst["w"], inst["cap"], rng.permutation(n))
125
+ for _ in range(cfg["n_random_starts"])]
126
+
127
+ search = MDLS(evaluate, moves=moves, instance=inst, verbose=cfg["verbose"])
128
+ return inst, search, init
129
+
130
+
131
+ def cmd_run(cfg):
132
+ """One search; prints the summary and writes the front as CSV."""
133
+ _, search, init = build(cfg)
134
+ res = search.run(init, max_eval=cfg["max_eval"], seed=cfg["seeds"][0])
135
+ print(res.summary())
136
+ f = -res.objectives
137
+ f = f[np.argsort(f[:, 0])]
138
+ np.savetxt(cfg["front_csv"], f, delimiter=",", header="profit_0,profit_1", comments="", fmt="%.0f")
139
+ k = res.archive.knee()
140
+ print(f"knee: {-res.objectives[k, 0]:.0f}, {-res.objectives[k, 1]:.0f}; front written to {cfg['front_csv']}")
141
+
142
+
143
+ def cmd_plot(cfg):
144
+ """Fronts and archive growth for up to 4 seeds, one colour each."""
145
+ import matplotlib
146
+ matplotlib.use("Agg")
147
+ import matplotlib.pyplot as plt
148
+ seeds = cfg["seeds"][:4]
149
+ _, search, init = build(cfg)
150
+ fig, (a1, a2) = plt.subplots(1, 2, figsize=(10, 4))
151
+ for c, s in zip(COLOURS, seeds):
152
+ res = search.run(init, max_eval=cfg["max_eval"], seed=s, record_every=cfg["record_every"])
153
+ f = -res.objectives
154
+ f = f[np.argsort(f[:, 0])]
155
+ a1.plot(f[:, 0], f[:, 1], color=GREY, lw=0.8, zorder=1)
156
+ a1.scatter(f[:, 0], f[:, 1], color=c, s=14, zorder=2, label=f"seed {s}")
157
+ if res.history:
158
+ h = np.array(res.history)
159
+ a2.plot(h[:, 1], h[:, 2], color=GREY, lw=0.8, zorder=1)
160
+ a2.scatter(h[:, 1], h[:, 2], color=c, s=10, zorder=2, label=f"seed {s}")
161
+ a1.set_xlabel("Profit 1"); a1.set_ylabel("Profit 2")
162
+ a2.set_xlabel("Evaluations"); a2.set_ylabel("Front size")
163
+ a1.legend(); a2.legend()
164
+ fig.tight_layout()
165
+ fig.savefig(cfg["plot_png"], dpi=150)
166
+ print(f"plot written to {cfg['plot_png']}")
167
+
168
+
169
+ def main(argv=None):
170
+ """Parse the command line, load the config, apply overrides, dispatch."""
171
+ ap = argparse.ArgumentParser(description="MDLS on a bi-objective knapsack")
172
+ ap.add_argument("command", choices=["run", "plot"])
173
+ ap.add_argument("--config", default=None,
174
+ help="parameter file (JSON); defaults to params.json next to this script")
175
+ ap.add_argument("--set", nargs="*", default=[], metavar="KEY=VALUE",
176
+ help="override config entries, e.g. --set max_eval=2000 seeds=[1,2]")
177
+ args = ap.parse_args(argv)
178
+ config = Path(args.config) if args.config else Path(__file__).with_name("params.json")
179
+ with config.open() as fh:
180
+ cfg = json.load(fh)
181
+ for kv in args.set:
182
+ key, val = kv.split("=", 1)
183
+ if key not in cfg:
184
+ ap.error(f"unknown config key {key!r}")
185
+ try:
186
+ cfg[key] = json.loads(val)
187
+ except json.JSONDecodeError:
188
+ cfg[key] = val # not a JSON literal: keep as string
189
+ logging.basicConfig(level=logging.INFO if cfg["verbose"] else logging.WARNING,
190
+ format="%(message)s")
191
+ {"run": cmd_run, "plot": cmd_plot}[args.command](cfg)
192
+
193
+
194
+ if __name__ == "__main__":
195
+ main()
@@ -0,0 +1,16 @@
1
+ {
2
+ "n_items": 100,
3
+ "capacity_fraction": 0.5,
4
+ "instance_seed": 1,
5
+ "n_random_starts": 5,
6
+ "n_drop": 3,
7
+ "noise": 0.2,
8
+ "use_swap": true,
9
+ "swap_tries": 50,
10
+ "max_eval": 6000,
11
+ "seeds": [0, 1, 2, 3],
12
+ "record_every": 50,
13
+ "verbose": 0,
14
+ "front_csv": "front.csv",
15
+ "plot_png": "front.png"
16
+ }
@@ -0,0 +1,76 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.25"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "pymdls"
7
+ dynamic = ["version"]
8
+ description = "Multi-directional local search with user-supplied moves"
9
+ readme = "README.md"
10
+ license = { text = "MIT" }
11
+ license-files = ["LICENSE"]
12
+ requires-python = ">=3.10"
13
+ authors = [{ name = "Sushanta Nigudkar" }]
14
+ keywords = [
15
+ "multi-objective optimisation",
16
+ "multi-objective optimization",
17
+ "local search",
18
+ "pareto",
19
+ "metaheuristics",
20
+ "mdls",
21
+ ]
22
+ classifiers = [
23
+ "Development Status :: 4 - Beta",
24
+ "Intended Audience :: Science/Research",
25
+ "License :: OSI Approved :: MIT License",
26
+ "Operating System :: OS Independent",
27
+ "Programming Language :: Python :: 3",
28
+ "Programming Language :: Python :: 3 :: Only",
29
+ "Programming Language :: Python :: 3.10",
30
+ "Programming Language :: Python :: 3.11",
31
+ "Programming Language :: Python :: 3.12",
32
+ "Topic :: Scientific/Engineering :: Mathematics",
33
+ ]
34
+ dependencies = [
35
+ "numpy>=1.23",
36
+ "numba>=0.57",
37
+ ]
38
+
39
+ [project.optional-dependencies]
40
+ examples = ["matplotlib>=3.6"]
41
+ dev = [
42
+ "build>=1.0",
43
+ "pytest>=7.0",
44
+ "twine>=5.0",
45
+ ]
46
+
47
+ [project.urls]
48
+ Homepage = "https://github.com/nsushant/ORBITAL/tree/MDLS"
49
+ Repository = "https://github.com/nsushant/ORBITAL/tree/MDLS"
50
+ Documentation = "https://github.com/nsushant/ORBITAL/blob/MDLS/user_guide.md"
51
+ Issues = "https://github.com/nsushant/ORBITAL/issues"
52
+ Changelog = "https://github.com/nsushant/ORBITAL/blob/MDLS/CHANGELOG.md"
53
+
54
+ [tool.hatch.version]
55
+ path = "src/mdls/__init__.py"
56
+
57
+ [tool.hatch.build.targets.wheel]
58
+ packages = ["src/mdls"]
59
+
60
+ [tool.hatch.build.targets.sdist]
61
+ include = [
62
+ "/src/mdls",
63
+ "/tests",
64
+ "/examples",
65
+ "/README.md",
66
+ "/LICENSE",
67
+ "/CHANGELOG.md",
68
+ "/CITATION.cff",
69
+ "/user_guide.md",
70
+ "/pyproject.toml",
71
+ ]
72
+
73
+ [tool.pytest.ini_options]
74
+ testpaths = ["tests"]
75
+ addopts = "-ra"
76
+ pythonpath = ["src"]