-
Notifications
You must be signed in to change notification settings - Fork 2
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
Generic scoring #59
Conversation
Will duplicate my comment here from issue #56 now a PR is open:
I am currently working on #58 and #2 so be aware we may have conflict resolution at some point (lets cross that bridge when we get there). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks @TomHall2020
The addition of the FloatArray type is great.
However, as you suggest the wider code would benefit from this, so I would consider moving it to consts module. This takes it beyond the scope of this PR, but I may incorporate it as part of #58 or in its own one (unless you want to do so...?).
The code would also improve if you stick the different target specs in a dictionary at the top of the module, and then the long elif
structure could be replaced by a single call using spec[system]
. I agree this can be simplified further, however, by...
associating spec
with the Target
class, as it is integral to the target type.
That way these functions can just be passed a Target
, greatly simplifying inputs.
I think if a target scoring system is specified as a string (as is the case at present) then the constructor should auto-assign the face_spec
attribute from a dict of pre-defined specs (as described in the above paragraph). I would then add the option of passing "custom"
, in which case the user is also required to provide an optional face_spec
argument. How does that sound?
Edit: On reflection below, I think perhaps we should implement a @classmethod called custom_scoring or similar that takes a spec
and returns a Target
using it. See here for an example.
Thoughts?
The other think I note is that itertools.chain
itertools.Pairwise
is a python 3.10 feature, so removes the ability to support python v3.9 (EoL Oct 2025). Don't worry about this for now - I'll have a think if it makes sense to drop support of if there is a sensible workaround. It doesn't make sense to just refuse new features that will become mainstream in future.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some specific comments in addition to the above:
I could do a seperate typing PR, also had spotted a few other things before I worked on this that could be cleaned up. Included it in this inittially to keep mypy quiet.
Yes! I as soon as I made
So these were the two methods I was going to write up to choose between, handing the spec in the constructor vs having a seperate classmethod. The former makes for a more complex
|
Happy for that.
Yes, I had also been eyeing up making a
👍
Yeah, realised I made a typo. This is the annoying thing - it doesn't make sense not to use features when they exist and are a great match, but at the same time keeping support can be useful. It's a small use so can worry about it later. |
Ah I see what happened here, to clarify for the record its import itertools as itr
def _pairwise(iterable):
a, b = itr.tee(iterable)
next(b, None)
return zip(a, b)
if not hasattr(itr, "pairwise"):
setattr(itr, "pairwise", _pairwise) Or just implement it with the tee method and not worry about realiasing, with a note to replace with pairwise when supporting 3.10+ |
Yes, I saw that solution when I read up on this. |
cf0b457
to
740eeff
Compare
Resubmitted coverage and looks like you need tests for the returns of the Is this still WIP? Hit the request re-review when you want me to take a closer look. You also probably want to rebase off of main (and resolve the conflicts) - they don't look too bad. |
860404a
to
43d2821
Compare
09c4ecf
to
fb81c76
Compare
Right, so this is now up to date and based off main after the big rewrite of #78 Fundamentally the functionality hasn't changed in the latest version here, but to take stock:
|
fb81c76
to
bcf7c34
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK, had a proper read of this now and I's very nice.
My main observation is that I think things could be a lot tidier and standardised if it was just expected that every target has a _face_spec
.
So as part of the __init__
in the Target
class it should just call get_face_spec
and assign as appropriate from the scoring system supplied.
This would then allow you to remove the Rings
class (and in fact also scoring_system_data
) which smells a bit off to me and just use the max/min of _facespec
I think?
I would still keep the default constructor as using 'common' names, with from_facespec
being an overload.
Few other (mostly smallish) comments/suggestions in the comments.
Check the version of ruff you are running - the ...
not being on the same line caught me out previously.
If we merge #82 you can get rid of the faffing for satisfying 3.9 and a couple of other things.
Having had a proper read of the code I'm inclined to say keep _s_bar
rather than merging into arrow_score
as it keeps things nice and compartmentalised.
This makes a lot of sense but does open up one can of worms I haven't dared address yet: mutability. Generating the spec on inititalisation and storing makes sense, but we would have to then disallow modification of the target. Easy enough to do if it were a dataclass, I'm not sure what the best approach here would be.
Huh, same thing happened, my system ruff ran in place of the virtual enviroment one and it was on 0.22.
Ah ok, I missed reading this before commenting, fine with that. |
Unlikely to really happen, but I agree it should be guarded against/prevented if going down this route. Using a setter method for diameter might work...? |
I'm brewing on this one. Properties and setters could work but I haven't immediately wrapped my head around how that interacts with the smart behaviour in the constructor (accepting tuple with unit or scalar value). Another option is immutability lite, I kind of like the approach in this stack overflow answer of "freezing" the object after init by raising an Error from diff --git a/archeryutils/targets.py b/archeryutils/targets.py
index 261d79d..14ad77c 100644
--- a/archeryutils/targets.py
+++ b/archeryutils/targets.py
@@ -92,6 +92,7 @@ class Target:
"""
+ _frozen = False
_face_spec: FaceSpec
# Lookup data for min and max scores for supported scoring systems.
@@ -162,6 +163,7 @@ class Target:
self.distance = distance
self.native_dist_unit = Length.definitive_unit(native_dist_unit)
self.indoor = indoor
+ self._frozen = True
@classmethod
def from_face_spec(
@@ -221,9 +223,16 @@ class Target:
}
target = cls("Custom", diameter, distance, indoor)
- target._face_spec = face_spec # noqa: SLF001 private member access
+ object.__setattr__(target, "_face_spec", face_spec)
return target
+ def __setattr__(self, name, value):
+ """Prevent modification of attributes after initialisation."""
+ if self._frozen:
+ msg = "Cannot modify attributes on Target instance"
+ raise AttributeError(msg)
+ object.__setattr__(self, name, value)
+
def __repr__(self) -> str:
"""Return a representation of a Target instance."""
diam, diamunit = self.native_diameter |
Handles special case where empty spec passed to custom target, is consitent with implmentation of _s_bar which returns an average score of 0 in such cases
… for all targets Can remove tests for deliberate mutation of private _scoring_system after initialisation as the behaviour is now undefined. Simplifies logic in face_spec getter property and removes checks from the happy path. Re raises an error in case the user accidently tries to create a custom scoring target via the regular constructor.
… instead of just native units
All pass properties are now a straight passthrough to the corresponding matching target properties. Round.max_distance refactored to replace manual iteration with native max(), and use new pass properties.
… custom scoring targets
Reorder source code and docstrings to be consistent in ordering of diameter and distance with function signature.
…ies causing Sphinx to display duplicates. Also make internal data on supported systems and units private.
Avoids confusion of initialising single instance and passing around. All important data for calculating conversions and aliases was already stored as global constants in the module, so now just use these directly from functions instead of incorporating into class
Add note to Quanity explaining why explicitly passing units is recommended Make Quantity accessible from package namespace
Now always returns a Quantity so no reason for users not to know what units the return value is and avoids potential footguns. Existing library code adjusted to extract the raw value without futher changes
Allows deduplication of explicitly listing supported scoring systems inside the Target docstring and instead refers to the existing ScoringSystem type alias.
…r type aliases, create links for FaceSpec
…rom_face_spec docstring
…to target constructor for more details
84d6c6f
to
879e9bc
Compare
Codecov ReportAll modified and coverable lines are covered by tests ✅
Additional details and impacted files@@ Coverage Diff @@
## main #59 +/- ##
==========================================
+ Coverage 97.47% 97.61% +0.13%
==========================================
Files 28 28
Lines 1505 1592 +87
==========================================
+ Hits 1467 1554 +87
Misses 38 38
|
Changes incorporated, rebased onto main and dealt with the merge conflicts, squashed a few of the tiny commits together while I was rebasing to clean the history up a little bit. Yeah, I really wanted to get this done as well, just didn't quite get it over the line before the weekends selection tournament. Hoping this is the one now. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hi @TomHall2020 - grabbing a few morning minutes to get this over the line!
Happy for this to go in now, and thanks for your work and patience.
I'm really pleased with this as, though there look to be a lot of changes, the representation of target faces is now a lot more versatile and clean!
Closes #56
Initial implementation of generic scoring systems for targets. First stage of this PR primarily introduces the concept of a target specification (
spec
), which is a mapping of target ring sizes in metres to ring scores. This can then be used to calculate expected score using sigma_r and arrow diameter in a very similar way to what was already being done, but with arbitrary inputs.Hardcoded calculations of expected score are replaced with a calculation from target specifications, and specifications for the supported scoring systems can be derived automatically so existing code is unaffected.
There is not yet a constructor for targets/passes with custom scoring, that its something I wanted to get suggestions on, so this is not a final version by any means.
Other side effects of sorting out the linting/mypy for this include improved type hinting in handicap_equations with type vars, so that functions return type matches the input type rather than being incorrectly hinted as returning the union of floats/arrays of floats. I didn't clean up anything beyond this file but that may allow some other parts of the codebase to be smoother over.