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

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
  • vMetersPerSecond
  • vKMh (km/h)
  • vAccelMS2 (m/s²)
  • speedAccuracy (ignore; see ISC documentation)
  • hMetersPerSecond
  • hKMh (km/h)
  • 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:
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

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:
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

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:
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

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:
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)

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:
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)

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:
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)

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:
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    try:
608        version = detectFlySightFileVersionOf(buffer)
609    except Exception:
610        tag = '%s:INVALID' % bufferName
611        rawData = None
612    else:
613        if version == FlySightVersion.V1:
614            rawData = _readVersion1CSV(stringIO)
615            tag = _tagVersion1From(bufferName)
616        elif version == FlySightVersion.V2:
617            rawData = _readVersion2CSV(stringIO)
618            tag = _tagVersion2From(bufferName)
619    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:
622def getFlySightDataFromCSVFileName(jumpFile) -> tuple:
623    """
624    Ingress a known FlySight or SkyTrax file into memory for SSScoring
625    processing.
626
627    Arguments
628    ---------
629        jumpFile
630    A string or `pathlib.Path` object; can be a relative or an asbolute path.
631
632    Returns
633    -------
634    A `tuple` with two items:
635        - `rawData` - a dataframe representation of the CSV with the original
636          headers but without the data type header
637        - `tag` - a string with an identifying tag derived from the path name
638          and file version in the form `some name:vX`.  It uses the current
639          path as metadata to infer the name.  There's no semantics enforcement.
640
641    Raises
642    ------
643    `SSScoringError` if the CSV file is invalid in any way.
644    """
645    from ssscoring.flysight import validFlySightHeaderIn
646
647    if isinstance(jumpFile, Path):
648        jumpFile = jumpFile.as_posix()
649    elif isinstance(jumpFile, str):
650        pass
651    else:
652        raise SSScoringError('jumpFile must be a string or a Path object')
653    if not validFlySightHeaderIn(jumpFile):
654        raise SSScoringError('%s is an invalid speed skydiving file')
655    try:
656        version = detectFlySightFileVersionOf(jumpFile)
657    except Exception:
658        tag = 'NA'
659        rawData = None
660    else:
661        if version == FlySightVersion.V1:
662            rawData = _readVersion1CSV(jumpFile)
663            tag = _tagVersion1From(jumpFile)
664        elif version == FlySightVersion.V2:
665            rawData = _readVersion2CSV(jumpFile)
666            tag = _tagVersion2From(jumpFile)
667    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:
670def processAllJumpFiles(jumpFiles: list, altitudeDZMeters = 0.0) -> dict:
671    """
672    Process all jump files in a list of valid FlySight files.  Returns a
673    dictionary of jump results with a human-readable version of the file name.
674    The `jumpFiles` list can be generated by hand or the output of the
675    `ssscoring.fs1.getAllSpeedJumpFilesFrom` called to operate on a data lake.
676
677    Arguments
678    ---------
679        jumpFiles
680    A list of file things that could represent one of these:
681    - file things relative or absolute path names to individual FlySight CSV
682      files.
683    - A specialization of BytesIO, such as the bags of bytes that Streamlit.io
684      generates after uploading and reading a file into the Streamlit
685      environment
686
687        altitudeDZMeters : float
688    Drop zone height above MSL
689
690    Returns
691    -------
692        dict
693    A dictionary of jump results.  The key is a human-readable version of a
694    `jumpFile` name with the extension, path, and extraneous spaces eliminated
695    or replaced by appropriate characters.  File names use Unicode, so accents
696    and non-ANSI characters are allowed in file names.
697
698    Raises
699    ------
700    `SSScoringError` if the jumpFiles object is empty, or if the individual
701    objects in the list aren't `BytesIO`, file name strings, or `Path`
702    instances.
703    """
704    jumpResults = dict()
705    if not len(jumpFiles):
706        raise SSScoringError('jumpFiles must have at least one element')
707    if not isinstance(jumpFiles, dict) and not isinstance(jumpFiles, list):
708        raise SSScoringError('dict with jump file names and FS versions or list of byte bags expected')
709    if isinstance(jumpFiles, dict):
710        objectsList = sorted(list(jumpFiles.keys()))
711    elif isinstance(jumpFiles, list):
712        objectsList = jumpFiles
713    obj = objectsList[0]
714    if not isinstance(obj, Path) and not isinstance(obj, str) and not isinstance(obj, BytesIO):
715        raise SSScoringError('jumpFiles must contain file-like things or BytesIO objects')
716    for jumpFile in objectsList:
717        if isinstance(jumpFile, BytesIO):
718            rawData, tag = getFlySightDataFromCSVBuffer(jumpFile.getvalue(), jumpFile.name)
719        else:
720            rawData, tag = getFlySightDataFromCSVFileName(jumpFile)
721        try:
722            jumpResult = processJump(convertFlySight2SSScoring(rawData, altitudeDZMeters = altitudeDZMeters))
723        except Exception:
724            jumpResult = JumpResults(None, 0.0, 0.0, None, None, None, JumpStatus.INVALID_SPEED_FILE)
725        jumpResults[tag] = jumpResult
726    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:
729def aggregateResults(jumpResults: dict) -> pd.DataFrame:
730    """
731    Aggregate all the results in a table fashioned after Marco Hepp's and Nklas
732    Daniel's score tracking data.
733
734    Arguments
735    ---------
736        jumpResults: dict
737    A dictionary of jump results, in which each result corresponds to a FlySight
738    file name.  See `ssscoring.processAllJumpFiles` for details.
739
740    Returns
741    -------
742    A dataframe featuring these columns:
743
744    - Score
745    - Speeds at 5, 10, 15, 20, and 25 second tranches
746    - Final time contemplated in the analysis
747    - Max speed
748
749    The dataframe rows are identified by the human readable jump file name.
750
751    Raises
752    ------
753    `SSSCoringError` if the `jumpResults` object is empty.
754    """
755    if not len(jumpResults):
756        raise SSScoringError('jumpResults is empty - impossible to collate angles')
757    speeds = pd.DataFrame()
758    for jumpResultIndex in sorted(list(jumpResults.keys())):
759        jumpResult = jumpResults[jumpResultIndex]
760        if jumpResult.status == JumpStatus.OK:
761            t = jumpResult.table
762            finalTime = t.iloc[-1].time
763            t.iloc[-1].time = LAST_TIME_TRANCHE
764            t = pd.pivot_table(t, columns = t.time)
765            d = pd.DataFrame([ jumpResult.score, ], index = [ jumpResultIndex, ], columns = [ 'score', ], dtype = object)
766            for column in t.columns:
767                d[column] = t[column].vKMh
768            d['finalTime'] = [ finalTime, ]
769            d['maxSpeed'] = jumpResult.maxSpeed
770
771            if speeds.empty:
772                speeds = d.copy()
773            else:
774                speeds = pd.concat([ speeds, d, ])
775    speeds = speeds.replace(np.nan, 0.0)
776    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:
779def roundedAggregateResults(aggregate: pd.DataFrame) -> pd.DataFrame:
780    """
781    Aggregate all the results in a table fashioned after Marco Hepp's and Nklas
782    Daniel's score tracking data.  All speed results are rounded at `n > x.5`
783    for any value.
784
785    Arguments
786    ---------
787        aggregate: pd.DataFrame
788    A dataframe output of `ssscoring.fs1.aggregateResults`.
789
790    Returns
791    -------
792    A dataframe featuring the **rounded values** for these columns:
793
794    - Score
795    - Speeds at 5, 10, 15, 20, and 25 second tranches
796    - Max speed
797
798    The `finalTime` column is ignored.
799
800    The dataframe rows are identified by the human readable jump file name.
801
802    This is a less precise version of the `ssscoring.aggregateResults`
803    dataframe, useful during training to keep rounded results available for
804    review.
805
806    Raises
807    ------
808    `SSSCoringError` if the `jumpResults` object is empty.
809    """
810    for column in [col for col in aggregate.columns if 'Time' not in str(col)]:
811        aggregate[column] = aggregate[column].apply(round)
812
813    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:
816def collateAnglesByTimeFromExit(jumpResults: dict) -> pd.DataFrame:
817    """
818    Collate all the angles by time from the `jumpResults` into a dataframe that
819    features the jump tag as index, the time tranches and the angles at each
820    time tranche.
821
822    Arguments
823    ---------
824        jumpResults: dict
825    A dictionary of jump results, in which each result corresponds to a FlySight
826    file name.  See `ssscoring.processAllJumpFiles` for details.
827
828    Returns
829    -------
830    A dataframe featuring these columns:
831
832    - Score
833    - Angles at 5, 10, 15, 20, and 25 second tranches
834    - Final time contemplated in the analysis
835
836    Raises
837    ------
838    `SSSCoringError` if the `jumpResults` object is empty.
839    """
840    if not len(jumpResults):
841        raise SSScoringError('jumpResults is empty - impossible to collate angles')
842    angles = pd.DataFrame()
843    for jumpResultIndex in sorted(list(jumpResults.keys())):
844        jumpResult = jumpResults[jumpResultIndex]
845        if jumpResult.status == JumpStatus.OK:
846            t = jumpResult.table
847            finalTime = t.iloc[-1].time
848            t.iloc[-1].time = LAST_TIME_TRANCHE
849            t = pd.pivot_table(t, columns = t.time)
850            d = pd.DataFrame([ jumpResult.score, ], index = [ jumpResultIndex, ], columns = [ 'score', ], dtype = object)
851            for column in t.columns:
852                d[column] = t[column].speedAngle
853            d['finalTime'] = [ finalTime, ]
854
855            if angles.empty:
856                angles = d.copy()
857            else:
858                angles = pd.concat([ angles, d, ])
859    cols = sorted([ column for column in angles.columns if isinstance(column, float) ])
860    cols = [ 'score', ]+cols+[ 'finalTime', ]
861    angles = angles[cols]
862    angles = angles.replace(np.nan, 0.0)
863    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:
866def totalResultsFrom(aggregate: pd.DataFrame) -> pd.DataFrame:
867    """
868    Calculates the total and mean speeds for an aggregation of speed jumps.
869
870    Arguments
871    ---------
872        aggregate: pd.DataFrame
873    The aggregate results dataframe resulting from calling `ssscoring.aggregateResults`
874    with valid results.
875
876    Returns
877    -------
878    A dataframe with one row and two columns:
879
880    - totalSpeed ::= the sum of all speeds in the aggregated results
881    - meanSpeed ::= the mean of all speeds
882    - maxSpeed := the absolute max speed over the speed runs set
883    - meanSpeedSTD := scored speeds standar deviation
884    - maxScore ::= the max score among all the speed scores
885    - maxScoreSTD := the max scores standard deviation
886
887    Raises
888    ------
889    `AttributeError` if aggregate is an empty dataframe or `None`, or if the
890    `aggregate` dataframe doesn't conform to the output of `ssscoring.aggregateResults`.
891    """
892    if aggregate is None:
893        raise AttributeError('aggregate dataframe is empty')
894    elif isinstance(aggregate, pd.DataFrame) and not len(aggregate):
895        raise AttributeError('aggregate dataframe is empty')
896
897    totals = pd.DataFrame({
898        'totalScore': [ round(aggregate.score.sum(), 2), ],
899        'mean': [ round(aggregate.score.mean(), 2), ],
900        'deviation': [ round(aggregate.score.std(), 2), ],
901        'maxScore': [ round(aggregate.score.max(), 2), ],
902        'absMaxSpeed': [ round(aggregate.maxSpeed.max(), 2), ], }, index = [ 'totalSpeed'],)
903    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.