ssscoring.calc

Functions and logic for analyzing and manipulating FlySight dataframes.

  1# See: https://github.com/pr3d4t0r/SSScoring/blob/master/LICENSE.txt
  2
  3"""
  4Functions and logic for analyzing and manipulating FlySight dataframes.
  5"""
  6
  7
  8from io import BytesIO
  9from io import StringIO
 10from pathlib import Path
 11
 12from haversine import haversine
 13from haversine import Unit
 14
 15from ssscoring.constants import BREAKOFF_ALTITUDE
 16from ssscoring.constants import DEG_IN_RADIANS
 17from ssscoring.constants import EXIT_SPEED
 18from ssscoring.constants import FLYSIGHT_FILE_ENCODING
 19from ssscoring.constants import FT_IN_M
 20from ssscoring.constants import KMH_AS_MS
 21from ssscoring.constants import LAST_TIME_TRANCHE
 22from ssscoring.constants import MAX_ALTITUDE_METERS
 23from ssscoring.constants import MAX_VALID_ELEVATION
 24from ssscoring.constants import MPS_2_KMH
 25from ssscoring.constants import PERFORMANCE_WINDOW_LENGTH
 26from ssscoring.constants import SCORING_INTERVAL
 27from ssscoring.constants import SPEED_ACCURACY_THRESHOLD
 28from ssscoring.constants import TABLE_INTERVAL
 29from ssscoring.constants import VALIDATION_WINDOW_LENGTH
 30from ssscoring.datatypes import JumpResults
 31from ssscoring.datatypes import JumpStatus
 32from ssscoring.datatypes import PerformanceWindow
 33from ssscoring.errors import SSScoringError
 34from ssscoring.flysight import FlySightVersion
 35from ssscoring.flysight import detectFlySightFileVersionOf
 36
 37import math
 38import warnings
 39
 40import numpy as np
 41import pandas as pd
 42
 43
 44# +++ functions +++
 45
 46def isValidMinimumAltitude(altitude: float) -> bool:
 47    """
 48    Reports whether an `altitude` is below the IPC and USPA valid parameters,
 49    or within `BREAKOFF_ALTITUDE` and `PERFORMACE_WINDOW_LENGTH`.  In invalid
 50    altitude doesn't invalidate a FlySight data file.  This function can be used
 51    for generating warnings.  The stock FlySightViewer scores a speed jump even
 52    if the exit was below the minimum altitude.
 53
 54    See:  FAI Competition Rules Speed Skydiving section 5.3 for details.
 55
 56    Arguments
 57    ---------
 58        altitude
 59    An altitude in meters, calculated as data.hMSL - DZ altitude.
 60
 61    Returns
 62    -------
 63    `True` if the altitude is valid.
 64    """
 65    if not isinstance(altitude, float):
 66        altitude = float(altitude)
 67    minAltitude = BREAKOFF_ALTITUDE+PERFORMANCE_WINDOW_LENGTH
 68    return altitude >= minAltitude
 69
 70
 71def isValidMaximumAltitude(altitude: float) -> bool:
 72    """
 73    Reports whether an `altitude` is above the maximum altitude allowed by the
 74    rules.
 75
 76    See:  FAI Competition Rules Speed Skydiving section 5.3 for details.
 77
 78    Arguments
 79    ---------
 80        altitude
 81    An altitude in meters, calculated as data.hMSL - DZ altitude.
 82
 83    Returns
 84    -------
 85    `True` if the altitude is valid.
 86
 87    See
 88    ---
 89    `ssscoring.constants.MAX_ALTITUDE_FT`
 90    `ssscoring.constants.MAX_ALTITUDE_METERS`
 91    """
 92    if not isinstance(altitude, float):
 93        altitude = float(altitude)
 94    return altitude <= MAX_ALTITUDE_METERS
 95
 96
 97def isValidJumpISC(data: pd.DataFrame,
 98                window: PerformanceWindow) -> bool:
 99    """
100    **DEPRECATED** - Validates the jump according to ISC/FAI/USPA competition rules.  A jump is
101    valid when the speed accuracy parameter is less than 3 m/s for the whole
102    validation window duration.
103
104    Arguments
105    ---------
106        data : pd.DataFramce
107    Jumnp data in SSScoring format
108        window : ssscoring.PerformanceWindow
109    Performance window start, end values in named tuple format
110
111    Returns
112    -------
113    `True` if the jump is valid according to ISC/FAI/USPA rules.
114    """
115    warnings.warn('This function is DEPRECATED as of version 2.4.0', UserWarning)
116    if len(data) > 0:
117        accuracy = data[data.altitudeAGL <= window.validationStart].speedAccuracyISC.max()
118        return accuracy < SPEED_ACCURACY_THRESHOLD
119    else:
120        return False
121
122
123def validateJumpISC(data: pd.DataFrame,
124                window: PerformanceWindow) -> JumpStatus:
125    """
126    Validates the jump according to ISC/FAI/USPA competition rules.  A jump is
127    valid when the speed accuracy parameter is less than 3 m/s for the whole
128    validation window duration.
129
130    Arguments
131    ---------
132        data : pd.DataFramce
133    Jumnp data in SSScoring format
134        window : ssscoring.PerformanceWindow
135    Performance window start, end values in named tuple format
136
137    Returns
138    -------
139    - `JumpStatus.OK` if `data` reflects a valid jump according to ISC rules,
140    where all speed accuracy values < 'SPEED_ACCURACY_THRESHOLD'.
141    - `JumpStatus.SPEED_ACCURACY_EXCEEDS_LIMIT' if `data` has one or more values
142    within the validation window with a value >= 'SPEED_ACCURACY_THRESHOLD'.
143
144    Raises
145    ------
146    `SSScoringError' if `data` has a length of zero or it's not initialized.
147    """
148    if len(data) > 0:
149        accuracy = data[data.altitudeAGL <= window.validationStart].speedAccuracyISC.max()
150        return JumpStatus.OK if accuracy < SPEED_ACCURACY_THRESHOLD else JumpStatus.SPEED_ACCURACY_EXCEEDS_LIMIT
151    else:
152        raise SSScoringError('data length of zero or invalid')
153
154
155def calculateDistance(start: tuple, end: tuple) -> float:
156    """
157    Calculate the distance between two terrestrial coordinates points.
158
159    Arguments
160    ---------
161        start
162    A latitude, longitude tuple of floating point numbers.
163
164        end
165    A latitude, longitude tuple of floating point numbers.
166
167    Returns
168    -------
169    The distance, in meters, between both points.
170    """
171    return haversine(start, end, unit = Unit.METERS)
172
173
174def convertFlySight2SSScoring(rawData: pd.DataFrame,
175                              altitudeDZMeters = 0.0,
176                              altitudeDZFt = 0.0):
177    """
178    Converts a raw dataframe initialized from a FlySight CSV file into the
179    SSScoring file format.  The SSScoring format uses more descriptive column
180    headers, adds the altitude in feet, and uses UNIX time instead of an ISO
181    string.
182
183    If both `altitudeDZMeters` and `altitudeDZFt` are zero then hMSL is used.
184    Otherwise, this function adjusts the effective altitude with the value.  If
185    both meters and feet values are set this throws an error.
186
187    Arguments
188    ---------
189        rawData : pd.DataFrame
190    FlySight CSV input as a dataframe
191
192        altitudeDZMeters : float
193    Drop zone height above MSL
194
195        altitudeDZFt
196    Drop zone altitudde above MSL
197
198    Returns
199    -------
200    A dataframe in SSScoring format, featuring these columns:
201
202    - timeUnix
203    - altitudeMSL
204    - altitudeAGL
205    - altitudeMSLFt
206    - altitudeAGLFt
207    - hMetersPerSecond
208    - hKMh (km/h)
209    - vMetersPerSecond
210    - vKMh (km/h)
211    - angle
212    - speedAccuracy (ignore; see ISC documentation)
213    - hMetersPerSecond
214    - hKMh
215    - latitude
216    - longitude
217    - verticalAccuracy
218    - speedAccuracyISC
219
220    Errors
221    ------
222    `SSScoringError` if the DZ altitude is set in both meters and feet.
223    """
224    if not isinstance(rawData, pd.DataFrame):
225        raise SSScoringError('convertFlySight2SSScoring input must be a FlySight CSV dataframe')
226
227    if altitudeDZMeters and altitudeDZFt:
228        raise SSScoringError('Cannot set altitude in meters and feet; pick one')
229
230    if altitudeDZMeters:
231        altitudeDZFt = FT_IN_M*altitudeDZMeters
232    if altitudeDZFt:
233        altitudeDZMeters = altitudeDZFt/FT_IN_M
234
235    data = rawData.copy()
236
237    data['altitudeMSLFt'] = data['hMSL'].apply(lambda h: FT_IN_M*h)
238    data['altitudeAGL'] = data.hMSL-altitudeDZMeters
239    data['altitudeAGLFt'] = data.altitudeMSLFt-altitudeDZFt
240    data['timeUnix'] = np.round(data['time'].apply(lambda t: pd.Timestamp(t).timestamp()), decimals = 2)
241    data['hMetersPerSecond'] = (data.velE**2.0+data.velN**2.0)**0.5
242    speedAngle = data['hMetersPerSecond']/data['velD']
243    speedAngle = np.round(90.0-speedAngle.apply(math.atan)/DEG_IN_RADIANS, decimals = 2)
244    speedAccuracyISC = np.round(data.vAcc.apply(lambda a: (2.0**0.5)*a/3.0), decimals = 2)
245
246    data = pd.DataFrame(data = {
247        'timeUnix': data.timeUnix,
248        'altitudeMSL': data.hMSL,
249        'altitudeAGL': data.altitudeAGL,
250        'altitudeMSLFt': data.altitudeMSLFt,
251        'altitudeAGLFt': data.altitudeAGLFt,
252        'vMetersPerSecond': data.velD,
253        'vKMh': 3.6*data.velD,
254        'speedAngle': speedAngle,
255        'speedAccuracy': data.sAcc,
256        'hMetersPerSecond': data.hMetersPerSecond,
257        'hKMh': 3.6*data.hMetersPerSecond,
258        'latitude': data.lat,
259        'longitude': data.lon,
260        'verticalAccuracy': data.vAcc,
261        'speedAccuracyISC': speedAccuracyISC,
262    })
263
264    return data
265
266
267def _dataGroups(data):
268    data_ = data.copy()
269    data_['positive'] = (data_.vMetersPerSecond > 0)
270    data_['group'] = (data_.positive != data_.positive.shift(1)).astype(int).cumsum()-1
271
272    return data_
273
274
275def getSpeedSkydiveFrom(data: pd.DataFrame) -> tuple:
276    """
277    Take the skydive dataframe and get the speed skydiving data:
278
279    - Exit
280    - Speed skydiving window
281    - Drops data before exit and below breakoff altitude
282
283    Arguments
284    ---------
285        data : pd.DataFrame
286    Jump data in SSScoring format
287
288    Returns
289    -------
290    A tuple of two elements:
291
292    - A named tuple with performance and validation window data
293    - A dataframe featuring only speed skydiving data
294
295    Warm up FlySight files and non-speed skydiving files may return invalid
296    values:
297
298    - `None` for the `PerformanceWindow` instance
299    - `data`, most likely empty
300    """
301    if len(data):
302        data = _dataGroups(data)
303        groups = data.group.max()+1
304
305        freeFallGroup = -1
306        MIN_DATA_POINTS = 100 # heuristic
307        MIN_MAX_SPEED = 200 # km/h, heuristic; slower ::= no free fall
308        for group in range(groups):
309            subset = data[data.group == group]
310            if len(subset) >= MIN_DATA_POINTS and subset.vKMh.max() >= MIN_MAX_SPEED:
311                freeFallGroup = group
312
313        data = data[data.group == freeFallGroup]
314        data = data.drop('group', axis = 1).drop('positive', axis = 1)
315
316    data = data[data.altitudeAGL <= MAX_VALID_ELEVATION]
317    if len(data) > 0:
318        exitTime = data[data.vMetersPerSecond > EXIT_SPEED].head(1).timeUnix.iat[0]
319        data = data[data.timeUnix >= exitTime]
320        data = data[data.altitudeAGL >= BREAKOFF_ALTITUDE]
321
322        if len(data):
323            windowStart = data.iloc[0].altitudeAGL
324            windowEnd = windowStart-PERFORMANCE_WINDOW_LENGTH
325            if windowEnd < BREAKOFF_ALTITUDE:
326                windowEnd = BREAKOFF_ALTITUDE
327
328            validationWindowStart = windowEnd+VALIDATION_WINDOW_LENGTH
329            data = data[data.altitudeAGL >= windowEnd]
330            performanceWindow = PerformanceWindow(windowStart, windowEnd, validationWindowStart)
331        else:
332            performanceWindow = None
333
334        return performanceWindow, data
335    else:
336        return None, data
337
338
339def _verticalAcceleration(vKMh: pd.Series, time: pd.Series, interval=TABLE_INTERVAL) -> pd.Series:
340    vAcc = ((vKMh/KMH_AS_MS).diff()/time.diff()).fillna(vKMh/KMH_AS_MS/interval)
341    return vAcc
342
343
344def jumpAnalysisTable(data: pd.DataFrame) -> pd.DataFrame:
345    """
346    Generates the HCD jump analysis table, with speed data at 5-second intervals
347    after exit.
348
349    Arguments
350    ---------
351        data : pd.DataFrame
352    Jump data in SSScoring format
353
354    Returns
355    -------
356    A tuple with a pd.DataFrame and the max speed recorded for the jump:
357
358    - A table dataframe with time and speed
359    - a floating point number
360    """
361    table = None
362    distanceStart = (data.iloc[0].latitude, data.iloc[0].longitude)
363    for column in pd.Series([ 5.0, 10.0, 15.0, 20.0, 25.0, ]):
364        for interval in range(int(column)*10, 10*(int(column)+1)):
365            # Use the next 0.1 sec interval if the current interval tranche has
366            # NaN values.
367            columnRef = interval/10.0
368            timeOffset = data.iloc[0].timeUnix+columnRef
369            tranche = data.query('timeUnix == %f' % timeOffset).copy()
370            tranche['time'] = [ column, ]
371            currentPosition = (tranche.iloc[0].latitude, tranche.iloc[0].longitude)
372            tranche['distanceFromExit'] = [ round(calculateDistance(distanceStart, currentPosition), 2), ]
373            if not tranche.isnull().any().any():
374                break
375
376        if pd.isna(tranche.iloc[-1].vKMh):
377            tranche = data.tail(1).copy()
378            currentPosition = (tranche.iloc[0].latitude, tranche.iloc[0].longitude)
379            tranche['time'] = tranche.timeUnix-data.iloc[0].timeUnix
380            tranche['distanceFromExit'] = [ round(calculateDistance(distanceStart, currentPosition), 2), ]
381
382        if table is not None:
383            table = pd.concat([ table, tranche, ])
384        else:
385            table = tranche
386    table = pd.DataFrame({
387                'time': table.time,
388                'vKMh': table.vKMh,
389                'deltaV': table.vKMh.diff().fillna(table.vKMh),
390                'vAccel m/s²': _verticalAcceleration(table.vKMh, table.time),
391                'speedAngle': table.speedAngle,
392                'angularVel º/s': (table.speedAngle.diff()/table.time.diff()).fillna(table.speedAngle/TABLE_INTERVAL),
393                'deltaAngle': table.speedAngle.diff().fillna(table.speedAngle),
394                'hKMh': table.hKMh,
395                'distanceFromExit (m)': table.distanceFromExit,
396                'altitude (ft)': table.altitudeAGLFt,
397            })
398    return table
399
400
401def dropNonSkydiveDataFrom(data: pd.DataFrame) -> pd.DataFrame:
402    """
403    Discards all data rows before maximum altitude, and all "negative" altitude
404    rows because we don't skydive underground (FlySight bug?).
405
406    This is a more accurate mean velocity calculation from a physics point of
407    view, but it differs from the ISC definition using in scoring - which, if we
408    get technical, is the one that counts.
409
410    Arguments
411    ---------
412        data : pd.DataFrame
413    Jump data in SSScoring format (headers differ from FlySight format)
414
415    Returns
416    -------
417    The jump data for the skydive
418    """
419    timeMaxAlt = data[data.altitudeAGL == data.altitudeAGL.max()].timeUnix.iloc[0]
420    data = data[data.timeUnix > timeMaxAlt]
421
422    data = data[data.altitudeAGL > 0]
423
424    return data
425
426
427def calcScoreMeanVelocity(data: pd.DataFrame) -> tuple:
428    """
429    Calculates the speeds over a 3-second interval as the mean of all the speeds
430    recorded within that 3-second window and resolves the maximum speed.
431
432    Arguments
433    ---------
434        data
435    A `pd.dataframe` with speed run data.
436
437    Returns
438    -------
439    A `tuple` with the best score throughout the speed run, and a dicitionary
440    of the meanVSpeed:spotInTime used in determining the exact scoring speed
441    at every datat point during the speed run.
442
443    Notes
444    -----
445    This implementation uses iteration instead of binning/factorization because
446    some implementers may be unfamiliar with data manipulation in dataframes
447    and this is a critical function that may be under heavy review.  Future
448    versions may revert to dataframe and series/np.array factorization.
449    """
450    scores = dict()
451    for spot in data.plotTime[::1]:
452        subset = data[(data.plotTime <= spot) & (data.plotTime >= (spot-SCORING_INTERVAL))]
453        scores[np.round(subset.vKMh.mean(), decimals = 2)] = spot
454    return (max(scores), scores)
455
456
457def calcScoreISC(data: pd.DataFrame) -> tuple:
458    """
459    Calculates the speeds over a 3-second interval as the ds/dt and dt is the
460    is a 3-second sliding interval from exit.  The window slider moves along the
461    `plotTime` axis in the dataframe.
462
463    Arguments
464    ---------
465        data
466    A `pd.dataframe` with speed run data.
467
468    Returns
469    -------
470    A `tuple` with the best score throughout the speed run, and a dicitionary
471    of the meanVSpeed:spotInTime used in determining the exact scoring speed
472    at every datat point during the speed run.
473    """
474    scores = dict()
475    step = data.plotTime.diff().dropna().mode().iloc[0]
476    end = data.plotTime[-1:].iloc[0]-SCORING_INTERVAL
477    for spot in np.arange(0.0, end, step):
478        intervalStart = np.round(spot, decimals = 2)
479        intervalEnd = np.round(intervalStart+SCORING_INTERVAL, decimals = 2)
480        try:
481            h1 = data[data.plotTime == intervalStart].altitudeAGL.iloc[0]
482            h2 = data[data.plotTime == intervalEnd].altitudeAGL.iloc[0]
483        except IndexError:
484            # TODO: Decide whether to log the missing FlySight samples.
485            continue
486        intervalScore = np.round(MPS_2_KMH*abs(h1-h2)/SCORING_INTERVAL, decimals = 2)
487        scores[intervalScore] = intervalStart
488    return (max(scores), scores)
489
490
491def processJump(data: pd.DataFrame) -> JumpResults:
492    """
493    Take a dataframe in SSScoring format and process it for display.  It
494    serializes all the steps that would be taken from the ssscoring module, but
495    includes some text/HTML data in the output.
496
497    Arguments
498    ---------
499        data: pd.DataFrame
500    A dataframe in SSScoring format
501
502    Returns
503    -------
504    A `JumpResults` named tuple with these items:
505
506    - `score` speed score
507    - `maxSpeed` maximum speed during the jump
508    - `scores` a Series of every 3-second window scores from exit to breakoff
509    - `data` an updated SSScoring dataframe `plotTime`, where 0 = exit, used
510      for plotting
511    - `window` a named tuple with the exit, breakoff, and validation window
512      altitudes
513    - `table` a dataframe featuring the speeds and altitudes at 5-sec intervals
514    - `color` a string that defines the color for the jump result; possible
515      values are _green_ for valid jump, _red_ for invalid jump, per ISC rules
516    - `result` a string with the legend of _valid_ or _invalid_ jump
517    """
518    workData = data.copy()
519    workData = dropNonSkydiveDataFrom(workData)
520    window, workData = getSpeedSkydiveFrom(workData)
521    if workData.empty and not window:
522        workData = None
523        maxSpeed = -1.0
524        score = -1.0
525        scores = None
526        table = None
527        window = None
528        jumpStatus = JumpStatus.WARM_UP_FILE
529    else:
530        jumpStatus = validateJumpISC(workData, window)
531        score = None
532        scores = None
533        table = None
534        baseTime = workData.iloc[0].timeUnix
535        workData['plotTime'] = round(workData.timeUnix-baseTime, 2)
536        if jumpStatus == JumpStatus.OK:
537            table = None
538            table = jumpAnalysisTable(workData)
539            maxSpeed = data.vKMh.max()
540            score, scores = calcScoreISC(workData)
541        else:
542            maxSpeed = -1
543            if not len(workData):
544                jumpStatus = JumpStatus.INVALID_SPEED_FILE
545    return JumpResults(workData, maxSpeed, score, scores, table, window, jumpStatus)
546
547
548def _readVersion1CSV(fileThing: str) -> pd.DataFrame:
549    return pd.read_csv(fileThing, skiprows = (1, 1), index_col = False)
550
551
552def _tagVersion1From(fileThing: str) -> str:
553    return fileThing.replace('.CSV', '').replace('.csv', '').replace('/data', '').replace('/', ' ').strip()+':v1'
554
555
556def _tagVersion2From(fileThing: str) -> str:
557    if '/' in fileThing:
558        return fileThing.split('/')[-2]+':v2'
559    else:
560        return fileThing.replace('.CSV', '').replace('.csv', '')+':v2'
561
562
563
564def _readVersion2CSV(jumpFile: str) -> pd.DataFrame:
565    from ssscoring.constants import FLYSIGHT_2_HEADER
566    from ssscoring.flysight import skipOverFS2MetadataRowsIn
567
568    rawData = pd.read_csv(jumpFile, names = FLYSIGHT_2_HEADER, skiprows = 6, index_col = False)
569    rawData = skipOverFS2MetadataRowsIn(rawData)
570    rawData.drop('GNSS', inplace = True, axis = 1)
571    return rawData
572
573
574def getFlySightDataFromCSVBuffer(buffer:bytes, bufferName:str) -> tuple:
575    """
576    Ingress a buffer with known FlySight or SkyTrax file data for SSScoring
577    processing.
578
579    Arguments
580    ---------
581        buffer
582    A binary data buffer, bag of bytes, containing a known FlySight track file.
583
584        bufferName
585    An arbitrary name for the buffer of type `str`.  It's used for constructing
586    the full buffer tag value for human identification.
587
588    Returns
589    -------
590    A `tuple` with two items:
591        - `rawData` - a dataframe representation of the CSV with the original
592          headers but without the data type header
593        - `tag` - a string with an identifying tag derived from the path name
594          and file version in the form `some name:vX`.  It uses the current
595          path as metadata to infer the name.  There's no semantics enforcement.
596
597    Raises
598    ------
599    `SSScoringError` if the CSV file is invalid in any way.
600    """
601    if not isinstance(buffer, bytes):
602        raise SSScoringError('buffer must be an instance of bytes, a bytes buffer')
603    try:
604        stringIO = StringIO(buffer.decode(FLYSIGHT_FILE_ENCODING))
605    except Exception as e:
606        raise SSScoringError('invalid buffer endcoding - %s' % str(e))
607    version = detectFlySightFileVersionOf(buffer)
608    if version == FlySightVersion.V1:
609        rawData = _readVersion1CSV(stringIO)
610        tag = _tagVersion1From(bufferName)
611    elif version == FlySightVersion.V2:
612        rawData = _readVersion2CSV(stringIO)
613        tag = _tagVersion2From(bufferName)
614    return (rawData, tag)
615
616
617def getFlySightDataFromCSVFileName(jumpFile) -> tuple:
618    """
619    Ingress a known FlySight or SkyTrax file into memory for SSScoring
620    processing.
621
622    Arguments
623    ---------
624        jumpFile
625    A string or `pathlib.Path` object; can be a relative or an asbolute path.
626
627    Returns
628    -------
629    A `tuple` with two items:
630        - `rawData` - a dataframe representation of the CSV with the original
631          headers but without the data type header
632        - `tag` - a string with an identifying tag derived from the path name
633          and file version in the form `some name:vX`.  It uses the current
634          path as metadata to infer the name.  There's no semantics enforcement.
635
636    Raises
637    ------
638    `SSScoringError` if the CSV file is invalid in any way.
639    """
640    from ssscoring.flysight import validFlySightHeaderIn
641
642    if isinstance(jumpFile, Path):
643        jumpFile = jumpFile.as_posix()
644    elif isinstance(jumpFile, str):
645        pass
646    else:
647        raise SSScoringError('jumpFile must be a string or a Path object')
648    if not validFlySightHeaderIn(jumpFile):
649        raise SSScoringError('%s is an invalid speed skydiving file')
650    version = detectFlySightFileVersionOf(jumpFile)
651    if version == FlySightVersion.V1:
652        rawData = _readVersion1CSV(jumpFile)
653        tag = _tagVersion1From(jumpFile)
654    elif version == FlySightVersion.V2:
655        rawData = _readVersion2CSV(jumpFile)
656        tag = _tagVersion2From(jumpFile)
657    return (rawData, tag)
658
659
660def processAllJumpFiles(jumpFiles: list, altitudeDZMeters = 0.0) -> dict:
661    """
662    Process all jump files in a list of valid FlySight files.  Returns a
663    dictionary of jump results with a human-readable version of the file name.
664    The `jumpFiles` list can be generated by hand or the output of the
665    `ssscoring.fs1.getAllSpeedJumpFilesFrom` called to operate on a data lake.
666
667    Arguments
668    ---------
669        jumpFiles
670    A list of file things that could represent one of these:
671    - file things relative or absolute path names to individual FlySight CSV
672      files.
673    - A specialization of BytesIO, such as the bags of bytes that Streamlit.io
674      generates after uploading and reading a file into the Streamlit
675      environment
676
677        altitudeDZMeters : float
678    Drop zone height above MSL
679
680    Returns
681    -------
682        dict
683    A dictionary of jump results.  The key is a human-readable version of a
684    `jumpFile` name with the extension, path, and extraneous spaces eliminated
685    or replaced by appropriate characters.  File names use Unicode, so accents
686    and non-ANSI characters are allowed in file names.
687
688    Raises
689    ------
690    `SSScoringError` if the jumpFiles object is empty, or if the individual
691    objects in the list aren't `BytesIO`, file name strings, or `Path`
692    instances.
693    """
694    jumpResults = dict()
695    if not len(jumpFiles):
696        raise SSScoringError('jumpFiles must have at least one element')
697    if not isinstance(jumpFiles, dict) and not isinstance(jumpFiles, list):
698        raise SSScoringError('dict with jump file names and FS versions or list of byte bags expected')
699    if isinstance(jumpFiles, dict):
700        objectsList = sorted(list(jumpFiles.keys()))
701    elif isinstance(jumpFiles, list):
702        objectsList = jumpFiles
703    obj = objectsList[0]
704    if not isinstance(obj, Path) and not isinstance(obj, str) and not isinstance(obj, BytesIO):
705        raise SSScoringError('jumpFiles must contain file-like things or BytesIO objects')
706    for jumpFile in objectsList:
707        if isinstance(jumpFile, BytesIO):
708            rawData, tag = getFlySightDataFromCSVBuffer(jumpFile.getvalue(), jumpFile.name)
709        else:
710            rawData, tag = getFlySightDataFromCSVFileName(jumpFile)
711        jumpResult = processJump(convertFlySight2SSScoring(rawData, altitudeDZMeters = altitudeDZMeters))
712        jumpResults[tag] = jumpResult
713    return jumpResults
714
715
716def aggregateResults(jumpResults: dict) -> pd.DataFrame:
717    """
718    Aggregate all the results in a table fashioned after Marco Hepp's and Nklas
719    Daniel's score tracking data.
720
721    Arguments
722    ---------
723        jumpResults: dict
724    A dictionary of jump results, in which each result corresponds to a FlySight
725    file name.  See `ssscoring.processAllJumpFiles` for details.
726
727    Returns
728    -------
729    A dataframe featuring these columns:
730
731    - Score
732    - Speeds at 5, 10, 15, 20, and 25 second tranches
733    - Final time contemplated in the analysis
734    - Max speed
735
736    The dataframe rows are identified by the human readable jump file name.
737
738    Raises
739    ------
740    `SSSCoringError` if the `jumpResults` object is empty.
741    """
742    if not len(jumpResults):
743        raise SSScoringError('jumpResults is empty - impossible to collate angles')
744    speeds = pd.DataFrame()
745    for jumpResultIndex in sorted(list(jumpResults.keys())):
746        jumpResult = jumpResults[jumpResultIndex]
747        if jumpResult.status == JumpStatus.OK:
748            t = jumpResult.table
749            finalTime = t.iloc[-1].time
750            t.iloc[-1].time = LAST_TIME_TRANCHE
751            t = pd.pivot_table(t, columns = t.time)
752            d = pd.DataFrame([ jumpResult.score, ], index = [ jumpResultIndex, ], columns = [ 'score', ], dtype = object)
753            for column in t.columns:
754                d[column] = t[column].vKMh
755            d['finalTime'] = [ finalTime, ]
756            d['maxSpeed'] = jumpResult.maxSpeed
757
758            if speeds.empty:
759                speeds = d.copy()
760            else:
761                speeds = pd.concat([ speeds, d, ])
762    speeds = speeds.replace(np.nan, 0.0)
763    return speeds.sort_index()
764
765
766def roundedAggregateResults(aggregate: pd.DataFrame) -> pd.DataFrame:
767    """
768    Aggregate all the results in a table fashioned after Marco Hepp's and Nklas
769    Daniel's score tracking data.  All speed results are rounded at `n > x.5`
770    for any value.
771
772    Arguments
773    ---------
774        aggregate: pd.DataFrame
775    A dataframe output of `ssscoring.fs1.aggregateResults`.
776
777    Returns
778    -------
779    A dataframe featuring the **rounded values** for these columns:
780
781    - Score
782    - Speeds at 5, 10, 15, 20, and 25 second tranches
783    - Max speed
784
785    The `finalTime` column is ignored.
786
787    The dataframe rows are identified by the human readable jump file name.
788
789    This is a less precise version of the `ssscoring.aggregateResults`
790    dataframe, useful during training to keep rounded results available for
791    review.
792
793    Raises
794    ------
795    `SSSCoringError` if the `jumpResults` object is empty.
796    """
797    for column in [col for col in aggregate.columns if 'Time' not in str(col)]:
798        aggregate[column] = aggregate[column].apply(round)
799
800    return aggregate
801
802
803def collateAnglesByTimeFromExit(jumpResults: dict) -> pd.DataFrame:
804    """
805    Collate all the angles by time from the `jumpResults` into a dataframe that
806    features the jump tag as index, the time tranches and the angles at each
807    time tranche.
808
809    Arguments
810    ---------
811        jumpResults: dict
812    A dictionary of jump results, in which each result corresponds to a FlySight
813    file name.  See `ssscoring.processAllJumpFiles` for details.
814
815    Returns
816    -------
817    A dataframe featuring these columns:
818
819    - Score
820    - Angles at 5, 10, 15, 20, and 25 second tranches
821    - Final time contemplated in the analysis
822
823    Raises
824    ------
825    `SSSCoringError` if the `jumpResults` object is empty.
826    """
827    if not len(jumpResults):
828        raise SSScoringError('jumpResults is empty - impossible to collate angles')
829    angles = pd.DataFrame()
830    for jumpResultIndex in sorted(list(jumpResults.keys())):
831        jumpResult = jumpResults[jumpResultIndex]
832        if jumpResult.status == JumpStatus.OK:
833            t = jumpResult.table
834            finalTime = t.iloc[-1].time
835            t.iloc[-1].time = LAST_TIME_TRANCHE
836            t = pd.pivot_table(t, columns = t.time)
837            d = pd.DataFrame([ jumpResult.score, ], index = [ jumpResultIndex, ], columns = [ 'score', ], dtype = object)
838            for column in t.columns:
839                d[column] = t[column].speedAngle
840            d['finalTime'] = [ finalTime, ]
841
842            if angles.empty:
843                angles = d.copy()
844            else:
845                angles = pd.concat([ angles, d, ])
846    cols = sorted([ column for column in angles.columns if isinstance(column, float) ])
847    cols = [ 'score', ]+cols+[ 'finalTime', ]
848    angles = angles[cols]
849    angles = angles.replace(np.nan, 0.0)
850    return angles.sort_index()
851
852
853def totalResultsFrom(aggregate: pd.DataFrame) -> pd.DataFrame:
854    """
855    Calculates the total and mean speeds for an aggregation of speed jumps.
856
857    Arguments
858    ---------
859        aggregate: pd.DataFrame
860    The aggregate results dataframe resulting from calling `ssscoring.aggregateResults`
861    with valid results.
862
863    Returns
864    -------
865    A dataframe with one row and two columns:
866
867    - totalSpeed ::= the sum of all speeds in the aggregated results
868    - meanSpeed ::= the mean of all speeds
869    - maxSpeed := the absolute max speed over the speed runs set
870    - meanSpeedSTD := scored speeds standar deviation
871    - maxScore ::= the max score among all the speed scores
872    - maxScoreSTD := the max scores standard deviation
873
874    Raises
875    ------
876    `AttributeError` if aggregate is an empty dataframe or `None`, or if the
877    `aggregate` dataframe doesn't conform to the output of `ssscoring.aggregateResults`.
878    """
879    if aggregate is None:
880        raise AttributeError('aggregate dataframe is empty')
881    elif isinstance(aggregate, pd.DataFrame) and not len(aggregate):
882        raise AttributeError('aggregate dataframe is empty')
883
884    totals = pd.DataFrame({
885        'totalScore': [ round(aggregate.score.sum(), 2), ],
886        'mean': [ round(aggregate.score.mean(), 2), ],
887        'deviation': [ round(aggregate.score.std(), 2), ],
888        'maxScore': [ round(aggregate.score.max(), 2), ],
889        'absMaxSpeed': [ round(aggregate.maxSpeed.max(), 2), ], }, index = [ 'totalSpeed'],)
890    return totals
def isValidMinimumAltitude(altitude: float) -> bool:
47def isValidMinimumAltitude(altitude: float) -> bool:
48    """
49    Reports whether an `altitude` is below the IPC and USPA valid parameters,
50    or within `BREAKOFF_ALTITUDE` and `PERFORMACE_WINDOW_LENGTH`.  In invalid
51    altitude doesn't invalidate a FlySight data file.  This function can be used
52    for generating warnings.  The stock FlySightViewer scores a speed jump even
53    if the exit was below the minimum altitude.
54
55    See:  FAI Competition Rules Speed Skydiving section 5.3 for details.
56
57    Arguments
58    ---------
59        altitude
60    An altitude in meters, calculated as data.hMSL - DZ altitude.
61
62    Returns
63    -------
64    `True` if the altitude is valid.
65    """
66    if not isinstance(altitude, float):
67        altitude = float(altitude)
68    minAltitude = BREAKOFF_ALTITUDE+PERFORMANCE_WINDOW_LENGTH
69    return altitude >= minAltitude

