Skip to content

Commit

Permalink
Merge pull request #59 from TomHall2020/generic-scoring
Browse files Browse the repository at this point in the history
Generic scoring
  • Loading branch information
jatkinson1000 authored Mar 20, 2024
2 parents e59da86 + 879e9bc commit fd2f524
Show file tree
Hide file tree
Showing 21 changed files with 1,117 additions and 564 deletions.
3 changes: 2 additions & 1 deletion archeryutils/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@

from archeryutils import classifications, handicaps
from archeryutils.rounds import Pass, Round
from archeryutils.targets import Target
from archeryutils.targets import Quantity, Target
from archeryutils.utils import versions

__all__ = [
"classifications",
"handicaps",
"Pass",
"Round",
"Quantity",
"Target",
"versions",
]
6 changes: 3 additions & 3 deletions archeryutils/classifications/agb_outdoor_classifications.py
Original file line number Diff line number Diff line change
Expand Up @@ -325,7 +325,7 @@ def _assign_outdoor_prestige(

# Check all other rounds based on distance
for roundname in distance_check:
if ALL_OUTDOOR_ROUNDS[roundname].max_distance() >= np.min(max_dist):
if ALL_OUTDOOR_ROUNDS[roundname].max_distance().value >= np.min(max_dist):
prestige_rounds.append(roundname)

return prestige_rounds
Expand Down Expand Up @@ -469,7 +469,7 @@ def _check_prestige_distance(

# If not prestige, what classes are ineligible based on distance
to_del: list[str] = []
round_max_dist = ALL_OUTDOOR_ROUNDS[roundname].max_distance()
round_max_dist = ALL_OUTDOOR_ROUNDS[roundname].max_distance().value
for class_i_name, class_i_data in class_data.items():
if class_i_data["min_dist"] > round_max_dist:
to_del.append(class_i_name)
Expand Down Expand Up @@ -557,7 +557,7 @@ def agb_outdoor_classification_scores(
class_scores[0:3] = [-9999] * 3

# If not prestige, what classes are eligible based on category and distance
round_max_dist = ALL_OUTDOOR_ROUNDS[roundname].max_distance()
round_max_dist = ALL_OUTDOOR_ROUNDS[roundname].max_distance().value
for i in range(3, len(class_scores)):
if group_data["min_dists"][i] > round_max_dist:
class_scores[i] = -9999
Expand Down
155 changes: 0 additions & 155 deletions archeryutils/constants.py

This file was deleted.

146 changes: 50 additions & 96 deletions archeryutils/handicaps/handicap_scheme.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"""

import itertools as itr
import warnings
from abc import ABC, abstractmethod
from typing import Optional, TypeVar, Union, overload
Expand All @@ -42,6 +43,18 @@

FloatArray = TypeVar("FloatArray", float, npt.NDArray[np.float64])

# itertools.pairwise not available until python 3.10
# workaround can be removed when support for 3.9 is dropped
# ignore for coverage (runner is > 3.10, ci shows this works on 3.9)
if not hasattr(itr, "pairwise"): # pragma: no cover

def _pairwise(iterable):
a, b = itr.tee(iterable)
next(b, None)
return zip(a, b)

setattr(itr, "pairwise", _pairwise) # noqa: B010


class HandicapScheme(ABC):
r"""
Expand Down Expand Up @@ -162,7 +175,7 @@ def sigma_r(self, handicap: FloatArray, dist: float) -> FloatArray:
sig_r = dist * sig_t
return sig_r

def arrow_score( # noqa: PLR0912 Too many branches
def arrow_score(
self,
handicap: FloatArray,
target: targets.Target,
Expand Down Expand Up @@ -216,104 +229,45 @@ def arrow_score( # noqa: PLR0912 Too many branches
arw_d = self.arw_d_out

arw_rad = arw_d / 2.0

tar_dia = target.diameter
spec = target.face_spec
sig_r = self.sigma_r(handicap, target.distance)
return self._s_bar(spec, arw_rad, sig_r)

if target.scoring_system == "5_zone":
s_bar = (
9.0
- 2.0
* sum(
np.exp(-((((n * tar_dia / 10.0) + arw_rad) / sig_r) ** 2))
for n in range(1, 5)
)
- np.exp(-((((5.0 * tar_dia / 10.0) + arw_rad) / sig_r) ** 2))
)

elif target.scoring_system == "10_zone":
s_bar = 10.0 - sum(
np.exp(-((((n * tar_dia / 20.0) + arw_rad) / sig_r) ** 2))
for n in range(1, 11)
)

elif target.scoring_system == "10_zone_6_ring":
s_bar = (
10.0
- sum(
np.exp(-((((n * tar_dia / 20.0) + arw_rad) / sig_r) ** 2))
for n in range(1, 6)
)
- 5.0 * np.exp(-((((6.0 * tar_dia / 20.0) + arw_rad) / sig_r) ** 2))
)

elif target.scoring_system == "10_zone_compound":
s_bar = (
10.0
- np.exp(-((((tar_dia / 40.0) + arw_rad) / sig_r) ** 2))
- sum(
np.exp(-((((n * tar_dia / 20.0) + arw_rad) / sig_r) ** 2))
for n in range(2, 11)
)
)

elif target.scoring_system == "10_zone_5_ring":
s_bar = (
10.0
- sum(
np.exp(-((((n * tar_dia / 20.0) + arw_rad) / sig_r) ** 2))
for n in range(1, 5)
)
- 6.0 * np.exp(-((((5.0 * tar_dia / 20.0) + arw_rad) / sig_r) ** 2))
)

elif target.scoring_system == "10_zone_5_ring_compound":
s_bar = (
10.0
- np.exp(-((((tar_dia / 40) + arw_rad) / sig_r) ** 2))
- sum(
np.exp(-((((n * tar_dia / 20) + arw_rad) / sig_r) ** 2))
for n in range(2, 5)
)
- 6.0 * np.exp(-((((5 * tar_dia / 20) + arw_rad) / sig_r) ** 2))
)

elif target.scoring_system == "WA_field":
s_bar = (
6.0
- np.exp(-((((tar_dia / 20.0) + arw_rad) / sig_r) ** 2))
- sum(
np.exp(-((((n * tar_dia / 10.0) + arw_rad) / sig_r) ** 2))
for n in range(1, 6)
)
)

elif target.scoring_system == "IFAA_field":
s_bar = (
5.0
- np.exp(-((((tar_dia / 10.0) + arw_rad) / sig_r) ** 2))
- np.exp(-((((3.0 * tar_dia / 10.0) + arw_rad) / sig_r) ** 2))
- 3.0 * np.exp(-((((5.0 * tar_dia / 10.0) + arw_rad) / sig_r) ** 2))
)

elif target.scoring_system == "Beiter_hit_miss":
s_bar = 1.0 - np.exp(-((((tar_dia / 2.0) + arw_rad) / sig_r) ** 2))

elif target.scoring_system in ("Worcester", "IFAA_field_expert"):
s_bar = 5.0 - sum(
np.exp(-((((n * tar_dia / 10.0) + arw_rad) / sig_r) ** 2))
for n in range(1, 6)
)
def _s_bar(
self, target_specs: targets.FaceSpec, arw_rad: float, sig_r: FloatArray
) -> FloatArray:
"""Calculate expected score directly from target ring sizes.
elif target.scoring_system == "Worcester_2_ring":
s_bar = (
5.0
- np.exp(-((((tar_dia / 10.0) + arw_rad) / sig_r) ** 2))
- 4.0 * np.exp(-((((2 * tar_dia / 10.0) + arw_rad) / sig_r) ** 2))
)
# No need for else with error as invalid scoring systems handled in Target class
Parameters
----------
target_specs : FaceSpec
Mapping of target ring *diameters* in [metres], to points scored
arw_rad : float
arrow radius in [metres]
sig_r : float
standard deviation of group size [metres]
return s_bar
Returns
-------
s_bar : float
expected average score per arrow
Notes
-----
Assumes that:
- target rings are concentric
- score decreases monotonically as ring sizes increase
"""
target_specs = dict(sorted(target_specs.items()))
ring_sizes = target_specs.keys()
ring_scores = list(itr.chain(target_specs.values(), [0]))
score_drops = (inner - outer for inner, outer in itr.pairwise(ring_scores))
max_score = max(ring_scores)

return max_score - sum(
score_drop * np.exp(-(((arw_rad + (ring_diam / 2)) / sig_r) ** 2))
for ring_diam, score_drop in zip(ring_sizes, score_drops)
)

def score_for_passes(
self,
Expand Down Expand Up @@ -505,7 +459,7 @@ def handicap_from_score(
To get an integer value as would appear in the handicap tables use
``int_prec=True``:
>>> agb_scheme.handicap_from_score(999, wa_outdoor.wa1440_90), int_prec=True)
>>> agb_scheme.handicap_from_score(999, wa_outdoor.wa1440_90, int_prec=True)
44.0
"""
Expand Down
Loading

0 comments on commit fd2f524

Please sign in to comment.