Source code for archeryutils.classifications.agb_old_field_classifications

"""
Code for calculating old Archery GB classifications.

Extended Summary
----------------
Code to add functionality to the basic handicap equations code
in handicap_equations.py including inverse function and display.

Routine Listings
----------------
_make_old_agb_field_classification_dict
calculate_old_agb_field_classification
old_agb_field_classification_scores

"""

from typing import Tuple, TypedDict

import archeryutils.classifications.classification_utils as cls_funcs
from archeryutils import load_rounds
from archeryutils.classifications.AGB_data import AGB_ages, AGB_bowstyles, AGB_genders
from archeryutils.rounds import Round

ALL_FIELD_ROUNDS = load_rounds.read_json_to_round_dict(
    [
        "WA_field.json",
    ],
)

AGB_FIELD_CLASSES = [
    "GMB",
    "MB",
    "B",
    "1C",
    "2C",
    "3C",
]
UNCLASSIFIED = "UC"

old_field_bowstyles = (
    AGB_bowstyles.COMPOUND
    | AGB_bowstyles.RECURVE
    | AGB_bowstyles.BAREBOW
    | AGB_bowstyles.ENGLISHLONGBOW
    | AGB_bowstyles.TRADITIONAL
    | AGB_bowstyles.FLATBOW
    | AGB_bowstyles.COMPOUNDLIMITED
    | AGB_bowstyles.COMPOUNDBAREBOW
)

sighted_bowstyles = (
    AGB_bowstyles.COMPOUND | AGB_bowstyles.RECURVE | AGB_bowstyles.COMPOUNDLIMITED
)

old_field_ages = AGB_ages.ADULT | AGB_ages.UNDER_18


class GroupData(TypedDict):
    """Structure for AGB Field classification data."""

    classes: list[str]
    class_scores: list[int]


def _get_old_field_groupname(
    bowstyle: AGB_bowstyles,
    gender: AGB_genders,
    age_group: AGB_ages,
) -> str:
    """
    Wrap function to generate string id for a particular category with old field guards.

    Parameters
    ----------
    bowstyle : AGB_bowstyles
        archer's bowstyle under old AGB field rules
    gender : AGB_genders
        archer's gender under old AGB field rules
    age_group : AGB_ages
        archer's age group under old AGB field rules

    Returns
    -------
    groupname : str
        single str id for this category
    """
    if bowstyle not in AGB_bowstyles or bowstyle not in old_field_bowstyles:
        msg = (
            f"{bowstyle} is not a recognised bowstyle for old field classifications. "
            f"Please select from `{old_field_bowstyles}`."
        )
        raise ValueError(msg)
    if gender not in AGB_genders:
        msg = (
            f"{gender} is not a recognised gender group for old field "
            "classifications. Please select from `archeryutils.AGB_genders`."
        )
        raise ValueError(msg)
    if age_group not in AGB_ages or age_group not in old_field_ages:
        msg = (
            f"{age_group} is not a recognised age group for old field "
            f"classifications. Please select from `{old_field_ages}`."
        )
        raise ValueError(msg)
    return cls_funcs.get_groupname(bowstyle, gender, age_group)