Reports whether an altitude is below the IPC and USPA valid parameters, or within BREAKOFF_ALTITUDE and PERFORMACE_WINDOW_LENGTH. In invalid altitude doesn't invalidate a FlySight data file. This function can be used for generating warnings. The stock FlySightViewer scores a speed jump even if the exit was below the minimum altitude.

See: FAI Competition Rules Speed Skydiving section 5.3 for details.

Arguments

altitude

An altitude in meters, calculated as data.hMSL - DZ altitude.

Returns

True if the altitude is valid.

def isValidMaximumAltitude(altitude: float) -> bool:
72def isValidMaximumAltitude(altitude: float) -> bool:
73    """
74    Reports whether an `altitude` is above the maximum altitude allowed by the
75    rules.
76
77    See:  FAI Competition Rules Speed Skydiving section 5.3 for details.
78
79    Arguments
80    ---------
81        altitude
82    An altitude in meters, calculated as data.hMSL - DZ altitude.
83
84    Returns
85    -------
86    `True` if the altitude is valid.
87
88    See
89    ---
90    `ssscoring.constants.MAX_ALTITUDE_FT`
91    `ssscoring.constants.MAX_ALTITUDE_METERS`
92    """
93    if not isinstance(altitude, float):
94        altitude = float(altitude)
95    return altitude <= MAX_ALTITUDE_METERS

