Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
ad2faa0
Move common test strategies out to top-level
daffidwilde Oct 24, 2024
23859e7
Write tests for HR init
daffidwilde Oct 24, 2024
7424a84
Move existing code around to avoid mass failure
daffidwilde Oct 25, 2024
fb03f9d
Write tests for alt HR instantiations
daffidwilde Oct 25, 2024
cfc0866
Implement instantiation
daffidwilde Oct 25, 2024
730a468
Point HR example at the old version in README
daffidwilde Nov 4, 2024
66a5ca4
Move to `uv` (#178)
daffidwilde Mar 20, 2025
f4530ca
Merge branch 'dev-2.0.0' into rank-hr
henrywilde-cs Mar 20, 2025
34748c0
Clean up README
henrywilde-cs Mar 20, 2025
4d79cf4
Close out example tests (temporarily)
henrywilde-cs Mar 20, 2025
7c0fbec
Make a stab at the HR algorithm; break matchings!
henrywilde-cs Mar 21, 2025
74b50ac
Write algorithm tests
daffidwilde May 15, 2025
2b0c493
Write tests for HR solver
daffidwilde May 15, 2025
8e9617f
Shuffle method order in game classes
daffidwilde May 15, 2025
f8a095c
Write preference matching converter test; clean up
daffidwilde May 15, 2025
17caee8
Ensure we catch warnings in existing tests
daffidwilde May 15, 2025
e1255b8
Write tests for input validator
daffidwilde May 15, 2025
492922d
Implement input validator
daffidwilde May 15, 2025
5470a71
Update HR tutorial
daffidwilde May 15, 2025
842c910
Clean up from preferences method
daffidwilde May 15, 2025
27b640d
Fix README examples
daffidwilde May 15, 2025
17d4a47
Update all tutorials and the docs dependencies
daffidwilde May 15, 2025
648cefd
Update how-tos and CI workflow
daffidwilde May 15, 2025
8de017d
Reach 100% coverage on HR components
daffidwilde May 15, 2025
04879b6
Write tests for the matching classes
daffidwilde May 15, 2025
8a31275
Get to 100% coverage on SM tests
daffidwilde May 15, 2025
ff6061e
Enable branch coverage
daffidwilde May 15, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 17 additions & 19 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,35 +16,33 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
python-version: ["3.10", "3.11", "3.12"]
python-version: ["3.10", "3.11", "3.12", "3.13"]

steps:
- name: Check out repository
uses: actions/checkout@v4

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
- name: Install uv and set the python version
uses: astral-sh/setup-uv@v5
with:
python-version: ${{ matrix.python-version }}
cache: "pip"

- name: Update pip and install dependencies
- name: Install the project
run: uv sync --all-extras

- name: Run linters (3.13-ubuntu only)
if: |
matrix.python-version == '3.13' &&
matrix.os == 'ubuntu-latest'
run: |
python -m pip install --upgrade pip
python -m pip install ".[docs,test]"
uv run ruff format --check .
uv run ruff check .

- name: Run tests
- name: Run documentation tests
run: |
python -m doctest README.md
python -m pytest docs --nbval --nbval-current-env -p no:randomly
python -m pytest tests \
--cov=matching --cov-fail-under=100 --hypothesis-profile=ci
uv run python -m doctest README.md
uv run pytest docs --nbval --nbval-current-env -p no:randomly

- name: Install and run linters (3.12-ubuntu only)
if: |
matrix.python-version == '3.12' &&
matrix.os == 'ubuntu-latest'
- name: Run unit tests
run: |
python -m pip install ".[lint]"
python -m ruff check .
python -m ruff format --check .
uv run pytest tests --cov --cov-fail-under=100 --hypothesis-profile=ci
24 changes: 15 additions & 9 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,23 @@ jobs:
contents: write
steps:
- name: Check out repository
uses: actions/checkout@v4
uses: actions/checkout@v3

- name: Set up Quarto
uses: quarto-dev/quarto-actions/setup@v2
- name: Install Python and dependencies
uses: actions/setup-python@v5
with:
python-version: "3.12"
cache: "pip"
- run: |
python -m pip install ".[dev]"
quartodoc build

- name: Install uv
uses: astral-sh/setup-uv@v5

- name: Set up Python
run: |
uv python install

- name: Install package and build docs
run: |
uv sync --all-extras
uv run quartodoc build

- name: Render and publish
uses: quarto-dev/quarto-actions/publish@v2
with:
Expand Down
26 changes: 13 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,22 +21,22 @@ In `matching`, we deal with four types of matching game:

## Installation

Matching requires Python 3.5 or above, and relies only on
[NumPy](http://www.numpy.org/) for general use.
Matching requires Python 3.10 or above, and relies only on the scientific stack ([NumPy](http://www.numpy.org/) and
[SciPy](https://scipy.org/)) for general use.

The library is most easily installed using `pip`:

```bash
$ python -m pip install matching
$ python -m pip install matching
```

However, if you would like to install it from source then go ahead and
clone the GitHub repository:

```bash
$ git clone https://github.com/daffidwilde/matching.git
$ cd matching
$ python -m pip install .
$ git clone https://github.com/daffidwilde/matching.git
$ cd matching
$ python -m pip install .
```

## Documentation
Expand Down Expand Up @@ -95,11 +95,11 @@ largely look and behave like one. It is in fact an instance of the

```python
>>> type(matching)
<class 'matching.matchings.single.SingleMatching'>
<class 'matching.matchings.SMMatching'>
>>> isinstance(matching, dict)
True
>>> matching
SingleMatching({'F': 'C', 'D': 'B', 'E': 'A'}, keys="reviewers", values="suitors")
SMMatching({'F': 'C', 'D': 'B', 'E': 'A'}, keys="reviewers", values="suitors")

```

Expand All @@ -118,7 +118,7 @@ One of the limitations of this library is the time complexities of the
algorithm implementations. In practical terms, the running time of any
of the algorithms in Matching is negligible but the theoretic complexity
of each has not yet been attained. For example, an instance of HR with
400 applicants and 20 hospitals is solved in less than one tenth of a
400 applicants and 20 hospitals is solved in around one tenth of a
second:

```python
Expand All @@ -127,18 +127,18 @@ second:
>>> prng = np.random.default_rng(0)
>>> num_residents, num_hospitals = 400, 20
>>> resident_prefs = {
... r: np.argsort(prng.random(size=num_hospitals))
... r: list(np.argsort(prng.random(size=num_hospitals)))
... for r in range(num_residents)
... }
>>> hospital_prefs = {
... h: np.argsort(prng.random(size=num_residents))
... h: list(np.argsort(prng.random(size=num_residents)))
... for h in range(num_hospitals)
... }
>>> capacities = {h: num_hospitals for h in hospital_prefs}
>>> game = HospitalResident.create_from_dictionaries(
>>> game = HospitalResident.from_preferences(
... resident_prefs, hospital_prefs, capacities
... )
>>> _ = game.solve() # 48.6 ms ± 963 µs per loop
>>> _ = game.solve() # 118 ms ± 847 µs per loop

```

Expand Down
10 changes: 4 additions & 6 deletions docs/how-to/check_matching_status.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,7 @@
"\n",
"project_supervisors = {\"X1\": \"X\", \"X2\": \"X\", \"Y1\": \"Y\", \"Y2\": \"Y\"}\n",
"project_capacities = {project: 1 for project in project_supervisors}\n",
"supervisor_capacities = {\n",
" supervisor: 2 for supervisor in supervisor_preferences\n",
"}\n",
"supervisor_capacities = {supervisor: 2 for supervisor in supervisor_preferences}\n",
"\n",
"\n",
"game = StudentAllocation.create_from_dictionaries(\n",
Expand Down Expand Up @@ -85,9 +83,9 @@
],
"metadata": {
"kernelspec": {
"display_name": "matching-docs",
"display_name": ".venv",
"language": "python",
"name": "matching-docs"
"name": "python3"
},
"language_info": {
"codemirror_mode": {
Expand All @@ -99,7 +97,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.8.16"
"version": "3.13.1"
}
},
"nbformat": 4,
Expand Down
16 changes: 6 additions & 10 deletions docs/how-to/choose_optimality.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@
{
"data": {
"text/plain": [
"SingleMatching({'Y': 'C', 'Z': 'B', 'X': 'A'}, keys=\"reviewers\", values=\"suitors\")"
"SMMatching({'Y': 'C', 'Z': 'B', 'X': 'A'}, keys=\"reviewers\", values=\"suitors\")"
]
},
"execution_count": 2,
Expand All @@ -56,9 +56,7 @@
}
],
"source": [
"game = StableMarriage.from_preferences(\n",
" suitor_preferences, reviewer_preferences\n",
")\n",
"game = StableMarriage.from_preferences(suitor_preferences, reviewer_preferences)\n",
"\n",
"game.solve(optimal=\"suitor\")"
]
Expand All @@ -71,7 +69,7 @@
{
"data": {
"text/plain": [
"SingleMatching({'Y': 'A', 'Z': 'B', 'X': 'C'}, keys=\"reviewers\", values=\"suitors\")"
"SMMatching({'Y': 'A', 'Z': 'B', 'X': 'C'}, keys=\"reviewers\", values=\"suitors\")"
]
},
"execution_count": 3,
Expand All @@ -80,17 +78,15 @@
}
],
"source": [
"game = StableMarriage.from_preferences(\n",
" suitor_preferences, reviewer_preferences\n",
")\n",
"game = StableMarriage.from_preferences(suitor_preferences, reviewer_preferences)\n",
"\n",
"game.solve(optimal=\"reviewer\")"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "matching",
"display_name": ".venv",
"language": "python",
"name": "python3"
},
Expand All @@ -104,7 +100,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.10"
"version": "3.13.1"
}
},
"nbformat": 4,
Expand Down
4 changes: 1 addition & 3 deletions docs/how-to/create_from_dictionaries.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,7 @@
"\n",
"project_supervisors = {\"X1\": \"X\", \"X2\": \"X\", \"Y1\": \"Y\", \"Y2\": \"Y\"}\n",
"project_capacities = {project: 1 for project in project_supervisors}\n",
"supervisor_capacities = {\n",
" supervisor: 2 for supervisor in supervisor_preferences\n",
"}"
"supervisor_capacities = {supervisor: 2 for supervisor in supervisor_preferences}"
]
},
{
Expand Down
36 changes: 19 additions & 17 deletions docs/tutorials/hospital_resident.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
"outputs": [],
"source": [
"import urllib\n",
"import warnings\n",
"\n",
"import yaml\n",
"\n",
Expand Down Expand Up @@ -126,9 +127,11 @@
"source": [
"from matching.games import HospitalResident\n",
"\n",
"game = HospitalResident.create_from_dictionaries(\n",
" resident_preferences, hospital_preferences, hospital_capacities\n",
")"
"with warnings.catch_warnings():\n",
" warnings.simplefilter(\"ignore\")\n",
" game = HospitalResident.from_preferences(\n",
" resident_preferences, hospital_preferences, hospital_capacities\n",
" )"
]
},
{
Expand Down Expand Up @@ -171,19 +174,20 @@
"name": "stdout",
"output_type": "stream",
"text": [
"Dewi Sant (30 / 30): [067, 022, 023, 158, 139, 065, 160, 131, 011, 137, 039, 045, 013, 046, 072, 037, 086, 152, 144, 154, 130, 040, 010, 159, 083, 019, 169, 193, 168, 079]\n",
"Prince Charles (29 / 30): [027, 133, 106, 081, 051, 044, 069, 157, 110, 119, 129, 107, 135, 034, 007, 194, 198, 061, 087, 041, 183, 136, 059, 178, 009, 008, 031, 070, 026]\n",
"Prince of Wales (30 / 30): [143, 128, 048, 175, 078, 132, 151, 030, 124, 138, 088, 004, 199, 173, 017, 097, 064, 025, 112, 181, 171, 196, 111, 035, 185, 156, 140, 001, 197, 177]\n",
"Royal Glamorgan (30 / 30): [073, 118, 096, 089, 014, 126, 142, 053, 021, 018, 104, 015, 147, 153, 033, 113, 146, 076, 123, 042, 117, 024, 029, 000, 016, 134, 058, 166, 075, 174]\n",
"Royal Gwent (27 / 30): [028, 105, 115, 095, 054, 006, 120, 161, 187, 164, 091, 141, 036, 184, 071, 155, 066, 182, 189, 002, 191, 068, 090, 145, 163, 121, 180]\n",
"St. David (30 / 30): [149, 101, 150, 172, 165, 020, 049, 094, 060, 116, 056, 005, 093, 188, 043, 108, 192, 092, 167, 114, 012, 063, 077, 162, 085, 195, 032, 099, 084, 127]\n",
"University (24 / 30): [109, 003, 057, 170, 176, 100, 122, 080, 038, 082, 102, 052, 062, 055, 047, 074, 050, 179, 125, 186, 148, 103, 098, 190]\n"
"Dewi Sant (30 / 30): ['010', '011', '013', '019', '022', '023', '037', '039', '040', '045', '046', '065', '067', '072', '079', '083', '086', '130', '131', '137', '139', '144', '152', '154', '158', '159', '160', '168', '169', '193']\n",
"Prince Charles (29 / 30): ['007', '008', '009', '026', '027', '031', '034', '041', '044', '051', '059', '061', '069', '070', '087', '107', '110', '119', '129', '133', '135', '136', '157', '178', '183', '194', '198', '106', '081']\n",
"Prince of Wales (30 / 30): ['001', '004', '017', '030', '035', '048', '064', '078', '088', '097', '111', '112', '124', '128', '132', '138', '140', '143', '151', '156', '171', '173', '175', '177', '181', '185', '196', '197', '199', '025']\n",
"Royal Glamorgan (30 / 30): ['000', '014', '015', '016', '018', '021', '024', '029', '033', '042', '053', '058', '073', '075', '076', '089', '096', '104', '113', '117', '118', '123', '126', '134', '142', '146', '147', '153', '166', '174']\n",
"Royal Gwent (27 / 30): ['002', '006', '028', '036', '054', '068', '071', '090', '091', '105', '120', '121', '141', '145', '155', '161', '163', '164', '180', '182', '184', '187', '189', '191', '066', '115', '095']\n",
"St. David (30 / 30): ['005', '012', '020', '032', '043', '049', '056', '060', '063', '077', '084', '085', '092', '093', '094', '099', '101', '108', '114', '116', '127', '149', '150', '162', '165', '167', '172', '188', '192', '195']\n",
"University (24 / 30): ['038', '047', '050', '052', '055', '057', '062', '074', '080', '082', '098', '100', '102', '103', '109', '122', '148', '170', '176', '179', '186', '190', '125', '003']\n"
]
}
],
"source": [
"for hospital, residents in solution.items():\n",
" print(f\"{hospital} ({len(residents)} / {hospital.capacity}): {residents}\")"
" capacity = hospital_capacities[hospital]\n",
" print(f\"{hospital} ({len(residents)} / {capacity}): {residents}\")"
]
},
{
Expand Down Expand Up @@ -217,11 +221,9 @@
"matched_residents = []\n",
"for _, residents in solution.items():\n",
" for resident in residents:\n",
" matched_residents.append(resident.name)\n",
" matched_residents.append(resident)\n",
"\n",
"unmatched_residents = set(resident_preferences.keys()).difference(\n",
" matched_residents\n",
")\n",
"unmatched_residents = set(resident_preferences.keys()).difference(matched_residents)\n",
"unmatched_residents"
]
},
Expand All @@ -238,7 +240,7 @@
],
"metadata": {
"kernelspec": {
"display_name": "matching",
"display_name": ".venv",
"language": "python",
"name": "python3"
},
Expand All @@ -252,7 +254,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.10"
"version": "3.13.1"
}
},
"nbformat": 4,
Expand Down
6 changes: 3 additions & 3 deletions docs/tutorials/stable_marriage.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@
{
"data": {
"text/plain": [
"SingleMatching({'Lydia': 'Wickham', 'Elizabeth': 'Darcy', 'Jane': 'Bingley', 'Charlotte': 'Collins'}, keys=\"reviewers\", values=\"suitors\")"
"SMMatching({'Lydia': 'Wickham', 'Elizabeth': 'Darcy', 'Jane': 'Bingley', 'Charlotte': 'Collins'}, keys=\"reviewers\", values=\"suitors\")"
]
},
"execution_count": 3,
Expand All @@ -123,7 +123,7 @@
],
"metadata": {
"kernelspec": {
"display_name": "matching",
"display_name": ".venv",
"language": "python",
"name": "python3"
},
Expand All @@ -137,7 +137,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.10"
"version": "3.13.1"
}
},
"nbformat": 4,
Expand Down
4 changes: 2 additions & 2 deletions docs/tutorials/stable_roommates.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@
],
"metadata": {
"kernelspec": {
"display_name": "matching",
"display_name": ".venv",
"language": "python",
"name": "python3"
},
Expand All @@ -144,7 +144,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.10"
"version": "3.13.1"
}
},
"nbformat": 4,
Expand Down
Loading
Loading