1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 """
17 ranking.py - Classes to produce rankings of objects that implement
18 Rankable. Each class that has a ranking (currently
19 Course and Category) has to inherit from Rankable.
20 Rankable specifies the interface of rankable classes.
21 """
22
23 from datetime import timedelta, datetime
24 from copy import copy
25 from traceback import print_exc
26 import sys, re
27
28 from storm.exceptions import NotOneError
29 from storm.locals import *
32 """Defines the interface for all rankable items (currently Runner, Team, Run).
33 This interfaces specifies the methods to access information about the RankableItem
34 like name and score (time, points, ...) in an independent way. This is ensures that
35 RankingFormatters don't need information about the objects in the ranking."""
36
38 raise UnscoreableException('You have to override start to rank this object with this scoreing strategy.')
39
41 raise UnscoreableException('You have to override finish to rank this object with this scoreing strategy.')
42
44 raise ValidationError('You have to override the complete property to validate this object with this validation strategy.')
45 complete = property(_get_complete)
46
48 return 'override __str__ for a more meaningful value'
49
51 """A Ranking objects combines a scoreing strategy, a validation strategy and a
52 rankable object (course or category) and computes a ranking. The Ranking object
53 is an interable object, which means you can use it much like a list. It returns
54 a new iterator on every call to __iter__.
55 The order of the ranking is defined by the scoreing strategy. Strategy has the be
56 a subclass of AbstractScoreing compatible with the RankableItem objects of this Rankable.
57 The ranking is generated in lowest first order. Reverse rankings are possible.
58
59 The iterator returns dictionaries with the keys 'rank', 'scoreing', 'validation',
60 'item'.
61
62 Rankings are lazyly computed, but not updated unless you eihter call the update mehtod
63 or iterate over them.
64 """
65
66 - def __init__(self, rankable, event, scoreing_class = None, validator_class = None,
67 scoreing_args = None, validator_args = None, reverse = False):
68 self.rankable = rankable
69 self._event = event
70 self._scoreing_class = scoreing_class
71 self._validator_class = validator_class
72 self.scoreing_args = scoreing_args
73 self.validator_args = validator_args
74 self._reverse = reverse
75 self._ranking_list = []
76 self._ranking_dict = {}
77
78
79 self._initialized = False
80
82 """
83 Iterating over the ranking automatically updates the ranking. This is mainly for
84 backwards compatibility.
85 """
86
87 def ranking_generator(ranking_list):
88 for result in ranking_list:
89 yield result
90
91 self.update()
92
93 return ranking_generator(self._ranking_list)
94
96 if not self._initialized:
97 self.update()
98
99 return self._ranking_list[key]
100
101 - def rank(self, item):
102 """
103 @param item: Item ranked in this ranking
104 @return: Rank of this item
105 """
106 return self.info(item)['rank']
107
109 """
110 @param item: Item ranked in this ranking
111 @return: Score of this item
112 """
113 return self.info(item)['scoreing']['score']
114
115 - def info(self, item):
116 """
117 Return information dict for one item
118 @param item: Item ranked in this ranking
119 @return: dict with all information about this item (same as when iterating
120 over the ranking)
121 """
122
123 if not self._initialized:
124 self.update()
125
126 try:
127 return self._ranking_dict[item]
128 except KeyError:
129 raise KeyError('%s not in ranking.' % item)
130
131 @property
133 if not self._initialized:
134 self.update()
135 return self._member_count
136
137 @property
139 if not self._initialized:
140 self.update()
141 return self._completed_count
142
144
145 self._ranking_list = []
146 self._member_count = 0
147 self._completed_count = 0
148
149
150
151 if not len(list(self.rankable.members)) > 0:
152
153 return
154
155 for m in self.rankable.members:
156 try:
157
158 args = None if self.scoreing_args is None else self.scoreing_args.copy()
159 score = self._event.score(m, self._scoreing_class, args)
160 except UnscoreableException:
161 score = {'score': timedelta(0)}
162
163 try:
164
165 args = None if self.validator_args is None else self.validator_args.copy()
166 valid = self._event.validate(m, self._validator_class, args)
167 except ValidationError:
168 print_exc(file=sys.stderr)
169 continue
170
171 self._member_count += 1
172 if valid['status'] != Validator.NOT_COMPLETED:
173 self._completed_count += 1
174
175 self._ranking_list.append({'scoreing': score,
176 'validation': valid,
177 'item': m})
178
179
180
181 from run import Run
182 self._ranking_list.sort(key = lambda x: (type(x['item']) == Run) and x['item'].sicard.runner and (x['item'].sicard.runner.number or '0') or x['item'].number or '0')
183 self._ranking_list.sort(key = lambda x: x['scoreing']['score'], reverse = self._reverse)
184 self._ranking_list.sort(key = lambda x: x['validation']['status'])
185
186 rank = 1
187 winner_score = self._ranking_list[0]['scoreing']['score']
188 for i, m in enumerate(self._ranking_list):
189
190 if (i > 0 and (self._ranking_list[i]['scoreing']['score']
191 > self._ranking_list[i-1]['scoreing']['score'])
192 or (self._reverse and (self._ranking_list[i]['scoreing']['score']
193 < self._ranking_list[i-1]['scoreing']['score']))):
194 rank = i + 1
195
196 m['rank'] = rank if m['validation']['status'] == Validator.OK else None
197 m['scoreing']['behind'] = ((m['scoreing']['score'] - winner_score) * (self._reverse and -1 or 1)
198 if m['validation']['status'] == Validator.OK
199 else None)
200
202
203 self._ranking_dict = {}
204 for obj in self._ranking_list:
205 self._ranking_dict[obj['item']] = obj
206
214
216
218 self._update_ranking_list()
219
220 leg_rankings = {}
221 for leg in self._event.list_legs(self.rankable):
222 r = self._event.ranking(leg)
223 leg_rankings.update([(k, r) for k in leg.course_list])
224
225
226 split_rankings = []
227 for i in range(len(self._event.list_legs(self.rankable))):
228
229
230 r = Ranking(self.rankable, self._event, scoreing_args = {'legs': i+1},
231 validator_args = {'legs': i+1})
232 split_rankings.append(r)
233
234 for team in self._ranking_list:
235 team['runs'] = []
236 team['splits'] = []
237 for i, run in enumerate(team['scoreing']['runs']):
238 if run:
239 team['runs'].append(leg_rankings[run.course].info(run))
240 else:
241 team['runs'].append(None)
242 team['splits'].append(split_rankings[i].info(team['item']))
243
244 self._update_ranking_dict()
245 self._initialized = True
246
248 """Defines the interface for rankable objects like courses and categories.
249 The following attributes must be available in subclasses:
250 - members
251 """
252 pass
253
255
256 - def __init__(self, store, control = None):
257 """
258 @param store: Store for the open runs. This class uses a Storm store to
259 search for open runs, but it is not a Storm object itself!
260 @param control: Only list open runs which should pass at this control or
261 punched this control. (not yet implemented)
262 """
263 self._store = store
264 self._control = control
265
269
270 members = property(_get_runs)
271
273 """Cache for scoreing and validation results."""
274
276 """
277 @param observer: Observer wich notifys the cache of changes in
278 cached objects.
279 @type observer: object of class EventObserver
280 """
281 self._cache = {}
282 self._observer = observer
283
285 (obj, func) = key
286 return self._cache[obj][func]
287
289 (obj, func) = key
290 if not obj in self._cache:
291 self._cache[obj] = {}
292 self._cache[obj][func] = value
293 if self._observer:
294 self._observer.register(self, obj)
295
297 if self._observer:
298 self._observer.unregister(self, obj)
299 del self._cache[obj]
300
302 (obj, func) = key
303 return obj in self._cache and func in self._cache[obj]
304
307
310
312 """
313 Sets an observer for this cache.
314 """
315 if self._observer is not None:
316 self.remove_observer()
317 for obj in self._cache:
318 observer.register(self, obj)
319 self._observer = observer
320
322 """
323 Removes any existing observer for this cache.
324 """
325 if self._observer is not None:
326 for obj in self._cache:
327 self._observer.unregister(self, obj)
328 self._observer = None
329
331 """Common parent class for all caching objects."""
332
334 """
335 @param cache: scoreing cache
336 """
337 self._cache = cache
338
340 """
341 Check cache and return cached result if it exists.
342 @param func: function which produced the result
343 @param obj: Object of the startegy.
344 @type obj: object of class RankableItem
345 @raises: KeyError if object is not in the cache
346 """
347 if self._cache:
348 return self._cache[(obj, func)]
349 else:
350 raise KeyError
351
353 """
354 Add result to cache
355 @param func: function which produced the result
356 @param obj: Object of the startegy.
357 @type obj: object of class RankableItem
358 """
359 if self._cache:
360 self._cache[(obj, func)] = result
361
364 """Defines a strategy for scoring objects (runs, runners, teams). The scoreing
365 strategy is tightly coupled to the objects it scores.
366
367 Computed scores can (and should!) be stored in a cache.
368 """
369
370 - def score(self, object):
371 """Returns the score of the given objects. Throws UnscoreableException if the
372 object can't be scored with this strategy. The returned score must implement
373 __cmp__."""
374 raise UnscoreableException('Can\'t score with AbstractScoreing')
375
376 """
377 descriptive string about the special parameters set for this
378 Scoreing. Override if you have to pass additional information
379 to rankings (and formatted rankings).
380 string or None
381 """
382 information = None
383
385 """Builds the score from difference of start and finish times. The start time is
386 calculated by the start time strategy object. The finish time is the time of the
387 finish punch.
388 Subclasses can override _start and _finish the extract the start and finsh times in
389 other ways."""
390
391 - def __init__(self, starttime_strategy, cache = None):
392 """
393 @param start_time_strategy: Strategy to find the start time
394 @type start_time_strategy: object of class StartTimeStrategy or a subclass
395 """
396 AbstractScoreing.__init__(self, cache)
397 self._starttime_strategy = starttime_strategy
398
400 """Returns the start time as a datetime object."""
401 return self._starttime_strategy.starttime(obj)
402
404 """Returns a timedelta object as the score by calling start and finish on
405 the object."""
406 try:
407 return self._from_cache(self.score, obj)
408 except KeyError:
409 pass
410
411 result = {}
412 result['start'] = self._start(obj)
413 result['finish'] = obj.finish_time
414 try:
415 result['score'] = result['finish'] - result['start']
416 except TypeError:
417
418 result['score'] = timedelta(0)
419
420 if result['score'] < timedelta(0):
421 raise UnscoreableException('Scoreing Error, negative runtime: %(finish)s - %(start)s = %(score)s'
422 % result)
423
424 self._to_cache(self.score, obj, result)
425 return result
426
428 """Basic start time strategy. """
429
432
434 """StarttimeStrategy for Selfstart"""
435
438
440 """Returns start time relative to a fixed mass start time."""
441
442 - def __init__(self, starttime, cache = None):
443 """
444 @param starttime: Mass start time
445 @type starttime: datetime
446 """
447
448 CachingObject.__init__(self, cache)
449 self._starttime = starttime
450
457
459 """Returns start time computed from starttime == finish time of the previous runner
460 in a relay team. If the computed time is later than the mass start time, the
461 mass start time is returned."""
462
463 - def __init__(self, massstart_time, ordered = True, cache = None):
464 """
465 @param massstart_time: Time of the mass start
466 @type massstart_time: datetime
467 @param ordered: If true this assumes that the runners follow in a
468 order defined by their number.
469 @type ordered: boolean
470 """
471
472 MassstartStarttime.__init__(self, massstart_time, cache)
473 self._prev_finish = (ordered and self._prev_finish_ordered
474 or self._prev_finish_unordered)
475
497
499
500
501 team = obj.sicard.runner.team
502
503
504
505 from course import SIStation
506 from run import Punch
507 store = Store.of(obj)
508
509
510
511
512 reftime = obj.finish_time
513 if reftime is None:
514 punchlist = [p for p,c in obj.punchlist()]
515 if len(punchlist) == 0:
516
517 reftime = datetime.max
518 else:
519
520 reftime = obj.punchlist()[-1][0].punchtime
521
522 prev_finish = store.execute(
523 """SELECT MAX(COALESCE(run.manual_finish_time, run.card_finish_time))
524 FROM team JOIN runner ON team.id = runner.team
525 JOIN sicard ON runner.id = sicard.runner
526 JOIN run ON sicard.id = run.sicard
527 WHERE team.id = %s
528 AND COALESCE(run.manual_finish_time, run.card_finish_time) < %s
529 AND run.complete = true""",
530
531
532
533
534
535
536
537
538
539
540
541
542 params = (team.id,
543 reftime,
544 )
545 ).get_one()[0]
546
547 return prev_finish
548
550 start = Starttime.starttime(self, obj)
551 if start:
552 return start
553
554 prev_finish = self._prev_finish(obj)
555 if prev_finish is None:
556 return self._starttime
557 else:
558 return prev_finish
559
561
563 start = Starttime.starttime(self, obj)
564 if start:
565 return start
566
567 prev_finish = self._prev_finish(obj)
568 if prev_finish is None:
569 return self._starttime
570 else:
571 return self._starttime < prev_finish and self._starttime or prev_finish
572
574 """Defines a strategy for validating objects (runs, runners, teams). The validation
575 strategy is tightly coupled to the objects it validates."""
576
577
578
579
580 OK = 1
581 NOT_COMPLETED = 2
582 MISSING_CONTROLS = 3
583 DID_NOT_FINISH = 4
584 DISQUALIFIED = 5
585 DID_NOT_START = 6
586
588 """Returns OK for every object. Override in subclasses for more meaningfull
589 validations."""
590 return {'status':Validator.OK}
591
593 """Validation strategy for courses."""
594
595 - def __init__(self, course, cache = None):
598
630
632 """Validation strategy for a normal orienteering course.
633 This class uses a dynamic programming "longest common subsequence"
634 algorithm to validate the run and find missing and additional punches.
635 @see http://en.wikipedia.org/wiki/Longest_common_subsequence_problem.
636 """
637
638 - def __init__(self, course, reorder = None, cache = None):
639
640 CourseValidator.__init__(self, course, cache)
641
642 if reorder is not None:
643
644 self._reorder = map(lambda x: x-1, reorder)
645 else:
646 self._reorder = None
647
648
649 self._controllist = self._course.controllist()
650
651 @staticmethod
653 """check if plist exactly matches clist
654 @param plist: list of punches
655 @param clist: list of controls. All controls with no sistations or which are
656 overriden must be removed!
657 @return: True or False
658 """
659 if len(plist) != len(clist):
660 return False
661
662 for i,c in enumerate(clist):
663 if not plist[i][1] is c:
664 return False
665
666 return True
667
668 @staticmethod
670 """Builds the matrix of lcs subsequence lengths.
671 @param plist: list of punches
672 @param clist: list of controls. All controls with no sistations or which are
673 overriden must be removed!
674 @return: matrix of lcs subsequence lengths.
675 """
676
677 m = len(plist)
678 n = len(clist)
679
680
681 C = [[0] * (n+1) for i in range(m+1) ]
682
683 i = j = 1
684 for i in range(1, m+1):
685 for j in range(1, n+1):
686 if plist[i-1][1] is clist[j-1]:
687 C[i][j] = C[i-1][j-1] + 1
688 else:
689 C[i][j] = max(C[i][j-1], C[i-1][j])
690
691 return C
692
693 @staticmethod
694 - def _backtrack(C, plist, clist, i = None, j = None):
695 """Backtrack through the LCS Matrix to find one of possibly
696 several longest common subsequences."""
697
698 if i is None:
699 i = len(plist)
700 if j is None:
701 j = len(clist)
702
703 if i == 0 or j == 0:
704 return []
705 elif plist[i-1][1] is clist[j-1]:
706 return SequenceCourseValidator._backtrack(C, plist, clist, i-1, j-1) + [ clist[j-1] ]
707 else:
708 if C[i][j-1] > C[i-1][j]:
709 return SequenceCourseValidator._backtrack(C, plist, clist, i, j-1)
710 else:
711 return SequenceCourseValidator._backtrack(C, plist, clist, i-1, j)
712
713 @staticmethod
714 - def _diff(C, plist, clist, i = None, j = None):
715 """
716 @return: list of (status, punch or control) tuples. status is 'ok', 'additional' or
717 'missing' or '' (for special SIStations)
718 """
719
720 from course import SIStation
721
722 if i is None:
723 i = len(plist)
724 if j is None:
725 j = len(clist)
726
727 if i > 0 and j > 0 and plist[i-1][1] is clist[j-1]:
728 result_list = SequenceCourseValidator._diff(C, plist, clist, i-1, j-1)
729 result_list.append(('ok',plist[i-1][0]))
730 else:
731 if j > 0 and (i == 0 or C[i][j-1] >= C[i-1][j]):
732 result_list = SequenceCourseValidator._diff(C, plist, clist, i, j-1)
733 result_list.append(('missing', clist[j-1]))
734 elif i > 0 and (j == 0 or C[i][j-1] < C[i-1][j]):
735 result_list = SequenceCourseValidator._diff(C, plist, clist, i-1, j)
736 result_list.append(((plist[i-1][0].sistation.id > SIStation.SPECIAL_MAX
737 and 'additional'
738 or ''),
739 plist[i-1][0]))
740 else:
741 result_list = []
742 return result_list
743
745 """Validate the run if it is valid for this course."""
746
747 try:
748 return self._from_cache(self.validate, run)
749 except KeyError:
750 pass
751
752 from course import SIStation
753
754
755 result = super(type(self), self).validate(run)
756
757 punchlist = run.punchlist()
758
759 if SequenceCourseValidator._exact_match(punchlist, self._controllist):
760 diff_list = [('ok', p[0]) for p in punchlist]
761 else:
762 C = SequenceCourseValidator._build_lcs_matrix(punchlist, self._controllist)
763 diff_list = SequenceCourseValidator._diff(C,
764 punchlist,
765 self._controllist)
766
767
768 ignorelist = run.punchlist(ignored=True)
769 i = 0
770 for p,c in ignorelist:
771 while i < len(diff_list):
772 if (diff_list[i][0] == 'missing'
773 or p.punchtime > diff_list[i][1].punchtime):
774 i += 1
775 else:
776 break
777 diff_list.insert(i, ('ignored', p))
778
779 if result['status'] == Validator.OK and result['override'] is False:
780 if 'missing' in dict(diff_list):
781 result['status'] = Validator.MISSING_CONTROLS
782
783 result['punchlist'] = diff_list
784
785 if self._reorder:
786
787 from run import ShiftedPunch
788
789
790 orig_punchlist = [(status, punch) for status, punch in result['punchlist'] if status in ('ok', 'missing')]
791 punchlist = []
792 for i, control_pos in enumerate(self._reorder):
793
794 if i == control_pos:
795 punchlist.append(orig_punchlist[i])
796 elif (orig_punchlist[control_pos-1][0] == 'ok'
797 and orig_punchlist[control_pos][0] == 'ok'
798 and punchlist[i-1][0] == 'ok'):
799
800
801
802 orig_punch = orig_punchlist[control_pos][1]
803 orig_punchtime = orig_punch.punchtime
804 legtime = orig_punchtime - orig_punchlist[control_pos-1][1].punchtime
805
806 shifted_punchtime = punchlist[i-1][1].punchtime + legtime
807 punchlist.append(('ok', ShiftedPunch(orig_punch, shifted_punchtime - orig_punchtime)))
808 elif orig_punchlist[control_pos][0] == 'missing':
809
810 punchlist.append(('missing', orig_punchlist[control_pos][1]))
811 else:
812
813 punchlist.append(('missing', orig_punchlist[control_pos][1].sistation.control))
814
815 result['reordered_punchlist'] = punchlist
816
817 self._to_cache(self.validate, run, result)
818 return result
819
821 """Base class for all relay scoreing classes. This class contains some
822 common mehtods used in relay scoreing.
823 """
824
825 - def __init__(self, event, cache = None):
828
830 """
831 Return a sorted list of all completed runs of a team that have a course out of a given set.
832 @return: dict with the following keys:
833 * finish: finish time of the run
834 * runner: runner id of the run
835 * course: course of the run
836 * validation: validation code
837 * lkm: lkm run on this code
838 * score: score of the run
839 * expected_time: expected time of the run
840 """
841 try:
842 return self._from_cache(self._runs, team)
843 except KeyError:
844 runs = []
845 for r in team.runs:
846 if r.complete is False:
847 continue
848 if r.course is None:
849 continue
850 if not r.course.code in self._courses:
851 continue
852 runs.append( {'finish':r.finish_time,
853 'runner':r.sicard.runner.id,
854 'course':r.course.code,
855 'validation':self._event.validate(r)['status']})
856 if runs[-1]['validation'] == Validator.OK:
857 runs[-1]['lkm'] = r.course.lkm()
858 else:
859 runs[-1]['score'] = self._event.score(r)['score']
860 runs[-1]['expected_time'] = r.course.expected_time(self._speed)
861
862 runs.sort(key=lambda x:x['finish'] or datetime.max)
863 self._to_cache(self._runs, team, runs)
864 return runs
865
867 """Combined validator and scoreing class for relay teams. This
868 class validates and scores teams in a classical relay with
869 different legs and a fixed order of the legs. It supports optional
870 legs with a default time if the leg is not run successfully or if
871 a runner runs longer than the default time. It also supports
872 multiple course variants for a leg."""
873
874 - def __init__(self, legs, event, cache=None):
875 """
876 @param legs: list of dicts with the following keys:
877 * 'variants': tuple of course codes that are valid variants for this leg.
878 * 'starttime': start time for all non replaced runners, type datetime
879 * 'defaulttime': time scored if no runner of the team successfully
880 completes this leg or if the runner on this legs needs more time than
881 the defaulttime, type timedelta or None if there is no defaulttime
882 @type legs: dict
883 @param event: Event object used to validate and score individual runs.
884 @type event: instance of a class derived from Event
885 """
886 super(RelayScoreing, self).__init__(event,cache)
887 self._legs = legs
888 self._speed = 0
889 self._courses = []
890 for l in self._legs:
891 self._courses.extend(l['variants'])
892
894 """
895 Return a list of all completed runs for this team. Ordered by leg. If there is
896 no run for a leg the list contains None at this index. If there is more than one run for
897 a leg the result is undefined.
898 @type team: instance of Team
899 @return type: list of runs
900 """
901 try:
902 return self._from_cache(self._runs, team)
903 except KeyError:
904 pass
905
906 runs = dict([(r.course.code, r) for r in team.runs
907 if r.course is not None])
908
909 result = []
910 for l in self._legs:
911 for v in l['variants']:
912 if v in runs.keys():
913 result.append(runs[v])
914 break
915 else:
916
917 result.append(None)
918
919 self._to_cache(self._runs, team, result)
920 return result
921
923 """Validates a relay team.
924 @see: Validator
925 """
926 try:
927 return self._from_cache(self.validate, team)
928 except KeyError:
929 pass
930
931 from runner import RunnerException
932
933 result = {'override':False}
934
935 if team.override is not None:
936 result['status'] = team.override
937 result['override'] = True
938 else:
939 runners = team.members.order_by('number')
940 i = 0
941 for l in self._legs:
942 try:
943 run = runners[i].run
944 except IndexError:
945
946 if l['defaulttime'] is not None:
947
948 continue
949 else:
950 result['status'] = Validator.DISQUALIFIED
951 break
952 except RunnerException:
953
954 if l['defaulttime'] is not None:
955 i += 1
956 continue
957 else:
958 result['status'] = Validator.DISQUALIFIED
959 break
960
961 try:
962 code = run.course.code
963 except AttributeError:
964
965 result['status'] = Validator.DISQUALIFIED
966 break
967
968 valid = self._event.validate(run)['status']
969 if code in l['variants'] and (l['defaulttime'] is not None or valid == Validator.OK):
970
971 i += 1
972 continue
973 elif l['defaulttime'] is not None:
974
975 continue
976 elif code in l['variants']:
977
978 result['status'] = valid
979 break
980 else:
981
982 result['status'] = Validator.DISQUALIFIED
983 break
984
985 if not 'status' in result:
986
987 result['status'] = Validator.OK
988
989 self._to_cache(self.validate, team, result)
990 return result
991
993 """Score a relay team.
994 @see: AbstractScoreing
995 """
996
997 try:
998 return self._from_cache(self.score, team)
999 except KeyError:
1000 pass
1001
1002 time = timedelta(0)
1003
1004
1005
1006
1007 runs = self._runs(team)
1008 for i,l in enumerate(self._legs):
1009 default = l['defaulttime']
1010
1011 if runs[i] is not None and self._event.validate(runs[i])['status'] == Validator.OK:
1012
1013 legscore = self._event.score(runs[i])['score']
1014 if default is None or legscore < default:
1015 time += legscore
1016 else:
1017 time += default
1018 else:
1019
1020 if default is None:
1021
1022
1023 time = timedelta(0)
1024 break
1025 else:
1026 time += default
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074 result = {'score': time,
1075 'runs': runs}
1076
1077 self._to_cache(self.score, team, result)
1078 return result
1079
1081 """This class is both a validation strategy and a scoreing strategy. The strategies
1082 are combined because they use some common private functions. This class validates
1083 and scores teams for the 24h relay."""
1084
1085 START = re.compile('^SF[1-4]$')
1086 NIGHT = re.compile('^[LS][DE]N[1-5]$')
1087 DAY = re.compile('^[LS][DE][1-4]$')
1088 FINISH = re.compile('^FF[1-6]$')
1089
1090 POOLNAMES = ['start','night', 'day','finish']
1091
1092 - def __init__(self, starttime, speed, duration, event, method = 'runcount',
1093 blocks = 'finish', cache = None):
1094 """
1095 @param starttime: Start time of the event.
1096 @type starttime: datetime.datetime
1097 @param speed: expected speed in minutes per kilometers. Used to compute
1098 penalty time for invalid runs
1099 @type speed: int
1100 @param event: object of class Event to score and
1101 validate single runs
1102 @param duration: Duration of the event
1103 @param method: score calculation method, currently implemented:
1104 runcount: count valid runs
1105 lkm: sum of lkms of all valid runs
1106 @type method: str
1107 @param blocks: score until the end of this block. Possible blocks:
1108 'start', 'night', 'day', 'finish' (default)
1109 """
1110 AbstractRelayScoreing.__init__(self, event, cache)
1111 self._starttime = starttime
1112 self._speed = speed
1113 self._duration = duration
1114 self._method = method
1115
1116 self._courses = []
1117 all_courses = self._event.list_courses()
1118 if blocks in self.POOLNAMES:
1119 self._courses.extend([ c.code for c in all_courses
1120 if self.START.match(c.code) ])
1121 if blocks in self.POOLNAMES[1:]:
1122 self._courses.extend([ c.code for c in all_courses
1123 if self.NIGHT.match(c.code) ])
1124 if blocks in self.POOLNAMES[2:]:
1125 self._courses.extend([ c.code for c in all_courses
1126 if self.DAY.match(c.code) ])
1127 if blocks in self.POOLNAMES[3:]:
1128 self._courses.extend([ c.code for c in all_courses
1129 if self.FINISH.match(c.code) ])
1130
1131 self._pool = [[ c for c in self._courses if self.START.match(c) ],
1132 [ c for c in self._courses if self.NIGHT.match(c) ],
1133 [ c for c in self._courses if self.DAY.match(c) ],
1134 ]
1135
1136 self._blocks = blocks
1137
1139 """This is an utility function to not duplicate code in _check_order and
1140 _omitted_runners. It loops over all runs, counts the omitted runners and
1141 checks the order. Returns a tuple
1142 (validation_code, number_of_omitted_runners)."""
1143
1144
1145 try:
1146 return self._from_cache(self._loop_over_runs, team)
1147 except KeyError:
1148 pass
1149
1150
1151 members = [ r.id for r in team.members.order_by('number')]
1152 runs = self._runs(team)
1153
1154
1155 remaining = copy(members)
1156 next_runner = 0
1157 status = Validator.OK
1158 for i,r in enumerate(runs):
1159 while not r['runner'] == remaining[next_runner]:
1160 if i < len(members):
1161
1162 status = Validator.DISQUALIFIED
1163
1164 del(remaining[next_runner])
1165
1166 if len(remaining) == 0:
1167 status = Validator.DISQUALIFIED
1168 break
1169 if next_runner >= len(remaining):
1170
1171 next_runner = 0
1172
1173 if len(remaining) == 0:
1174 break
1175
1176 next_runner = (next_runner+1)%len(remaining)
1177
1178 result = (status, len(members) - len(remaining))
1179
1180
1181 self._to_cache(self._loop_over_runs, team, result)
1182 return result
1183
1185 """Checks if the order of the runners is correct."""
1186 return self._loop_over_runs(team)[0]
1187
1189 """Count the runners that were omitted during the event and gave up."""
1190 return self._loop_over_runs(team)[1]
1191
1193
1194
1195
1196 pool = [ copy(p) for p in self._pool ]
1197
1198 i = 0
1199 runs = self._runs(team)
1200 remaining_runs = copy(runs)
1201 for r in runs:
1202 while i <= 2 and len(pool[i]) == 0:
1203
1204 i += 1
1205
1206 if i > 2:
1207
1208 break
1209
1210 try:
1211 pool[i].remove(r['course'])
1212 remaining_runs.remove(r)
1213 except ValueError:
1214 return {'status': Validator.DISQUALIFIED,
1215 'unfinished pool': self.POOLNAMES[i],
1216 'run': r['course']}
1217
1218
1219
1220 finish_pool = [ c for c in self._courses if self.FINISH.match(c) ]
1221 finish_pool.sort()
1222 for i,c in enumerate(finish_pool):
1223 if i >= len(remaining_runs):
1224
1225 break
1226
1227 if c != remaining_runs[i]['course']:
1228 return {'status': Validator.DISQUALIFIED,
1229 'unfinished pool': self.POOLNAMES[3],
1230 'run': remaining_runs[i]['course']}
1231
1232 return {'status': Validator.OK}
1233
1235 """Validate the runs of this team according to the rules
1236 of the 24h orienteering event.
1237 The following rules apply:
1238 - runners must run in the defined order, omitted runners may not
1239 run again
1240 - every runner must run at least once at the beginning of the event
1241 - runs must be run in the following order:
1242 - 4 start courses (Codes SF1-4), fixed order defined for each
1243 team
1244 - night courses (Codes LDNx, LENx, SDNx, SENx), free order
1245 choosen by team
1246 - day courses (Codes LDx, LEx, SDx, SEx), free order
1247 choosen by team
1248 - 6 finish courses (Codes FF1-6), same order for each team"""
1249
1250 try:
1251 return self._from_cache(self.validate, team)
1252 except KeyError:
1253 pass
1254
1255 result = {'override':False}
1256
1257 if team.override is not None:
1258 result['status'] = team.override
1259 result['override'] = True
1260 else:
1261
1262 result['status'] = self._check_order(team)
1263 if result['status'] == Validator.OK:
1264 result = self._check_pools(team)
1265 else:
1266 result['runner order'] = False
1267
1268 result['information'] = {'blocks': self._blocks}
1269 self._to_cache(self.validate, team, result)
1270 return result
1271
1273 """Compute score for a 24h team according to the following rules:
1274 - compute finish time:
1275 - 24h - (omitted_runners - 1) * 30min - sum(expected_time -
1276 running_time) for all failed runs
1277 - count valid runs completed before the finish time
1278 - record finish time of the last VALID run
1279 The result is a 24hScore object consisting of the number of runs
1280 and the finish time of the last run (not equal to the finish time
1281 computed in the first step!)."""
1282
1283 try:
1284 return self._from_cache(self.score, team)
1285 except KeyError:
1286 pass
1287
1288 runs = self._runs(team)
1289
1290 failed = []
1291 for r in runs:
1292 status = r['validation']
1293 if status in [Validator.MISSING_CONTROLS,
1294 Validator.DID_NOT_FINISH,
1295 Validator.DISQUALIFIED]:
1296 failed.append(r)
1297
1298 fail_penalty = timedelta(0)
1299 for f in failed:
1300 penalty = (f['expected_time']
1301 - f['score'])
1302 if penalty < timedelta(0):
1303
1304 penalty = timedelta(0)
1305 fail_penalty += penalty
1306
1307 give_up_penalty = (self._omitted_runners(team)-1) * timedelta(minutes=30)
1308 if give_up_penalty < timedelta(0):
1309
1310 give_up_penalty = timedelta(0)
1311
1312 finish_time = (self._starttime + self._duration
1313 - fail_penalty - give_up_penalty)
1314
1315 valid_runs = [ r for r in runs
1316 if r['validation'] == Validator.OK
1317 and r['finish'] <= finish_time]
1318
1319 if len(valid_runs) > 0:
1320 runtime = max(valid_runs, key=lambda x: x['finish'])['finish'] - self._starttime
1321 else:
1322 runtime = timedelta(0)
1323
1324 if self._method == 'runcount':
1325 result = Relay24hScore(len(valid_runs),runtime)
1326 elif self._method in ['lkm', 'speed']:
1327 lkm = 0
1328 for r in valid_runs:
1329 lkm += r['lkm']
1330 if self._method == 'lkm':
1331 result = Relay24hScore(lkm, runtime)
1332 elif self._method == 'speed':
1333 result = lkm > 0 and Relay24hScore((runtime.seconds/60.0)/lkm, runtime) or Relay24hScore(0, runtime)
1334 else:
1335 raise UnscoreableException("Unknown scoreing method '%s'." % self._method)
1336
1337 ret = {'score':result,
1338 'finishtime': finish_time,
1339 'information': {'method': self._method,
1340 'blocks': self._blocks},
1341 }
1342 self._to_cache(self.score, team, ret)
1343 return ret
1344
1346 """This class is both a valiadtion and a scoreing strategy. It implements the
1347 different validation algorithm of the 12h relay."""
1348
1349 START = re.compile('^SF[1-2]$')
1350 NIGHT = re.compile('^$')
1351 DAY = re.compile('^[LS][DE][1-4]$')
1352 FINISH = re.compile('^FF[1-2]$')
1353
1355 """The order of the runners for the 12h relay is free. So no runner may
1356 be omitted. Runner that give up don't cause a time penalty."""
1357 return 0
1358
1360 """Validate the runs of this team according to the rules
1361 of the 12h orienteering event.
1362 The following rules apply:
1363 - the order of the runners is free the team may change it at
1364 any time
1365 - runs must be run in the following order:
1366 - 2 start course (Code SF1-2), individually for each team
1367 - day courses (Codes LDx, LEx, SDx, SEx), free order
1368 choosen by team
1369 - 2 finish courses (Codes FF1-2), same order for each team"""
1370
1371 try:
1372 return self._from_cache(self.validate, team)
1373 except KeyError:
1374 pass
1375
1376
1377 result = self._check_pools(team)
1378
1379 result['information'] = {'blocks':self._blocks}
1380
1381 self._to_cache(self.validate, team, result)
1382 return result
1383
1385
1387 self.runs = runs
1388 self.time = time
1389
1391 """compares two Relay24hScore objects."""
1392
1393 if self.runs > other.runs:
1394 return -1
1395 elif self.runs < other.runs:
1396 return 1
1397 else:
1398 if self.time < other.time:
1399 return -1
1400 elif self.time > other.time:
1401 return 1
1402 else:
1403 return 0
1404
1406 """Subtracts Relay24hScore objects. This is mainly usefull to calculate
1407 differences between teams."""
1408 return Relay24hScore(self.runs - other.runs, self.time - other.time)
1409
1411 """Multiplication of Relay24hScore objects. This is to theoretically allow
1412 reverse Rankings (behind score multiplied by -1).
1413 """
1414 return Relay24hScore(self.runs * other, self.time * other)
1415
1417 return "Runs: %s, Time: %s" % (self.runs, self.time)
1418
1420 """Scores according to the (expected) absolute punchtime at a given control.
1421 This is not intended for preliminary results but to watch a control near the
1422 finish and to show incomming runners.
1423 To make effective use of this you need a rankable with all open runs as members.
1424 RankableItems scored with this strategy should be open runs.
1425 This is a combined scoreing and validation strategy.
1426 """
1427
1428 - def __init__(self, control_list, distance_to_finish = 0,
1429 cache = None):
1430 """
1431 @param control_list: score at these controls
1432 @param distance_to_finish: distance form control to the finish.
1433 This is used to compute the expected
1434 time at this control (not yet implemented)
1435 """
1436 AbstractScoreing.__init__(self, cache)
1437 self._controls = control_list
1438 self._distance_to_finish = distance_to_finish
1439
1460
1462 """
1463 @return: time of the punch or datetime.min if punchtime is unknown.
1464 If more than one of the control was punched, the punchtime
1465 of the first in the list is returned.
1466 """
1467 try:
1468 return self._from_cache(self.score, run)
1469 except KeyError:
1470 pass
1471
1472 try:
1473 (controls, punchtimes) = zip(*[(p.sistation.control, p.punchtime)
1474 for p in run.punches])
1475
1476 result = datetime.min
1477 for c in self._controls:
1478 try:
1479 index = list(controls).index(c)
1480 result = punchtimes[index]
1481 break
1482 except ValueError:
1483 pass
1484
1485 except ValueError:
1486
1487 result = datetime.min
1488
1489 ret = {'score':result}
1490 self._to_cache(self.score, run, ret)
1491 return ret
1492
1494 """This scoreing strategy computes the score as the count of complete runs of a course.
1495 This is mostly usefull for non orienteering events where runners have to run as much rounds
1496 as possible in a predifined time intervall.
1497 Be aware that SI Card 5 and SI Card 8 only hold 30 punches!
1498
1499 This is a combined validator and scoreing strategy.
1500 """
1501
1502 - def __init__(self, course, mindiff = timedelta(0), cache = None):
1503 """
1504 @param course Course object with all controls on the roundcourse. In the simplest case
1505 this course has only 1 control.
1506 @param mindiff Minimal time difference between valid punches. This is necessary to avoid
1507 counting two runs if runners punch the only control of the course two times
1508 without running the round in between.
1509 @type mindiff timedelta
1510 """
1511 super(RoundCountScoreing, self).__init__(cache)
1512 self._course = course
1513 self._mindiff = mindiff
1514
1515
1516 self._controllist = self._course.controllist()
1517
1561
1565
1568
1571