Reports whether an altitude is above the maximum altitude allowed by the rules.

See: FAI Competition Rules Speed Skydiving section 5.3 for details.

Arguments

altitude

An altitude in meters, calculated as data.hMSL - DZ altitude.

Returns

True if the altitude is valid.

See

ssscoring.constants.MAX_ALTITUDE_FT ssscoring.constants.MAX_ALTITUDE_METERS

def isValidJumpISC( data: pandas.core.frame.DataFrame, window: ssscoring.datatypes.PerformanceWindow) -> bool:
 98def isValidJumpISC(data: pd.DataFrame,
 99                window: PerformanceWindow) -> bool:
100    """
101    **DEPRECATED** - Validates the jump according to ISC/FAI/USPA competition rules.  A jump is
102    valid when the speed accuracy parameter is less than 3 m/s for the whole
103    validation window duration.
104
105    Arguments
106    ---------
107        data : pd.DataFramce
108    Jumnp data in SSScoring format
109        window : ssscoring.PerformanceWindow
110    Performance window start, end values in named tuple format
111
112    Returns
113    -------
114    `True` if the jump is valid according to ISC/FAI/USPA rules.
115    """
116    warnings.warn('This function is DEPRECATED as of version 2.4.0', UserWarning)
117    if len(data) > 0:
118        accuracy = data[data.altitudeAGL <= window.validationStart].speedAccuracyISC.max()
119        return accuracy < SPEED_ACCURACY_THRESHOLD
120    else:
121        return False

