Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
40 changes: 18 additions & 22 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,39 +14,35 @@ jobs:
run:
shell: bash
strategy:
max-parallel: 10
matrix:
os: [ubuntu-latest, windows-latest]
python-version: [3.7, 3.8, 3.9, "3.10", "3.11"]
python-version: ["3.10", "3.11", "3.12", "3.13"]

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

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
- 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 doctest paper.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.11-ubuntu only)
if: |
matrix.python-version == '3.11' &&
matrix.os == 'ubuntu-latest'
- name: Run unit tests
run: |
python -m pip install ".[lint]"
python -m black --check .
python -m ruff 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 @@ -13,19 +13,25 @@ jobs:
steps:
- name: Check out repository
uses: actions/checkout@v3

- name: Set up Quarto
uses: quarto-dev/quarto-actions/setup@v2
- name: Install Python and dependencies
uses: actions/setup-python@v4
with:
python-version: "3.9"
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:
target: gh-pages
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
18 changes: 18 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.6.0
hooks:
- id: check-added-large-files
name: Check for files larger than 5 MB
args: [ "--maxkb=5120" ]
- id: end-of-file-fixer
name: Check for a blank line at the end of scripts (auto-fixes)
exclude: '\.Rd'
- id: trailing-whitespace
name: Check for trailing whitespaces (auto-fixes)
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.7.0
hooks:
- id: ruff
args: [ --fix ]
- id: ruff-format
2 changes: 1 addition & 1 deletion CITATION.cff
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,4 @@ keywords:
- matching
- python
- game
license: MIT
license: MIT
68 changes: 25 additions & 43 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 @@ -76,67 +76,49 @@ We can construct these preferences using dictionaries:
Then to solve this matching game, we make use of the `StableMarriage`
class, like so:


```python
>>> from matching.games import StableMarriage
>>> game = StableMarriage.create_from_dictionaries(
>>> game = StableMarriage.from_preferences(
... suitor_preferences, reviewer_preferences
... )
>>> game.solve()
{A: E, B: D, C: F}
>>> matching = game.solve()
>>> dict(matching)
{'F': 'C', 'D': 'B', 'E': 'A'}

```

## The `Matching` object

This matching is not a standard Python dictionary, though it does
The matching itself is not a standard Python dictionary, though it does
largely look and behave like one. It is in fact an instance of the
`SingleMatching` class:

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

```

This dictionary-like object is primarily useful as a teaching device
that eases the process of manipulating a matching after a solution has
been found.

## `Player` classes

Despite passing dictionaries of strings here, the matching displays
instances of `matching.player.Player`:
This object allows for straightforward manipulation of the underlying
dictionary, for instance by inverting it:

```python
>>> matching = game.matching
>>> for suitor in matching:
... print(type(suitor))
<class 'matching.players.player.Player'>
<class 'matching.players.player.Player'>
<class 'matching.players.player.Player'>
>>> dict(matching.invert())
{'C': 'F', 'B': 'D', 'A': 'E'}

```

This is because `create_from_dictionaries` creates instances of the
appropriate player classes first and passes them to the game class.
Using dictionaries like this can be an efficient way of creating large
games but it does require the names of the players in each party to be
unique.

With all games, Matching uses a `Player` class to represent the members
of the "applying" party, i.e. residents and students. For HR and SA,
there are specific classes to represent the roles of `Hospital`,
`Project` and `Supervisor`.

## A note on performance

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 @@ -145,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
6 changes: 3 additions & 3 deletions _quarto.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ website:
contents: docs/discussion
- title: reference
contents: docs/reference


page-footer:
left: >
Expand All @@ -53,15 +53,15 @@ bibliography: docs/assets/bibliography.bib

toc: true

metadata-files:
metadata-files:
- docs/_sidebar.yml

quartodoc:
title: API reference
package: matching
dir: docs/reference
sidebar: docs/_sidebar.yml

sections:
- title: Games
desc: Objects for handling game instances.
Expand Down
6 changes: 3 additions & 3 deletions docs/discussion/stable_marriage.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,10 +91,10 @@ efficient, robust extension of the original algorithm, taken from
@GI89, is given below.

0. Assign all suitors and reviewers to be unmatched.
1. Take any suitor $s$ that is not currently matched, and consider
their favourite reviewer $r$.
1. Take any suitor $s$ that is not currently matched but has a non-empty
preference list, and consider their favourite reviewer $r$.
2. If $r$ is matched, get their current match $s' = M^{-1}(r)$ and
unmatch the pair.
unmatch them.
3. Match $s$ and $r$, i.e. set $M(s) = r$.
4. For each successor, $t$, to $s$ in $g(r)$, delete the pair $(t, r)$
from the game by removing $r$ from $f(t)$ and $t$ from $g(r)$.
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
18 changes: 7 additions & 11 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": [
"{A: X, B: Z, C: Y}"
"SMMatching({'Y': 'C', 'Z': 'B', 'X': 'A'}, keys=\"reviewers\", values=\"suitors\")"
]
},
"execution_count": 2,
Expand All @@ -56,9 +56,7 @@
}
],
"source": [
"game = StableMarriage.create_from_dictionaries(\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": [
"{A: Y, B: Z, C: X}"
"SMMatching({'Y': 'A', 'Z': 'B', 'X': 'C'}, keys=\"reviewers\", values=\"suitors\")"
]
},
"execution_count": 3,
Expand All @@ -80,19 +78,17 @@
}
],
"source": [
"game = StableMarriage.create_from_dictionaries(\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-docs",
"display_name": ".venv",
"language": "python",
"name": "matching-docs"
"name": "python3"
},
"language_info": {
"codemirror_mode": {
Expand All @@ -104,7 +100,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.8.16"
"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
Loading