GithubHelp home page GithubHelp logo

jatkinson1000 / archeryutils Goto Github PK

View Code? Open in Web Editor NEW
11.0 3.0 3.0 540 KB

A collection of archery code and utilities in python

Home Page: https://archeryutils.readthedocs.io

License: MIT License

Python 91.40% Jupyter Notebook 8.60%
archery python sport

archeryutils's People

Contributors

jatkinson1000 avatar liampattinson avatar tomhall2020 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar

archeryutils's Issues

Tidy Misc Rounds

  • Remove preceding c_ from 252 (but issues with starting with number?)
  • Typo in 2 doz distances
  • Move number of doz to end, start with distance?

Project Documentation needs improving

Currently very sparse project documentation.
Add information on how to:

  • install
  • use
  • contribute - ownership etc.
  • code of conduct
  • license
  • run examples - update the examples file!!
  • support
  • coverage and formatting badges

Single Classification wrapper function

Currently classifications are called depending on the type of classification required.

Consider a single generic wrapper function that can be called that then recursively calls the existing functions.

Support and testing for python <= 3.7

As a result of #25 the minimum python version was set at 3.8 to satisfy the minimum numpy of 1.22, which in turn was required to pass mypy --strict (full type hinting support of internal functions was only added in numpy 1.22).

However, the code runs perfectly fine on python 3.7.

Can we remove mypy as a dependency and install/run it only during the CI process?
And if so can we set things up so that we run CI testing on python <= 3.7, but not mypy?

One Qn - if we do this how do we indicate this in documentation given we will be passing strict mypy, but only on certain versions?

Bump to Python 3.10

