wnd-demo / app.py
mijtsma3's picture
Axes move with "Now"
cc1a80e
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#240
end_time = 600#600
start_date = datetime.datetime(2024, 6, 7, 12, 0, 0)
end_date = start_date + datetime.timedelta(seconds=end_time)
# Initialize the Dash app
app = dash.Dash(__name__, suppress_callback_exceptions=True)
# Global dictionary to store data
data_store = {}
def parse_action_trace(path, exclude = []):
# Parses action data from CSV files into a dictionary.
# This function reads CSV files specified in a dictionary where each key-value pair
# corresponds to an condition ID and its associated file. It extracts columns related to
# time, action name, agent name, action end time, and attributes from each file and
# stores them in a nested dictionary structure.
# Args:
# files_dict (dict): A dictionary where keys are action IDs and values are file paths.
# Returns:
# dict: A dictionary where each key is an condition ID and the value is another dictionary
# containing parsed data from the corresponding CSV file.
df = pd.read_csv(path, engine = "python")
# Convert to events list for timeline plotting
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):
# Get the list of subfolders in the 'data' folder
subfolders = [f.name for f in os.scandir(path) if f.is_dir()]
# Load all data at the start of the program
# Load the CSV files into pandas DataFrames from the selected subfolder
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'),
# 'all_action_trace': parse_action_trace(f'{path}/{subfolder}/actionTrace.csv', exclude),
}
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
# Create the layout for the app
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("Contingency Time: "),
# dcc.Input( id='cont-time', type='text', placeholder='Time at which the lost link event occurs (set to "ALONGTIMEAWAY" (no quotes) if no lost link event needs to occur)', style={'width': '80%'}),
# html.Br(),
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, # Add this line to make the slider stick to the marks
updatemode='drag'
),
# style={'width': '100%'}, # Set the width of the div containing the slider to 80% of the container
# dcc.Input( id='IFthreshold', type='text', placeholder='A `double` which represents the interaction factor at which Blaze will make a decision.', style={'width': '80%'}),
# html.Br(),
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, # Add this line to make the slider stick to the marks
updatemode='drag'
),
# dcc.Input( id='TAthreshold', type='text', placeholder='An `integer` which represents how much time needs to be left before Blaze makes a decision.', style={'width': '80%'}),
# html.Br(),
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, # Add this line to make the slider stick to the marks
updatemode='drag'
),
# dcc.Input( id='updateTime', type='text', placeholder='An `integer` which represents the frequency at which the `DecisionAction` `Responding` is updated.', style={'width': '80%'}),
# html.Br(),
# html.Button('Simulate!', id='button', n_clicks=0),
], style={'width': '60%', 'border': '1px solid gray'}),
# html.Div([
# html.Div("Next, input the relative path to your results folder, then click 'Submit'. This will load the data:", id='text2'),
# dcc.Input(
# id='path-input',
# # value='data',
# type='text',
# placeholder='Type your path... For example, ../wmc5.1/Scenario/AAMv2/Results',
# style={'width': '70%'} # Make the input wider
# ),
# html.Button('Submit', id='submit-button', n_clicks=0),
# # Create a dropdown menu with the subfolders as options
# # html.Div("Once all subfolders are loaded (representing the different conditions you ran in WMC), you can select which one to plot:", id='text2'),
# # dcc.Dropdown(
# # id='subfolder-dropdown',
# # value=None #subfolders[0] # Default value
# # )
# ], style={'width': '50%', '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%'} # Set the width of the div containing the slider to 80% of the container
], 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'), # Second graph
dcc.Dropdown(
id='variable-dropdown',
# options=[{'label': i, 'value': i} for i in subfolders],
value='airspeed_kts' #subfolders[0] # Default value
),
dcc.Graph(id='altitude-series') # Third graph
], 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('button', 'n_clicks'), # Add this output to reset button clicks
# [dash.dependencies.Input('cont-time', 'value')],
# [dash.dependencies.Input('backgroundDivert', 'value')],
# [dash.dependencies.Input('IFthreshold', 'value')],
# [dash.dependencies.Input('TAthreshold', 'value')],
# [dash.dependencies.Input('updateTime', 'value')],
# [dash.dependencies.Input('button', 'n_clicks')],
# )
# def run_wmc(selected_time, backgroundDivert, IFthreshold, TAthreshold, update_time, nc_clicks):
# if nc_clicks > 0:
# # try:
# print("Running WMC with input arguments",selected_time,"...")
# process = subprocess.Popen(['wmc/WNUAM', 'Example', selected_time, backgroundDivert, IFthreshold, TAthreshold, update_time]) # return f"WMC was run!"
# # process = subprocess.Popen(['./UAM', 'Example', '240', 'True']) # return f"WMC was run!"
# try:
# process.wait(timeout=50) # Wait for the process to complete
# except subprocess.TimeoutExpired:
# process.terminate() # Terminate the process after timeout
# # Continue with the rest of the code
# print("Completed running WMC...")
# return 0
# except:
# return f"error!"
# Load data when the app starts and store it in the hidden div
# @app.callback(
# # dash.dependencies.Output('subfolder-dropdown', 'options'),
# # dash.dependencies.Output('variable-dropdown', 'options'),
# dash.dependencies.Output('submit-button', 'n_clicks'), # Add this output to reset submit button clicks
# [dash.dependencies.Input('submit-button', 'n_clicks')],
# [dash.dependencies.Input('path-input', 'value')],
# )
# def load_data(n_clicks, selected_path):
# print("selected path is",selected_path)
# global data_store
# if n_clicks > 0:
# if selected_path is None or not os.path.isdir(selected_path):
# return [], []
# subfolder_labels, data = parse_all_data(selected_path)
# print('subfolders',subfolder_labels)
# if data is None:
# data = {}
# # Store data in the global dictionary
# data_store = data
# labels = list(data[list(data.keys())[0]]['aircraft_data'][list(data[list(data.keys())[0]]['aircraft_data'].keys())[0]].columns.tolist()) # Extracting column names for the first aircraft-specific dataframe
# return 0
# else:
# # return [], [], 0
# return 0
# Define the callback to update the graphs based on the slider value
@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.Output('timeline', 'figure'),
# dash.dependencies.Input('path-input', 'value'),
# [dash.dependencies.Input('subfolder-dropdown', 'value'),
[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_subfolder, selected_time):
# # Load the CSV files into pandas DataFrames from the selected subfolder
# df1 = pd.read_csv(f'data/{selected_subfolder}/actionTrace_Agent_PIC Blaze.csv')
# df2 = pd.read_csv(f'data/{selected_subfolder}/SNLBlaze_acstate.csv')
# df3 = pd.read_csv(f'data/{selected_subfolder}/GCCRaven_acstate.csv')
# exclude = ['Detect_crash','Detect_conflict','Flight_Dynamics','Reroute_flight','Show_radar','Direct_to_waypoint','Change_heading']
# events = parse_action_trace(f'data/{selected_subfolder}/actionTrace.csv', exclude)
def update_graph(selected_time, selected_variable, backgroundDivert, RequiredCertainty, TimeBuffer, WaitInterval): #selected_subfolder,
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#, go.Figure()
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() # Create a local copy of the event
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]))#, yaxis=dict(range=[0, 1200])
fig3['layout']['uirevision'] = 'Hello world!'
else:
fig3 = go.Figure(layout=dict(autosize=True, width=None, height=300))
# fig4 = go.Figure(layout=dict(autosize=True, width=None, height=700))
# fig4 = viz.timeline(fig4, filtered_all_action_trace, selected_time, start_date, end_date)
# fig4['layout']['uirevision'] = 'Hello world!'
return fig2, fig1, fig3, labels#, fig4
# Run the app
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()) # Extracting column names for the first aircraft-specific dataframe
print(subfolder_labels)
# webbrowser.open_new("http://127.0.0.1:8050/")
# app.run(debug=True)
app.run(debug=True, host='0.0.0.0', port=7860)