DEPRECATED - Validates the jump according to ISC/FAI/USPA competition rules. A jump is valid when the speed accuracy parameter is less than 3 m/s for the whole validation window duration.

Arguments

data : pd.DataFramce

Jumnp data in SSScoring format window : ssscoring.PerformanceWindow Performance window start, end values in named tuple format

Returns

True if the jump is valid according to ISC/FAI/USPA rules.

def validateJumpISC( data: pandas.core.frame.DataFrame, window: ssscoring.datatypes.PerformanceWindow) -> ssscoring.datatypes.JumpStatus:
124def validateJumpISC(data: pd.DataFrame,
125                window: PerformanceWindow) -> JumpStatus:
126    """
127    Validates the jump according to ISC/FAI/USPA competition rules.  A jump is
128    valid when the speed accuracy parameter is less than 3 m/s for the whole
129    validation window duration.
130
131    Arguments
132    ---------
133        data : pd.DataFramce
134    Jumnp data in SSScoring format
135        window : ssscoring.PerformanceWindow
136    Performance window start, end values in named tuple format
137
138    Returns
139    -------
140    - `JumpStatus.OK` if `data` reflects a valid jump according to ISC rules,
141    where all speed accuracy values < 'SPEED_ACCURACY_THRESHOLD'.
142    - `JumpStatus.SPEED_ACCURACY_EXCEEDS_LIMIT' if `data` has one or more values
143    within the validation window with a value >= 'SPEED_ACCURACY_THRESHOLD'.
144
145    Raises
146    ------
147    `SSScoringError' if `data` has a length of zero or it's not initialized.
148    """
149    if len(data) > 0:
150        accuracy = data[data.altitudeAGL <= window.validationStart].speedAccuracyISC.max()
151        return JumpStatus.OK if accuracy < SPEED_ACCURACY_THRESHOLD else JumpStatus.SPEED_ACCURACY_EXCEEDS_LIMIT
152    else:
153        raise SSScoringError('data length of zero or invalid')