It has come up in a couple of PRs (#67 #59) that features of python 3.10+ would be useful.
It is a shame to be held back from using features/have to implement messy conditional workarounds.

Whilst 3.9 is still 'supported' by python, the scientific python SPEC0 suggests dropping support towards end of 2023.

Therefore it does not seem unreasonable to raise minimum support to 3.10

Feature Request: Add 11-zone (Lancaster) Target to scoring systems

It would be good to add the Lancaster Archery target as a possible scoring system (X is 11, compound and full face), and the Lancaster Classic round (18m, 40cm).

See chapter 4 of the Lancaster Classic Rules.

e.g.

import archeryutils as au

myLancastertarget = au.Target("Lancaster", 40, 80)
myLancasterpass = au.Pass(30, "Lancaster", 40, 80, indoor=True)
myLancasterClassicRound = au.Round("lancaster_classic", [myLancasterpass, myLancasterpass])

myLancastertarget_comp = au.Target("Lancaster_6_ring", 40, 80)
myLancasterpass_comp = au.Pass(30, "Lancaster_6_ring", 40, 80, indoor=True)
myLancasterClassicRound_comp = au.Round("lancaster_classic_compound", [myLancasterpass_comp, myLancasterpass_comp])

hc_eq.score_for_round should not return a tuple

Currently hc_eq.score_for_round returns a tuple of the score for the round and the score for each distance.
This is not an intuitive use, however.

Need a function hc_eq.score_for_round that returns just a score.
Need a new function hc_eq.score_for_round_distances that returns an array.

Need to rectify this soon as it will have a lot of knock on effects elsewhere such as classifications and generating handicap tables.

Documentation: Describe supported scoring systems (with visual examples?)

It would be helpful to have a section in the docs with a bit of a blurb on each of the supported scoring systems to help users figure out which one they want to use. Some graphical representations of the targets would be even better.

Not going to sign up to implement this one as I'd like to get #56 squared off and then I need to focus back on archr for a while, but its probably not urgent.

Documentation

Aiming for a release in #49 we should add some documentation.

Sphinx may be a good option

Data from read_json functions could be stored as constants

I noticed during work on #67 that the read_json functions are called seperately in each AGB disciplines make_classification_dict function and stored to locals.

These could be called once and saved as constants, as any change to the data being loaded would require installing an updated version of archeryutils.

Bug: Possibility to define a round with no passes

It is possible to define a round with no passes leading to undefined behaviour when subsequently used.
It is unlikely one would do this intentionally, but perhaps there should be a check that the input parameter is a list of Pass (or a single Pass?).

Round object has to be defined with a name

At present a Round object has to defined with a name and a list of Passes.
However, for abstract use we may just want to define passes and no name.
Consider a refactor to make name an optional argument.

Will have knock on effects where Round is used as calls will need to change.
Breaking change?

Requires:

  • Review how much of a breaking change this will be
  • Update Rounds.py Round class
  • Update Rounds tests
  • Update knock on effects in rest of code
  • Update any archerycalculator code affected
  • Notify of changes in docs?

get_max_score_handicap is slowing down test suite

Running the test suite locally on my machine takes around 4-6 seconds, but it is obvious from watching it that there are a few particularly slow tests that I have to wait to finish one by one. pytest --durations shows that the slowest tests are all related to get_max_score_handicap, in fact 12 tests account for about 2.5s out of the total execution time of 3.5s here.

❯ python -W ignore -m pytest --durations 20
=============================================================== test session starts ===============================================================
platform darwin -- Python 3.11.5, pytest-7.4.4, pluggy-1.3.0
rootdir: /Users/Tom/dev/forks/archeryutils
plugins: mock-3.12.0
collected 369 items

archeryutils/classifications/tests/test_agb_field.py ...............................                                                        [  8%]
archeryutils/classifications/tests/test_agb_indoor.py .......................................                                               [ 18%]
archeryutils/classifications/tests/test_agb_old_indoor.py .....................                                                             [ 24%]
archeryutils/classifications/tests/test_agb_outdoor.py ..................................................                                   [ 38%]
archeryutils/classifications/tests/test_classification_utils.py ..........                                                                  [ 40%]
archeryutils/handicaps/tests/test_handicap_tables.py .......................                                                                [ 47%]
archeryutils/handicaps/tests/test_handicaps.py ............................................................................................ [ 72%]
.......                                                                                                                                     [ 73%]
archeryutils/tests/test_constants.py ..............                                                                                         [ 77%]
archeryutils/tests/test_rounds.py ...................................                                                                       [ 87%]
archeryutils/tests/test_targets.py ...............................................                                                          [100%]

============================================================== slowest 20 durations ===============================================================
0.32s call     archeryutils/handicaps/tests/test_handicaps.py::TestHandicapFromScore::test_maximum_score[AGB-testround0-720-11]
0.32s call     archeryutils/handicaps/tests/test_handicaps.py::TestHandicapFromScore::test_get_max_score_handicap[testround0-AGB-True-11]
0.26s call     archeryutils/handicaps/tests/test_handicaps.py::TestHandicapFromScore::test_maximum_score[AA-testround1-720-107]
0.26s call     archeryutils/handicaps/tests/test_handicaps.py::TestHandicapFromScore::test_get_max_score_handicap[testround1-AA-True-107]
0.21s call     archeryutils/handicaps/tests/test_handicaps.py::TestHandicapFromScore::test_get_max_score_handicap[testround2-AGB-False-9.89]
0.21s call     archeryutils/handicaps/tests/test_handicaps.py::TestHandicapFromScore::test_maximum_score[AGB-testround2-864-9]
0.18s call     archeryutils/handicaps/tests/test_handicaps.py::TestHandicapFromScore::test_get_max_score_handicap[testround3-AGBold-True-6]
0.18s call     archeryutils/handicaps/tests/test_handicaps.py::TestHandicapFromScore::test_maximum_score[AGBold-testround3-864-6]
0.17s call     archeryutils/handicaps/tests/test_handicaps.py::TestHandicapFromScore::test_get_max_score_handicap[testround4-AGB-True-3]
0.17s call     archeryutils/handicaps/tests/test_handicaps.py::TestHandicapFromScore::test_maximum_score[AGB-testround4-300-3]
0.13s call     archeryutils/handicaps/tests/test_handicaps.py::TestHandicapFromScore::test_get_max_score_handicap[testround5-AA-False-118.38]
0.13s call     archeryutils/handicaps/tests/test_handicaps.py::TestHandicapFromScore::test_maximum_score[AA-testround5-300-119]
0.02s call     archeryutils/handicaps/tests/test_handicap_tables.py::TestHandicapTable::test_check_print_table_inputs[hcs0-False-expected0]

(7 durations < 0.005s hidden.  Use -vv to show these durations.)
=============================================================== 369 passed in 3.47s ===============================================================

I profiled the get_max_score_handicap function with cProfile and found that it was taking about 6500 iterations to find the handicap for max score on a York round, which is a relatively generous case as that is an AGB handicap of -9.8. Easier rounds will take even more iterations to solve, and are relatively more likely to be requested (maximum scores on a York are pretty unlikely for most...)

❯ python -W ignore scratch/debug_slow_max_score.py
         247587 function calls in 0.353 seconds

   Ordered by: standard name

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1    0.000    0.000    0.353    0.353 <string>:1(<module>)
     6515    0.001    0.000    0.001    0.000 fromnumeric.py:2172(_sum_dispatcher)
     6515    0.010    0.000    0.047    0.000 fromnumeric.py:2177(sum)
     6515    0.012    0.000    0.036    0.000 fromnumeric.py:71(_wrapreduction)
     6515    0.006    0.000    0.006    0.000 fromnumeric.py:72(<dictcomp>)
    19545    0.037    0.000    0.037    0.000 handicap_equations.py:174(sigma_t)
    19545    0.011    0.000    0.048    0.000 handicap_equations.py:287(sigma_r)
    19545    0.073    0.000    0.257    0.000 handicap_equations.py:338(arrow_score)
    97725    0.109    0.000    0.109    0.000 handicap_equations.py:421(<genexpr>)
     6515    0.008    0.000    0.286    0.000 handicap_equations.py:517(score_for_passes)
     6515    0.017    0.000    0.274    0.000 handicap_equations.py:570(<listcomp>)
     6515    0.012    0.000    0.347    0.000 handicap_equations.py:580(score_for_round)
        1    0.006    0.006    0.353    0.353 handicap_functions.py:150(get_max_score_handicap)
        3    0.000    0.000    0.000    0.000 rounds.py:143(max_score)
        1    0.000    0.000    0.000    0.000 rounds.py:236(max_score)
        4    0.000    0.000    0.000    0.000 rounds.py:245(<genexpr>)
        3    0.000    0.000    0.000    0.000 targets.py:176(max_score)
        1    0.000    0.000    0.000    0.000 {built-in method _warnings.warn}
        1    0.000    0.000    0.353    0.353 {built-in method builtins.exec}
     6515    0.002    0.000    0.002    0.000 {built-in method builtins.isinstance}
    19546    0.027    0.000    0.136    0.000 {built-in method builtins.sum}
     6515    0.005    0.000    0.005    0.000 {built-in method numpy.array}
        1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}
     6515    0.001    0.000    0.001    0.000 {method 'items' of 'dict' objects}
     6515    0.017    0.000    0.017    0.000 {method 'reduce' of 'numpy.ufunc' objects}


