ssscoring 1.5.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
ssscoring/__init__.py ADDED
@@ -0,0 +1,33 @@
1
+ # See: https://github.com/pr3d4t0r/SSScoring/blob/master/LICENSE.txt
2
+
3
+ """
4
+ Speed Skydiving Scoring tools package for processing FlySight v1 and v2 CSV data
5
+ files.
6
+
7
+ This library relies on NumPy and pandas. When running in a Lucyfer notebook,
8
+ it also requires the Bokeh plotting library.
9
+
10
+ The documentation for each module in this package is linked from the navigation
11
+ bar. The **fs1** module contains functions that process v1 files. The v2
12
+ implementation is in progress in a private Git branch.
13
+
14
+ ## Source code
15
+
16
+ You are welcome to **<a href='https://github.com/pr3d4t0r/SSScoring' target='_new'>fork SSScoring</a>**
17
+ on GitHub. You will need Python 3.9 or later, pandas, and NumPy as minimum
18
+ requirements.
19
+
20
+ You may see the source files here:
21
+
22
+ <a href='https://github.com/pr3d4t0r/SSScoring/tree/master/ssscoring' target='_new'>https://github.com/pr3d4t0r/SSScoring/tree/master/ssscoring</a>
23
+ """
24
+
25
+ import importlib.metadata
26
+
27
+
28
+ __VERSION__ = importlib.metadata.version('ssscoring')
29
+ """
30
+ @public
31
+ """
32
+
33
+
ssscoring/constants.py ADDED
@@ -0,0 +1,85 @@
1
+ # See: https://github.com/pr3d4t0r/SSScoring/blob/master/LICENSE.txt
2
+
3
+ """
4
+ Constants module and definitions.
5
+
6
+ **All measurements are expressed in meters unless noted otherwise.**
7
+ """
8
+
9
+ import math
10
+
11
+
12
+ # +++ implementation +++
13
+
14
+ BREAKOFF_ALTITUDE = 1707.0
15
+ """
16
+ Breakoff altitude or hard deck.
17
+ """
18
+
19
+ DEG_IN_RADIANS = math.pi/180.0
20
+ """
21
+ π/180º
22
+ """
23
+
24
+ EXIT_SPEED = 2*9.81
25
+ """
26
+ Guesstimate of the exit speed; 2*g
27
+ """
28
+
29
+ FLYSIGHT_1_HEADER = set([ 'time', 'lat', 'lon', 'hMSL', 'velN', 'velE', 'velD', 'hAcc', 'vAcc', 'sAcc', 'heading', 'cAcc', 'gpsFix', 'numSV', ])
30
+ """
31
+ FlySight v1 CSV file headers.
32
+ """
33
+
34
+ FT_IN_M = 3.2808
35
+ """
36
+ Number of feet in a meter.
37
+ """
38
+
39
+ IGNORE_LIST = [ '.ipynb_checkpoints', ]
40
+ """
41
+ Internal use - list of files to be ignored during bulk file processing in the
42
+ data lake (e.g. `./data`).
43
+ """
44
+
45
+ LAST_TIME_TRANCHE = 25.0
46
+ """
47
+ Times > 25 s are irrelevant because it means that the speed skydiver flew at
48
+ vSpeed < 400 km/h.
49
+ """
50
+
51
+ MAX_ALTITUDE_FT = 15500
52
+ """
53
+ Maximum suggested altitude but irrelevant in general. Max altitude without
54
+ oxygen is 15,500 ft AGL at most DZs. A jumnp from a higher altitude would
55
+ require oxygen and would be scored 3,000 ft higher than the hard deck.
56
+ """
57
+
58
+ MAX_ALTITUDE_METERS = MAX_ALTITUDE_FT/3.28
59
+ """
60
+ See
61
+ ---
62
+ ssscoring.constants.MAX_ALTITUDE_FT
63
+ """
64
+
65
+ MAX_SPEED_ACCURACY = 3.0
66
+ """
67
+ Speed accuracy for the FlySight device.
68
+ """
69
+
70
+ MIN_JUMP_FILE_SIZE = 1024*512
71
+ """
72
+ FlySight v1 files smaller than `MIN_JUMP_FILE_SIZE` are ignored because they
73
+ lack the minimum number of data points to contain a valid speed skydive.
74
+ **TODO:** Revise for FlySight v2.
75
+ """
76
+
77
+ PERFORMANCE_WINDOW_LENGTH = 2256.0
78
+ """
79
+ Performance window length as defined by ISSA/IPC/USPA.
80
+ """
81
+
82
+ VALIDATION_WINDOW_LENGTH = 1006.0
83
+ """
84
+ The validation window length as defined in the competition rules.
85
+ """
ssscoring/datatypes.py ADDED
@@ -0,0 +1,54 @@
1
+ # See: https://github.com/pr3d4t0r/SSScoring/blob/master/LICENSE.txt
2
+
3
+
4
+ """
5
+ SSScoring custom type definitions for easier symbolic manipuation.
6
+ """
7
+
8
+ from collections import namedtuple
9
+
10
+
11
+ # +++ implementation +++
12
+
13
+ JumpResults = namedtuple('JumpResults', 'color data maxSpeed result score scores table window')
14
+ """
15
+ A named tuple containing the score, maximum speed, scores throught the
16
+ performance window, the results table for a jump, the output color for the
17
+ result, and the result information string.
18
+
19
+ Attributes
20
+ ----------
21
+ - `color` - a string representing a color, green if the result is valid, red
22
+ otherwise
23
+ - `data` - dataframe containing all the data points for plotting and
24
+ calculations
25
+ - `maxSpeed` - maximum absolute speed registered during a skydive
26
+ - `result` - dataframe of results
27
+ - `score` - maximum mean speed during a 3-second window during the skydive
28
+ - `scores` - a series with all the scored ruding the sliding 3-sec window for
29
+ the whole speed skydive
30
+ - `table` - summary table of results of the speed run
31
+ - `window` - the scoring window data, an instance of `PerformanceWindow`
32
+ """
33
+
34
+
35
+ PerformanceWindow = namedtuple('PerformanceWindow', 'start end validationStart')
36
+ """
37
+ An object to handle the performance window (as defined in competition rules) as
38
+ a single object with all the properties necessary to interpret it and manipulate
39
+ it across different function or method calls.
40
+
41
+ Attributes
42
+ ----------
43
+ - `start` - beginning or start of the performance window, or exit from the
44
+ aircraft
45
+ - `end` - end of the performance window, or `start-PERFORMANCE_WINDOW_LENGTH`
46
+ - `validationStart` - end of the performance window - the `VALIDATION_WINDOW_END`
47
+
48
+ See
49
+ ---
50
+ ssscoring.constants.PERFORMANCE_WINDOW_LENGTH
51
+ ssscoring.constants.VALIDATION_WINDOW_END
52
+ """
53
+
54
+
ssscoring/errors.py ADDED
@@ -0,0 +1,30 @@
1
+ # See: https://github.com/pr3d4t0r/SSScoring/blob/master/LICENSE.txt
2
+
3
+
4
+ import json
5
+
6
+
7
+ # +++ classes +++
8
+
9
+ class SSScoringError(Exception):
10
+ """
11
+ Abstract class that defines all exceptions and errors and has a dictionary
12
+ representation of itself, for cleaner structured logging.
13
+
14
+ Arguments
15
+ ---------
16
+ message : str
17
+ A meaningful error message, human-readable
18
+ errno : int
19
+ An optional integer value, similar to ERRNO in C/UNIX
20
+ """
21
+ def __init__(self, message: str, errno: int = -1):
22
+ self._info = message
23
+ self._errno = errno
24
+
25
+
26
+ def __str__(self):
27
+ e = { 'SSScoringError': self._info, 'errno': self._errno, }
28
+
29
+ return json.dumps(e)
30
+
ssscoring/fs1.py ADDED
@@ -0,0 +1,553 @@
1
+ # See: https://github.com/pr3d4t0r/SSScoring/blob/master/LICENSE.txt
2
+
3
+ """
4
+ Functions and logic for detecting, validating, analyzing, and manipulating
5
+ FlySight 1 CSV files, including detection in the file system. The functions in
6
+ this module assume that a data lake exists somewhere in the file system (whether
7
+ local or cloud-based).
8
+ """
9
+
10
+
11
+ from ssscoring.constants import BREAKOFF_ALTITUDE
12
+ from ssscoring.constants import DEG_IN_RADIANS
13
+ from ssscoring.constants import EXIT_SPEED
14
+ from ssscoring.constants import FLYSIGHT_1_HEADER
15
+ from ssscoring.constants import FT_IN_M
16
+ from ssscoring.constants import IGNORE_LIST
17
+ from ssscoring.constants import LAST_TIME_TRANCHE
18
+ from ssscoring.constants import MAX_SPEED_ACCURACY
19
+ from ssscoring.constants import MIN_JUMP_FILE_SIZE
20
+ from ssscoring.constants import PERFORMANCE_WINDOW_LENGTH
21
+ from ssscoring.constants import VALIDATION_WINDOW_LENGTH
22
+ from ssscoring.datatypes import JumpResults
23
+ from ssscoring.datatypes import PerformanceWindow
24
+ from ssscoring.errors import SSScoringError
25
+
26
+ import csv
27
+ import math
28
+ import os
29
+
30
+ import pandas as pd
31
+
32
+
33
+ # +++ functions +++
34
+
35
+ def validFlySightHeaderIn(fileCSV: str) -> bool:
36
+ """
37
+ Checks if a file is a CSV in FlySight 1 format. The checks include:
38
+
39
+ - Whether the file is a CSV, using a comma delimiter
40
+ - Checks for the presence of all the documented FlySight headers
41
+
42
+ Arguments
43
+ ---------
44
+ fileCSV
45
+ A file name to verify as a valid FlySight file
46
+
47
+ Returns
48
+ -------
49
+ `True` if `fileCSV` is a FlySight CSV file, otherwise `False`.
50
+ """
51
+ delimiters = [',', ]
52
+ hasAllHeaders = False
53
+ with open(fileCSV, 'r') as inputFile:
54
+ try:
55
+ dialect = csv.Sniffer().sniff(inputFile.readline(), delimiters = delimiters)
56
+ except:
57
+ return False
58
+ if dialect.delimiter in delimiters:
59
+ inputFile.seek(0)
60
+ header = next(csv.reader(inputFile))
61
+ else:
62
+ return False
63
+ hasAllHeaders = FLYSIGHT_1_HEADER.issubset(header)
64
+ return hasAllHeaders
65
+
66
+
67
+ def isValidMinimumAltitude(altitude: float) -> bool:
68
+ """
69
+ Reports whether an `altitude` is within the IPC and USPA valid parameters,
70
+ or within `BREAKOFF_ALTITUDE` and `PERFORMACE_WINDOW_LENGTH`. In invalid
71
+ altitude doesn't invalidate a FlySight data file. This function can be used
72
+ for generating warnings. The stock FlySightViewer scores a speed jump even
73
+ if the exit was below the minimum altitude.
74
+
75
+ Arguments
76
+ ---------
77
+ altitude
78
+ An altitude in meters, often calculated as data.hMSL - DZ altitude.
79
+
80
+ Returns
81
+ -------
82
+ `True` if the altitude is valid.
83
+ """
84
+ minAltitude = BREAKOFF_ALTITUDE+PERFORMANCE_WINDOW_LENGTH
85
+ return altitude >= minAltitude
86
+
87
+
88
+ def getAllSpeedJumpFilesFrom(dataLake: str) -> list:
89
+ """
90
+ Get a list of all the speed jump files from a data lake, where data lake is
91
+ defined as a reachable path that contains one or more FlySight CSV files.
92
+ This function tests each file to ensure that it's a speed skydive FlySight
93
+ file in a valid format and length.
94
+
95
+ Arguments
96
+ ---------
97
+ dataLake: str
98
+ A valid (absolute or relative) path name to the top level directory where
99
+ the data lake starts.
100
+
101
+ Returns
102
+ -------
103
+ A list of speed jump file names for later SSScoring processing.
104
+ """
105
+ jumpFiles = list()
106
+ for root, dirs, files in os.walk(dataLake):
107
+ if any(name in root for name in IGNORE_LIST):
108
+ continue
109
+ for fileName in files:
110
+ if 'CSV' in fileName:
111
+ jumpFileName = os.path.join(root, fileName)
112
+ stat = os.stat(jumpFileName)
113
+ if stat.st_size >= MIN_JUMP_FILE_SIZE and validFlySightHeaderIn(jumpFileName):
114
+ jumpFiles.append(jumpFileName)
115
+
116
+ return jumpFiles
117
+
118
+
119
+ def convertFlySight2SSScoring(rawData: pd.DataFrame,
120
+ altitudeDZMeters = 0.0,
121
+ altitudeDZFt = 0.0):
122
+ """
123
+ Converts a raw dataframe initialized from a FlySight CSV file into the
124
+ SSScoring file format. The SSScoring format uses more descriptive column
125
+ headers, adds the altitude in feet, and uses UNIX time instead of an ISO
126
+ string.
127
+
128
+ If both `altitudeDZMeters` and `altitudeDZFt` are zero then hMSL is used.
129
+ Otherwise, this function adjusts the effective altitude with the value. If
130
+ both meters and feet values are set this throws an error.
131
+
132
+ Arguments
133
+ ---------
134
+ rawData : pd.DataFrame
135
+ FlySight CSV input as a dataframe
136
+
137
+ altitudeDZMeters : float
138
+ Drop zone height above MSL
139
+
140
+ altitudeDZFt
141
+ Drop zone altitudde above MSL
142
+
143
+ Returns
144
+ -------
145
+ A dataframe in SSScoring format, featuring these columns:
146
+
147
+ - timeUnix
148
+ - altitudeMSL
149
+ - altitudeASL
150
+ - altitudeMSLFt
151
+ - altitudeASLFt
152
+ - hMetersPerSecond
153
+ - hKMh (km/h)
154
+ - vMetersPerSecond
155
+ - vKMh (km/h)
156
+ - angle
157
+ - speedAccuracy
158
+
159
+ Errors
160
+ ------
161
+ `SSScoringError` if the DZ altitude is set in both meters and feet.
162
+ """
163
+ if not isinstance(rawData, pd.DataFrame):
164
+ raise SSScoringError('convertFlySight2SSScoring input must be a FlySight CSV dataframe')
165
+
166
+ if altitudeDZMeters and altitudeDZFt:
167
+ raise SSScoringError('Cannot set altitude in meters and feet; pick one')
168
+
169
+ if altitudeDZMeters:
170
+ altitudeDZFt = FT_IN_M*altitudeDZMeters
171
+ if altitudeDZFt:
172
+ altitudeDZMeters = altitudeDZFt/FT_IN_M
173
+
174
+ data = rawData.copy()
175
+
176
+ data['altitudeMSLFt'] = data['hMSL'].apply(lambda h: FT_IN_M*h)
177
+ data['altitudeASL'] = data.hMSL-altitudeDZMeters
178
+ data['altitudeASLFt'] = data.altitudeMSLFt-altitudeDZFt
179
+ data['timeUnix'] = data['time'].apply(lambda t: pd.Timestamp(t).timestamp())
180
+ data['hMetersPerSecond'] = (data.velE**2.0+data.velN**2.0)**0.5
181
+ speedAngle = data['hMetersPerSecond']/data['velD']
182
+ speedAngle = round(90.0-speedAngle.apply(math.atan)/DEG_IN_RADIANS, 1)
183
+
184
+ data = pd.DataFrame(data = {
185
+ 'timeUnix': data.timeUnix,
186
+ 'altitudeMSL': data.hMSL,
187
+ 'altitudeASL': data.altitudeASL,
188
+ 'altitudeMSLFt': data.altitudeMSLFt,
189
+ 'altitudeASLFt': data.altitudeASLFt,
190
+ 'vMetersPerSecond': data.velD,
191
+ 'vKMh': 3.6*data.velD,
192
+ 'speedAngle': speedAngle,
193
+ 'speedAccuracy': data.sAcc,
194
+ 'hMetersPerSecond': data.hMetersPerSecond,
195
+ 'hKMh': 3.6*data.hMetersPerSecond,
196
+ })
197
+
198
+ return data
199
+
200
+
201
+ def dropNonSkydiveDataFrom(data: pd.DataFrame) -> pd.DataFrame:
202
+ """
203
+ Discards all data rows before maximum altitude, and all "negative" altitude
204
+ rows because we don't skydive underground (FlySight bug?).
205
+
206
+ Arguments
207
+ ---------
208
+ data : pd.DataFrame
209
+ Jump data in SSScoring format (headers differ from FlySight format)
210
+
211
+ Returns
212
+ -------
213
+ The jump data for the skydive
214
+ """
215
+ timeMaxAlt = data[data.altitudeASL == data.altitudeASL.max()].timeUnix.iloc[0]
216
+ data = data[data.timeUnix > timeMaxAlt]
217
+
218
+ data = data[data.altitudeASL > 0]
219
+
220
+ return data
221
+
222
+
223
+ def _dataGroups(data):
224
+ data_ = data.copy()
225
+ data_['positive'] = (data_.vMetersPerSecond > 0)
226
+ data_['group'] = (data_.positive != data_.positive.shift(1)).astype(int).cumsum()-1
227
+
228
+ return data_
229
+
230
+
231
+ def getSpeedSkydiveFrom(data: pd.DataFrame) -> tuple:
232
+ """
233
+ Take the skydive dataframe and get the speed skydiving data:
234
+
235
+ - Exit
236
+ - Speed skydiving window
237
+ - Drops data before exit and below breakoff altitude
238
+
239
+ Arguments
240
+ ---------
241
+ data : pd.DataFrame
242
+ Jump data in SSScoring format
243
+
244
+ Returns
245
+ -------
246
+ A tuple of two elements:
247
+
248
+ - A named tuple with performance and validation window data
249
+ - A dataframe featuring only speed skydiving data
250
+ """
251
+ data = _dataGroups(data)
252
+ groups = data.group.max()+1
253
+
254
+ freeFallGroup = -1
255
+ MIN_DATA_POINTS = 100 # heuristic
256
+ MIN_MAX_SPEED = 200 # km/h, heuristic; slower ::= no free fall
257
+ for group in range(groups):
258
+ subset = data[data.group == group]
259
+ if len(subset) >= MIN_DATA_POINTS and subset.vKMh.max() >= MIN_MAX_SPEED:
260
+ freeFallGroup = group
261
+
262
+ data = data[data.group == freeFallGroup]
263
+ data = data.drop('group', axis = 1).drop('positive', axis = 1)
264
+
265
+ # Speed ~= 9.81 m/s; subtract 1 second for actual exit.
266
+ exitTime = data[data.vMetersPerSecond > EXIT_SPEED].head(1).timeUnix.iat[0]-2.0
267
+ # TODO: Delete this as the upper bounds the next time you see this note.
268
+ # data = data[data.vMetersPerSecond > EXIT_SPEED]
269
+ data = data[data.timeUnix >= exitTime]
270
+ data = data[data.altitudeASL >= BREAKOFF_ALTITUDE]
271
+
272
+ windowStart = data.iloc[0].altitudeASL
273
+ windowEnd = windowStart-PERFORMANCE_WINDOW_LENGTH
274
+ if windowEnd < BREAKOFF_ALTITUDE:
275
+ windowEnd = BREAKOFF_ALTITUDE
276
+
277
+ validationWindowStart = windowEnd+VALIDATION_WINDOW_LENGTH
278
+ data = data[data.altitudeASL >= windowEnd]
279
+
280
+ return PerformanceWindow(windowStart, windowEnd, validationWindowStart), data
281
+
282
+
283
+ def isValidJump(data: pd.DataFrame,
284
+ window: PerformanceWindow) -> bool:
285
+ """
286
+ Validates the jump according to ISC/FAI/USPA competition rules. A jump is
287
+ valid when the speed accuracy parameter is less than 3 m/s for the whole
288
+ validation window duration.
289
+
290
+ Arguments
291
+ ---------
292
+ data : pd.DataFramce
293
+ Jumnp data in SSScoring format
294
+ window : ssscoring.PerformanceWindow
295
+ Performance window start, end values in named tuple format
296
+
297
+ Returns
298
+ -------
299
+ `True` if the jump is valid according to ISC/FAI/USPA rules.
300
+ """
301
+ accuracy = data[data.altitudeASL < window.validationStart].speedAccuracy.max()
302
+ return accuracy < MAX_SPEED_ACCURACY
303
+
304
+
305
+ def jumpAnalysisTable(data: pd.DataFrame) -> pd.DataFrame:
306
+ """
307
+ Generates the HCD jump analysis table, with speed data at 5-second intervals
308
+ after exit.
309
+
310
+ Arguments
311
+ ---------
312
+ data : pd.DataFrame
313
+ Jump data in SSScoring format
314
+
315
+ Returns
316
+ -------
317
+ A tuple with a pd.DataFrame and the max speed recorded for the jump:
318
+
319
+ - A table dataframe with time and speed
320
+ - a floating point number
321
+ """
322
+ table = None
323
+
324
+ for column in pd.Series([ 5.0, 10.0, 15.0, 20.0, 25.0, ]):
325
+ for interval in range(int(column)*10, 10*(int(column)+1)):
326
+ # Use the next 0.1 sec interval if the current interval tranche has
327
+ # NaN values.
328
+ columnRef = interval/10.0
329
+ timeOffset = data.iloc[0].timeUnix+columnRef
330
+ tranche = data.query('timeUnix == %f' % timeOffset).copy()
331
+ tranche['time'] = [ column, ]
332
+ if not tranche.isnull().any().any():
333
+ break
334
+
335
+ if pd.isna(tranche.iloc[-1].vKMh):
336
+ tranche = data.tail(1).copy()
337
+ tranche['time'] = tranche.timeUnix-data.iloc[0].timeUnix
338
+
339
+ if table is not None:
340
+ table = pd.concat([ table, tranche, ])
341
+ else:
342
+ table = tranche
343
+
344
+ table = pd.DataFrame({
345
+ 'time': table.time,
346
+ 'vKMh': table.vKMh,
347
+ 'hKMh': table.hKMh,
348
+ 'speedAngle': table.speedAngle,
349
+ 'netVectorKMh': (table.vKMh**2+table.hKMh**2)**0.5,
350
+ 'altitude (ft)': table.altitudeASLFt, })
351
+
352
+ return (data.vKMh.max(), table)
353
+
354
+
355
+ def processJump(data: pd.DataFrame):
356
+ """
357
+ Take a dataframe in SSScoring format and process it for display. It
358
+ serializes all the steps that would be taken from the ssscoring module, but
359
+ includes some text/HTML data in the output.
360
+
361
+ Arguments
362
+ ---------
363
+ data: pd.DataFrame
364
+ A dataframe in SSScoring format
365
+
366
+ Returns
367
+ -------
368
+ A `JumpResults` named tuple with these items:
369
+
370
+ - `score` speed score
371
+ - `maxSpeed` maximum speed during the jump
372
+ - `scores` a Series of every 3-second window scores from exit to breakoff
373
+ - `data` an updated SSScoring dataframe `plotTime`, where 0 = exit, used
374
+ for plotting
375
+ - `window` a named tuple with the exit, breakoff, and validation window
376
+ altitudes
377
+ - `table` a dataframe featuring the speeds and altitudes at 5-sec intervals
378
+ - `color` a string that defines the color for the jump result; possible
379
+ values are _green_ for valid jump, _red_ for invalid jump, per ISC rules
380
+ - `result` a string with the legend of _valid_ or _invalid_ jump
381
+ """
382
+ data = data.copy()
383
+ data = dropNonSkydiveDataFrom(data)
384
+ window, data = getSpeedSkydiveFrom(data)
385
+ validJump = isValidJump(data, window)
386
+ score = 0.0
387
+ scores = dict()
388
+ table = None
389
+
390
+ if validJump:
391
+ maxSpeed, table = jumpAnalysisTable(data)
392
+ color = '#0f0'
393
+ result = '🟢 valid'
394
+ baseTime = data.iloc[0].timeUnix
395
+ data['plotTime'] = data.timeUnix-baseTime
396
+
397
+ for spot in data.plotTime:
398
+ r0 = data[data.plotTime == spot]
399
+ r1 = data[data.plotTime == spot+3.0]
400
+
401
+ if not r1.empty:
402
+ scores[0.5*(float(r0.vKMh.iloc[0])+float(r1.vKMh.iloc[0]))] = spot
403
+ score = max(scores)
404
+
405
+ else:
406
+ color = '#f00'
407
+ maxSpeed = -1
408
+ score = 0
409
+ result = '🔴 invalid'
410
+
411
+ return JumpResults(color, data, maxSpeed, result, score, scores, table, window)
412
+
413
+
414
+
415
+ def processAllJumpFiles(jumpFiles: list, altitudeDZMeters = 0.0) -> dict:
416
+ """
417
+ Process all jump files in a list of valid FlySight files. Returns a
418
+ dictionary of jump results with a human-readable version of the file name.
419
+ The `jumpFiles` list can be generated by hand or the output of the
420
+ `ssscoring.getAllSpeedJumpFilesFrom` called to operate on a data lake.
421
+
422
+ Arguments
423
+ ---------
424
+ jumpFiles
425
+ A list of relative or absolute path names to individual FlySight CSV files.
426
+
427
+ altitudeDZMeters : float
428
+ Drop zone height above MSL
429
+
430
+ dict
431
+ A dictionary of jump results. The key is a human-readable version of a
432
+ `jumpFile` name with the extension, path, and extraneous spaces eliminated
433
+ or replaced by appropriate characters. File names use Unicode, so accents
434
+ and non-ANSI characters are allowed in file names.
435
+ """
436
+ jumpResults = dict()
437
+ for jumpFile in jumpFiles:
438
+ jumpResult = processJump(
439
+ convertFlySight2SSScoring(pd.read_csv(jumpFile, skiprows = (1, 1)),
440
+ altitudeDZMeters = altitudeDZMeters))
441
+ tag = jumpFile.replace('CSV', '').replace('.', '').replace('/data', '').replace('/', ' ').strip()
442
+ if 'valid' in jumpResult.result:
443
+ jumpResults[tag] = jumpResult
444
+ return jumpResults
445
+
446
+
447
+ def aggregateResults(jumpResults: dict) -> pd.DataFrame:
448
+ """
449
+ Aggregate all the results in a table fashioned after Marco Hepp's and Nklas
450
+ Daniel's score tracking data.
451
+
452
+ Arguments
453
+ ---------
454
+ jumpResults: dict
455
+ A dictionary of jump results, in which each result corresponds to a FlySight
456
+ file name. See `ssscoring.processAllJumpFiles` for details.
457
+
458
+ Returns
459
+ -------
460
+ A dataframe featuring these columns:
461
+
462
+ - Score
463
+ - Speeds at 5, 10, 15, 20, and 25 second tranches
464
+ - Final time contemplated in the analysis
465
+ - Max speed
466
+
467
+ The dataframe rows are identified by the human readable jump file name.
468
+ """
469
+ speeds = pd.DataFrame()
470
+ for jumpResultIndex in sorted(list(jumpResults.keys())):
471
+ jumpResult = jumpResults[jumpResultIndex]
472
+ if jumpResult.score > 0.0:
473
+ t = jumpResult.table
474
+ finalTime = t.iloc[-1].time
475
+ t.iloc[-1].time = LAST_TIME_TRANCHE
476
+ t = pd.pivot_table(t, columns = t.time)
477
+ t.drop(['altitude (ft)'], inplace = True)
478
+ d = pd.DataFrame([ jumpResult.score, ], index = [ jumpResultIndex, ], columns = [ 'score', ], dtype = object)
479
+ for column in t.columns:
480
+ d[column] = t[column].iloc[3]
481
+ d['finalTime'] = [ finalTime, ]
482
+ d['maxSpeed'] = jumpResult.maxSpeed
483
+
484
+ if speeds.empty:
485
+ speeds = d.copy()
486
+ else:
487
+ speeds = pd.concat([ speeds, d, ])
488
+ return speeds.sort_index()
489
+
490
+
491
+ def roundedAggregateResults(jumpResults: dict) -> pd.DataFrame:
492
+ """
493
+ Aggregate all the results in a table fashioned after Marco Hepp's and Nklas
494
+ Daniel's score tracking data. All speed results are rounded at `n > x.5`
495
+ for any value.
496
+
497
+ Arguments
498
+ ---------
499
+ jumpResults: dict
500
+ A dictionary of jump results, in which each result corresponds to a FlySight
501
+ file name. See `ssscoring.processAllJumpFiles` for details.
502
+
503
+ Returns
504
+ -------
505
+ A dataframe featuring the **rounded values** for these columns:
506
+
507
+ - Score
508
+ - Speeds at 5, 10, 15, 20, and 25 second tranches
509
+ - Max speed
510
+
511
+ The `finalTime` column is ignored.
512
+
513
+ The dataframe rows are identified by the human readable jump file name.
514
+
515
+ This is a less precise version of the `ssscoring.aggregateResults`
516
+ dataframe, useful during training to keep rounded results available for
517
+ review.
518
+ """
519
+ aggregate = aggregateResults(jumpResults)
520
+ for column in [col for col in aggregate.columns if 'Time' not in str(col)]:
521
+ aggregate[column] = aggregate[column].apply(round)
522
+
523
+ return aggregate
524
+
525
+
526
+ def totalResultsFrom(aggregate: pd.DataFrame) -> pd.DataFrame:
527
+ """
528
+ Calculates the total and mean speeds for an aggregation of speed jumps.
529
+
530
+ Arguments
531
+ ---------
532
+ aggregate: pd.DataFrame
533
+ The aggregate results dataframe resulting from calling `ssscoring.aggregateResults`
534
+ with valid results.
535
+
536
+ Returns
537
+ -------
538
+ A dataframe with one row and two columns:
539
+
540
+ - totalSpeed ::= the sum of all speeds in the aggregated results
541
+ - meanSpeed ::= the mean of all speeds
542
+ - maxScore ::= the max score among all the speed scores
543
+
544
+ Raises
545
+ ------
546
+ `AttributeError` if aggregate is an empty dataframe or `None`, or if the
547
+ `aggregate` dataframe doesn't conform to the output of `ssscoring.aggregateResults`.
548
+ """
549
+ totals = pd.DataFrame({ 'totalSpeed': [ aggregate.score.sum(), ], 'meanSpeed': [ aggregate.score.mean(), ], 'maxScore': [ aggregate.score.max(), ], }, index = [ 'totalSpeed'],)
550
+
551
+ return totals
552
+
553
+
ssscoring/notebook.py ADDED
@@ -0,0 +1,245 @@
1
+ # See: https://github.com/pr3d4t0r/SSScoring/blob/master/LICENSE.txt
2
+
3
+ """
4
+ ## Utility reusable code for notebooks.
5
+ """
6
+
7
+
8
+ from bokeh.models import LinearAxis
9
+ from bokeh.models import Range1d
10
+
11
+ from ssscoring.constants import MAX_ALTITUDE_FT
12
+
13
+ import bokeh.io as bi
14
+ import bokeh.plotting as bp
15
+
16
+
17
+ # *** constants ***
18
+ DATA_LAKE_ROOT = './data' # Lucyfer default
19
+ SPEED_COLORS = colors = ('limegreen', 'blue', 'tomato', 'turquoise', 'deepskyblue', 'forestgreen', 'coral', 'darkcyan',)
20
+
21
+
22
+ # *** global initialization ***
23
+
24
+ bp.output_notebook(hide_banner = True)
25
+ # TODO: make this configurable:
26
+ bi.curdoc().theme = 'dark_minimal'
27
+
28
+
29
+ # *** functions ***
30
+
31
+ def initializePlot(jumpTitle: str,
32
+ height = 500,
33
+ width = 900,
34
+ xLabel = 'seconds from exit',
35
+ yLabel = 'km/h',
36
+ # TODO: roll back to 40.0?
37
+ # xMax = 40.0,
38
+ xMax = 35.0,
39
+ yMax = 550.0):
40
+ """
41
+ Initiialize a plotting area for notebook output.
42
+
43
+ Arguments
44
+ ---------
45
+ jumpTitle: str
46
+ A title to identify the plot.
47
+
48
+ height: int
49
+ Height of the plot in pixels. Default = 500.
50
+
51
+ width: int
52
+ Width of the plot in pixels. Default = 900.
53
+
54
+ xLabel: str
55
+ X axis label. Default: `'seconds from exit'`
56
+
57
+ yLabel: str
58
+ Y axis label. Default: `'km/h'`
59
+
60
+ xMax: float
61
+ The maximum rnage for the X axis. Default = 40.0
62
+
63
+ yMax: float
64
+ The maximum range for the Y axis. Default = 550
65
+ """
66
+ return bp.figure(title = jumpTitle,
67
+ height = height,
68
+ width = width,
69
+ x_axis_label = xLabel,
70
+ y_axis_label = yLabel,
71
+ x_range = (0.0, xMax),
72
+ y_range = (0.0, yMax))
73
+
74
+
75
+ def _graphSegment(plot,
76
+ x0 = 0.0,
77
+ y0 = 0.0,
78
+ x1 = 0.0,
79
+ y1 = 0.0,
80
+ lineWidth = 1,
81
+ color = 'black'):
82
+ plot.segment(x0 = [ x0, ],
83
+ y0 = [ y0, ],
84
+ x1 = [ x1, ],
85
+ y1 = [ y1, ],
86
+ line_width = lineWidth,
87
+ color = color)
88
+
89
+
90
+ def initializeExtraYRanges(plot,
91
+ startY: float = 0.0,
92
+ endY: float = MAX_ALTITUDE_FT):
93
+ """
94
+ Initialize an extra Y range for reporting other data trend (e.g. altitude)
95
+ in the plot.
96
+
97
+ Arguments
98
+ ---------
99
+ plot
100
+ A valid instance of `bp.figure` with an existing plot defined for it
101
+
102
+ startY: float
103
+ The Y range starting value
104
+
105
+ endY: float
106
+ The Y range ending value
107
+
108
+ Returns
109
+ -------
110
+ An instance of `bp.figure` updated to report an additional Y axis.
111
+ """
112
+ plot.extra_y_ranges = {
113
+ 'altitudeFt': Range1d(start = startY, end = endY),
114
+ 'angle': Range1d(start = 0.0, end = 90.0),
115
+ }
116
+ plot.add_layout(LinearAxis(y_range_name = 'altitudeFt', axis_label = 'Alt (ft)'), 'left')
117
+ plot.add_layout(LinearAxis(y_range_name = 'angle', axis_label = 'angle'), 'left')
118
+
119
+ return plot
120
+
121
+
122
+ def graphJumpResult(plot,
123
+ jumpResult,
124
+ lineColor = 'green',
125
+ legend = 'speed',
126
+ showIt = True):
127
+ """
128
+ Graph the jump results using the initialized plot.
129
+
130
+ Arguments
131
+ ---------
132
+ plot: bp.figure
133
+ A Bokeh figure where to render the plot.
134
+
135
+ jumpResult: ssscoring.JumpResults
136
+ A jump results named tuple with score, max speed, scores, data, etc.
137
+
138
+ lineColor: str
139
+ A valid color from the Bokeh palette: https://docs.bokeh.org/en/2.1.1/docs/reference/colors.html
140
+ This module defines 8 colors for rendering a competition's results. See:
141
+ `ssscoring.notebook.SPEED_COLORS` for the list.
142
+
143
+ leged: str
144
+ A title for the plot.
145
+
146
+ showIt: bool
147
+ A boolean flag for whether the call should render the plot upon the function
148
+ call. This flag is used for combining two or more jumps on the same plot.
149
+ In that case, a call to this function is made with a different `jumpResult`
150
+ and the `showIt` flag set to `False`. When the user is ready to view the
151
+ combined plot, issue a call to `bp.show(plot)`. Example:
152
+
153
+ ```python
154
+ for result in jumpResults:
155
+ graphJumpResult(plot, result, showIt = False)
156
+
157
+ bp.show(plot)
158
+
159
+ Returns
160
+ -------
161
+ `None`.
162
+ ```
163
+ """
164
+ data = jumpResult.data
165
+ scores = jumpResult.scores
166
+ score = jumpResult.score
167
+ plot.line(data.plotTime, data.vKMh, legend_label = legend, line_width = 2, line_color = lineColor)
168
+
169
+ if showIt:
170
+ plot.line(data.plotTime, data.hKMh, legend_label = 'H-speed', line_width = 2, line_color = 'red')
171
+ _graphSegment(plot, scores[score], 0.0, scores[score], score, 3, 'lightblue')
172
+ _graphSegment(plot, scores[score]+1.5, 0.0, scores[score]+1.5, score, 1, 'darkseagreen')
173
+ _graphSegment(plot, scores[score]-1.5, 0.0, scores[score]-1.5, score, 1, 'darkseagreen')
174
+ plot.scatter(x = [ scores[score], ], y = [ score, ], marker = 'square_cross', size = [ 20, ], line_color = 'lightblue', fill_color = None, line_width = 3)
175
+ bp.show(plot)
176
+
177
+
178
+ def graphAltitude(plot,
179
+ jumpResult,
180
+ label = 'Alt (ft)',
181
+ lineColor = 'palegoldenrod',
182
+ rangeName = 'altitudeFt'):
183
+ """
184
+ Graph a vertical axis with additional data, often used for altitude in ft
185
+ ASL.
186
+
187
+ Arguments
188
+ ---------
189
+ plot: pb.figure
190
+ A Bokeh figure where to render the plot.
191
+
192
+ jumpResult: ssscoring.JumpResults
193
+ A jump results named tuple with score, max speed, scores, data, etc.
194
+
195
+ label: str
196
+ The legend label for the new Y axis.
197
+
198
+ lineColor: str
199
+ A color name from the Bokeh palette.
200
+
201
+ rangeName: str
202
+ The range name to associate the `LinearAxis` layout with the data for
203
+ plotting.
204
+
205
+ Returns
206
+ -------
207
+ `None`.
208
+ """
209
+ data = jumpResult.data
210
+ plot.line(data.plotTime, data.altitudeASLFt, legend_label = label, line_width = 2, line_color = lineColor, y_range_name = rangeName)
211
+
212
+
213
+ def graphAngle(plot,
214
+ jumpResult,
215
+ label = 'angle',
216
+ lineColor = 'deepskyblue',
217
+ rangeName = 'angle'):
218
+ """
219
+ Graph the flight angle
220
+
221
+ Arguments
222
+ ---------
223
+ plot: pb.figure
224
+ A Bokeh figure where to render the plot.
225
+
226
+ jumpResult: ssscoring.JumpResults
227
+ A jump results named tuple with score, max speed, scores, data, etc.
228
+
229
+ label: str
230
+ The legend label for the new Y axis.
231
+
232
+ lineColor: str
233
+ A color name from the Bokeh palette.
234
+
235
+ rangeName: str
236
+ The range name to associate the `LinearAxis` layout with the data for
237
+ plotting.
238
+
239
+ Returns
240
+ -------
241
+ `None`.
242
+ """
243
+ data = jumpResult.data
244
+ plot.line(data.plotTime, data.speedAngle, legend_label = label, line_width = 2, line_color = lineColor, y_range_name = rangeName)
245
+
@@ -0,0 +1,29 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2018-2024 Eugene "pr3d4t0r" Ciurana
4
+ All rights reserved.
5
+
6
+ Redistribution and use in source and binary forms, with or without
7
+ modification, are permitted provided that the following conditions are met:
8
+
9
+ * Redistributions of source code must retain the above copyright notice, this
10
+ list of conditions and the following disclaimer.
11
+
12
+ * Redistributions in binary form must reproduce the above copyright notice,
13
+ this list of conditions and the following disclaimer in the documentation
14
+ and/or other materials provided with the distribution.
15
+
16
+ * Neither the name of the copyright holder nor the names of its
17
+ contributors may be used to endorse or promote products derived from
18
+ this software without specific prior written permission.
19
+
20
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,43 @@
1
+ Metadata-Version: 2.1
2
+ Name: ssscoring
3
+ Version: 1.5.0
4
+ Summary: ssscoring - Speed Skydiving scoring tools
5
+ Author-email: Eugene Ciurana pr3d4t0r <ssscoring.project@cime.net>
6
+ License: BSD-3-Clause
7
+ Classifier: Intended Audience :: Other Audience
8
+ Classifier: Operating System :: MacOS
9
+ Classifier: Operating System :: Unix
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.9
12
+ Requires-Python: >=3.9.9
13
+ Description-Content-Type: text/markdown
14
+ License-File: LICENSE.txt
15
+ Requires-Dist: click
16
+ Requires-Dist: jupyter-bokeh
17
+
18
+ % ssscoring(3) Version 1.5.0 | Spped Skydiving Scoring API documentation
19
+
20
+ Name
21
+ ====
22
+
23
+ **SSScoring** - Speed Skydiving Scoring high level library in Python
24
+
25
+
26
+ Synopsis
27
+ ========
28
+ ```python
29
+ # Short code snippet will go here.
30
+ ```
31
+
32
+
33
+ Description
34
+ ===========
35
+ Documentation in progress.
36
+
37
+
38
+ License
39
+ =======
40
+ The **SSScoring** package, documentation and examples are licensed under the
41
+ [BSD-3 open source license](https://github.com/pr3d4t0r/SSScoring/blob/master/LICENSE.txt).
42
+
43
+
@@ -0,0 +1,12 @@
1
+ ssscoring/__init__.py,sha256=SnLe-PTy5EdKXBi1jL-pT3PP7dP5vqMFx4evrWmnkHw,998
2
+ ssscoring/constants.py,sha256=L1EhRX0ampvm_GtO-6F3zJWdWBIT8w5ObB0aXWIdRIE,1784
3
+ ssscoring/datatypes.py,sha256=q3On1wuKfKLirIfA_GfZslhsjiSibxvVXdRv07WOAwA,1819
4
+ ssscoring/errors.py,sha256=jJqBEugEw-3lW0j2coj9b3bW0ZwtQODJ47NV_YMElWQ,707
5
+ ssscoring/fs1.py,sha256=Ixr0ciZukBB9DaSwg6tmdwuRW5WMnwdETyuiT0UWO0M,18178
6
+ ssscoring/notebook.py,sha256=Dl-NUFOLd94gs1ORNUmFdjTwSQszfbk8s8LTxgTBAX4,7052
7
+ ssscoring-1.5.0.dist-info/LICENSE.txt,sha256=Nk5_436gyC2j0OUFBhG14LHkhdTvW00ZdzgyaVxzLiA,1529
8
+ ssscoring-1.5.0.dist-info/METADATA,sha256=ZML4T7d6kFtpSUBBbSc19bFB9kvRQ9mnrw5cW_oBTRU,1021
9
+ ssscoring-1.5.0.dist-info/WHEEL,sha256=Mdi9PDNwEZptOjTlUcAth7XJDFtKrHYaQMPulZeBCiQ,91
10
+ ssscoring-1.5.0.dist-info/entry_points.txt,sha256=-hbXHRPOboL6yOxFX-x2Wj1jF5Qz71BNWof-pKb62M4,46
11
+ ssscoring-1.5.0.dist-info/top_level.txt,sha256=mNXsbNRRwarXyeFMPoqMQJN1KfrQR6R0_UkmpCjabKk,10
12
+ ssscoring-1.5.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (73.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ ssscoring = ssscoring:_main
@@ -0,0 +1 @@
1
+ ssscoring