Validates the jump according to ISC/FAI/USPA competition rules. A jump is valid when the speed accuracy parameter is less than 3 m/s for the whole validation window duration.

Arguments

data : pd.DataFramce

Jumnp data in SSScoring format window : ssscoring.PerformanceWindow Performance window start, end values in named tuple format

Returns

  • JumpStatus.OK if data reflects a valid jump according to ISC rules, where all speed accuracy values < 'SPEED_ACCURACY_THRESHOLD'.
  • JumpStatus.SPEED_ACCURACY_EXCEEDS_LIMIT' ifdata` has one or more values within the validation window with a value >= 'SPEED_ACCURACY_THRESHOLD'.

Raises

SSScoringError' ifdata` has a length of zero or it's not initialized.

def calculateDistance(start: tuple, end: tuple) -> float:
156def calculateDistance(start: tuple, end: tuple) -> float:
157    """
158    Calculate the distance between two terrestrial coordinates points.
159
160    Arguments
161    ---------
162        start
163    A latitude, longitude tuple of floating point numbers.
164
165        end
166    A latitude, longitude tuple of floating point numbers.
167
168    Returns
169    -------
170    The distance, in meters, between both points.
171    """
172    return haversine(start, end, unit = Unit.METERS)

Calculate the distance between two terrestrial coordinates points.

Arguments

start

A latitude, longitude tuple of floating point numbers.

end

A latitude, longitude tuple of floating point numbers.

Returns

The distance, in meters, between both points.

def convertFlySight2SSScoring( rawData: pandas.core.frame.DataFrame, altitudeDZMeters=0.0, altitudeDZFt=0.0):
175def convertFlySight2SSScoring(rawData: pd.DataFrame,
176                              altitudeDZMeters = 0.0,
177                              altitudeDZFt = 0.0):
178    """
179    Converts a raw dataframe initialized from a FlySight CSV file into the
180    SSScoring file format.  The SSScoring format uses more descriptive column
181    headers, adds the altitude in feet, and uses UNIX time instead of an ISO
182    string.
183
184    If both `altitudeDZMeters` and `altitudeDZFt` are zero then hMSL is used.
185    Otherwise, this function adjusts the effective altitude with the value.  If
186    both meters and feet values are set this throws an error.
187
188    Arguments
189    ---------
190        rawData : pd.DataFrame
191    FlySight CSV input as a dataframe
192
193        altitudeDZMeters : float
194    Drop zone height above MSL
195
196        altitudeDZFt
197    Drop zone altitudde above MSL
198
199    Returns
200    -------
201    A dataframe in SSScoring format, featuring these columns:
202
203    - timeUnix
204    - altitudeMSL
205    - altitudeAGL
206    - altitudeMSLFt
207    - altitudeAGLFt
208    - hMetersPerSecond
209    - hKMh (km/h)
210    - vMetersPerSecond
211    - vKMh (km/h)
212    - angle
213    - speedAccuracy (ignore; see ISC documentation)
214    - hMetersPerSecond
215    - hKMh
216    - latitude
217    - longitude
218    - verticalAccuracy
219    - speedAccuracyISC
220
221    Errors
222    ------
223    `SSScoringError` if the DZ altitude is set in both meters and feet.
224    """
225    if not isinstance(rawData, pd.DataFrame):
226        raise SSScoringError('convertFlySight2SSScoring input must be a FlySight CSV dataframe')
227
228    if altitudeDZMeters and altitudeDZFt:
229        raise SSScoringError('Cannot set altitude in meters and feet; pick one')
230
231    if altitudeDZMeters:
232        altitudeDZFt = FT_IN_M*altitudeDZMeters
233    if altitudeDZFt:
234        altitudeDZMeters = altitudeDZFt/FT_IN_M
235
236    data = rawData.copy()
237
238    data['altitudeMSLFt'] = data['hMSL'].apply(lambda h: FT_IN_M*h)
239    data['altitudeAGL'] = data.hMSL-altitudeDZMeters
240    data['altitudeAGLFt'] = data.altitudeMSLFt-altitudeDZFt
241    data['timeUnix'] = np.round(data['time'].apply(lambda t: pd.Timestamp(t).timestamp()), decimals = 2)
242    data['hMetersPerSecond'] = (data.velE**2.0+data.velN**2.0)**0.5
243    speedAngle = data['hMetersPerSecond']/data['velD']
244    speedAngle = np.round(90.0-speedAngle.apply(math.atan)/DEG_IN_RADIANS, decimals = 2)
245    speedAccuracyISC = np.round(data.vAcc.apply(lambda a: (2.0**0.5)*a/3.0), decimals = 2)
246
247    data = pd.DataFrame(data = {
248        'timeUnix': data.timeUnix,
249        'altitudeMSL': data.hMSL,
250        'altitudeAGL': data.altitudeAGL,
251        'altitudeMSLFt': data.altitudeMSLFt,
252        'altitudeAGLFt': data.altitudeAGLFt,
253        'vMetersPerSecond': data.velD,
254        'vKMh': 3.6*data.velD,
255        'speedAngle': speedAngle,
256        'speedAccuracy': data.sAcc,
257        'hMetersPerSecond': data.hMetersPerSecond,
258        'hKMh': 3.6*data.hMetersPerSecond,
259        'latitude': data.lat,
260        'longitude': data.lon,
261        'verticalAccuracy': data.vAcc,
262        'speedAccuracyISC': speedAccuracyISC,
263    })
264
265    return data

Converts a raw dataframe initialized from a FlySight CSV file into the SSScoring file format. The SSScoring format uses more descriptive column headers, adds the altitude in feet, and uses UNIX time instead of an ISO string.

If both altitudeDZMeters and altitudeDZFt are zero then hMSL is used. Otherwise, this function adjusts the effective altitude with the value. If both meters and feet values are set this throws an error.

Arguments

rawData : pd.DataFrame

FlySight CSV input as a dataframe

altitudeDZMeters : float

Drop zone height above MSL

altitudeDZFt

Drop zone altitudde above MSL

Returns

A dataframe in SSScoring format, featuring these columns:

  • timeUnix
  • altitudeMSL
  • altitudeAGL
  • altitudeMSLFt
  • altitudeAGLFt
  • hMetersPerSecond
  • hKMh (km/h)
  • vMetersPerSecond
  • vKMh (km/h)
  • angle
  • speedAccuracy (ignore; see ISC documentation)
  • hMetersPerSecond
  • hKMh
  • latitude
  • longitude
  • verticalAccuracy
  • speedAccuracyISC

Errors

SSScoringError if the DZ altitude is set in both meters and feet.

def getSpeedSkydiveFrom(data: pandas.core.frame.DataFrame) -> tuple:
276def getSpeedSkydiveFrom(data: pd.DataFrame) -> tuple:
277    """
278    Take the skydive dataframe and get the speed skydiving data:
279
280    - Exit
281    - Speed skydiving window
282    - Drops data before exit and below breakoff altitude
283
284    Arguments
285    ---------
286        data : pd.DataFrame
287    Jump data in SSScoring format
288
289    Returns
290    -------
291    A tuple of two elements:
292
293    - A named tuple with performance and validation window data
294    - A dataframe featuring only speed skydiving data
295
296    Warm up FlySight files and non-speed skydiving files may return invalid
297    values:
298
299    - `None` for the `PerformanceWindow` instance
300    - `data`, most likely empty
301    """
302    if len(data):
303        data = _dataGroups(data)
304        groups = data.group.max()+1
305
306        freeFallGroup = -1
307        MIN_DATA_POINTS = 100 # heuristic
308        MIN_MAX_SPEED = 200 # km/h, heuristic; slower ::= no free fall
309        for group in range(groups):
310            subset = data[data.group == group]
311            if len(subset) >= MIN_DATA_POINTS and subset.vKMh.max() >= MIN_MAX_SPEED:
312                freeFallGroup = group
313
314        data = data[data.group == freeFallGroup]
315        data = data.drop('group', axis = 1).drop('positive', axis = 1)
316
317    data = data[data.altitudeAGL <= MAX_VALID_ELEVATION]
318    if len(data) > 0:
319        exitTime = data[data.vMetersPerSecond > EXIT_SPEED].head(1).timeUnix.iat[0]
320        data = data[data.timeUnix >= exitTime]
321        data = data[data.altitudeAGL >= BREAKOFF_ALTITUDE]
322
323        if len(data):
324            windowStart = data.iloc[0].altitudeAGL
325            windowEnd = windowStart-PERFORMANCE_WINDOW_LENGTH
326            if windowEnd < BREAKOFF_ALTITUDE:
327                windowEnd = BREAKOFF_ALTITUDE
328
329            validationWindowStart = windowEnd+VALIDATION_WINDOW_LENGTH
330            data = data[data.altitudeAGL >= windowEnd]
331            performanceWindow = PerformanceWindow(windowStart, windowEnd, validationWindowStart)
332        else:
333            performanceWindow = None
334
335        return performanceWindow, data
336    else:
337        return None, data

