jatkinson1000 / archeryutils Goto Github PK
View Code? Open in Web Editor NEWA collection of archery code and utilities in python
Home Page: https://archeryutils.readthedocs.io
License: MIT License
A collection of archery code and utilities in python
Home Page: https://archeryutils.readthedocs.io
License: MIT License
c_
from 252 (but issues with starting with number?)Currently very sparse project documentation.
Add information on how to:
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.
Coverage is at 95%, but could be bumped higher.
Not urgent, however.
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?
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
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])
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.
Add clout rounds into the functionality.
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.
Aiming for a release in #49 we should add some documentation.
Sphinx may be a good option
See title.
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.
Full face is an option, so need to define full face and spots Vegas 300.
Tests for handicaps have been added, but are yet to be written for classifications.
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
?).
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:
Round
classRunning 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.
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)
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.
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.
See discussion in #52:
Originally posted by @jatkinson1000 in #52 (comment)
Combine magnitude and unit as a tuple, and allow either just magnitude (unit assumed), or magnitude and unit to be passed and processed.
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:
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")
Vegas 300 gives wrong scores for handicaps as it is using 18 m, not 20 yds.
See AGB Rules of Shooting.
Would be good if we could add some CI:
Outfit the code in preparation for the new indoor classifications:
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).
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.
The lists of methods are out of date in the testing routines.
Either remove them or update.
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
At present there is a complete lack of testing.
[![Documentation Status](https://readthedocs.org/projects/archeryutils/badge/?version=latest)](https://archeryutils.readthedocs.io/en/latest/?badge=latest)
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.
Range of (2, 7)
used rather than (1, 6)
.
Leads to targets (and hence scores) being too large.
Spotted by @TomHall2020
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.
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.
Classifications has grown to become rather behemothic.
Consider refactoring into separate files for each classification.
This requires:
Target
class__init__
in Target
classINCHES2M
and CM2M
to constants
fileThis 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?
VI and field rounds are not loaded.
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.
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.