|
34 | 34 | import warnings
|
35 | 35 | from abc import ABC, abstractmethod
|
36 | 36 | from typing import Optional, TypeVar, Union, overload
|
| 37 | +import itertools as itr |
37 | 38 |
|
38 | 39 | import numpy as np
|
39 | 40 | import numpy.typing as npt
|
|
42 | 43 |
|
43 | 44 | FloatArray = TypeVar("FloatArray", float, npt.NDArray[np.float_])
|
44 | 45 |
|
| 46 | +# itertools.pairwise not available until python 3.10 |
| 47 | +# workaround can be removed when support for 3.9 is dropped |
| 48 | +# ignore for coverage (runner is > 3.10, ci shows this works on 3.9) |
| 49 | +if not hasattr(itr, "pairwise"): # pragma: no cover |
| 50 | + |
| 51 | + def _pairwise(iterable): |
| 52 | + a, b = itr.tee(iterable) |
| 53 | + next(b, None) |
| 54 | + return zip(a, b) |
| 55 | + |
| 56 | + setattr(itr, "pairwise", _pairwise) |
45 | 57 |
|
46 | 58 | class HandicapScheme(ABC):
|
47 | 59 | r"""
|
@@ -216,104 +228,48 @@ def arrow_score(
|
216 | 228 | arw_d = self.arw_d_out
|
217 | 229 |
|
218 | 230 | arw_rad = arw_d / 2.0
|
219 |
| - |
220 |
| - tar_dia = target.diameter |
| 231 | + spec = target.get_face_spec() |
221 | 232 | sig_r = self.sigma_r(handicap, target.distance)
|
| 233 | + s_bar = self._s_bar(spec, arw_rad, sig_r) |
222 | 234 |
|
223 |
| - if target.scoring_system == "5_zone": |
224 |
| - s_bar = ( |
225 |
| - 9.0 |
226 |
| - - 2.0 |
227 |
| - * sum( |
228 |
| - np.exp(-((((n * tar_dia / 10.0) + arw_rad) / sig_r) ** 2)) |
229 |
| - for n in range(1, 5) |
230 |
| - ) |
231 |
| - - np.exp(-((((5.0 * tar_dia / 10.0) + arw_rad) / sig_r) ** 2)) |
232 |
| - ) |
233 |
| - |
234 |
| - elif target.scoring_system == "10_zone": |
235 |
| - s_bar = 10.0 - sum( |
236 |
| - np.exp(-((((n * tar_dia / 20.0) + arw_rad) / sig_r) ** 2)) |
237 |
| - for n in range(1, 11) |
238 |
| - ) |
239 |
| - |
240 |
| - elif target.scoring_system == "10_zone_6_ring": |
241 |
| - s_bar = ( |
242 |
| - 10.0 |
243 |
| - - sum( |
244 |
| - np.exp(-((((n * tar_dia / 20.0) + arw_rad) / sig_r) ** 2)) |
245 |
| - for n in range(1, 6) |
246 |
| - ) |
247 |
| - - 5.0 * np.exp(-((((6.0 * tar_dia / 20.0) + arw_rad) / sig_r) ** 2)) |
248 |
| - ) |
249 |
| - |
250 |
| - elif target.scoring_system == "10_zone_compound": |
251 |
| - s_bar = ( |
252 |
| - 10.0 |
253 |
| - - np.exp(-((((tar_dia / 40.0) + arw_rad) / sig_r) ** 2)) |
254 |
| - - sum( |
255 |
| - np.exp(-((((n * tar_dia / 20.0) + arw_rad) / sig_r) ** 2)) |
256 |
| - for n in range(2, 11) |
257 |
| - ) |
258 |
| - ) |
259 |
| - |
260 |
| - elif target.scoring_system == "10_zone_5_ring": |
261 |
| - s_bar = ( |
262 |
| - 10.0 |
263 |
| - - sum( |
264 |
| - np.exp(-((((n * tar_dia / 20.0) + arw_rad) / sig_r) ** 2)) |
265 |
| - for n in range(1, 5) |
266 |
| - ) |
267 |
| - - 6.0 * np.exp(-((((5.0 * tar_dia / 20.0) + arw_rad) / sig_r) ** 2)) |
268 |
| - ) |
269 |
| - |
270 |
| - elif target.scoring_system == "10_zone_5_ring_compound": |
271 |
| - s_bar = ( |
272 |
| - 10.0 |
273 |
| - - np.exp(-((((tar_dia / 40) + arw_rad) / sig_r) ** 2)) |
274 |
| - - sum( |
275 |
| - np.exp(-((((n * tar_dia / 20) + arw_rad) / sig_r) ** 2)) |
276 |
| - for n in range(2, 5) |
277 |
| - ) |
278 |
| - - 6.0 * np.exp(-((((5 * tar_dia / 20) + arw_rad) / sig_r) ** 2)) |
279 |
| - ) |
280 |
| - |
281 |
| - elif target.scoring_system == "WA_field": |
282 |
| - s_bar = ( |
283 |
| - 6.0 |
284 |
| - - np.exp(-((((tar_dia / 20.0) + arw_rad) / sig_r) ** 2)) |
285 |
| - - sum( |
286 |
| - np.exp(-((((n * tar_dia / 10.0) + arw_rad) / sig_r) ** 2)) |
287 |
| - for n in range(2, 7) |
288 |
| - ) |
289 |
| - ) |
| 235 | + return s_bar |
290 | 236 |
|
291 |
| - elif target.scoring_system == "IFAA_field": |
292 |
| - s_bar = ( |
293 |
| - 5.0 |
294 |
| - - np.exp(-((((tar_dia / 10.0) + arw_rad) / sig_r) ** 2)) |
295 |
| - - np.exp(-((((3.0 * tar_dia / 10.0) + arw_rad) / sig_r) ** 2)) |
296 |
| - - 3.0 * np.exp(-((((5.0 * tar_dia / 10.0) + arw_rad) / sig_r) ** 2)) |
297 |
| - ) |
| 237 | + def _s_bar( |
| 238 | + self, target_specs: dict[float, int], arw_rad: float, sig_r: FloatArray |
| 239 | + ) -> FloatArray: |
| 240 | + """Calculate expected score directly from target ring sizes. |
298 | 241 |
|
299 |
| - elif target.scoring_system == "Beiter_hit_miss": |
300 |
| - s_bar = 1.0 - np.exp(-((((tar_dia / 2.0) + arw_rad) / sig_r) ** 2)) |
| 242 | + Parameters |
| 243 | + ---------- |
| 244 | + target_specs : dict[float, int] |
| 245 | + Mapping of target ring *diameters* in [metres], to points scored |
| 246 | + arw_d : float |
| 247 | + arrow diameter in [metres] |
| 248 | + sig_r : float |
| 249 | + standard deviation of group size [metres] |
301 | 250 |
|
302 |
| - elif target.scoring_system in ("Worcester", "IFAA_field_expert"): |
303 |
| - s_bar = 5.0 - sum( |
304 |
| - np.exp(-((((n * tar_dia / 10.0) + arw_rad) / sig_r) ** 2)) |
305 |
| - for n in range(1, 6) |
306 |
| - ) |
| 251 | + Returns |
| 252 | + ------- |
| 253 | + s_bar : float |
| 254 | + expected average score per arrow |
| 255 | +
|
| 256 | + Notes |
| 257 | + ----- |
| 258 | + Assumes that: |
| 259 | + - target rings are concentric |
| 260 | + - score decreases monotonically as ring sizes increase |
| 261 | + """ |
| 262 | + target_specs = dict(sorted(target_specs.items())) |
| 263 | + ring_sizes = target_specs.keys() |
| 264 | + ring_scores = list(itr.chain(target_specs.values(), [0])) |
307 | 265 |
|
308 |
| - elif target.scoring_system == "Worcester_2_ring": |
309 |
| - s_bar = ( |
310 |
| - 5.0 |
311 |
| - - np.exp(-((((tar_dia / 10.0) + arw_rad) / sig_r) ** 2)) |
312 |
| - - 4.0 * np.exp(-((((2 * tar_dia / 10.0) + arw_rad) / sig_r) ** 2)) |
313 |
| - ) |
314 |
| - # No else raising error needed as invalid scoring systems handled in Target class |
| 266 | + score_drops = (inner - outer for inner, outer in itr.pairwise(ring_scores)) |
| 267 | + max_score = max(ring_scores) |
315 | 268 |
|
316 |
| - return s_bar |
| 269 | + return max_score - sum( |
| 270 | + score_drop * np.exp(-(((arw_rad + (ring_diam / 2)) / sig_r) ** 2)) |
| 271 | + for ring_diam, score_drop in zip(ring_sizes, score_drops) |
| 272 | + ) |
317 | 273 |
|
318 | 274 | def score_for_passes(
|
319 | 275 | self,
|
|
0 commit comments