Take the skydive dataframe and get the speed skydiving data:

  • Exit
  • Speed skydiving window
  • Drops data before exit and below breakoff altitude

Arguments

data : pd.DataFrame

Jump data in SSScoring format

Returns

A tuple of two elements:

  • A named tuple with performance and validation window data
  • A dataframe featuring only speed skydiving data

Warm up FlySight files and non-speed skydiving files may return invalid values:

  • None for the PerformanceWindow instance
  • data, most likely empty
def jumpAnalysisTable(data: pandas.core.frame.DataFrame) -> pandas.core.frame.DataFrame:
345def jumpAnalysisTable(data: pd.DataFrame) -> pd.DataFrame:
346    """
347    Generates the HCD jump analysis table, with speed data at 5-second intervals
348    after exit.
349
350    Arguments
351    ---------
352        data : pd.DataFrame
353    Jump data in SSScoring format
354
355    Returns
356    -------
357    A tuple with a pd.DataFrame and the max speed recorded for the jump:
358
359    - A table dataframe with time and speed
360    - a floating point number
361    """
362    table = None
363    distanceStart = (data.iloc[0].latitude, data.iloc[0].longitude)
364    for column in pd.Series([ 5.0, 10.0, 15.0, 20.0, 25.0, ]):
365        for interval in range(int(column)*10, 10*(int(column)+1)):
366            # Use the next 0.1 sec interval if the current interval tranche has
367            # NaN values.
368            columnRef = interval/10.0
369            timeOffset = data.iloc[0].timeUnix+columnRef
370            tranche = data.query('timeUnix == %f' % timeOffset).copy()
371            tranche['time'] = [ column, ]
372            currentPosition = (tranche.iloc[0].latitude, tranche.iloc[0].longitude)
373            tranche['distanceFromExit'] = [ round(calculateDistance(distanceStart, currentPosition), 2), ]
374            if not tranche.isnull().any().any():
375                break
376
377        if pd.isna(tranche.iloc[-1].vKMh):
378            tranche = data.tail(1).copy()
379            currentPosition = (tranche.iloc[0].latitude, tranche.iloc[0].longitude)
380            tranche['time'] = tranche.timeUnix-data.iloc[0].timeUnix
381            tranche['distanceFromExit'] = [ round(calculateDistance(distanceStart, currentPosition), 2), ]
382
383        if table is not None:
384            table = pd.concat([ table, tranche, ])
385        else:
386            table = tranche
387    table = pd.DataFrame({
388                'time': table.time,
389                'vKMh': table.vKMh,
390                'deltaV': table.vKMh.diff().fillna(table.vKMh),
391                'vAccel m/s²': _verticalAcceleration(table.vKMh, table.time),
392                'speedAngle': table.speedAngle,
393                'angularVel º/s': (table.speedAngle.diff()/table.time.diff()).fillna(table.speedAngle/TABLE_INTERVAL),
394                'deltaAngle': table.speedAngle.diff().fillna(table.speedAngle),
395                'hKMh': table.hKMh,
396                'distanceFromExit (m)': table.distanceFromExit,
397                'altitude (ft)': table.altitudeAGLFt,
398            })
399    return table

Generates the HCD jump analysis table, with speed data at 5-second intervals after exit.

Arguments

data : pd.DataFrame

Jump data in SSScoring format

Returns

A tuple with a pd.DataFrame and the max speed recorded for the jump:

  • A table dataframe with time and speed
  • a floating point number
def dropNonSkydiveDataFrom(data: pandas.core.frame.DataFrame) -> pandas.core.frame.DataFrame:
402def dropNonSkydiveDataFrom(data: pd.DataFrame) -> pd.DataFrame:
403    """
404    Discards all data rows before maximum altitude, and all "negative" altitude
405    rows because we don't skydive underground (FlySight bug?).
406
407    This is a more accurate mean velocity calculation from a physics point of
408    view, but it differs from the ISC definition using in scoring - which, if we
409    get technical, is the one that counts.
410
411    Arguments
412    ---------
413        data : pd.DataFrame
414    Jump data in SSScoring format (headers differ from FlySight format)
415
416    Returns
417    -------
418    The jump data for the skydive
419    """
420    timeMaxAlt = data[data.altitudeAGL == data.altitudeAGL.max()].timeUnix.iloc[0]
421    data = data[data.timeUnix > timeMaxAlt]
422
423    data = data[data.altitudeAGL > 0]
424
425    return data

Discards all data rows before maximum altitude, and all "negative" altitude rows because we don't skydive underground (FlySight bug?).

This is a more accurate mean velocity calculation from a physics point of view, but it differs from the ISC definition using in scoring - which, if we get technical, is the one that counts.

Arguments

data : pd.DataFrame

Jump data in SSScoring format (headers differ from FlySight format)

Returns

The jump data for the skydive

def calcScoreMeanVelocity(data: pandas.core.frame.DataFrame) -> tuple:
428def calcScoreMeanVelocity(data: pd.DataFrame) -> tuple:
429    """
430    Calculates the speeds over a 3-second interval as the mean of all the speeds
431    recorded within that 3-second window and resolves the maximum speed.
432
433    Arguments
434    ---------
435        data
436    A `pd.dataframe` with speed run data.
437
438    Returns
439    -------
440    A `tuple` with the best score throughout the speed run, and a dicitionary
441    of the meanVSpeed:spotInTime used in determining the exact scoring speed
442    at every datat point during the speed run.
443
444    Notes
445    -----
446    This implementation uses iteration instead of binning/factorization because
447    some implementers may be unfamiliar with data manipulation in dataframes
448    and this is a critical function that may be under heavy review.  Future
449    versions may revert to dataframe and series/np.array factorization.
450    """
451    scores = dict()
452    for spot in data.plotTime[::1]:
453        subset = data[(data.plotTime <= spot) & (data.plotTime >= (spot-SCORING_INTERVAL))]
454        scores[np.round(subset.vKMh.mean(), decimals = 2)] = spot
455    return (max(scores), scores)

Calculates the speeds over a 3-second interval as the mean of all the speeds recorded within that 3-second window and resolves the maximum speed.

Arguments

data

A pd.dataframe with speed run data.

Returns

A tuple with the best score throughout the speed run, and a dicitionary of the meanVSpeed:spotInTime used in determining the exact scoring speed at every datat point during the speed run.

Notes

This implementation uses iteration instead of binning/factorization because some implementers may be unfamiliar with data manipulation in dataframes and this is a critical function that may be under heavy review. Future versions may revert to dataframe and series/np.array factorization.

def calcScoreISC(data: pandas.core.frame.DataFrame) -> tuple:
458def calcScoreISC(data: pd.DataFrame) -> tuple:
459    """
460    Calculates the speeds over a 3-second interval as the ds/dt and dt is the
461    is a 3-second sliding interval from exit.  The window slider moves along the
462    `plotTime` axis in the dataframe.
463
464    Arguments
465    ---------
466        data
467    A `pd.dataframe` with speed run data.
468
469    Returns
470    -------
471    A `tuple` with the best score throughout the speed run, and a dicitionary
472    of the meanVSpeed:spotInTime used in determining the exact scoring speed
473    at every datat point during the speed run.
474    """
475    scores = dict()
476    step = data.plotTime.diff().dropna().mode().iloc[0]
477    end = data.plotTime[-1:].iloc[0]-SCORING_INTERVAL
478    for spot in np.arange(0.0, end, step):
479        intervalStart = np.round(spot, decimals = 2)
480        intervalEnd = np.round(intervalStart+SCORING_INTERVAL, decimals = 2)
481        try:
482            h1 = data[data.plotTime == intervalStart].altitudeAGL.iloc[0]
483            h2 = data[data.plotTime == intervalEnd].altitudeAGL.iloc[0]
484        except IndexError:
485            # TODO: Decide whether to log the missing FlySight samples.
486            continue
487        intervalScore = np.round(MPS_2_KMH*abs(h1-h2)/SCORING_INTERVAL, decimals = 2)
488        scores[intervalScore] = intervalStart
489    return (max(scores), scores)

Calculates the speeds over a 3-second interval as the ds/dt and dt is the is a 3-second sliding interval from exit. The window slider moves along the plotTime axis in the dataframe.

Arguments

data

A pd.dataframe with speed run data.

Returns

A tuple with the best score throughout the speed run, and a dicitionary of the meanVSpeed:spotInTime used in determining the exact scoring speed at every datat point during the speed run.