round_=Round('York')
Result of get_max_score_handicap(york, 'AGB')
===================
-9.869999999998377

Fundamentally the issue is just taking too many iterations at a small step size of 0.01 on the handicap scale. That accuracy is only needed when we are very close to a handicap that will produce a sub-maximum score, everything before then is wasted. I think I was able to fix this and have a PR ready that implements it.

Feature Request: Define Pass using existing Target

Often we define a target before defining a Pass, especially in the examples.
However, we then pass exactly the same information to the Pass constructor that internally re-makes a Target.
It would be nice, if we have already constructed a Target, to pass this directly to an alternative Pass constructor.

May need to be careful with ensuring that we use a copy rather than a reference in the Pass...

import archeryutils as au

mytarget = au.Target("10_zone", (122, "cm"), (70.0, "m"))

mypass = au.Pass(36, mytarget)

Make some handicap_functions methods private

All except handicap_from_score and print_handicap_table should be private.
To do this requires removing the explicit tests for any private functions and instead testing them implicitly through the above two.

This will require tests writing for print_handicap_table which should bump coverage up.

Allow handicap calculation for generic scoring systems

Presently the supported scoring systems are definied in the Target class and used to guide the calculation of expected arrow score, by dispatching to an inlined formula. Therefore only explicitly listed scoring systems are usable. This also makes the mathematical part of the code in arrow_score() harder to read and maintain

It would be useful to make this generic and be able to directly calculate the expected arrow_score and therefore handicap for any custom scoring system (eg 11zone or kings of archery scoring for indoors) , by directly providing the parameters defining the rings of a target face, or also in my intended use case to just provide the target information directly rather than having to maintain a mapping across to the archeryutils named scoring systems for different targets.

I've got an implementation part cooked up already having reverse engineered the formulas in handicap_equations, so the more interesting bit for me is how to expose an API to use it.

Get CI green (address pylint)

