ssscoring.ssscoremultiple

Experimental

Process a group of jumps uploaded from a file uploader.

  1# See: https://github.com/pr3d4t0r/SSScoring/blob/master/LICENSE.txtl
  2
  3"""
  4## Experimental
  5
  6Process a group of jumps uploaded from a file uploader.
  7"""
  8
  9from ssscoring import __VERSION__
 10from ssscoring.appcommon import displayJumpDataIn
 11from ssscoring.appcommon import displayTrackOnMap
 12from ssscoring.appcommon import fetchInstructionsHTML
 13from ssscoring.appcommon import fetchResource
 14from ssscoring.appcommon import initFileUploaderState
 15from ssscoring.appcommon import interpretJumpResult
 16from ssscoring.appcommon import plotJumpResult
 17from ssscoring.appcommon import setSideBarAndMain
 18from ssscoring.calc import aggregateResults
 19from ssscoring.calc import collateAnglesByTimeFromExit
 20from ssscoring.calc import dropNonSkydiveDataFrom
 21from ssscoring.calc import processAllJumpFiles
 22from ssscoring.calc import totalResultsFrom
 23from ssscoring.constants import DEFAULT_PLOT_INCREMENT
 24from ssscoring.constants import DEFAULT_PLOT_MAX_V_SCALE
 25from ssscoring.constants import M_2_FT
 26from ssscoring.constants import SPEED_ACCURACY_THRESHOLD
 27from ssscoring.datatypes import JumpStatus
 28from ssscoring.datatypes import PerformanceWindow
 29from ssscoring.mapview import multipleSpeedJumpsTrajectories
 30from ssscoring.mapview import speedJumpTrajectory
 31from ssscoring.notebook import graphForwardDisplacement
 32from ssscoring.notebook import graphGroundTrack
 33from ssscoring.notebook import graphJumpResult
 34from ssscoring.notebook import initializeGroundTrackPlot
 35from ssscoring.notebook import initializePlot
 36from ssscoring.notebook import resolveJumpColors
 37# TODO: Remove this if present after 20260531
 38# from streamlit_bokeh import streamlit_bokeh
 39
 40import pandas as pd
 41import streamlit as st
 42
 43
 44# +++ implementation +++
 45
 46def _selectDZState(*args, **kwargs):
 47    if st.session_state.elevation:
 48        st.session_state.uploaderKey += 1
 49        st.session_state.trackFiles = None
 50
 51
 52def _styleShowMaxIn(scores: pd.Series) -> pd.DataFrame:
 53    return [
 54        'background-color: mediumseagreen' if v == scores.max() else \
 55        '' for v in scores ]
 56
 57
 58def _displayAllJumpDataIn(data: pd.DataFrame):
 59    if data is not None:
 60        columns = [ 'plotTime' ] + [ column for column in data.columns if column != 'plotTime' and column != 'timeUnix' ]
 61        st.html('<h3>All jump data from exit</h3>')
 62        st.dataframe(data,
 63            column_order=columns,
 64            column_config={
 65                'plotTime': st.column_config.NumberColumn(format='%.02f'),
 66                'speedAngle': st.column_config.NumberColumn(format='%.02f'),
 67                'speedAccuracyISC': st.column_config.NumberColumn(format='%.02f'),
 68            },
 69            hide_index=True)
 70
 71
 72def _displayScoresIn(scoresData: dict):
 73    if scoresData is not None:
 74        st.html('<h3>All 3-sec sliding window scores</h3>')
 75        data = pd.DataFrame.from_dict({ 'time': scoresData.values(), 'score': scoresData.keys(), })
 76        data.time = data.time.apply(lambda x: '%.2f' % x)
 77        st.dataframe(data, hide_index=True)
 78
 79
 80def _displayBadRowsISCAccuracyExceeded(data: pd.DataFrame, window: PerformanceWindow):
 81    badRows = data[data.speedAccuracyISC >= SPEED_ACCURACY_THRESHOLD]
 82    badRows = dropNonSkydiveDataFrom(badRows)
 83    times = pd.to_datetime(badRows.timeUnix, unit='s').dt.strftime('%Y-%m-%d %H:%M:%S.%f').str[:-4]
 84    badRows.insert(0, 'time', times)
 85    badRows.drop(columns = [
 86        'timeUnix',
 87        'altitudeMSL',
 88        'altitudeMSLFt',
 89        'speedAccuracy',
 90        'hMetersPerSecond',
 91        'hKMh',
 92        'speedAngle',
 93        'latitude',
 94        'longitude',
 95        'verticalAccuracy', ], inplace=True)
 96    st.html('<h3>Performance window:<br>start = %.2f m (%.2f ft)<br>end = %.2f m (%.2f ft)<br>validation start = %.2f m (%.2f ft)</h3>' % \
 97                    (window.start, M_2_FT*window.start, window.end, M_2_FT*window.end, window.validationStart, M_2_FT*window.validationStart))
 98    st.html('<h3>%d track rows where the ISC speed accuracy threshold was exceeded during the speed run:</h3>' % len(badRows))
 99    st.dataframe(badRows, hide_index=True)
