| import pandas as pd |
| import dash |
| from dash import dcc, html |
| import webbrowser |
| from src.viz import Visualizer as viz |
| import plotly.graph_objects as go |
| import os |
| import datetime |
| import plotly.express as px |
| import json |
| import subprocess |
|
|
| start_time = 0 |
| end_time = 600 |
|
|
| start_date = datetime.datetime(2024, 6, 7, 12, 0, 0) |
| end_date = start_date + datetime.timedelta(seconds=end_time) |
|
|
| |
| app = dash.Dash(__name__, suppress_callback_exceptions=True) |
|
|
| |
| data_store = {} |
|
|
| def parse_action_trace(path, exclude = []): |
| |
|
|
| |
| |
| |
| |
|
|
| |
| |
|
|
| |
| |
| |
|
|
| df = pd.read_csv(path, engine = "python") |
| |
| |
| events = [] |
|
|
| for i in range(len(df['time'])): |
| if not any(excl in df['actionName'][i] for excl in exclude): |
| event = { |
| 'agent': df['agentName'][i], |
| 'name': df['actionName'][i], |
| 'start': start_date + datetime.timedelta(seconds=df['time'][i]), |
| 'end': start_date + datetime.timedelta(seconds=df['time'][i] + max(df['actionEndTime'][i], 2)), |
| 'duration': max(df['actionEndTime'][i], 2), |
| 'attributes': df['attributes'][i] |
| } |
| events.append(event) |
| |
| return events |
|
|
| def parse_all_data(path): |
|
|
| |
| subfolders = [f.name for f in os.scandir(path) if f.is_dir()] |
|
|
| |
| |
| data = {} |
| exclude = ['Detect_crash','Detect_conflict','Flight_Dynamics','Reroute_flight','Show_radar','Direct_to_waypoint','Change_heading','fly'] |
| for subfolder in subfolders: |
| data[subfolder] = { |
| 'wnd_action_trace': pd.read_csv(f'{path}/{subfolder}/actionTrace_Agent_PIC Blaze.csv'), |
| |
| } |
| data[subfolder]['aircraft_data'] = {} |
| for file in os.listdir(f'{path}/{subfolder}'): |
| if file.endswith('_acstate.csv'): |
| aircraft_id = file.split('_')[0] |
| data[subfolder]['aircraft_data'][aircraft_id] = pd.read_csv(f'{path}/{subfolder}/{file}') |
|
|
| subfolder_labels = [{'label': i, 'value': i} for i in subfolders] |
|
|
| return subfolder_labels, data |
|
|
| |
| app.layout = html.Div([ |
| html.Div([ |
| html.B("This is an interactive demonstration of the What's Next Diagram for evaluating the ability of an operator to coordinate with envisioned automated systems, given a particular system design."), |
| html.Br(), |
| dcc.Markdown("More information can be found in the associated proceedings paper: Post, A., Nijveldt, R., Woods, D.D., IJtsma, M. (2024). Determining What's Next: Visual Analytics for Evaluating Human-Automation Coordination. Proceedings of the 2024 Human Factor and Ergonomics Annual Meeting, Phoenix, AZ. This work is partially funded by NSF Early Career Award #2238402 to Dr. Martijn IJtsma. This interactive demo also builds on earlier work under NASA grant 80NSSC23CA121.", id="explanation") |
| ], style={'width': '100%', 'border': '1px solid gray'}), |
| html.Div([ |
| dcc.Markdown("The example highlights a coordination problem in the operations of Unmanned Aerial Vehicle (UAV), which is the loss of a command and control (C2) link to the ground operator when the UAV *GCCRaven* is in busy airspace (i.e., there is surrounding traffic). The C2 link is used to receive data in real-time about the state/activity of GCCRaven, onboard automation, and to (re-)direct the automation to change GCCRaven's behavior. We use the What's Next Diagram to identify how the pilot of a second aircraft, *SNLBlaze* can coordinate with *GCCRaven*.", id="explanation") |
| ], style={'width': '100%', 'border': '1px solid gray'}), |
| html.Div([ |
| dcc.Markdown("It is envisioned that in future operatoins, each UAV is programmed with one or more contingency procedures that are to be executed in the case of a lost link. For this example, we assume the contingency procedure is to divert to and land at the nearest vertiport. However, even with contingency procedures onboard, there is potential for ambiguity in what GCCRaven will do next, depending on the logic that GCCRaven uses for determining what the nearest vertiport is, feedback it provides (Woods & Balkin, 2018), and--quite possibly--any related or additional failures onboard the UAV. The operator of *SNLBlaze* must therefore determine in real-time if what they think is the controlling contingency procedure is in fact being followed.", id="explanation2") |
| ], style={'width': '100%', 'border': '1px solid gray'}), |
| |
| html.Div([ |
| html.Div([ |
| dcc.Markdown("Multiple projections of what *GCCRaven* will do may exist simultaneously. We illustrate two possible projections of the GCCRaven's behavior. Should GCCRaven indeed divert to the nearest vertiport, it will require the pilot of SNLBlaze to reroute to avoid a loss of separation. On the other hand, should GCCRaven not divert, SNLBlaze should continue to fly along its strategically deconflicted flight path to avoid a conflict. The more informative the indication is of what is next, the more predictable GCCRaven's behavior. The challenge is the needed feedback is not available or minimal because of the lost C2 link. Here, we simulate an back-up ADSB link that is envisioned--by the aviation community--to provide very low bandwidth (four digit) cues about GCCRaven's current behavior and its intent. ", id="explanation3") |
| ], style={'width': '40%', 'border': '1px solid gray'}), |
| html.Div([ |
| dcc.Markdown("**Here you can play around with several factors that influence decision-making and its timing to see how the strategy on the What's Next Diagram changes:**", id='text1'), |
| |
| |
| |
| html.Label("What will the actual behavior of GCCRaven be when a lost link is detected?"), |
| dcc.RadioItems( |
| id='backgroundDivert', |
| options=[ |
| {'label': 'It will divert to the East vertiport', 'value': 'True'}, |
| {'label': 'It will not divert and continue to fly southbound', 'value': 'False'} |
| ], |
| value='True', |
| labelStyle={'display': 'inline-block'} |
| ), |
| html.Br(), |
| html.Label("Required certainty: How certain should the pilot of SNLBlaze be about what GCCRaven will do next before they decide whether to maneuver or not?"), |
| html.Br(), |
| dcc.Slider( |
| id='IFthreshold', |
| min=65, |
| max=85, |
| value=75, |
| marks={str(time): str(time) for time in range(65, 85+1, 10)}, |
| step=None, |
| updatemode='drag' |
| ), |
| |
| |
| |
| html.Label("Time buffer: How many seconds before the SNLBlazes 'point of no return' should SNLBlaze make its decision, even if not quite certain about what GCCRaven will do next?"), |
| dcc.Slider( |
| id='TAthreshold', |
| min=10, |
| max=40, |
| value=20, |
| marks={str(time): str(time) for time in range(10, 40+1, 10)}, |
| step=None, |
| updatemode='drag' |
| ), |
| |
| |
| html.Label("Wait interval: How often should SNLBlaze receive new information and revise its projections of GCCRaven's behavior?"), |
| dcc.Slider( |
| id='updateTime', |
| min=20, |
| max=40, |
| value=30, |
| marks={str(time): str(time) for time in range(20, 40+1, 10)}, |
| step=None, |
| updatemode='drag' |
| ), |
| |
| |
| |
| ], style={'width': '60%', 'border': '1px solid gray'}), |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| ], style={'display': 'flex'}), |
|
|
| html.Div([ |
| html.Label("Use the slider to move through time and see how projections of What's Next change with new information coming in over time. You can compare the projections with the location of the aircraft to see when and how decisions are made. The data shown in this demostration was generated with Work Models that Compute (WMC). WMC simulates actual aircraft dynamics and how they interact with the actions of human and automated agents in the systems."), |
| dcc.Slider( |
| id='time-slider', |
| min=start_time, |
| max=end_time, |
| value=470, |
| marks={str(time): str(time) for time in range(start_time, end_time+1, 50)}, |
| updatemode='drag' |
| ), |
| |
| ], style={'width': '100%', 'border': '1px solid gray'} |
| ), |
| html.Div([ |
| html.Div([ |
| html.Label("View of UAV trajectories. Use the dropdown menu to look at specific UAV state information."), |
| dcc.Graph(id='map'), |
| dcc.Dropdown( |
| id='variable-dropdown', |
| |
| value='airspeed_kts' |
| ), |
| dcc.Graph(id='altitude-series') |
| ], style={'width': '39%', 'display': 'inline-block', 'border': '1px solid gray'}), |
| html.Div([ |
| html.Label("What's Next Diagram. The width of the lines indicates the level of certainty. Click on the legend to toggle on/off alternate projections."), |
| dcc.Graph(id='time-series'), |
| ], style={'width': '59%', 'display': 'inline-block', 'border': '1px solid gray'}) |
| ], style={'display': 'flex'}), |
| html.Label("All figures copyrighted Martijn IJtsma, David Woods, Renske Nijveldt, Abigail Post. For questions, please email ijtsma.1@osu.edu."), |
|
|
| ]) |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| |
| @app.callback( |
| dash.dependencies.Output('time-series', 'figure'), |
| dash.dependencies.Output('map', 'figure'), |
| dash.dependencies.Output('altitude-series', 'figure'), |
| dash.dependencies.Output('variable-dropdown', 'options'), |
| |
| |
| |
| [dash.dependencies.Input('time-slider', 'value'), |
| dash.dependencies.Input('variable-dropdown','value'), |
| dash.dependencies.Input('backgroundDivert','value'), |
| dash.dependencies.Input('IFthreshold','value'), |
| dash.dependencies.Input('TAthreshold','value'), |
| dash.dependencies.Input('updateTime','value')] |
| ) |
| |
| |
| |
| |
| |
|
|
| |
| |
|
|
| def update_graph(selected_time, selected_variable, backgroundDivert, RequiredCertainty, TimeBuffer, WaitInterval): |
| |
| selected_subfolder = "exampleTime=200"+backgroundDivert+"_"+str(RequiredCertainty)+"_"+str(TimeBuffer)+"_"+str(WaitInterval) |
| print(selected_subfolder) |
| if selected_subfolder not in data_store: |
| print("Not found") |
| return go.Figure(), go.Figure(), go.Figure(layout=dict(autosize=True, width=None, height=300)), labels |
|
|
| wnd_action_trace = data_store[selected_subfolder].get('wnd_action_trace', pd.DataFrame()) |
| all_action_trace = data_store[selected_subfolder].get('all_action_trace', []) |
| aircraft_data = data_store[selected_subfolder].get('aircraft_data', {}) |
|
|
| filtered_wnd_action_trace = wnd_action_trace[(wnd_action_trace['time'] >= start_time) & (wnd_action_trace['time'] <= selected_time)] |
| |
| filtered_all_action_trace = [] |
| for event in all_action_trace: |
| if (event['start'] - start_date).total_seconds() <= selected_time: |
| event_copy = event.copy() |
| if (event_copy['end'] - start_date).total_seconds() > selected_time: |
| event_copy['end'] = start_date + datetime.timedelta(seconds=selected_time) |
| filtered_all_action_trace.append(event_copy) |
|
|
| ac_data = {} |
| for aircraft_id, df in aircraft_data.items(): |
| filtered_df = df[(df['time'] >= start_time) & (df['time'] <= selected_time)] |
| ac_data[aircraft_id] = filtered_df |
|
|
| fig1 = go.Figure(layout=dict(autosize=True, width=None, height=500)) |
| fig1['layout']['uirevision'] = 'Hello world!' |
| fig1 = viz.plot_trajectory(fig1, ac_data) |
|
|
| fig2 = go.Figure(layout=dict(autosize=True, width=900, height=700)) |
| fig2 = viz.wnd_visualization(fig2, filtered_wnd_action_trace, selected_time, start_time, end_time) |
| fig2['layout']['uirevision'] = 'Hello world!' |
|
|
| if selected_variable is not None: |
| fig3 = go.Figure(layout=dict(autosize=True, width=None, height=300)) |
| for aircraft_id, df in ac_data.items(): |
| fig3.add_trace(go.Scatter(x=df['time'], y=df[selected_variable], mode='lines', name=aircraft_id)) |
| fig3.update_layout(xaxis_title='Real Time', yaxis_title=selected_variable, showlegend=True, xaxis=dict(range=[start_time, end_time])) |
| fig3['layout']['uirevision'] = 'Hello world!' |
|
|
| else: |
| fig3 = go.Figure(layout=dict(autosize=True, width=None, height=300)) |
| |
| |
| |
| |
|
|
| return fig2, fig1, fig3, labels |
| |
| |
| if __name__ == '__main__': |
|
|
| subfolder_labels, data_store = parse_all_data("Results") |
| labels = list(data_store[list(data_store.keys())[0]]['aircraft_data'][list(data_store[list(data_store.keys())[0]]['aircraft_data'].keys())[0]].columns.tolist()) |
| print(subfolder_labels) |
| |
| |
| app.run(debug=True, host='0.0.0.0', port=7860) |
|
|