def processJump(data: pandas.core.frame.DataFrame) -> ssscoring.datatypes.JumpResults:
492def processJump(data: pd.DataFrame) -> JumpResults:
493    """
494    Take a dataframe in SSScoring format and process it for display.  It
495    serializes all the steps that would be taken from the ssscoring module, but
496    includes some text/HTML data in the output.
497
498    Arguments
499    ---------
500        data: pd.DataFrame
501    A dataframe in SSScoring format
502
503    Returns
504    -------
505    A `JumpResults` named tuple with these items:
506
507    - `score` speed score
508    - `maxSpeed` maximum speed during the jump
509    - `scores` a Series of every 3-second window scores from exit to breakoff
510    - `data` an updated SSScoring dataframe `plotTime`, where 0 = exit, used
511      for plotting
512    - `window` a named tuple with the exit, breakoff, and validation window
513      altitudes
514    - `table` a dataframe featuring the speeds and altitudes at 5-sec intervals
515    - `color` a string that defines the color for the jump result; possible
516      values are _green_ for valid jump, _red_ for invalid jump, per ISC rules
517    - `result` a string with the legend of _valid_ or _invalid_ jump
518    """
519    workData = data.copy()
520    workData = dropNonSkydiveDataFrom(workData)
521    window, workData = getSpeedSkydiveFrom(workData)
522    if workData.empty and not window:
523        workData = None
524        maxSpeed = -1.0
525        score = -1.0
526        scores = None
527        table = None
528        window = None
529        jumpStatus = JumpStatus.WARM_UP_FILE
530    else:
531        jumpStatus = validateJumpISC(workData, window)
532        score = None
533        scores = None
534        table = None
535        baseTime = workData.iloc[0].timeUnix
536        workData['plotTime'] = round(workData.timeUnix-baseTime, 2)
537        if jumpStatus == JumpStatus.OK:
538            table = None
539            table = jumpAnalysisTable(workData)
540            maxSpeed = data.vKMh.max()
541            score, scores = calcScoreISC(workData)
542        else:
543            maxSpeed = -1
544            if not len(workData):
545                jumpStatus = JumpStatus.INVALID_SPEED_FILE
546    return JumpResults(workData, maxSpeed, score, scores, table, window, jumpStatus)

Take a dataframe in SSScoring format and process it for display. It serializes all the steps that would be taken from the ssscoring module, but includes some text/HTML data in the output.

Arguments

data: pd.DataFrame

A dataframe in SSScoring format

Returns

A JumpResults named tuple with these items:

  • score speed score
  • maxSpeed maximum speed during the jump
  • scores a Series of every 3-second window scores from exit to breakoff
  • data an updated SSScoring dataframe plotTime, where 0 = exit, used for plotting
  • window a named tuple with the exit, breakoff, and validation window altitudes
  • table a dataframe featuring the speeds and altitudes at 5-sec intervals
  • color a string that defines the color for the jump result; possible values are _green_ for valid jump, _red_ for invalid jump, per ISC rules
  • result a string with the legend of _valid_ or _invalid_ jump
def getFlySightDataFromCSVBuffer(buffer: bytes, bufferName: str) -> tuple:
575def getFlySightDataFromCSVBuffer(buffer:bytes, bufferName:str) -> tuple:
576    """
577    Ingress a buffer with known FlySight or SkyTrax file data for SSScoring
578    processing.
579
580    Arguments
581    ---------
582        buffer
583    A binary data buffer, bag of bytes, containing a known FlySight track file.
584
585        bufferName
586    An arbitrary name for the buffer of type `str`.  It's used for constructing
587    the full buffer tag value for human identification.
588
589    Returns
590    -------
591    A `tuple` with two items:
592        - `rawData` - a dataframe representation of the CSV with the original
593          headers but without the data type header
594        - `tag` - a string with an identifying tag derived from the path name
595          and file version in the form `some name:vX`.  It uses the current
596          path as metadata to infer the name.  There's no semantics enforcement.
597
598    Raises
599    ------
600    `SSScoringError` if the CSV file is invalid in any way.
601    """
602    if not isinstance(buffer, bytes):
603        raise SSScoringError('buffer must be an instance of bytes, a bytes buffer')
604    try:
605        stringIO = StringIO(buffer.decode(FLYSIGHT_FILE_ENCODING))
606    except Exception as e:
607        raise SSScoringError('invalid buffer endcoding - %s' % str(e))
608    version = detectFlySightFileVersionOf(buffer)
609    if version == FlySightVersion.V1:
610        rawData = _readVersion1CSV(stringIO)
611        tag = _tagVersion1From(bufferName)
612    elif version == FlySightVersion.V2:
613        rawData = _readVersion2CSV(stringIO)
614        tag = _tagVersion2From(bufferName)
615    return (rawData, tag)

Ingress a buffer with known FlySight or SkyTrax file data for SSScoring processing.

Arguments

buffer

A binary data buffer, bag of bytes, containing a known FlySight track file.

bufferName

An arbitrary name for the buffer of type str. It's used for constructing the full buffer tag value for human identification.

Returns

A tuple with two items: - rawData - a dataframe representation of the CSV with the original headers but without the data type header - tag - a string with an identifying tag derived from the path name and file version in the form some name:vX. It uses the current path as metadata to infer the name. There's no semantics enforcement.

Raises

SSScoringError if the CSV file is invalid in any way.

def getFlySightDataFromCSVFileName(jumpFile) -> tuple:
618def getFlySightDataFromCSVFileName(jumpFile) -> tuple:
619    """
620    Ingress a known FlySight or SkyTrax file into memory for SSScoring
621    processing.
622
623    Arguments
624    ---------
625        jumpFile
626    A string or `pathlib.Path` object; can be a relative or an asbolute path.
627
628    Returns
629    -------
630    A `tuple` with two items:
631        - `rawData` - a dataframe representation of the CSV with the original
632          headers but without the data type header
633        - `tag` - a string with an identifying tag derived from the path name
634          and file version in the form `some name:vX`.  It uses the current
635          path as metadata to infer the name.  There's no semantics enforcement.
636
637    Raises
638    ------
639    `SSScoringError` if the CSV file is invalid in any way.
640    """
641    from ssscoring.flysight import validFlySightHeaderIn
642
643    if isinstance(jumpFile, Path):
644        jumpFile = jumpFile.as_posix()
645    elif isinstance(jumpFile, str):
646        pass
647    else:
648        raise SSScoringError('jumpFile must be a string or a Path object')
649    if not validFlySightHeaderIn(jumpFile):
650        raise SSScoringError('%s is an invalid speed skydiving file')
651    version = detectFlySightFileVersionOf(jumpFile)
652    if version == FlySightVersion.V1:
653        rawData = _readVersion1CSV(jumpFile)
654        tag = _tagVersion1From(jumpFile)
655    elif version == FlySightVersion.V2:
656        rawData = _readVersion2CSV(jumpFile)
657        tag = _tagVersion2From(jumpFile)
658    return (rawData, tag)

Ingress a known FlySight or SkyTrax file into memory for SSScoring processing.

Arguments

jumpFile

A string or pathlib.Path object; can be a relative or an asbolute path.

Returns

A tuple with two items: - rawData - a dataframe representation of the CSV with the original headers but without the data type header - tag - a string with an identifying tag derived from the path name and file version in the form some name:vX. It uses the current path as metadata to infer the name. There's no semantics enforcement.

Raises

SSScoringError if the CSV file is invalid in any way.

def processAllJumpFiles(jumpFiles: list, altitudeDZMeters=0.0) -> dict:
661def processAllJumpFiles(jumpFiles: list, altitudeDZMeters = 0.0) -> dict:
662    """
663    Process all jump files in a list of valid FlySight files.  Returns a
664    dictionary of jump results with a human-readable version of the file name.
665    The `jumpFiles` list can be generated by hand or the output of the
666    `ssscoring.fs1.getAllSpeedJumpFilesFrom` called to operate on a data lake.
667
668    Arguments
669    ---------
670        jumpFiles
671    A list of file things that could represent one of these:
672    - file things relative or absolute path names to individual FlySight CSV
673      files.
674    - A specialization of BytesIO, such as the bags of bytes that Streamlit.io
675      generates after uploading and reading a file into the Streamlit
676      environment
677
678        altitudeDZMeters : float
679    Drop zone height above MSL
680
681    Returns
682    -------
683        dict
684    A dictionary of jump results.  The key is a human-readable version of a
685    `jumpFile` name with the extension, path, and extraneous spaces eliminated
686    or replaced by appropriate characters.  File names use Unicode, so accents
687    and non-ANSI characters are allowed in file names.
688
689    Raises
690    ------
691    `SSScoringError` if the jumpFiles object is empty, or if the individual
692    objects in the list aren't `BytesIO`, file name strings, or `Path`
693    instances.
694    """
695    jumpResults = dict()
696    if not len(jumpFiles):
697        raise SSScoringError('jumpFiles must have at least one element')
698    if not isinstance(jumpFiles, dict) and not isinstance(jumpFiles, list):
699        raise SSScoringError('dict with jump file names and FS versions or list of byte bags expected')
700    if isinstance(jumpFiles, dict):
701        objectsList = sorted(list(jumpFiles.keys()))
702    elif isinstance(jumpFiles, list):
703        objectsList = jumpFiles
704    obj = objectsList[0]
705    if not isinstance(obj, Path) and not isinstance(obj, str) and not isinstance(obj, BytesIO):
706        raise SSScoringError('jumpFiles must contain file-like things or BytesIO objects')
707    for jumpFile in objectsList:
708        if isinstance(jumpFile, BytesIO):
709            rawData, tag = getFlySightDataFromCSVBuffer(jumpFile.getvalue(), jumpFile.name)
710        else:
711            rawData, tag = getFlySightDataFromCSVFileName(jumpFile)
712        jumpResult = processJump(convertFlySight2SSScoring(rawData, altitudeDZMeters = altitudeDZMeters))
713        jumpResults[tag] = jumpResult
714    return jumpResults

Process all jump files in a list of valid FlySight files. Returns a dictionary of jump results with a human-readable version of the file name. The jumpFiles list can be generated by hand or the output of the ssscoring.fs1.getAllSpeedJumpFilesFrom called to operate on a data lake.

Arguments

jumpFiles

A list of file things that could represent one of these:

  • file things relative or absolute path names to individual FlySight CSV files.
  • A specialization of BytesIO, such as the bags of bytes that Streamlit.io generates after uploading and reading a file into the Streamlit environment
altitudeDZMeters : float

Drop zone height above MSL

Returns

dict