Currently the CI checks fail for pylint (added in #25 ).
Whilst some of these are legitimate to ignore (variable names) others hint at the need for some refactoring and will be addressed in some other issues.

After looking at these we should then return to place any ignores that we feel are legitimately warranted.

Issues to tackle that will help address this:

HcParams leads to a complicated interface

It'd be good to hide a lot of the HCParams stuff, maybe by making it an optional argument to the functions that use it, and falling back to default values if it isn't provided. I think the ideal system would be something like:

from archeryutils import score_from_handicap, handicap_from_score, rounds

# Default to current AGB methods
myscore = score_from_handicap(rounds.york, 38)
myhc = handicap_from_score(rounds.portsmouth, 575)

Alternative handicap systems could be used by setting a context using fancy with methods, or as optional args to the standard functions. Or we could just have a global context setter:

archeryutils.set_handicap_system("some_other_method")

Bug in Vegas300 round

Vegas 300 gives wrong scores for handicaps as it is using 18 m, not 20 yds.

See AGB Rules of Shooting.

New indoor classifications

Outfit the code in preparation for the new indoor classifications:

  • Deprecate current outdoor functions
  • Update json data files to have indoor and outdoor data
    • Update existing classification functions to use new parameters for outdoors
  • Implement new indoor classification structure
    • start with dict formed identical as outdoor
      • update ages to be a functions
      • Investigate new parameters
    • Get classification and scores needs to:
      • Handle trad/flatbow/asiatic
      • handle [ ] triple faces and [ ] compound scoring
        • WA
        • Portsmouth
        • Stafford
        • Other?
  • Xs on Worcester and Vegas? - May be separate issue...

Exact comparisons with floating points in tests

There are a few instances in the tests where we assert exact equality between floating point numbers. In some cases this makes sense, such as assert target.diameter == 1.22. Other cases might be prone to unexpectedly failing in the future due to floating point round-off errors (I've had this happen in other projects due to NumPy/SciPy updates).

Upload to pypi

Is there much else that needs to be done before this can be uploaded?

Am currently working on a project using this and being able to properly install and version it rather than installing from source would be quite helpful.

Happy to try and contribute if anything needs doing to prepare for this.

Type hinting improvements

Typing could be made clearer in the project by replacing several Union types for inputs and return values, and replacing them with generic type variables to make it explict that eg floats as input give floats as output, arrays in give arrays out.

Also the imported container types (Tuple, List, Dict) can be replaced with the builtin types, since these are deprecated since python 3.9

Simplify importing classes/functions

I think it'd be a good idea to expose some classes/functions to the top-level namespace. It'd be nice if we could do stuff like:

import archeryutils as au
my_target = au.Target(...)

We'll have to be fairly selective though, as most of the internals probably don't need to be part of the user API.

Typing updates

As the project is for Python 3.10+, a few of the type hints could be updated to use union type expressions:

Union[int, float] -> int | float
Optional[int] -> int | None

It shouldn't make any difference to type checkers, but it seems to be the modern way of doing things, and I suppose it saves a few keystrokes.

Better tests needed for Archery Australia Algorithm

Currently there is no easily and reliable data available for the Archery Australia Algorithms so there are no tests against external values in the code for this algorithm.

AA officially uses Archer's Diary for ratings, but there are inconsistencies in the results which I have raised as a bug report there.
Bow international published an article with decimal numbers but these do not match our results (approx 0.15 out), though this could be due to differences in rounding procedures between codes.

Consider searching for external sources or contacting James Park.

Refactor classifications

Classifications has grown to become rather behemothic.
Consider refactoring into separate files for each classification.

Units for target face diameter

Discussed in #17

  • Add option for user to specify target face diameter unit
  • Set default to be [cm]
  • Store under the hood as [m] to preserve SI and not affect code elsewhere

This requires:

  • Add optional argument to Target class
  • Add conversion code as part of __init__ in Target class
  • Add INCHES2M and CM2M to constants file
  • Add tests for conversion process
  • Update and round data files accordingly

UserWarning spam on module import

This one shouldn't be hard but definitely needs fixing, at the moment loading the default rounds raises a ton of UserWarnings for not setting round families. Given this happens at module import time and the user has absolutely no choice about it, this is pretty annoying and fills up the terminal in interactive sessions, test output etc.

Since its fine to instantiate a round directly in the API without specifying family, location etc, this seems like a dev/debug hint, so better to suppress them and have a way to opt back in for dev builds, or just decide if the data is in a good state now and get rid of the warnings in read_json_to_round_dict entirely?

Feature Request: Move to ruff for linting

At the suggestion of @TomHall2020 I have been looking into using ruff for linting purposes.

In terms of formatting it is compatible with black.

For linting there is reasonable compatibility with pylint, though a number of rules are yet to be implemented (see astral-sh/ruff#970 (comment)).
Therefore I would consider using both, or at least encourage checking locally with pylint to maintain quality.

For documentation I note that the pydocstyle project has now been deprecated.
ruff contains tools to cover this, and can be set up for similar monitoring of numpy docstring conventions as are used in archeryutils.

As an all-in-one tool ruff contains a compatible implementation of isort (incompatibilities with other tools were why isort could not be included in #63).

I have been working on a local configuration as part of pyproject.toml that I will upload should I move forward with this.

In terms of workflows I would like to keep checks separate (formatting, linting, docstrings) as I feel this is useful. This will require looking into how to override the pyproject.toml in some way, but ideally without creating a fully separate configuration that must be co-maintained.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.