Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Generic scoring #59

Merged
merged 43 commits into from
Mar 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
3a849e2
Add custom scoring capabilities to Target class
TomHall2020 Mar 4, 2024
70a510e
Clarify Pass.at_target docs for input parameters
TomHall2020 Mar 4, 2024
43c97b4
Replace implementation of arrow_score with generic calculation from f…
TomHall2020 Mar 4, 2024
841b2b7
Add indirect tests for _s_bar via arrow_score function
TomHall2020 Mar 4, 2024
4915a28
Linting and formatting to bring up to date with new use of Ruff
TomHall2020 Mar 6, 2024
753639e
Infer units in error messages for unsupported distance/diameter units
TomHall2020 Mar 6, 2024
4d266d0
Clear up scoring system data into target class and test overlap with …
TomHall2020 Mar 6, 2024
79dd1aa
Formatting, Rename alternative target constructor
TomHall2020 Mar 6, 2024
ffe72bb
Explicit test for get_face_spec on an invalid scoring system
TomHall2020 Mar 6, 2024
13fbad6
Simplify return statement in arrow_score
TomHall2020 Mar 6, 2024
86214de
Reactivate commented equality test in Pass.at_target constructor test
TomHall2020 Mar 6, 2024
9e4e5e4
fixup: also derive units in error message on from_face_spec constructor
TomHall2020 Mar 6, 2024
5f2575a
Refactor optional tuple parsing logic from Target to Length class
TomHall2020 Mar 7, 2024
0086d16
Refactor face_spec to property and set in all instances
TomHall2020 Mar 7, 2024
e63fa18
Manage diameter in property to recaulate face spec after changes
TomHall2020 Mar 8, 2024
1cb4052
Manage distance and scoring system as read only properties, return na…
TomHall2020 Mar 8, 2024
cdd6033
Clear up type hinting for FaceSpec, make read_only
TomHall2020 Mar 8, 2024
c4d6110
Increased test coverage of Length utilities
TomHall2020 Mar 8, 2024
6fa21af
Refactor Length classmethods to instance methods and provide instance…
TomHall2020 Mar 8, 2024
47b9c25
Enforce default 0 values for min and max score
TomHall2020 Mar 9, 2024
91536c9
Refactor face_spec property to be automatically set on initialisation…
TomHall2020 Mar 9, 2024
939da6c
Refactor target properties to store native lengths as Quantity tuples…
TomHall2020 Mar 9, 2024
ca8585c
Simplify equality check and remove unused is_custom computed property
TomHall2020 Mar 9, 2024
00e584d
Refactor pass properties and methods to match new target properties.
TomHall2020 Mar 9, 2024
e7b8860
Rename load_rounds.custom to load_rounds.misc to avoid confusion with…
TomHall2020 Mar 9, 2024
b057829
Documentation for custom scoring targets.
TomHall2020 Mar 9, 2024
ad568a6
Test length.known_units convinience property and simplify return type
TomHall2020 Mar 10, 2024
9cba392
Test case to cover generating face spec with invalid scoring system
TomHall2020 Mar 10, 2024
fa93843
Clean Target docstrings to remove old attributes that are now propert…
jatkinson1000 Mar 10, 2024
c5d4963
Refactor Length class to module and rename to convert
TomHall2020 Mar 11, 2024
88438c1
Revert name aliasing typing imports, rename convert back to length
TomHall2020 Mar 11, 2024
e9ba3ea
Docstring fixes in length module and examples in Quantity
TomHall2020 Mar 11, 2024
f3e5242
Add default value of metre for default parameter in parse_optional_units
TomHall2020 Mar 11, 2024
9de8048
Simplifiy signature and return type of Round.max_distance()
TomHall2020 Mar 11, 2024
9f8fcb3
Document recommened use of Quantity for parameters
TomHall2020 Mar 12, 2024
eca1bc6
Document supported units for distance and diameter under private clas…
TomHall2020 Mar 12, 2024
d252ce2
Use napoleon to process types explicitly
TomHall2020 Mar 12, 2024
ff055c4
Fix end of baseclases documentation, inline documentation comments fo…
TomHall2020 Mar 12, 2024
e0239a6
Repeat limitations on face_spec described in quickstart into Target.f…
TomHall2020 Mar 12, 2024
995681f
Seperate type aliases to their own page in API docs
TomHall2020 Mar 12, 2024
269b8a2
Update property docs for passthrough attributes on Pass
TomHall2020 Mar 13, 2024
a4bd132
Rearrange types in docs, touch up FaceSpec doc-comment and link from …
TomHall2020 Mar 18, 2024
879e9bc
Clarify in docstrings that ScoringSystem is a literal string value
TomHall2020 Mar 18, 2024
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
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
Loading