[docs] def coax_old_field_group( bowstyle: AGB_bowstyles, gender: AGB_genders, age_group: AGB_ages, ) -> cls_funcs.AGBCategory: """ Coax category not conforming to old field classification rules to one that does. Parameters ---------- bowstyle : AGB_bowstyles archer's bowstyle gender : AGB_genders archer's gender under AGB age_group : AGB_ages archer's age group Returns ------- TypedDict typed dict of archer's bowstyle, gender, and age_group under AGB coaxed to old field rules """ coax_bowstyle = bowstyle coax_gender = gender if age_group in (AGB_ages.UNDER_21 | AGB_ages.OVER_50): coax_age_group = AGB_ages.ADULT elif age_group != AGB_ages.ADULT: coax_age_group = AGB_ages.UNDER_18 else: coax_age_group = age_group return { "bowstyle": coax_bowstyle, "gender": coax_gender, "age_group": coax_age_group, }
def _make_agb_old_field_classification_dict() -> dict[str, GroupData]: """ Generate old (pre-2025) AGB field classification data. Generate a dictionary of dictionaries providing handicaps for each classification band. Parameters ---------- None Returns ------- classification_dict : dict of str : dict of str: list dictionary indexed on group name (e.g 'adult_female_recurve') containing list of scores associated with each classification References ---------- ArcheryGB 2023 Rules of Shooting ArcheryGB Shooting Administrative Procedures - SAP7 (2023) """ agb_field_scores = { ( AGB_bowstyles.COMPOUND, AGB_genders.MALE, AGB_ages.ADULT, ): [393, 377, 344, 312, 279, 247], ( AGB_bowstyles.COMPOUND, AGB_genders.FEMALE, AGB_ages.ADULT, ): [376, 361, 330, 299, 268, 237], ( AGB_bowstyles.RECURVE, AGB_genders.MALE, AGB_ages.ADULT, ): [338, 317, 288, 260, 231, 203], ( AGB_bowstyles.RECURVE, AGB_genders.FEMALE, AGB_ages.ADULT, ): [322, 302, 275, 247, 220, 193], ( AGB_bowstyles.BAREBOW, AGB_genders.MALE, AGB_ages.ADULT, ): [328, 307, 279, 252, 224, 197], ( AGB_bowstyles.BAREBOW, AGB_genders.FEMALE, AGB_ages.ADULT, ): [303, 284, 258, 233, 207, 182], ( AGB_bowstyles.LONGBOW, AGB_genders.MALE, AGB_ages.ADULT, ): [201, 188, 171, 155, 137, 121], ( AGB_bowstyles.LONGBOW, AGB_genders.FEMALE, AGB_ages.ADULT, ): [152, 142, 129, 117, 103, 91], ( AGB_bowstyles.TRADITIONAL, AGB_genders.MALE, AGB_ages.ADULT, ): [262, 245, 223, 202, 178, 157], ( AGB_bowstyles.TRADITIONAL, AGB_genders.FEMALE, AGB_ages.ADULT, ): [197, 184, 167, 152, 134, 118], ( AGB_bowstyles.FLATBOW, AGB_genders.MALE, AGB_ages.ADULT, ): [262, 245, 223, 202, 178, 157], ( AGB_bowstyles.FLATBOW, AGB_genders.FEMALE, AGB_ages.ADULT, ): [197, 184, 167, 152, 134, 118], ( AGB_bowstyles.COMPOUNDLIMITED, AGB_genders.MALE, AGB_ages.ADULT, ): [338, 317, 288, 260, 231, 203], ( AGB_bowstyles.COMPOUNDLIMITED, AGB_genders.FEMALE, AGB_ages.ADULT, ): [322, 302, 275, 247, 220, 193], ( AGB_bowstyles.COMPOUNDBAREBOW, AGB_genders.MALE, AGB_ages.ADULT, ): [328, 307, 279, 252, 224, 197], ( AGB_bowstyles.COMPOUNDBAREBOW, AGB_genders.FEMALE, AGB_ages.ADULT, ): [303, 284, 258, 233, 207, 182], ( AGB_bowstyles.COMPOUND, AGB_genders.MALE, AGB_ages.UNDER_18, ): [385, 369, 337, 306, 273, 242], ( AGB_bowstyles.COMPOUND, AGB_genders.FEMALE, AGB_ages.UNDER_18, ): [357, 343, 314, 284, 255, 225], ( AGB_bowstyles.RECURVE, AGB_genders.MALE, AGB_ages.UNDER_18, ): [311, 292, 265, 239, 213, 187], ( AGB_bowstyles.RECURVE, AGB_genders.FEMALE, AGB_ages.UNDER_18, ): [280, 263, 239, 215, 191, 168], ( AGB_bowstyles.BAREBOW, AGB_genders.MALE, AGB_ages.UNDER_18, ): [298, 279, 254, 229, 204, 179], ( AGB_bowstyles.BAREBOW, AGB_genders.FEMALE, AGB_ages.UNDER_18, ): [251, 236, 214, 193, 172, 151], ( AGB_bowstyles.LONGBOW, AGB_genders.MALE, AGB_ages.UNDER_18, ): [161, 150, 137, 124, 109, 96], ( AGB_bowstyles.LONGBOW, AGB_genders.FEMALE, AGB_ages.UNDER_18, ): [122, 114, 103, 94, 83, 73], ( AGB_bowstyles.TRADITIONAL, AGB_genders.MALE, AGB_ages.UNDER_18, ): [210, 196, 178, 161, 143, 126], ( AGB_bowstyles.TRADITIONAL, AGB_genders.FEMALE, AGB_ages.UNDER_18, ): [158, 147, 134, 121, 107, 95], ( AGB_bowstyles.FLATBOW, AGB_genders.MALE, AGB_ages.UNDER_18, ): [210, 196, 178, 161, 143, 126], ( AGB_bowstyles.FLATBOW, AGB_genders.FEMALE, AGB_ages.UNDER_18, ): [158, 147, 134, 121, 107, 95], ( AGB_bowstyles.COMPOUNDLIMITED, AGB_genders.MALE, AGB_ages.UNDER_18, ): [311, 292, 265, 239, 213, 187], ( AGB_bowstyles.COMPOUNDLIMITED, AGB_genders.FEMALE, AGB_ages.UNDER_18, ): [280, 263, 239, 215, 191, 168], ( AGB_bowstyles.COMPOUNDBAREBOW, AGB_genders.MALE, AGB_ages.UNDER_18, ): [298, 279, 254, 229, 204, 179], ( AGB_bowstyles.COMPOUNDBAREBOW, AGB_genders.FEMALE, AGB_ages.UNDER_18, ): [251, 236, 214, 193, 172, 151], } # Generate dict of classifications classification_dict = {} for group, scores in agb_field_scores.items(): groupdata: GroupData = {"classes": AGB_FIELD_CLASSES, "class_scores": scores} groupname = _get_old_field_groupname(*group) classification_dict[groupname] = groupdata return classification_dict old_agb_field_classifications = _make_agb_old_field_classification_dict() del _make_agb_old_field_classification_dict def _check_round_eligibility(archery_round: Round | str) -> Tuple[Round, str]: """ Check round is eligible for old field classifications. Parameters ---------- archery_round : Round | str an archeryutils Round object as suitable for this scheme alternatively the round codename as a str can be used Returns ------- archery_round : Round an archeryutils Round from the value passed in roundname : str codename of the round as it appears in the rounds dict Raises ------ ValueError If requested round is not in the rounds dict for this scheme """ if isinstance(archery_round, str) and archery_round in ALL_FIELD_ROUNDS: roundname = archery_round archery_round = ALL_FIELD_ROUNDS[roundname] elif ( isinstance(archery_round, Round) and archery_round in ALL_FIELD_ROUNDS.values() ): # Get string key for this round: roundname = list(ALL_FIELD_ROUNDS.keys())[ list(ALL_FIELD_ROUNDS.values()).index(archery_round) ] else: error = ( "This round is not recognised for the purposes of old AGB field " "classifications.\n" "Please select an appropriate option using `archeryutils.load_rounds`." ) raise ValueError(error) return archery_round, roundname
[docs] def calculate_agb_old_field_classification( archery_round: Round | str, score: float, bowstyle: AGB_bowstyles, gender: AGB_genders, age_group: AGB_ages, ) -> str: """ Calculate old (pre-2025) AGB field classification from score. Subroutine to calculate a classification from a score given suitable inputs. Parameters ---------- score : float numerical score on the round to calculate classification for archery_round : Round | str an archeryutils Round object as suitable for this scheme alternatively the round codename as a str can be used bowstyle : AGB_bowstyles archer's bowstyle under old AGB field rules gender : AGB_genders archer's gender under old AGB field rules age_group : AGB_ages archer's age group under old AGB field rules Returns ------- classification_from_score : str the classification appropriate for this score Raises ------ ValueError If an invalid score for the requested round is provided References ---------- ArcheryGB 2023 Rules of Shooting ArcheryGB Shooting Administrative Procedures - SAP7 (2023) Examples -------- >>> from archeryutils import classifications as cf >>> from archeryutils import load_rounds >>> wa_field = load_rounds.WA_field >>> cf.calculate_agb_old_field_classification( ... 247, ... wa_field.wa_field_24_red_marked, ... cf.AGB_bowstyles.RECURVE, ... cf.AGB_genders.MALE, ... cf.AGB_ages.ADULT, ... ) '2nd Class' """ archery_round, roundname = _check_round_eligibility(archery_round) # Check score is valid if score < 0 or score > archery_round.max_score(): msg = ( f"Invalid score of {score} for a {archery_round.name}. " f"Should be in range 0-{archery_round.max_score()}." ) raise ValueError(msg) groupname = _get_old_field_groupname(bowstyle, gender, age_group) # Get scores required on this round for each classification group_data = old_agb_field_classifications[groupname] # Check Round is appropriate: # Sighted can have any Red 24, unsighted can have any blue 24 if bowstyle in sighted_bowstyles and "wa_field_24_red_" not in roundname: return UNCLASSIFIED if bowstyle not in sighted_bowstyles and "wa_field_24_blue_" not in roundname: return UNCLASSIFIED # What is the highest classification this score gets? class_scores = dict( zip(group_data["classes"], group_data["class_scores"], strict=True) ) for classification, classification_score in class_scores.items(): if classification_score > score: continue else: return classification # if lower than 3rd class score return "UC" return UNCLASSIFIED
[docs] def agb_old_field_classification_scores( archery_round: Round | str, bowstyle: AGB_bowstyles, gender: AGB_genders, age_group: AGB_ages, ) -> list[int]: """ Calculate old (pre-2025) AGB field classification scores for category. Subroutine to calculate classification scores for a specific category and round. Appropriate ArcheryGB age groups and classifications. Parameters ---------- archery_round : Round | str an archeryutils Round object as suitable for this scheme alternatively the round codename as a str can be used bowstyle : AGB_bowstyles archer's bowstyle under old AGB field rules gender : AGB_genders archer's gender under old AGB field rules age_group : AGB_ages archer's age group under old AGB field rules Returns ------- classification_scores : list of int scores required for each classification in descending order References ---------- ArcheryGB Rules of Shooting ArcheryGB Shooting Administrative Procedures - SAP7 Examples -------- >>> from archeryutils import classifications as cf >>> from archeryutils import load_rounds >>> wa_field = load_rounds.WA_field >>> cf.agb_old_field_classification_scores( ... wa_field.wa_field_24_red_marked, ... cf.AGB_bowstyles.RECURVE, ... cf.AGB_genders.MALE, ... cf.AGB_ages.ADULT, ... ) [338, 317, 288, 260, 231, 203] """ archery_round, _ = _check_round_eligibility(archery_round) groupname = _get_old_field_groupname(bowstyle, gender, age_group) group_data = old_agb_field_classifications[groupname] # Get scores required on this round for each classification return group_data["class_scores"]