Skip to content
Open
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
1 change: 1 addition & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@ exclude_lines =
[run]
omit =
tests/*
benchmarks/*
6 changes: 6 additions & 0 deletions .github/workflows/python-package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,12 @@ jobs:
python -m pip install --upgrade pip
pip install -e ".[${{ matrix.extras }}]"

- name: Verify ASV benchmark suite (Python 3.12, bench extra)
if: matrix.python-version == '3.12' && matrix.extras != 'test'
run: |
pip install -e ".[bench]"
cd benchmarks && asv check --python=same

- name: Verify Numba installation (fast extra)
if: contains(matrix.extras, 'fast')
run: |
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/sphinx-docs-preview.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ jobs:
pip install -e ".[docs]"

- name: Build HTML
run: sphinx-build -b html docs docs/_build/html
run: python -m sphinx.cmd.build -b html docs docs/_build/html

- name: Upload HTML artifact
uses: actions/upload-artifact@v4
Expand Down
46 changes: 38 additions & 8 deletions .github/workflows/sphinx-to-github-pages.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
# https://github.com/marketplace/actions/sphinx-to-github-pages
# Publish Sphinx docs and ASV benchmark HTML to GitHub Pages.

name: Deploy Sphinx documentation to Pages

# Production deploy only when docs/source land on the default branch(es).
on:
push:
branches: [main, master]
Expand All @@ -17,9 +16,40 @@ jobs:
pages: write
id-token: write
steps:
- id: deployment
uses: sphinx-notes/pages@v3
with:
# Doc deps live only in pyproject.toml [project.optional-dependencies.docs].
requirements_path: ""
pyproject_extras: docs
- uses: actions/checkout@v6
with:
fetch-depth: 0

- uses: actions/setup-python@v6
with:
python-version: "3.12"
cache: pip

- name: Install documentation and benchmark dependencies
run: |
python -m pip install --upgrade pip
pip install .[docs,fast,bench]

- name: Run ASV and publish benchmark site into Sphinx extra path
run: |
mkdir -p docs/_benchmark_site/benchmarks
cd benchmarks
asv machine --yes
asv run master --show-stderr
asv publish -o ../docs/_benchmark_site/benchmarks

- name: Build HTML documentation
run: python -m sphinx.cmd.build -b html docs /tmp/sphinx-html

- uses: actions/configure-pages@v6

- name: Fix file permissions for Pages upload
run: |
chmod -c -R +rX /tmp/sphinx-html || true

- uses: actions/upload-pages-artifact@v5.0.0
with:
path: /tmp/sphinx-html

- id: deployment
uses: actions/deploy-pages@v5
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,12 @@ __marimo__/
# sphinx
_autosummary/

# airspeed velocity (see benchmarks/asv.conf.json)
benchmarks/env/
benchmarks/results/
benchmarks/html/
docs/_benchmark_site/

# Build-time copy of examples into docs (see conf.py prepare_examples_doc)
docs/examples/README.md
docs/examples/results/
19 changes: 19 additions & 0 deletions benchmarks/asv.conf.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"version": 1,
"project": "elsim",
"project_url": "https://endolith.github.io/elsim/",
"repo": "..",
"branches": ["master"],
"show_commit_url": "https://github.com/endolith/elsim/commit/",
"pythons": ["3.12"],
"environment_type": "virtualenv",
"install_command": [
"in-dir={build_dir} python -m pip install -e \".[fast]\""
],
"benchmark_dir": "benchmarks",
"env_dir": "env",
"results_dir": "results",
"html_dir": "html",
"default_benchmark_timeout": 300,
"build_cache_size": 8
}
Empty file.
38 changes: 38 additions & 0 deletions benchmarks/benchmarks/elections.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
"""ASV timings for ``elsim.elections``."""

import numpy as np

from elsim.elections import (
impartial_culture,
normal_electorate,
normed_dist_utilities,
random_utilities,
)

N_VOTERS = 800
N_CANDS = 7


class ElectionsSuite:
"""Fixed electorate size; spatial model uses two dimensions by default."""

def setup_cache(self):
rng = np.random.default_rng(42)
spatial = normal_electorate(N_VOTERS, N_CANDS, random_state=rng)
return {
"rng": np.random.default_rng(43),
"spatial": spatial,
}

def time_random_utilities(self, cache):
random_utilities(N_VOTERS, N_CANDS, random_state=cache["rng"])

def time_impartial_culture(self, cache):
impartial_culture(N_VOTERS, N_CANDS, random_state=cache["rng"])

def time_normal_electorate(self, cache):
normal_electorate(N_VOTERS, N_CANDS, random_state=cache["rng"])

def time_normed_dist_utilities(self, cache):
voters, cands = cache["spatial"]
normed_dist_utilities(voters, cands)
115 changes: 115 additions & 0 deletions benchmarks/benchmarks/methods.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
"""ASV timings for ``elsim.methods``."""

import numpy as np

from elsim.elections import impartial_culture, random_utilities
from elsim.methods import (
approval,
black,
borda,
combined_approval,
condorcet,
condorcet_from_matrix,
coombs,
fptp,
irv,
matrix_from_scores,
ranked_election_to_matrix,
runoff,
score,
sntv,
star,
utility_winner,
)
from elsim.strategies import approval_optimal, honest_normed_scores

N_VOTERS = 800
N_CANDS = 7


class RankedBallotMethods:
def setup_cache(self):
election = impartial_culture(N_VOTERS, N_CANDS, random_state=42)
return np.asarray(election)

def time_black(self, election):
black(election)

def time_borda(self, election):
borda(election)

def time_fptp(self, election):
fptp(election)

def time_sntv(self, election):
sntv(election, n=2)

def time_runoff(self, election):
runoff(election)

def time_irv(self, election):
irv(election)

def time_coombs(self, election):
coombs(election)


class ScoredBallotMethods:
def setup_cache(self):
utilities = random_utilities(N_VOTERS, N_CANDS, random_state=42)
election = honest_normed_scores(utilities)
return np.asarray(election)

def time_score(self, election):
score(election)

def time_star(self, election):
star(election)

def time_matrix_from_scores(self, election):
matrix_from_scores(election)


class ApprovalBallotMethods:
def setup_cache(self):
utilities = random_utilities(N_VOTERS, N_CANDS, random_state=42)
election = approval_optimal(utilities)
return np.asarray(election)

def time_approval(self, election):
approval(election)


class CombinedApprovalBallots:
def setup_cache(self):
rng = np.random.default_rng(42)
return rng.integers(-1, 2, size=(N_VOTERS, N_CANDS))

def time_combined_approval(self, election):
combined_approval(election)


class UtilityWinnerBallots:
def setup_cache(self):
return random_utilities(N_VOTERS, N_CANDS, random_state=42)

def time_utility_winner(self, utilities):
utility_winner(utilities)


class CondorcetSuite:
def setup_cache(self):
election = np.asarray(
impartial_culture(N_VOTERS, N_CANDS, random_state=42)
)
matrix = ranked_election_to_matrix(election)
return {"election": election, "matrix": matrix}

def time_ranked_election_to_matrix(self, data):
ranked_election_to_matrix(data["election"])

def time_condorcet(self, data):
condorcet(data["election"])

def time_condorcet_from_matrix(self, data):
condorcet_from_matrix(data["matrix"])
29 changes: 29 additions & 0 deletions benchmarks/benchmarks/strategies.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
"""ASV timings for ``elsim.strategies``."""

from elsim.elections import random_utilities
from elsim.strategies import (
approval_optimal,
honest_normed_scores,
honest_rankings,
vote_for_k,
)

N_VOTERS = 800
N_CANDS = 7


class StrategiesSuite:
def setup_cache(self):
return random_utilities(N_VOTERS, N_CANDS, random_state=42)

def time_honest_rankings(self, utilities):
honest_rankings(utilities)

def time_honest_normed_scores(self, utilities):
honest_normed_scores(utilities)

def time_approval_optimal(self, utilities):
approval_optimal(utilities)

def time_vote_for_k(self, utilities):
vote_for_k(utilities, 3)
16 changes: 16 additions & 0 deletions codecov.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# https://docs.codecov.com/docs/codecovyml-reference
coverage:
status:
project:
default:
paths:
- "elsim/"
- "tests/"
patch:
default:
paths:
- "elsim/"
- "tests/"

ignore:
- "benchmarks/"
14 changes: 14 additions & 0 deletions docs/benchmarks.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
Benchmarks
==========

Performance of the public API is tracked with `Airspeed Velocity (ASV)`_.

.. _Airspeed Velocity (ASV): https://asv.readthedocs.io/en/stable/

An `interactive ASV report`_ is published alongside this documentation when the default branch is deployed (same GitHub Pages site).

.. _interactive ASV report: ../benchmarks/

The CI job runs on Ubuntu with Python 3.12 and installs optional-dependencies ``fast`` so Numba-backed code paths are exercised. Results accrue on that runner; for local comparisons or additional commits, install optional-dependencies ``bench``, ``cd`` into ``benchmarks/``, and follow the `ASV workflow`_ (configuration is in ``asv.conf.json``).

.. _ASV workflow: https://asv.readthedocs.io/en/stable/using.html
12 changes: 10 additions & 2 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
# list see the documentation:
# https://www.sphinx-doc.org/en/master/usage/configuration.html

import os
import shutil

# -- Path setup --------------------------------------------------------------

# If extensions (or modules to document with autodoc) are in another directory,
Expand Down Expand Up @@ -95,8 +98,13 @@
# Paths in that file (./results/) are then relative to the document and resolve
# correctly. Copy repo examples/README.md and examples/results/ into docs/examples/
# at build start so the doc and Sphinx can find them.
import os
import shutil

# ASV HTML is optional locally; CI writes docs/_benchmark_site/benchmarks before build.
_docs_dir = os.path.dirname(os.path.abspath(__file__))
_benchmark_site = os.path.join(_docs_dir, '_benchmark_site')
html_extra_path = []
if os.path.isdir(_benchmark_site):
html_extra_path.append('_benchmark_site')


def prepare_examples_doc(app, config):
Expand Down
1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
:caption: Contents:

examples/index
benchmarks
modules


Expand Down
4 changes: 4 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,14 @@ docs = [
"sphinxcontrib-apidoc",
"linkify-it-py",
]
bench = [
"asv",
]

[project.urls]
Homepage = "https://github.com/endolith/elsim"
Documentation = "https://endolith.github.io/elsim/"
Benchmarks = "https://endolith.github.io/elsim/benchmarks/"

[tool.hatch.version]
path = "elsim/__init__.py"
Expand Down
Loading