PyScore

001: # -*- python -*-
002: ####### series.py ####### python package PyScore.tabulate module series #######
003: 
004: # PyScore
005: # a race scoring programme
006: # written by Matt Draisey
007: # 2004 April 6
008: 
009: reloadables=[]
010: 
011: ####### series.py ####### python package PyScore.tabulate module series #######
012: 
013: from relational import base
014: from tabulate import enumcond,racesheet,scoresheet
015: from utility.verbose import vv,vvv,vvvv,debug
016: 
017: ######## ######## some actual working code ######## ########
018: 
019: def startcompleted(start):
020:     """
021:     Races are assumed to be completed for any race with startsheet entries
022:     even if there are no handicapsheet entries in this start; however,
023:     racesheet entries by themselves do not imply a corresponding start
024:     in all or any divisions.
025: 
026:     An explicit "abandoned" attribute in either the racesheet or startsheet
027:     is required if the existence of a startsheet entry is to be overruled.
028:     """
029: 
030:     try: start.abandoned
031:     except AttributeError:
032:         try: start.race.abandoned
033:         except AttributeError: pass
034:         else: raise AttributeError
035:     else: raise AttributeError
036:     return start
037: 
038: def tabulate_all_series(
039:     racesheetvalues,startsheetvalues,handicapsheet,rcsheet,
040:     seriestab,seriesdivisionnew,seriesdivisiontab,
041:     seriesscorenew,seriesscoretab,
042: ):
043:     """
044:     Work on each series in turn, separating out racesheet,
045:     startsheet, handicapsheet and rcsheet entries that apply
046:     by series.
047:     """
048: 
049:     print>>vv,"TABULATING SERIES RESULTS"
050: 
051:     for (
052:         seriesname,[races,starts,handicaps,rcs]
053:     ) in base.polyhierarchy_filter(
054:         ["seriesname"],
055:         base.adjoin_sort( # could be heap
056:             racesheetvalues.converse_filter(["abandoned"]),
057:             [("racename",lambda r,n: n.seriesname)]
058:         ),
059:         base.adjoin_sort( # could be heap
060:             startsheetvalues,
061:             [("race",lambda s,r:
062:                 startcompleted(s) and r.racename.seriesname
063:             )]
064:         ),
065:         base.adjoin_sort( # could be heap
066:             handicapsheet.obverse_filter([
067:                 "starter",
068:                 ("fleet",lambda entry,fleet: fleet.inseries)
069:             ]),
070:             [("start",lambda e,s:
071:                 startcompleted(s) and s.race.racename.seriesname
072:             )]
073:         ),
074:         base.adjoin_sort( # could be heap
075:             rcsheet,
076:             [("race",lambda s,r: r.racename.seriesname)]
077:         ),
078:     ):
079:         print>>vv,"  SERIES:",seriesname
080: 
081:         racelist=racesheet.SheetList(races)
082:         startlist=racesheet.SheetList(starts)
083:         handicaplist=racesheet.SheetList(handicaps)
084:         rclist=racesheet.SheetList(rcs)
085: 
086:         scoreseries=scoresheet.ScoreList()
087:         infer_registration(
088:             handicaplist,
089:             seriesscorenew(seriesname),scoreseries
090:         )
091:         attribute_rc_redress(
092:             handicaplist,rclist,scoreseries
093:         )
094:         collect_by_start(
095:             startlist,handicaplist,rclist,
096:             seriesdivisionnew(seriesname),scoreseries
097:         )
098: 
099:         series=seriestab[seriesname]
100:         series.races=racelist
101: 
102: def infer_registration(handicapsheet,scorenew,scoreseries):
103:     """
104:     Sometimes we may have boats registered for different divisions
105:     in the same series.  This is a no-no.  We resolve the ambiguity,
106:     on a per boat basis, in favour of the division with the greater
107:     number of starts, or with the latter start if tied.
108:     """
109: 
110:     print>>vvv,"    INFER REGISTRATION"
111: 
112:     for (boat,divisions) in base.adjoin_hierarchy(
113:         handicapsheet,
114:         ["boat",("start",lambda entry,start: start.divisionname)]
115:     ):
116:         maxstarts=[0]
117:         registered=[]
118:         for (divisionname,entries) in divisions:
119:             startentries=[e.start.race for e in entries]
120:             startentries.reverse()
121:             starts=[len(startentries)]+startentries
122:             if starts>maxstarts:
123:                 maxstarts=starts
124:                 registered=[divisionname]
125:             elif starts==maxstarts:
126:                 registered+=[divisionname]
127:         assert len(registered)>0
128: 
129:         for r in registered:
130:             seriesscore=scorenew(boat,r)
131:             if len(registered)>1:
132:                 seriesscore.duplicates=registered
133:                 # with erroneous duplicate results
134:                 # and unreconcilable registration
135:             scoreseries.append(seriesscore)
136: 
137: def attribute_rc_redress(handicapsheet,rcsheet,scoreseries):
138:     """
139:     Creating automatic redress of average series points for
140:     each boat that missed a start because of RC duties.
141:     """
142: 
143:     print>>vvv,"    ATTRIBUTE RC REDRESS"
144: 
145:     for (race,boats) in base.polyhierarchy_filter(
146:         ["race","boat"],
147:         handicapsheet.adjoin_sort(
148:             [("start",lambda e,s: s.race,lambda e: e.race),"boat"]
149:         ),
150:         rcsheet.adjoin_sort(["race","start"]),
151:     ):
152:         for (boat,jointentries) in boats:
153:             for (entry,rc) in base.outer_product(jointentries):
154:                 print>>vvv,"    RC ENTRY COLLISION: %8s:%-18s",(
155:                     entry.boat.sailnumber,
156:                     entry.boat.boatname,
157:                 )
158: 
159:     for (boat,jointentries) in base.adjoin_polyhierarchy(
160:         scoreseries,rcsheet,["boat"]
161:     ):
162:         for (registered,rc) in base.outer_product(jointentries):
163:             rc.seriesdivision=registered.seriesdivision
164: 
165: def collect_by_start(startsheet,handicapsheet,rcsheet,divisionnew,scoreseries):
166:     """
167:     Walk through each division, collecting races which have starts in
168:     that division and determine number of exclusions.
169: 
170:     Work on each start in turn, working on only registered boats.
171:     """
172: 
173:     print>>vvv,"    COLLECT BY START"
174: 
175:     for (
176:         divisionname,
177:         [startentries,scoreentries,handicapentries,rcentries],
178:     ) in base.polyhierarchy_filter(
179:         ["divisionname"],
180:         startsheet.adjoin_sort(["divisionname"]), # could be heap
181:         base.adjoin_sort( # could be heap
182:             scoreseries.adjoin_filter(["boat"]),
183:             [("seriesdivision",lambda e,s:s.divisionname)]
184:         ),
185:         base.adjoin_sort( # could be heap
186:             handicapsheet.adjoin_filter(["boat"]),
187:             [("start",lambda e,s: s.divisionname)]
188:         ),
189:         base.adjoin_sort( # could be heap
190:             rcsheet.adjoin_filter(["boat"]),
191:             [("seriesdivision",lambda e,s:s.divisionname)]
192:         ),
193:     ):
194:         print>>vvvv,"      DIVISION",divisionname
195: 
196:         starts=[s for s in startentries]
197:         seriesindex=dict([(s.race,i) for (i,s) in enumerate(starts)])
198: 
199:         seriesdivision=divisionnew(divisionname)
200:         seriesdivision.starts=starts
201:         # we add these in a non-relational fashion to seriesdivision
202:         # entry, instead of to the start entry in a relational way,
203:         # to keep handicaps not dependent upon scores and avoid the
204:         # clutter of additional glue relations
205: 
206:         completed=len(starts)
207:         excluded=seriesdivision.series.seriesname.exclusions[completed]
208:         included=completed-excluded
209:         seriesdivision.completed=completed
210:         seriesdivision.excluded=excluded
211:         seriesdivision.included=included
212: 
213:         if not starts:
214:             continue
215: 
216:         scorelist=scoresheet.ScoreList(scoreentries)
217: 
218:         seriesdivision.numberofentrants=len(scorelist)
219:         seriesdivision.dnc=seriesdivision.numberofentrants+1
220: 
221:         registeredentries=[
222:             jointentry
223:             for (boat,jointentries) in base.polyhierarchy_filter(
224:                 ["boat"],scorelist,handicapentries
225:             )
226:             for jointentry in base.outer_product(jointentries)
227:         ]
228:         
229:         for (
230:             start,entries
231:         ) in base.adjoin_hierarchy(registeredentries,["start"]):
232:             entrylist=racesheet.SheetList(entries)
233: 
234:             calculate_series_points(start,entrylist)
235: 
236:         for (
237:             boatscore,entries
238:         ) in base.hierarchy_filter(["score"],registeredentries):
239:             entrylist=scoresheet.ScoreList(entries)
240: 
241:             infer_fleet(boatscore,entrylist)
242: 
243:             boatscore.handicaps=[None]*completed
244:             boatscore.displays=[None]*completed
245:             boatscore.points=[None]*completed
246: 
247:             for (start,entry) in base.adjoin_filter(
248:                 entrylist,["start"]
249:             ):
250:                 i=seriesindex[start.race]
251:                 boatscore.handicaps[i]=entry
252: 
253:                 points=entry.seriespoints
254:                 if isinstance(points,str):
255:                     pts=points.upper()
256:                 else:
257:                     pts="%3.1f"%points
258: 
259:                 boatscore.displays[i]=pts
260:                 boatscore.points[i]=points
261: 
262:             for (i,d) in enumerate(boatscore.displays):
263:                 if d==None:
264:                     boatscore.displays[i]="DNC"
265: 
266:         registeredrcs=[
267:             jointentry
268:             for (boat,jointentries) in base.polyhierarchy_filter(
269:                 ["boat"],scorelist,rcentries
270:             )
271:             for jointentry in base.outer_product(jointentries)
272:         ]
273: 
274:         for (
275:             boatscore,entries
276:         ) in base.hierarchy_filter(["score"],registeredrcs):
277:             entrylist=scoresheet.ScoreList(entries)
278: 
279:             for (race,entry) in base.adjoin_filter(
280:                 entrylist,["race"]
281:             ):
282:                 i=seriesindex[race]
283:                 boatscore.handicaps[i]=entry
284: 
285:                 boatscore.displays[i]="AVG"
286:                 boatscore.points[i]="AVG"
287: 
288:         determine_other_series_points(seriesdivision,scorelist)
289:         total_scores(seriesdivision,scorelist)
290: 
291: def calculate_series_points(start,handicapsheet):
292:     """
293:     Calculate series points only for boats that are registered
294:     for the series.  Boats ineligible for series points shall
295:     not be ranked as a finisher in those races in which they
296:     appeared when series points are calculated.
297:     """
298: 
299:     rank=1
300:     for (finishcondition,timed) in base.adjoin_hierarchy(
301:       handicapsheet.obverse_filter([("starter",True)]),
302:       [("finishcondition",enumcond.FinishCondEnum.FIN_COND),"zipcorrected"]
303:     ):
304:         #assert finishcondition==enumcond.FinishCondEnum.FIN
305:         assert finishcondition.fin_cond
306:         for (zipcorrected,entries) in timed:
307:             sharedrank=rank
308:             sharedentries=[e for e in entries]
309:             n=len(sharedentries)
310:             sharedpoints=rank+round((n-1)/2.0,1)
311:             for entry in sharedentries:
312:                 entry.seriespoints=sharedpoints
313:             rank+=n
314: 
315:     sharedrank=rank
316:     sharedentries=base.converse_sort(
317:       handicapsheet.obverse_filter([("starter",True)]),
318:       [("finishcondition",enumcond.FinishCondEnum.FIN_COND),"zipcorrected"]
319:     )
320:     n=len(sharedentries)
321:     sharedpoints=rank+n
322:     for entry in sharedentries:
323:         entry.seriespoints=sharedpoints
324:     rank=sharedpoints
325:     start.seriesnumberofstarters=rank-1
326: 
327: def infer_fleet(boatscore,handicapsheet):
328:     """
329:     We determine fleet subdivisions of a division, which are scored
330:     together per race, yet are ranked separately per series.
331:     """
332: 
333:     maxstarts=[0]
334:     registered=[]
335:     for (fleet,entries) in handicapsheet.adjoin_hierarchy(["fleet"]):
336:         startentries=[e.start.race for e in entries]
337:         startentries.reverse()
338:         starts=[len(startentries)]+startentries
339:         if starts>maxstarts:
340:             maxstarts=starts
341:             registered=[fleet]
342:         elif starts==maxstarts:
343:             registered+=[fleet]
344:     assert len(registered)>0
345:     registered.sort()
346:     boatscore.fleet=registered[0]
347:     if len(registered)>1:
348:         boatscore.fleetduplicates=registered
349: 
350:     # boats can only belong to one fleet within a division, so when we
351:     # can't otherwise decide, arbitrarily choose the first in fleet order.
352: 
353: def determine_other_series_points(seriesdivision,scoretab):
354:     """
355:     Calculate DNC and series average redresses, RC redresses, etc.
356:     """
357: 
358:     for score in scoretab:
359:         
360:         for (i,points) in enumerate(score.points):
361:             if points==None:
362:                 score.points[i]=seriesdivision.dnc
363: 
364:         averaging=[p for p in score.points if not isinstance(p,str)]
365:         if len(averaging):
366:             average=round(sum(averaging)/len(averaging),1)
367:             for (i,points) in enumerate(score.points):
368:                 if isinstance(points,str):
369:                     score.points[i]=average
370: 
371: def total_scores(seriesdivision,scoretab):
372:     """
373:     Sum all included races and provide sufficient information
374:     to rank boats according to rule A8.
375:     """
376: 
377:     for score in scoretab:
378:         sorted=[(p,-i) for (i,p) in enumerate(score.points)]
379:         sorted.sort()
380: 
381:         score.excluded=[False]*seriesdivision.completed
382:         for (p,i) in sorted[seriesdivision.included:]:
383:             score.excluded[-i]=True
384: 
385:         del sorted[seriesdivision.included:]
386:         sorted=[p for (p,i) in sorted]
387:         score.totalpoints=sum(sorted)
388: 
389:         reversed=score.points[:]
390:         reversed.reverse()
391: 
392:         score.total=(score.totalpoints,sorted,reversed)
393: 
394:     rank=1
395:     for (total,scoreentries) in scoretab.adjoin_hierarchy(["total"]):
396:         sharedrank=rank
397:         sharedtotals=[e for e in scoreentries]
398:         for seriesscore in sharedtotals:
399:             seriesscore.ranking=sharedrank
400:             rank+=1
401: 
402:     for (fleet,totals) in scoretab.adjoin_hierarchy(["fleet","total"]):
403:         rank=1
404:         for (total,scoreentries) in totals:
405:             sharedrank=rank
406:             sharedtotals=[e for e in scoreentries]
407:             for seriesscore in sharedtotals:
408:                 seriesscore.fleetranking=sharedrank
409:                 rank+=1
410: 
411: ####### series.py ####### python package PyScore.tabulate module series #######