PyScore

001: # -*- python -*-
002: ###### handicap.py ###### python package PyScore.tabulate module handicap ######
003: 
004: # PyScore
005: # a race scoring programme
006: # written by Matt Draisey
007: # 2004 April 6
008: 
009: reloadables=[]
010: 
011: ###### handicap.py ###### python package PyScore.tabulate module handicap ######
012: 
013: from relational import base
014: from standing import enumstat,eventstat
015: from tabulate import enumcond,racesheet
016: 
017: ######## ######## some actual working code ######## ########
018: 
019: def tabulate_all_races(handicapsheet):
020:     for (race,entries) in handicapsheet.adjoin_hierarchy(
021:         [("start",lambda e,s: s.race,lambda e: e.race)]
022:     ):
023:         handicaplist=racesheet.SheetList(entries)
024: 
025:         mark_duplicate_boats(handicaplist)
026:         mark_peculiar_finishes(handicaplist)
027:         for dname in [eventstat.DivisionName.SHARK]:
028:             infer_level_racing_finish_times(handicaplist,dname)
029: 
030:         tabulate_race_by_start(handicaplist)
031:     
032: def mark_duplicate_boats(handicapsheet):
033:     """
034:     Most computer programmes force the incoming data into a consistent
035:     state as soon as possible.  This is usually a bad idea as it hinders
036:     our ability to recognize and correct them in a useful way. Instead we
037:     deal with inconsistencies as late as possible.
038:     
039:     Here we mark duplicate entries for a single boat that may spread
040:     across start/division boundaries and so be hidden from later processing.
041:     """
042: 
043:     for (boat,entries) in handicapsheet.adjoin_hierarchy(["boat"]):
044:         duplicates=[e for e in entries]
045:         if len(duplicates)>1:
046:             for entry in duplicates:
047:                 entry.duplicates=duplicates
048: 
049: def mark_peculiar_finishes(handicapsheet):
050:     """
051:     Mark entries which have both a DNC, DNS or DNF and a finish time
052:     """
053: 
054:     for entry in handicapsheet.obverse_filter(
055:       [("finishcondition",enumcond.FinishCondEnum.TIMELESS),"finishhms"]
056:     ):
057:         entry.peculiar=True
058: 
059: def infer_level_racing_finish_times(handicapsheet,divisionname):
060:     """
061:     When level racing classes and handicapped classes are mixed
062:     together on one course, we generally prefer to handicap
063:     everyone, even if handicapping is not required in a level
064:     racing class; however clever RCs may omit finish times for
065:     these finishers leaving us in an inconsistent state.  This
066:     function aims to restore consistency by inferring lower
067:     limits on finish times in level racing classes.
068:     
069:     For any pair of boats in a level racing class, the
070:     inferred time for the second is the max of all intervening
071:     actual recorded times and the inferred time for the first.
072:     """
073: 
074:     assert divisionname.levelracing
075:     boundhms=(0,0,0)
076:     for entry in handicapsheet.obverse_sort(["entrynumber"]): # could be heap
077:         try:
078:             entrydivname=entry.start.divisionname
079:         except AttributeError:
080:             entrydivname=None
081:         if entrydivname==divisionname:
082:             try:
083:                 boundhms=hms=entry.finishhms
084:             except AttributeError:
085:                 hms=boundhms
086:             entry.inferredhms=hms
087:         else:
088:             try:
089:                 if boundhms<entry.finishhms:
090:                     boundhms=entry.finishhms
091:             except AttributeError:
092:                 pass
093: 
094: def tabulate_race_by_start(handicapsheet):
095:     """
096:     Work on each start/division in turn.
097:     """
098: 
099:     for (start,entries) in handicapsheet.adjoin_hierarchy(["start"]):
100:         entrylist=racesheet.SheetList(entries)
101: 
102:         scratch_classes(start,entrylist)
103:         apply_handicaps(start,entrylist)
104:         reckon_starters(start,entrylist)
105:         rank_by_corrected_time(start,entrylist)
106: 
107: def scratch_classes(start,handicapsheet,distance=None):
108:     """
109:     Determine the scratch boat in each class and calculate handicaps
110:     relative to that boat.  Absolute corrections are always rounded
111:     relative to PHRF rating 0 before relative corrections are
112:     determined; this yields the necessarily consistent roundings.
113:     """
114: 
115:     try:
116:         distance=start.distance
117:     except AttributeError:
118:         distance=start.race.distance
119: 
120:     try:
121:         scratchco=round(start.levelrating*distance)
122:     except AttributeError:
123:         for (
124:             rating,entries
125:         ) in handicapsheet.adjoin_hierarchy(["rating"]):
126:             scratchco=round(rating*distance)
127:             break
128:         else:
129:             scratchco=0
130: 
131:     for entry in handicapsheet.obverse_filter(["rating"]):
132:         correction=round(entry.rating*distance)
133:         entry.relativeco=correction-scratchco
134: 
135: def apply_handicaps(start,handicapsheet):
136:     """
137:     Order doesn't matter in applying handicaps, but whether this is
138:     level racing or handicapped racing does.  We treat both in a
139:     the context of handicapping.
140:     """
141: 
142:     try:
143:         start.levelracing
144:     except AttributeError:
145:         for (
146:             relativeco,(hh,mm,ss),entry
147:         ) in handicapsheet.adjoin_filter(["relativeco","finishhms"]):
148:             corrected=(hh*60+mm)*60+ss-relativeco
149:             (hm,ss)=divmod(corrected,60)
150:             (hh,mm)=divmod(hm,60)
151:             entry.correctedhms=(hh,mm,ss)
152:             entry.zipcorrected=(
153:                 entry.finishcondition,
154:                 entry.correctedhms,
155:             )
156:     else:
157:         for (
158:             relativeco,(hh,mm,ss),entry
159:         ) in handicapsheet.adjoin_filter(["relativeco","inferredhms"]):
160:             corrected=(hh*60+mm)*60+ss-relativeco
161:             (hm,ss)=divmod(corrected,60)
162:             (hh,mm)=divmod(hm,60)
163:             entry.correctedhms=(hh,mm,ss)
164:             entry.zipcorrected=(
165:                 entry.finishcondition,
166:                 entry.correctedhms,
167:                 entry.entrynumber,
168:             )
169: 
170:     for entry in base.converse_filter(
171:         handicapsheet.converse_filter(["correctedhms"]),
172:         [("finishcondition",enumcond.FinishCondEnum.FIN_COND)]
173:     ):
174:         entry.zipcorrected=(entry.finishcondition,)
175: 
176:     # zipcorrected is defined for all entries where it is well defined
177:     # i.e. in all level rating classes, and in handicapped classes where
178:     # either a nonnull finish condition is defined or where sufficient
179:     # information exists to calculate a corrected time
180: 
181: def reckon_starters(start,handicapsheet):
182:     """
183:     Starters must be registered.  From registered boats determine
184:     starters as those boats that finish condition is not DNC
185:     and has either a finish condition or a corrected finish time (or has
186:     an entry number (i.e. all) and was racing in a level rating class).
187:     
188:     Further, if we have duplicate entries for a single boat, we only
189:     use the entry corresponding to the latest finish; this has the
190:     highest probability of being the correct interpretation, as boats
191:     may need to take a penalty and refinish.
192:     """
193: 
194:     for (boat,finishes) in base.adjoin_hierarchy(
195:         base.converse_filter(
196:             base.converse_filter(
197:                 handicapsheet.obverse_filter(["rating"]),
198:                 [("fleet",enumstat.FleetEnum.UNREGISTERED)]
199:             ),
200:             [("finishcondition",enumcond.FinishCondEnum.DNC_COND)]
201:         ),
202:         ["boat","zipcorrected"]
203:     ):
204:         lastduplicate=[
205:             entry
206:             for (finish,entries) in finishes
207:             for entry in entries
208:         ][-1]
209:         lastduplicate.starter=True
210:         try:
211:             lastduplicate.fin_cond
212:         except AttributeError:
213:             lastduplicate.finisher=False
214:         else:
215:             lastduplicate.finisher=True
216: 
217: def rank_by_corrected_time(start,handicapsheet):
218:     """
219:     Simply sort by zipcorrected attribute, and collect those with the
220:     tied finishes together to distribute rankings and points.
221: 
222:     Also calculating casual scores by the mid-fleet formula.  All boats
223:     get a casual score even if they are not registered as such.
224:     """
225: 
226:     withfinish=handicapsheet.obverse_list(["starter"])
227:     numberofstarters=len(withfinish)
228: 
229:     casualweight=numberofstarters-1
230: 
231:     rank=1
232:     casualrank=-casualweight
233:     for (finishcondition,timed) in withfinish.adjoin_hierarchy(
234:         [("finishcondition",enumcond.FinishCondEnum.FIN_COND),"zipcorrected"]
235:     ):
236:         #assert finishcondition==enumcond.FinishCondEnum.FIN
237:         assert finishcondition.fin_cond
238:         for (zipcorrected,entries) in timed:
239:             sharedrank=rank
240:             sharedcasualrank=casualrank
241:             sharedentries=[e for e in entries]
242:             n=len(sharedentries)
243:             sharedpoints=rank+round((n-1)/2.0,1)
244:             sharedcasualpoints=casualrank+(n-1)
245:             for entry in sharedentries:
246:                 entry.ranking=sharedrank
247:                 entry.points=sharedpoints
248:                 entry.casualpoints=sharedcasualpoints
249:             rank+=n
250:             casualrank+=2*n
251: 
252:     sharedrank=rank
253:     sharedcasualrank=casualrank
254:     sharedentries=withfinish.converse_sort(
255:         [("finishcondition",enumcond.FinishCondEnum.FIN_COND),"zipcorrected"]
256:     )
257:     n=len(sharedentries)
258:     sharedpoints=rank+n
259:     sharedcasualpoints=casualrank+2*n
260:     for entry in sharedentries:
261:         entry.ranking=sharedrank
262:         entry.points=sharedpoints
263:         entry.casualpoints=sharedcasualpoints
264:     rank=sharedpoints
265:     casualrank=sharedcasualpoints
266: 
267:     assert numberofstarters==rank-1==casualrank-1
268: 
269:     start.numberofstarters=numberofstarters
270:     start.casualweight=casualweight
271: 
272: ###### handicap.py ###### python package PyScore.tabulate module handicap ######