Skip to content

Commit

Permalink
Improve error handling
Browse files Browse the repository at this point in the history
  • Loading branch information
dustalov committed Jul 13, 2024
1 parent a6176aa commit 58749d7
Show file tree
Hide file tree
Showing 10 changed files with 203 additions and 157 deletions.
36 changes: 26 additions & 10 deletions conftest.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,22 @@
from __future__ import annotations

from itertools import product
from pathlib import Path
from typing import TYPE_CHECKING, Any, NamedTuple, cast
from typing import TYPE_CHECKING, Literal, NamedTuple, cast

import evalica
import numpy as np
import numpy.typing as npt
import pandas as pd
import pytest
from hypothesis import strategies as st
from hypothesis.strategies import composite

if TYPE_CHECKING:
from collections.abc import Callable

from _pytest.fixtures import TopRequest
from hypothesis.strategies import DrawFn


class Example(NamedTuple):
Expand All @@ -24,18 +27,31 @@ class Example(NamedTuple):
ws: list[evalica.Winner] | pd.Series[evalica.Winner] # type: ignore[type-var]


@st.composite
def elements(draw: Callable[[st.SearchStrategy[Any]], Any]) -> Example: # type: ignore[type-var]
def enumerate_sizes(n: int) -> list[tuple[int, ...]]:
return [xs for xs in product([0, 1], repeat=n) if 0 < sum(xs) < n]


@composite
def elements(
draw: DrawFn,
shape: Literal["good", "bad"] = "good",
) -> Example: # type: ignore[type-var]
length = draw(st.integers(0, 5))

xys = st.lists(st.text(max_size=length), min_size=length, max_size=length)
ws = st.lists(st.sampled_from(evalica.WINNERS), min_size=length, max_size=length)
if shape == "good":
xs = st.lists(st.text(max_size=length), min_size=length, max_size=length)
ys = st.lists(st.text(max_size=length), min_size=length, max_size=length)
ws = st.lists(st.sampled_from(evalica.WINNERS), min_size=length, max_size=length)
else:
min_x, min_y, min_z = draw(st.sampled_from(enumerate_sizes(3)))

return Example(
xs=draw(xys),
ys=draw(xys),
ws=draw(ws),
)
length_x, length_y, length_z = (1 + length) * min_x, (1 + length) * min_y, (1 + length) * min_z

xs = st.lists(st.text(max_size=length_x), min_size=length_x, max_size=length_x)
ys = st.lists(st.text(max_size=length_y), min_size=length_y, max_size=length_y)
ws = st.lists(st.sampled_from(evalica.WINNERS), min_size=length_z, max_size=length_z)

return Example(xs=draw(xs), ys=draw(ys), ws=draw(ws))


@pytest.fixture()
Expand Down
8 changes: 5 additions & 3 deletions python/evalica/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import pandas as pd