100
101
102    workData = data.copy()
103    workData = dropNonSkydiveDataFrom(workData)
104    times = pd.to_datetime(workData.timeUnix, unit='s').dt.strftime('%Y-%m-%d %H:%M:%S.%f').str[:-4]
105    workData.insert(0, 'time', times)
106    st.html('<h3>Full speed run data (%d rows)</h3>' % len(workData))
107    st.dataframe(workData, hide_index=True)
108
109
110def _styleShowMinMaxIn(scores: pd.Series) -> pd.DataFrame:
111    return [
112        'background-color: green' if v == scores.max() else \
113        'background-color: orangered' if v == scores.min() else \
114        '' for v in scores ]
115
116
117def _displayJumpsInSet(aggregate: pd.DataFrame):
118    with st.expander('**Jumps in this set**', expanded=True, icon=':material/dataset:'):
119        displayAggregate = aggregate.style.apply(_styleShowMinMaxIn, subset=[ 'score', ]).apply(_styleShowMaxIn, subset=[ 'maxSpeed', ]).format(precision=2)
120        st.dataframe(displayAggregate)
121
122
123def _displaySpeedSummary(aggregate: pd.DataFrame,
124                         allJumpsPlot):
125    with st.expander('Speed summary', expanded=True):
126        summary = totalResultsFrom(aggregate)
127
128        st.dataframe(
129            summary.style.format("{:.2f}"),
130            hide_index=True
131        )
132        st.plotly_chart(allJumpsPlot, width='stretch')
133
134
135def _displaySpeedAngles(jumpResults: dict):
136    with st.expander('**Speed angles**', icon=':material/arrow_back_ios_new:'):
137        angles = collateAnglesByTimeFromExit(jumpResults).style.format(precision=1)
138        st.dataframe(angles)
139
140
141def _displayAllTracksOnMap(jumpResults: dict, tagColors: dict):
142    with st.expander('**All jumps trajectories**', expanded=True):
143        displayTrackOnMap(multipleSpeedJumpsTrajectories(jumpResults, tagColors))
144
145
146_FILE_ERROR_LABELS = {
147    JumpStatus.UNSUPPORTED_PLD_FORMAT: 'Unsupported file format — not a FlySight v1, v2, or Deep & Steep Insight device',
148    JumpStatus.INVALID_SPEED_FILE: 'Invalid or corrupted file',
149    JumpStatus.WARM_UP_FILE: 'Warm-up file or SMD battery failure',
150    JumpStatus.SPEED_ACCURACY_EXCEEDS_LIMIT: 'Speed accuracy exceeds ISC threshold — RE-JUMP required',
151}
152
153
154def _displayFileErrorsIn(jumpResults: dict, jumpResultsSubset: dict):
155    errorFiles = {tag: result for tag, result in jumpResults.items() if tag not in jumpResultsSubset}
156    if not errorFiles:
157        return
158    with st.expander('**Files not included in scoring**', expanded=True, icon=':material/warning:'):
159        for tag, result in errorFiles.items():
160            label = _FILE_ERROR_LABELS.get(result.status, 'Did not meet scoring criteria')
161            st.html('<p>⚠ <b>%s</b> — <span style="color: red">%s</span></p>' % (tag, label))
162
163
164def _maxSpeedScaleFrom(jumpResults: dict) -> float:
165    maxScore = max(result.score if result.score != None else 0 for result in jumpResults.values())
166    try:
167        return DEFAULT_PLOT_MAX_V_SCALE if maxScore <= DEFAULT_PLOT_MAX_V_SCALE else maxScore + DEFAULT_PLOT_INCREMENT
168    except TypeError:
169        return DEFAULT_PLOT_MAX_V_SCALE
170
171
172def main():
173    st.set_page_config(
174        layout = 'wide',
175        page_title='SSScore %s' % __VERSION__,
176    )
177    initFileUploaderState('trackFiles')
178    setSideBarAndMain('🔢', False, _selectDZState)
179
180    if st.session_state.trackFiles:
181        jumpResults = processAllJumpFiles(st.session_state.trackFiles, altitudeDZMeters=st.session_state.elevation)
182        allJumpsPlot = initializePlot('All jumps', backgroundColorName='#2c2c2c', yMax=_maxSpeedScaleFrom(jumpResults))
183        jumpResultsSubset = dict()
184        resultTags = sorted(list(jumpResults.keys()), reverse=True)
185        tabs = st.tabs(['Totals']+resultTags)
186        index = 1
187        jumpStatus = JumpStatus.OK
188        for tag in resultTags:
189            jumpResult = jumpResults[tag]
190            with tabs[index]:
191                jumpStatusInfo,\
192                scoringInfo,\
193                badJumpLegend,\
194                jumpStatus = interpretJumpResult(tag, jumpResult, st.session_state.processBadJump)
195                if jumpStatus != JumpStatus.OK:
196                    st.toast('#### %s - %s' % (tag, str(jumpStatus)), icon='⚠️')
197                showJumpData = False
198                match jumpStatus:
199                    case JumpStatus.OK:
200                        showJumpData = True
201                    case JumpStatus.UNSUPPORTED_PLD_FORMAT:
202                        pass
203                    case _ if st.session_state.processBadJump:
204                        showJumpData = True
205                if showJumpData:
206                    jumpResultsSubset[tag] = jumpResult
207                st.html('<h3>'+jumpStatusInfo+scoringInfo+(str(badJumpLegend) if badJumpLegend else ''))
208                st.html("<br>If this was NOT a warm-up file, it's probably an ISC altitude violation; please report to Eugene/pr3d4t0r and attach the TRACK.CSV file</h3>" if jumpStatus in [ JumpStatus.WARM_UP_FILE, ] else '</h3>')
209                if showJumpData:
210                    displayJumpDataIn(jumpResult.table)
211                    with st.expander('Max score = crosshairs.  Max speed = diamond. V-accel = exponential mean average over 4 seconds.', expanded=True):
212                        plotJumpResult(tag, jumpResult)
213                    if jumpResult.data is not None:
214                        with st.expander('**Horizontal displacement** - optimal ≦ 500 m from exit', expanded=True):
215                            colGroundTrack, colForwardDisplacement = st.columns(2)
216                            with colGroundTrack:
217                                groundTrackFigure = initializeGroundTrackPlot(tag, backgroundColorName='#2c2c2c')
218                                graphGroundTrack(groundTrackFigure, jumpResult)
219                                st.plotly_chart(groundTrackFigure, width='stretch')
220                            with colForwardDisplacement:
221                                displacementFigure = initializePlot(tag, yLabel='forward (m)', backgroundColorName='#2c2c2c', height=450)
222                                graphForwardDisplacement(displacementFigure, jumpResult)
223                                st.plotly_chart(displacementFigure, width='stretch')
224                    with st.expander('Speed run / jump run', expanded=True):
225                        st.session_state.displayScorePoint = st.toggle('Display max score / max speed point', value=True, help='Show the fastest speed or score point along the flight path', key=tag)
226                        displayTrackOnMap(speedJumpTrajectory(jumpResult, st.session_state.displayScorePoint), st.session_state.displayScorePoint, showJumpRunLegend=True)
227                    _displayAllJumpDataIn(jumpResult.data)
228                    _displayScoresIn(jumpResult.scores)
229                elif jumpStatus == JumpStatus.SPEED_ACCURACY_EXCEEDS_LIMIT:
230                    _displayBadRowsISCAccuracyExceeded(jumpResult.data, jumpResult.window)
231            index += 1
232        with tabs[0]:
233            if len(resultTags):
234                _displayFileErrorsIn(jumpResults, jumpResultsSubset)
235                if jumpResultsSubset:
236                    tagColors = resolveJumpColors(jumpResultsSubset)
237                    for tag in sorted(jumpResultsSubset.keys(), reverse=True):
238                        jumpResult = jumpResultsSubset[tag]
239                        graphJumpResult(
240                            allJumpsPlot,
241                            jumpResult,
242                            lineColor=tagColors[tag],
243                            legend='%s = %.2f' % (tag, jumpResult.score if jumpResult.score else -1.0),
244                            showIt=False
245                        )
246                    aggregate = aggregateResults(jumpResultsSubset)
247                    if len(aggregate) > 0:
248                        _displayJumpsInSet(aggregate)
249                        _displaySpeedAngles(jumpResults)
250                        _displaySpeedSummary(aggregate, allJumpsPlot)
251                        _displayAllTracksOnMap(jumpResults, tagColors)
252    else:
253        st.write(fetchInstructionsHTML(), unsafe_allow_html=True)
254
255
256if '__main__' == __name__:
257    main()
def main():
173def main():
174    st.set_page_config(
175        layout = 'wide',
176        page_title='SSScore %s' % __VERSION__,
177    )
178    initFileUploaderState('trackFiles')
179    setSideBarAndMain('🔢', False, _selectDZState)
180
181    if st.session_state.trackFiles:
182        jumpResults = processAllJumpFiles(st.session_state.trackFiles, altitudeDZMeters=st.session_state.elevation)
183        allJumpsPlot = initializePlot('All jumps', backgroundColorName='#2c2c2c', yMax=_maxSpeedScaleFrom(jumpResults))
184        jumpResultsSubset = dict()
185        resultTags = sorted(list(jumpResults.keys()), reverse=True)
186        tabs = st.tabs(['Totals']+resultTags)
187        index = 1
188        jumpStatus = JumpStatus.OK
189        for tag in resultTags:
190            jumpResult = jumpResults[tag]
191            with tabs[index]:
192                jumpStatusInfo,\
193                scoringInfo,\
194                badJumpLegend,\
195                jumpStatus = interpretJumpResult(tag, jumpResult, st.session_state.processBadJump)
196                if jumpStatus != JumpStatus.OK:
197                    st.toast('#### %s - %s' % (tag, str(jumpStatus)), icon='⚠️')
198                showJumpData = False
199                match jumpStatus:
200                    case JumpStatus.OK:
201                        showJumpData = True
202                    case JumpStatus.UNSUPPORTED_PLD_FORMAT:
203                        pass
204                    case _ if st.session_state.processBadJump:
205                        showJumpData = True
206                if showJumpData:
207                    jumpResultsSubset[tag] = jumpResult
208                st.html('<h3>'+jumpStatusInfo+scoringInfo+(str(badJumpLegend) if badJumpLegend else ''))
209                st.html("<br>If this was NOT a warm-up file, it's probably an ISC altitude violation; please report to Eugene/pr3d4t0r and attach the TRACK.CSV file</h3>" if jumpStatus in [ JumpStatus.WARM_UP_FILE, ] else '</h3>')
210                if showJumpData:
211                    displayJumpDataIn(jumpResult.table)
212                    with st.expander('Max score = crosshairs.  Max speed = diamond. V-accel = exponential mean average over 4 seconds.', expanded=True):
213                        plotJumpResult(tag, jumpResult)
214                    if jumpResult.data is not None:
215                        with st.expander('**Horizontal displacement** - optimal ≦ 500 m from exit', expanded=True):
216                            colGroundTrack, colForwardDisplacement = st.columns(2)
217                            with colGroundTrack:
218                                groundTrackFigure = initializeGroundTrackPlot(tag, backgroundColorName='#2c2c2c')
219                                graphGroundTrack(groundTrackFigure, jumpResult)
220                                st.plotly_chart(groundTrackFigure, width='stretch')
221                            with colForwardDisplacement:
222                                displacementFigure = initializePlot(tag, yLabel='forward (m)', backgroundColorName='#2c2c2c', height=450)
223                                graphForwardDisplacement(displacementFigure, jumpResult)
224                                st.plotly_chart(displacementFigure, width='stretch')
225                    with st.expander('Speed run / jump run', expanded=True):
226                        st.session_state.displayScorePoint = st.toggle('Display max score / max speed point', value=True, help='Show the fastest speed or score point along the flight path', key=tag)
227                        displayTrackOnMap(speedJumpTrajectory(jumpResult, st.session_state.displayScorePoint), st.session_state.displayScorePoint, showJumpRunLegend=True)
228                    _displayAllJumpDataIn(jumpResult.data)
229                    _displayScoresIn(jumpResult.scores)
230                elif jumpStatus == JumpStatus.SPEED_ACCURACY_EXCEEDS_LIMIT:
231                    _displayBadRowsISCAccuracyExceeded(jumpResult.data, jumpResult.window)
232            index += 1
233        with tabs[0]:
234            if len(resultTags):
235                _displayFileErrorsIn(jumpResults, jumpResultsSubset)
236                if jumpResultsSubset:
237                    tagColors = resolveJumpColors(jumpResultsSubset)
238                    for tag in sorted(jumpResultsSubset.keys(), reverse=True):
239                        jumpResult = jumpResultsSubset[tag]
240                        graphJumpResult(
241                            allJumpsPlot,
242                            jumpResult,
243                            lineColor=tagColors[tag],
244                            legend='%s = %.2f' % (tag, jumpResult.score if jumpResult.score else -1.0),
245                            showIt=False
246                        )
247                    aggregate = aggregateResults(jumpResultsSubset)
248                    if len(aggregate) > 0:
249                        _displayJumpsInSet(aggregate)
250                        _displaySpeedAngles(jumpResults)
251                        _displaySpeedSummary(aggregate, allJumpsPlot)
252                        _displayAllTracksOnMap(jumpResults, tagColors)
253    else:
254        st.write(fetchInstructionsHTML(), unsafe_allow_html=True)