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 +33 -0
- ssscoring/constants.py +85 -0
- ssscoring/datatypes.py +54 -0
- ssscoring/errors.py +30 -0
- ssscoring/fs1.py +553 -0
- ssscoring/notebook.py +245 -0
- ssscoring-1.5.0.dist-info/LICENSE.txt +29 -0
- ssscoring-1.5.0.dist-info/METADATA +43 -0
- ssscoring-1.5.0.dist-info/RECORD +12 -0
- ssscoring-1.5.0.dist-info/WHEEL +5 -0
- ssscoring-1.5.0.dist-info/entry_points.txt +2 -0
- ssscoring-1.5.0.dist-info/top_level.txt +1 -0
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 @@
|
|
|
1
|
+
ssscoring
|