muoblpanalyzers 0.0.1__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.
- muoblpanalyzers-0.0.1/PKG-INFO +91 -0
- muoblpanalyzers-0.0.1/README.md +69 -0
- muoblpanalyzers-0.0.1/pyproject.toml +64 -0
- muoblpanalyzers-0.0.1/src/muoblpanalyzers/__init__.py +39 -0
- muoblpanalyzers-0.0.1/src/muoblpanalyzers/metric.py +15 -0
- muoblpanalyzers-0.0.1/src/muoblpanalyzers/metrics.py +124 -0
- muoblpanalyzers-0.0.1/src/muoblpanalyzers/native_metrics.py +50 -0
- muoblpanalyzers-0.0.1/src/muoblpanalyzers/py.typed +0 -0
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: muoblpanalyzers
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Analysis metrics for solved multi objective linear programs
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Author: Piotr Skowron
|
|
7
|
+
Author-email: p.skowron@mimuw.edu.pl
|
|
8
|
+
Requires-Python: >=3.13
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.15
|
|
13
|
+
Provides-Extra: native
|
|
14
|
+
Requires-Dist: muoblp (>=1.0.5,<2.0)
|
|
15
|
+
Requires-Dist: muoblpbindings (>=0.0.20,<0.1) ; extra == "native"
|
|
16
|
+
Requires-Dist: muoblpsolvers (>=0.0.15,<0.1)
|
|
17
|
+
Requires-Dist: pulp (==3.3.2)
|
|
18
|
+
Project-URL: Documentation, https://jasieksz.github.io/multiobjective-lp
|
|
19
|
+
Project-URL: Repository, https://github.com/algorithmic-econ/multiobjective-lp
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
|
|
22
|
+
# Analyzers
|
|
23
|
+
Metrics for analysing solved multi objective linear programs
|
|
24
|
+
(`muoblp.MultiObjectiveLpProblem` with variable values set, e.g. by a solver
|
|
25
|
+
from `muoblpsolvers`).
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## Installation
|
|
30
|
+
```shell
|
|
31
|
+
pip install muoblpanalyzers # python metrics
|
|
32
|
+
pip install "muoblpanalyzers[native]" # + C++ native metrics (muoblpbindings)
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Usage
|
|
36
|
+
```python
|
|
37
|
+
from muoblpanalyzers import Metric, get_metrics, ejr_plus
|
|
38
|
+
|
|
39
|
+
problem.solve(solver) # any solved MultiObjectiveLpProblem
|
|
40
|
+
|
|
41
|
+
get_metrics([Metric.EXCLUSION_RATIO, Metric.EJR_PLUS_NATIVE], problem)
|
|
42
|
+
# {"EXCLUSION_RATIO": {"exclusion_ratio": 0.25}, "EJR_PLUS_NATIVE": {"ejr_plus": 0}}
|
|
43
|
+
|
|
44
|
+
ejr_plus(problem) # single metric -> {"ejr_plus": 0}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Metrics
|
|
48
|
+
`Metric` values are stable keys (used in persisted results), never renamed.
|
|
49
|
+
|
|
50
|
+
| `Metric` | Function | Result | Notes |
|
|
51
|
+
|---|---|---|---|
|
|
52
|
+
| `EXCLUSION_RATIO` | `exclusion_ratio` | `{"exclusion_ratio": float}` | share of objectives (voters) with value 0 |
|
|
53
|
+
| `SUM_OBJECTIVES` | `sum_objectives` | `{"sum": float}` | sum of objective values |
|
|
54
|
+
| `EJR_PLUS` | `ejr_plus` | `{"ejr_plus": int}` | unelected projects violating EJR+ (up to one) |
|
|
55
|
+
| `CONSTRAINTS` | `invalid_constraints` | `{"pb_constraint": bool, "invalid_count": int}` | needs PB constraint |
|
|
56
|
+
| `INSTANCE_SIZE` | `instance_size` | `{"size": int}` | number of variables |
|
|
57
|
+
| `TOTAL_COST` | `total_cost` | `{"total_cost": float}` | needs PB constraint |
|
|
58
|
+
| `EXCLUSION_RATIO_NATIVE` | native | same as `EXCLUSION_RATIO` | needs `[native]` |
|
|
59
|
+
| `EJR_PLUS_NATIVE` | native | same as `EJR_PLUS` | needs `[native]` |
|
|
60
|
+
|
|
61
|
+
PB constraint = single `<=` constraint covering all candidates
|
|
62
|
+
(`muoblpsolvers.election_solver.validate_pb_constraint`); missing or ambiguous
|
|
63
|
+
raises.
|
|
64
|
+
|
|
65
|
+
## Native metrics
|
|
66
|
+
C++ implementations from `muoblpbindings.metrics`, each a twin of a python
|
|
67
|
+
metric (`PYTHON_COUNTERPART`) with identical results (parity-tested, exact).
|
|
68
|
+
* Imports are lazy: python metrics work without bindings; a native metric
|
|
69
|
+
without bindings raises `ImportError` (no silent python fallback).
|
|
70
|
+
* `get_metrics` builds one `SolvedInstance` handle per problem, shared by all
|
|
71
|
+
requested native metrics; a snapshot of variable values at build time.
|
|
72
|
+
|
|
73
|
+
## Adding a metric
|
|
74
|
+
* Python: function `(MultiObjectiveLpProblem) -> dict` in `metrics.py`,
|
|
75
|
+
`Metric` member in `metric.py`, entry in `PYTHON_STRATEGIES`.
|
|
76
|
+
* Native: follow "Adding new metric" in the
|
|
77
|
+
[bindings README](../bindings/README.md), then register strategy in
|
|
78
|
+
`NATIVE_STRATEGIES` and its python oracle in `PYTHON_COUNTERPART`
|
|
79
|
+
(`native_metrics.py`). Parity tests pick it up from `PYTHON_COUNTERPART`.
|
|
80
|
+
* `experiments` consumers: add matching `AnalyzerResult` field
|
|
81
|
+
(`experiments/src/helpers/analyzers/model.py`).
|
|
82
|
+
|
|
83
|
+
## Development
|
|
84
|
+
```shell
|
|
85
|
+
$ poetry install --all-extras
|
|
86
|
+
$ poetry run pytest
|
|
87
|
+
$ poetry run pyright
|
|
88
|
+
```
|
|
89
|
+
C++ edits need a reinstall of the bindings into this venv:
|
|
90
|
+
`poetry run pip install --force-reinstall --no-deps ../bindings`.
|
|
91
|
+
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# Analyzers
|
|
2
|
+
Metrics for analysing solved multi objective linear programs
|
|
3
|
+
(`muoblp.MultiObjectiveLpProblem` with variable values set, e.g. by a solver
|
|
4
|
+
from `muoblpsolvers`).
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Installation
|
|
9
|
+
```shell
|
|
10
|
+
pip install muoblpanalyzers # python metrics
|
|
11
|
+
pip install "muoblpanalyzers[native]" # + C++ native metrics (muoblpbindings)
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Usage
|
|
15
|
+
```python
|
|
16
|
+
from muoblpanalyzers import Metric, get_metrics, ejr_plus
|
|
17
|
+
|
|
18
|
+
problem.solve(solver) # any solved MultiObjectiveLpProblem
|
|
19
|
+
|
|
20
|
+
get_metrics([Metric.EXCLUSION_RATIO, Metric.EJR_PLUS_NATIVE], problem)
|
|
21
|
+
# {"EXCLUSION_RATIO": {"exclusion_ratio": 0.25}, "EJR_PLUS_NATIVE": {"ejr_plus": 0}}
|
|
22
|
+
|
|
23
|
+
ejr_plus(problem) # single metric -> {"ejr_plus": 0}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Metrics
|
|
27
|
+
`Metric` values are stable keys (used in persisted results), never renamed.
|
|
28
|
+
|
|
29
|
+
| `Metric` | Function | Result | Notes |
|
|
30
|
+
|---|---|---|---|
|
|
31
|
+
| `EXCLUSION_RATIO` | `exclusion_ratio` | `{"exclusion_ratio": float}` | share of objectives (voters) with value 0 |
|
|
32
|
+
| `SUM_OBJECTIVES` | `sum_objectives` | `{"sum": float}` | sum of objective values |
|
|
33
|
+
| `EJR_PLUS` | `ejr_plus` | `{"ejr_plus": int}` | unelected projects violating EJR+ (up to one) |
|
|
34
|
+
| `CONSTRAINTS` | `invalid_constraints` | `{"pb_constraint": bool, "invalid_count": int}` | needs PB constraint |
|
|
35
|
+
| `INSTANCE_SIZE` | `instance_size` | `{"size": int}` | number of variables |
|
|
36
|
+
| `TOTAL_COST` | `total_cost` | `{"total_cost": float}` | needs PB constraint |
|
|
37
|
+
| `EXCLUSION_RATIO_NATIVE` | native | same as `EXCLUSION_RATIO` | needs `[native]` |
|
|
38
|
+
| `EJR_PLUS_NATIVE` | native | same as `EJR_PLUS` | needs `[native]` |
|
|
39
|
+
|
|
40
|
+
PB constraint = single `<=` constraint covering all candidates
|
|
41
|
+
(`muoblpsolvers.election_solver.validate_pb_constraint`); missing or ambiguous
|
|
42
|
+
raises.
|
|
43
|
+
|
|
44
|
+
## Native metrics
|
|
45
|
+
C++ implementations from `muoblpbindings.metrics`, each a twin of a python
|
|
46
|
+
metric (`PYTHON_COUNTERPART`) with identical results (parity-tested, exact).
|
|
47
|
+
* Imports are lazy: python metrics work without bindings; a native metric
|
|
48
|
+
without bindings raises `ImportError` (no silent python fallback).
|
|
49
|
+
* `get_metrics` builds one `SolvedInstance` handle per problem, shared by all
|
|
50
|
+
requested native metrics; a snapshot of variable values at build time.
|
|
51
|
+
|
|
52
|
+
## Adding a metric
|
|
53
|
+
* Python: function `(MultiObjectiveLpProblem) -> dict` in `metrics.py`,
|
|
54
|
+
`Metric` member in `metric.py`, entry in `PYTHON_STRATEGIES`.
|
|
55
|
+
* Native: follow "Adding new metric" in the
|
|
56
|
+
[bindings README](../bindings/README.md), then register strategy in
|
|
57
|
+
`NATIVE_STRATEGIES` and its python oracle in `PYTHON_COUNTERPART`
|
|
58
|
+
(`native_metrics.py`). Parity tests pick it up from `PYTHON_COUNTERPART`.
|
|
59
|
+
* `experiments` consumers: add matching `AnalyzerResult` field
|
|
60
|
+
(`experiments/src/helpers/analyzers/model.py`).
|
|
61
|
+
|
|
62
|
+
## Development
|
|
63
|
+
```shell
|
|
64
|
+
$ poetry install --all-extras
|
|
65
|
+
$ poetry run pytest
|
|
66
|
+
$ poetry run pyright
|
|
67
|
+
```
|
|
68
|
+
C++ edits need a reinstall of the bindings into this venv:
|
|
69
|
+
`poetry run pip install --force-reinstall --no-deps ../bindings`.
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
version = "0.0.1"
|
|
3
|
+
name = "muoblpanalyzers"
|
|
4
|
+
description = "Analysis metrics for solved multi objective linear programs"
|
|
5
|
+
|
|
6
|
+
authors = [
|
|
7
|
+
{ name = "Piotr Skowron", email = "p.skowron@mimuw.edu.pl" },
|
|
8
|
+
{ name = "Jan Sznajd", email = "j.sznajd@uw.edu.pl" }
|
|
9
|
+
]
|
|
10
|
+
license = "MIT"
|
|
11
|
+
readme = "README.md"
|
|
12
|
+
|
|
13
|
+
requires-python = ">=3.13"
|
|
14
|
+
|
|
15
|
+
dependencies = [
|
|
16
|
+
"muoblp>=1.0.5,<2.0",
|
|
17
|
+
"muoblpsolvers>=0.0.15,<0.1",
|
|
18
|
+
"pulp==3.3.2",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
[project.optional-dependencies]
|
|
22
|
+
# native (C++) metrics: muoblpbindings.metrics, imported lazily
|
|
23
|
+
native = ["muoblpbindings>=0.0.20,<0.1"]
|
|
24
|
+
|
|
25
|
+
[project.urls]
|
|
26
|
+
repository = "https://github.com/algorithmic-econ/multiobjective-lp"
|
|
27
|
+
documentation = "https://jasieksz.github.io/multiobjective-lp"
|
|
28
|
+
|
|
29
|
+
[tool.poetry]
|
|
30
|
+
packages = [{include = "muoblpanalyzers", from = "src"}]
|
|
31
|
+
|
|
32
|
+
# Single-style policy: static [project.dependencies] holds the
|
|
33
|
+
# published version pins; this table ONLY enriches them with local path
|
|
34
|
+
# sources for dev installs. Poetry 2.x uses [project] for wheel METADATA
|
|
35
|
+
# and this table for install/lock -- do not move deps back here alone or
|
|
36
|
+
# wheels regain broken file:// refs.
|
|
37
|
+
[tool.poetry.dependencies]
|
|
38
|
+
muoblp = {path = "../core", develop = true}
|
|
39
|
+
muoblpsolvers = {path = "../solvers", develop = true}
|
|
40
|
+
# explicit path wiring: solvers' path enrichment for muoblpbindings
|
|
41
|
+
# doesn't propagate transitively on relock
|
|
42
|
+
muoblpbindings = {path = "../bindings", develop = false, optional = true}
|
|
43
|
+
|
|
44
|
+
[build-system]
|
|
45
|
+
requires = ["poetry-core>=2.0.0,<3.0.0"]
|
|
46
|
+
build-backend = "poetry.core.masonry.api"
|
|
47
|
+
|
|
48
|
+
[tool.ruff]
|
|
49
|
+
line-length = 79
|
|
50
|
+
preview = true
|
|
51
|
+
|
|
52
|
+
[tool.pytest.ini_options]
|
|
53
|
+
minversion = "8.0"
|
|
54
|
+
addopts = "-ra -q --import-mode=importlib"
|
|
55
|
+
testpaths = [
|
|
56
|
+
"tests",
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
[dependency-groups]
|
|
60
|
+
dev = [
|
|
61
|
+
"pytest (>=8.4,<9.0)",
|
|
62
|
+
"ruff (==0.15.1)", # = CI (ruff.yml); preview rules drift across patches
|
|
63
|
+
"pyright (>=1.1,<2.0)"
|
|
64
|
+
]
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
from muoblpanalyzers.metric import Metric
|
|
4
|
+
from muoblpanalyzers.metrics import (
|
|
5
|
+
PYTHON_STRATEGIES,
|
|
6
|
+
ejr_plus,
|
|
7
|
+
exclusion_ratio,
|
|
8
|
+
get_metric_strategy,
|
|
9
|
+
get_metrics,
|
|
10
|
+
instance_size,
|
|
11
|
+
invalid_constraints,
|
|
12
|
+
sum_objectives,
|
|
13
|
+
total_cost,
|
|
14
|
+
)
|
|
15
|
+
from muoblpanalyzers.native_metrics import (
|
|
16
|
+
NATIVE_STRATEGIES,
|
|
17
|
+
PYTHON_COUNTERPART,
|
|
18
|
+
build_solved_instance,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"NATIVE_STRATEGIES",
|
|
23
|
+
"PYTHON_COUNTERPART",
|
|
24
|
+
"PYTHON_STRATEGIES",
|
|
25
|
+
"Metric",
|
|
26
|
+
"build_solved_instance",
|
|
27
|
+
"ejr_plus",
|
|
28
|
+
"exclusion_ratio",
|
|
29
|
+
"get_metric_strategy",
|
|
30
|
+
"get_metrics",
|
|
31
|
+
"instance_size",
|
|
32
|
+
"invalid_constraints",
|
|
33
|
+
"sum_objectives",
|
|
34
|
+
"total_cost",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
logger = logging.getLogger(__name__)
|
|
38
|
+
|
|
39
|
+
logger.addHandler(logging.NullHandler())
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from enum import StrEnum
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Metric(StrEnum):
|
|
5
|
+
# values are on-disk keys (metrics.json, goldens): never rename
|
|
6
|
+
EXCLUSION_RATIO = "EXCLUSION_RATIO"
|
|
7
|
+
SUM_OBJECTIVES = "SUM_OBJECTIVES"
|
|
8
|
+
EJR_PLUS = "EJR_PLUS"
|
|
9
|
+
CONSTRAINTS = "CONSTRAINTS"
|
|
10
|
+
INSTANCE_SIZE = "INSTANCE_SIZE"
|
|
11
|
+
TOTAL_COST = "TOTAL_COST"
|
|
12
|
+
# native (C++ bindings) twin of EXCLUSION_RATIO
|
|
13
|
+
EXCLUSION_RATIO_NATIVE = "EXCLUSION_RATIO_NATIVE"
|
|
14
|
+
# native (C++ bindings) twin of EJR_PLUS
|
|
15
|
+
EJR_PLUS_NATIVE = "EJR_PLUS_NATIVE"
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
from collections.abc import Callable
|
|
2
|
+
from typing import cast
|
|
3
|
+
|
|
4
|
+
from muoblp.model.multi_objective_lp import MultiObjectiveLpProblem
|
|
5
|
+
from muoblpsolvers.election_solver import validate_pb_constraint
|
|
6
|
+
|
|
7
|
+
from muoblpanalyzers import native_metrics
|
|
8
|
+
from muoblpanalyzers.metric import Metric
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def get_metrics(
|
|
12
|
+
metrics: list[Metric], problem: MultiObjectiveLpProblem
|
|
13
|
+
) -> dict[str, dict]:
|
|
14
|
+
# native handle built once per problem, only if a native metric is asked
|
|
15
|
+
instance = (
|
|
16
|
+
native_metrics.build_solved_instance(problem)
|
|
17
|
+
if any(m in native_metrics.NATIVE_STRATEGIES for m in metrics)
|
|
18
|
+
else None
|
|
19
|
+
)
|
|
20
|
+
results: dict[str, dict] = {}
|
|
21
|
+
for metric in metrics:
|
|
22
|
+
if instance is not None and metric in native_metrics.NATIVE_STRATEGIES:
|
|
23
|
+
results[metric] = native_metrics.NATIVE_STRATEGIES[metric](
|
|
24
|
+
instance
|
|
25
|
+
)
|
|
26
|
+
else:
|
|
27
|
+
results[metric] = get_metric_strategy(metric)(problem)
|
|
28
|
+
return results
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def get_metric_strategy(
|
|
32
|
+
metric: Metric,
|
|
33
|
+
) -> Callable[[MultiObjectiveLpProblem], dict]:
|
|
34
|
+
# native metrics have no python strategy (see NATIVE_STRATEGIES)
|
|
35
|
+
try:
|
|
36
|
+
return PYTHON_STRATEGIES[metric]
|
|
37
|
+
except KeyError:
|
|
38
|
+
raise NotImplementedError("Metric not implemented") from None
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def exclusion_ratio(problem: MultiObjectiveLpProblem) -> dict:
|
|
42
|
+
zero_count = len([1 for obj in problem.objectives if obj.value() == 0])
|
|
43
|
+
return {
|
|
44
|
+
"exclusion_ratio": zero_count / len(problem.objectives),
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def sum_objectives(problem: MultiObjectiveLpProblem) -> dict:
|
|
49
|
+
# expects a solved problem: every variable has a value, so value() is
|
|
50
|
+
# never None here
|
|
51
|
+
return {
|
|
52
|
+
"sum": sum(cast(float, obj.value()) for obj in problem.objectives),
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def total_cost(problem: MultiObjectiveLpProblem) -> dict:
|
|
57
|
+
pb_constraint = validate_pb_constraint(problem)
|
|
58
|
+
costs = {candidate.name: cost for candidate, cost in pb_constraint.items()}
|
|
59
|
+
selected = [v for v in problem.variables() if v.value() == 1.0]
|
|
60
|
+
return {
|
|
61
|
+
"total_cost": sum(costs[c.name] for c in selected),
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def invalid_constraints(problem: MultiObjectiveLpProblem) -> dict:
|
|
66
|
+
total_budget_constraint = validate_pb_constraint(problem)
|
|
67
|
+
return {
|
|
68
|
+
"pb_constraint": total_budget_constraint.valid(),
|
|
69
|
+
"invalid_count": sum(
|
|
70
|
+
1 if not c.valid() else 0 for c in problem.constraints.values()
|
|
71
|
+
),
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def instance_size(problem: MultiObjectiveLpProblem) -> dict:
|
|
76
|
+
return {"size": len(problem.variables())}
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def ejr_plus(problem: MultiObjectiveLpProblem) -> dict:
|
|
80
|
+
# TODO: Check if utility was COST based
|
|
81
|
+
# TODO: Introduce metric options, i.e., pass up_to_one as config parameter
|
|
82
|
+
up_to_one = True
|
|
83
|
+
|
|
84
|
+
costs: dict[str, int] = {
|
|
85
|
+
candidate.name: cost
|
|
86
|
+
for constraint in problem.constraints.values()
|
|
87
|
+
for candidate, cost in constraint.items()
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
total_budget = abs(validate_pb_constraint(problem).constant)
|
|
91
|
+
|
|
92
|
+
voter_satisfaction = sorted(
|
|
93
|
+
[(voter, cast(float, voter.value())) for voter in problem.objectives],
|
|
94
|
+
key=lambda v_sat: v_sat[1],
|
|
95
|
+
)
|
|
96
|
+
voter_count = len(problem.objectives)
|
|
97
|
+
failures = []
|
|
98
|
+
|
|
99
|
+
for project in problem.variables():
|
|
100
|
+
if project.value() == 1: # skip elected
|
|
101
|
+
continue
|
|
102
|
+
coalition_size = 0
|
|
103
|
+
for voter, satisfaction in voter_satisfaction:
|
|
104
|
+
# if voter approved of project
|
|
105
|
+
if project in voter:
|
|
106
|
+
coalition_size += 1
|
|
107
|
+
ejr_satisfied = satisfaction >= (
|
|
108
|
+
coalition_size / voter_count
|
|
109
|
+
) * total_budget - (costs[project.name] if up_to_one else 0)
|
|
110
|
+
if not ejr_satisfied:
|
|
111
|
+
failures.append(project.name)
|
|
112
|
+
break
|
|
113
|
+
|
|
114
|
+
return {"ejr_plus": len(failures)}
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
PYTHON_STRATEGIES: dict[Metric, Callable[[MultiObjectiveLpProblem], dict]] = {
|
|
118
|
+
Metric.EXCLUSION_RATIO: exclusion_ratio,
|
|
119
|
+
Metric.SUM_OBJECTIVES: sum_objectives,
|
|
120
|
+
Metric.EJR_PLUS: ejr_plus,
|
|
121
|
+
Metric.CONSTRAINTS: invalid_constraints,
|
|
122
|
+
Metric.INSTANCE_SIZE: instance_size,
|
|
123
|
+
Metric.TOTAL_COST: total_cost,
|
|
124
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Native metrics: C++ implementations from `muoblpbindings.metrics`.
|
|
2
|
+
|
|
3
|
+
Each has its own Metric name and a python counterpart (oracle) for parity
|
|
4
|
+
tests. Imports are lazy: python-only analysis works without the metrics
|
|
5
|
+
module, and a native request fails loudly with ImportError.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from collections.abc import Callable
|
|
9
|
+
from typing import TYPE_CHECKING
|
|
10
|
+
|
|
11
|
+
from muoblp.model.multi_objective_lp import MultiObjectiveLpProblem
|
|
12
|
+
|
|
13
|
+
from muoblpanalyzers.metric import Metric
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from muoblpbindings.metrics import SolvedInstance
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def build_solved_instance(
|
|
20
|
+
problem: MultiObjectiveLpProblem,
|
|
21
|
+
) -> "SolvedInstance":
|
|
22
|
+
from muoblpbindings.metrics import SolvedInstance
|
|
23
|
+
|
|
24
|
+
return SolvedInstance(problem)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def exclusion_ratio_native(instance: "SolvedInstance") -> dict:
|
|
28
|
+
from muoblpbindings.metrics import exclusion_ratio
|
|
29
|
+
|
|
30
|
+
# same inner key as python metric -> values compare directly
|
|
31
|
+
return {"exclusion_ratio": exclusion_ratio(instance)}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def ejr_plus_native(instance: "SolvedInstance") -> dict:
|
|
35
|
+
from muoblpbindings.metrics import ejr_plus
|
|
36
|
+
|
|
37
|
+
# up_to_one=True, same as the python metric
|
|
38
|
+
return {"ejr_plus": ejr_plus(instance, up_to_one=True)}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
NATIVE_STRATEGIES: dict[Metric, Callable[["SolvedInstance"], dict]] = {
|
|
42
|
+
Metric.EXCLUSION_RATIO_NATIVE: exclusion_ratio_native,
|
|
43
|
+
Metric.EJR_PLUS_NATIVE: ejr_plus_native,
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
# parity tests iterate this map: every native metric must be registered here
|
|
47
|
+
PYTHON_COUNTERPART: dict[Metric, Metric] = {
|
|
48
|
+
Metric.EXCLUSION_RATIO_NATIVE: Metric.EXCLUSION_RATIO,
|
|
49
|
+
Metric.EJR_PLUS_NATIVE: Metric.EJR_PLUS,
|
|
50
|
+
}
|
|
File without changes
|