A dictionary of jump results. The key is a human-readable version of a jumpFile name with the extension, path, and extraneous spaces eliminated or replaced by appropriate characters. File names use Unicode, so accents and non-ANSI characters are allowed in file names.

Raises

SSScoringError if the jumpFiles object is empty, or if the individual objects in the list aren't BytesIO, file name strings, or Path instances.

def aggregateResults(jumpResults: dict) -> pandas.core.frame.DataFrame:
717def aggregateResults(jumpResults: dict) -> pd.DataFrame:
718    """
719    Aggregate all the results in a table fashioned after Marco Hepp's and Nklas
720    Daniel's score tracking data.
721
722    Arguments
723    ---------
724        jumpResults: dict
725    A dictionary of jump results, in which each result corresponds to a FlySight
726    file name.  See `ssscoring.processAllJumpFiles` for details.
727
728    Returns
729    -------
730    A dataframe featuring these columns:
731
732    - Score
733    - Speeds at 5, 10, 15, 20, and 25 second tranches
734    - Final time contemplated in the analysis
735    - Max speed
736
737    The dataframe rows are identified by the human readable jump file name.
738
739    Raises
740    ------
741    `SSSCoringError` if the `jumpResults` object is empty.
742    """
743    if not len(jumpResults):
744        raise SSScoringError('jumpResults is empty - impossible to collate angles')
745    speeds = pd.DataFrame()
746    for jumpResultIndex in sorted(list(jumpResults.keys())):
747        jumpResult = jumpResults[jumpResultIndex]
748        if jumpResult.status == JumpStatus.OK:
749            t = jumpResult.table
750            finalTime = t.iloc[-1].time
751            t.iloc[-1].time = LAST_TIME_TRANCHE
752            t = pd.pivot_table(t, columns = t.time)
753            d = pd.DataFrame([ jumpResult.score, ], index = [ jumpResultIndex, ], columns = [ 'score', ], dtype = object)
754            for column in t.columns:
755                d[column] = t[column].vKMh
756            d['finalTime'] = [ finalTime, ]
757            d['maxSpeed'] = jumpResult.maxSpeed
758
759            if speeds.empty:
760                speeds = d.copy()
761            else:
762                speeds = pd.concat([ speeds, d, ])
763    speeds = speeds.replace(np.nan, 0.0)
764    return speeds.sort_index()

Aggregate all the results in a table fashioned after Marco Hepp's and Nklas Daniel's score tracking data.

Arguments

jumpResults: dict

A dictionary of jump results, in which each result corresponds to a FlySight file name. See ssscoring.processAllJumpFiles for details.

Returns

A dataframe featuring these columns:

  • Score
  • Speeds at 5, 10, 15, 20, and 25 second tranches
  • Final time contemplated in the analysis
  • Max speed

The dataframe rows are identified by the human readable jump file name.

Raises

SSSCoringError if the jumpResults object is empty.

def roundedAggregateResults(aggregate: pandas.core.frame.DataFrame) -> pandas.core.frame.DataFrame:
767def roundedAggregateResults(aggregate: pd.DataFrame) -> pd.DataFrame:
768    """
769    Aggregate all the results in a table fashioned after Marco Hepp's and Nklas
770    Daniel's score tracking data.  All speed results are rounded at `n > x.5`
771    for any value.
772
773    Arguments
774    ---------
775        aggregate: pd.DataFrame
776    A dataframe output of `ssscoring.fs1.aggregateResults`.
777
778    Returns
779    -------
780    A dataframe featuring the **rounded values** for these columns:
781
782    - Score
783    - Speeds at 5, 10, 15, 20, and 25 second tranches
784    - Max speed
785
786    The `finalTime` column is ignored.
787
788    The dataframe rows are identified by the human readable jump file name.
789
790    This is a less precise version of the `ssscoring.aggregateResults`
791    dataframe, useful during training to keep rounded results available for
792    review.
793
794    Raises
795    ------
796    `SSSCoringError` if the `jumpResults` object is empty.
797    """
798    for column in [col for col in aggregate.columns if 'Time' not in str(col)]:
799        aggregate[column] = aggregate[column].apply(round)
800
801    return aggregate

Aggregate all the results in a table fashioned after Marco Hepp's and Nklas Daniel's score tracking data. All speed results are rounded at n > x.5 for any value.

Arguments

aggregate: pd.DataFrame

A dataframe output of ssscoring.fs1.aggregateResults.

Returns

A dataframe featuring the rounded values for these columns:

  • Score
  • Speeds at 5, 10, 15, 20, and 25 second tranches
  • Max speed

The finalTime column is ignored.

The dataframe rows are identified by the human readable jump file name.

This is a less precise version of the ssscoring.aggregateResults dataframe, useful during training to keep rounded results available for review.

Raises

SSSCoringError if the jumpResults object is empty.

def collateAnglesByTimeFromExit(jumpResults: dict) -> pandas.core.frame.DataFrame:
804def collateAnglesByTimeFromExit(jumpResults: dict) -> pd.DataFrame:
805    """
806    Collate all the angles by time from the `jumpResults` into a dataframe that
807    features the jump tag as index, the time tranches and the angles at each
808    time tranche.
809
810    Arguments
811    ---------
812        jumpResults: dict
813    A dictionary of jump results, in which each result corresponds to a FlySight
814    file name.  See `ssscoring.processAllJumpFiles` for details.
815
816    Returns
817    -------
818    A dataframe featuring these columns:
819
820    - Score
821    - Angles at 5, 10, 15, 20, and 25 second tranches
822    - Final time contemplated in the analysis
823
824    Raises
825    ------
826    `SSSCoringError` if the `jumpResults` object is empty.
827    """
828    if not len(jumpResults):
829        raise SSScoringError('jumpResults is empty - impossible to collate angles')
830    angles = pd.DataFrame()
831    for jumpResultIndex in sorted(list(jumpResults.keys())):
832        jumpResult = jumpResults[jumpResultIndex]
833        if jumpResult.status == JumpStatus.OK:
834            t = jumpResult.table
835            finalTime = t.iloc[-1].time
836            t.iloc[-1].time = LAST_TIME_TRANCHE
837            t = pd.pivot_table(t, columns = t.time)
838            d = pd.DataFrame([ jumpResult.score, ], index = [ jumpResultIndex, ], columns = [ 'score', ], dtype = object)
839            for column in t.columns:
840                d[column] = t[column].speedAngle
841            d['finalTime'] = [ finalTime, ]
842
843            if angles.empty:
844                angles = d.copy()
845            else:
846                angles = pd.concat([ angles, d, ])
847    cols = sorted([ column for column in angles.columns if isinstance(column, float) ])
848    cols = [ 'score', ]+cols+[ 'finalTime', ]
849    angles = angles[cols]
850    angles = angles.replace(np.nan, 0.0)
851    return angles.sort_index()

Collate all the angles by time from the jumpResults into a dataframe that features the jump tag as index, the time tranches and the angles at each time tranche.

Arguments

jumpResults: dict

A dictionary of jump results, in which each result corresponds to a FlySight file name. See ssscoring.processAllJumpFiles for details.

Returns

A dataframe featuring these columns:

  • Score
  • Angles at 5, 10, 15, 20, and 25 second tranches
  • Final time contemplated in the analysis

Raises

SSSCoringError if the jumpResults object is empty.

def totalResultsFrom(aggregate: pandas.core.frame.DataFrame) -> pandas.core.frame.DataFrame:
854def totalResultsFrom(aggregate: pd.DataFrame) -> pd.DataFrame:
855    """
856    Calculates the total and mean speeds for an aggregation of speed jumps.
857
858    Arguments
859    ---------
860        aggregate: pd.DataFrame
861    The aggregate results dataframe resulting from calling `ssscoring.aggregateResults`
862    with valid results.
863
864    Returns
865    -------
866    A dataframe with one row and two columns:
867
868    - totalSpeed ::= the sum of all speeds in the aggregated results
869    - meanSpeed ::= the mean of all speeds
870    - maxSpeed := the absolute max speed over the speed runs set
871    - meanSpeedSTD := scored speeds standar deviation
872    - maxScore ::= the max score among all the speed scores
873    - maxScoreSTD := the max scores standard deviation
874
875    Raises
876    ------
877    `AttributeError` if aggregate is an empty dataframe or `None`, or if the
878    `aggregate` dataframe doesn't conform to the output of `ssscoring.aggregateResults`.
879    """
880    if aggregate is None:
881        raise AttributeError('aggregate dataframe is empty')
882    elif isinstance(aggregate, pd.DataFrame) and not len(aggregate):
883        raise AttributeError('aggregate dataframe is empty')
884
885    totals = pd.DataFrame({
886        'totalScore': [ round(aggregate.score.sum(), 2), ],
887        'mean': [ round(aggregate.score.mean(), 2), ],
888        'deviation': [ round(aggregate.score.std(), 2), ],
889        'maxScore': [ round(aggregate.score.max(), 2), ],
890        'absMaxSpeed': [ round(aggregate.maxSpeed.max(), 2), ], }, index = [ 'totalSpeed'],)
891    return totals

Calculates the total and mean speeds for an aggregation of speed jumps.

Arguments

aggregate: pd.DataFrame

The aggregate results dataframe resulting from calling ssscoring.aggregateResults with valid results.

Returns

A dataframe with one row and two columns:

  • totalSpeed ::= the sum of all speeds in the aggregated results
  • meanSpeed ::= the mean of all speeds
  • maxSpeed := the absolute max speed over the speed runs set
  • meanSpeedSTD := scored speeds standar deviation
  • maxScore ::= the max score among all the speed scores
  • maxScoreSTD := the max scores standard deviation

Raises

AttributeError if aggregate is an empty dataframe or None, or if the aggregate dataframe doesn't conform to the output of ssscoring.aggregateResults.