from .evalica import (
LengthMismatchError,
Winner,
__version__,
bradley_terry_pyo3,
Expand Down Expand Up @@ -41,9 +42,9 @@ class IndexedElements(Generic[T], NamedTuple):


def index_elements(
xs: Iterable[T],
ys: Iterable[T],
index: pd.Index[T] | None = None, # type: ignore[type-var]
xs: Iterable[T],
ys: Iterable[T],
index: pd.Index[T] | None = None, # type: ignore[type-var]
) -> IndexedElements[T]: # type: ignore[type-var]
xy_index: dict[T, int] = {}

Expand Down Expand Up @@ -361,6 +362,7 @@ def pairwise_frame(scores: pd.Series[T]) -> pd.DataFrame: # type: ignore[type-v
"CountingResult",
"EigenResult",
"EloResult",
"LengthMismatchError",
"NewmanResult",
"PageRankResult",
"WINNERS",
Expand Down
4 changes: 4 additions & 0 deletions python/evalica/evalica.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ class Winner(Enum):
Ignore = ...


class LengthMismatchError(Exception):
...


def matrices_pyo3(
xs: npt.ArrayLike,
ys: npt.ArrayLike,
Expand Down
56 changes: 56 additions & 0 deletions python/evalica/test_evalica.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,14 @@ def test_matrices(example: Example) -> None:
assert result.tie_matrix.sum() == 2 * ties


@given(example=elements(shape="bad"))
def test_matrices_misshaped(example: Example) -> None:
xs, ys, ws = example

with pytest.raises(evalica.LengthMismatchError):
evalica.matrices(xs, ys, ws)


@given(example=elements())
def test_counting(example: Example) -> None:
xs, ys, ws = example
Expand All @@ -70,6 +78,14 @@ def test_counting(example: Example) -> None:
assert np.isfinite(result.scores).all()


@given(example=elements(shape="bad"))
def test_counting_misshaped(example: Example) -> None:
xs, ys, ws = example

with pytest.raises(evalica.LengthMismatchError):
evalica.counting(xs, ys, ws)


@given(example=elements())
def test_bradley_terry(example: Example) -> None:
xs, ys, ws = example
Expand All @@ -87,6 +103,14 @@ def test_bradley_terry(example: Example) -> None:
assert_series_equal(result_pyo3.scores, result_naive.scores, atol=tolerance)


@given(example=elements(shape="bad"))
def test_bradley_terry_misshaped(example: Example) -> None:
xs, ys, ws = example

with pytest.raises(evalica.LengthMismatchError):
evalica.bradley_terry(xs, ys, ws)


@given(example=elements())
def test_newman(example: Example) -> None:
xs, ys, ws = example
Expand All @@ -107,6 +131,14 @@ def test_newman(example: Example) -> None:
assert result_pyo3.v == pytest.approx(result_naive.v, abs=tolerance)


@given(example=elements(shape="bad"))
def test_newman_misshaped(example: Example) -> None:
xs, ys, ws = example

with pytest.raises(evalica.LengthMismatchError):
evalica.newman(xs, ys, ws)


@given(example=elements())
def test_elo(example: Example) -> None:
xs, ys, ws = example
Expand All @@ -121,6 +153,14 @@ def test_elo(example: Example) -> None:
assert_series_equal(result_pyo3.scores, result_naive.scores)


@given(example=elements(shape="bad"))
def test_elo_misshaped(example: Example) -> None:
xs, ys, ws = example

with pytest.raises(evalica.LengthMismatchError):
evalica.elo(xs, ys, ws)


@given(example=elements())
def test_eigen(example: Example) -> None:
xs, ys, ws = example
Expand All @@ -138,6 +178,14 @@ def test_eigen(example: Example) -> None:
assert_series_equal(result_pyo3.scores, result_naive.scores, atol=tolerance)


@given(example=elements(shape="bad"))
def test_eigen_misshaped(example: Example) -> None:
xs, ys, ws = example

with pytest.raises(evalica.LengthMismatchError):
evalica.eigen(xs, ys, ws)


@given(example=elements())
def test_pagerank(example: Example) -> None:
xs, ys, ws = example
Expand All @@ -152,6 +200,14 @@ def test_pagerank(example: Example) -> None:
assert not xs or result.iterations > 0


@given(example=elements(shape="bad"))
def test_pagerank_misshaped(example: Example) -> None:
xs, ys, ws = example

with pytest.raises(evalica.LengthMismatchError):
evalica.pagerank(xs, ys, ws)


def test_bradley_terry_simple(simple_elements: Example) -> None:
xs, ys, ws = simple_elements

Expand Down
50 changes: 16 additions & 34 deletions src/bradley_terry.rs
Original file line number Diff line number Diff line change
@@ -1,17 +1,15 @@
use ndarray::{Array1, Array2, ArrayView2, Axis};
use ndarray::{Array1, Array2, ArrayView2, Axis, ErrorKind, ShapeError};

use crate::utils::{nan_to_num, one_nan_to_num};

pub fn bradley_terry(
matrix: &ArrayView2<f64>,
tolerance: f64,
limit: usize,
) -> (Array1<f64>, usize) {
assert_eq!(
matrix.shape()[0],
matrix.shape()[1],
"The matrix must be square"
);
) -> Result<(Array1<f64>, usize), ShapeError> {
if !matrix.is_square() {
return Err(ShapeError::from_kind(ErrorKind::IncompatibleShape));
}

let totals = &matrix.t().clone() + matrix;
let active = totals.mapv(|x| x > 0.0);
Expand Down Expand Up @@ -47,7 +45,7 @@ pub fn bradley_terry(
scores.assign(&scores_new);
}

(scores, iterations)
Ok((scores, iterations))
}

pub fn newman(
Expand All @@ -56,27 +54,10 @@ pub fn newman(
v_init: f64,
tolerance: f64,
limit: usize,
) -> (Array1<f64>, f64, usize) {
assert_eq!(
win_matrix.shape(),
tie_matrix.shape(),
"The matrices must be have the same shape"
);

assert_eq!(
win_matrix.shape()[0],
win_matrix.shape()[1],
"The win matrix must be square"
);

assert_eq!(
tie_matrix.shape()[0],
tie_matrix.shape()[1],
"The tie matrix must be square"
);

assert!(v_init.is_normal());
assert!(v_init > 0.0);
) -> Result<(Array1<f64>, f64, usize), ShapeError> {
if win_matrix.shape() != tie_matrix.shape() || !win_matrix.is_square() {
return Err(ShapeError::from_kind(ErrorKind::IncompatibleShape));
}

let win_tie_half = win_matrix + &(tie_matrix / 2.0);

Expand Down Expand Up @@ -121,7 +102,7 @@ pub fn newman(
scores.assign(&scores_new);
}

(scores, v, iterations)
Ok((scores, v, iterations))
}

#[cfg(test)]
Expand All @@ -142,7 +123,7 @@ mod tests {
let ys = ArrayView1::from(&utils::fixtures::YS);
let ws = ArrayView1::from(&utils::fixtures::WS);

let (win_matrix, tie_matrix) = matrices(&xs, &ys, &ws, 1.0, 0.5);
let (win_matrix, tie_matrix) = matrices(&xs, &ys, &ws, 1.0, 0.5).unwrap();

let matrix = win_matrix + &tie_matrix;

Expand All @@ -154,7 +135,7 @@ mod tests {
0.34947527489923,
];

let (actual, iterations) = bradley_terry(&matrix.view(), tolerance, 100);
let (actual, iterations) = bradley_terry(&matrix.view(), tolerance, 100).unwrap();

assert_eq!(actual.len(), matrix.shape()[0]);
assert_ne!(iterations, 0);
Expand All @@ -172,7 +153,7 @@ mod tests {
let ys = ArrayView1::from(&utils::fixtures::YS);
let ws = ArrayView1::from(&utils::fixtures::WS);

let (win_matrix, tie_matrix) = matrices(&xs, &ys, &ws, 1.0, 1.0);
let (win_matrix, tie_matrix) = matrices(&xs, &ys, &ws, 1.0, 1.0).unwrap();

let expected_v = 3.4609664512240546;
let v_init = 0.5;
Expand All @@ -191,7 +172,8 @@ mod tests {
v_init,
tolerance,
100,
);
)
.unwrap();

assert_eq!(actual.len(), win_matrix.shape()[0]);
assert_eq!(actual.len(), tie_matrix.shape()[0]);
Expand Down
28 changes: 7 additions & 21 deletions src/counting.rs
Original file line number Diff line number Diff line change
@@ -1,35 +1,21 @@
use std::ops::AddAssign;

use ndarray::{Array1, ArrayView1};
use ndarray::{Array1, ArrayView1, ErrorKind, ShapeError};
use num_traits::Num;

use crate::Winner;
use crate::{match_lengths, Winner};

pub fn counting<A: Num + Copy + AddAssign>(
xs: &ArrayView1<usize>,
ys: &ArrayView1<usize>,
ws: &ArrayView1<Winner>,
win_weight: A,
tie_weight: A,
) -> Array1<A> {
assert_eq!(
xs.len(),
ys.len(),
"first and second length mismatch: {} vs. {}",
xs.len(),
ys.len()
);

assert_eq!(
xs.len(),
ws.len(),
"first and status length mismatch: {} vs. {}",
xs.len(),
ws.len()
);
) -> Result<Array1<A>, ShapeError> {
match_lengths!(xs.len(), ys.len(), ws.len());

if xs.is_empty() {
return Array1::zeros(0);
return Ok(Array1::zeros(0));
}

let n = 1 + std::cmp::max(*xs.iter().max().unwrap(), *ys.iter().max().unwrap());
Expand All @@ -48,7 +34,7 @@ pub fn counting<A: Num + Copy + AddAssign>(
}
}

scores
Ok(scores)
}

#[cfg(test)]
Expand All @@ -67,7 +53,7 @@ mod tests {

let expected = array![1.5, 3.0, 3.0, 4.5, 4.0];

let actual = counting(&xs.view(), &ys.view(), &ws.view(), 1.0, 0.5);
let actual = counting(&xs.view(), &ys.view(), &ws.view(), 1.0, 0.5).unwrap();

assert_eq!(actual, expected);
}
Expand Down
Loading

0 comments on commit 58749d7

Please sign in to comment.