За прошедший год OmniSci F1 Demo побывала на конференциях и встречах по всей территории Соединенных Штатов. Позволяя участникам проехать несколько кругов в видеоигре F1 и увидеть результаты телеметрии транспортных средств, передаваемые в OmniSciDB, команды сообщества и мероприятий могут продемонстрировать платформу OmniSci в увлекательной манере.
В этой статье я расскажу, как я создал панель мониторинга в режиме реального времени с использованием Plotly Dash, и обрисую несколько моментов, которые следует учитывать при использовании OmniSciDB в качестве источника данных для пользовательского приложения.
Весь код, описанный в этом блоге, доступен в репозитории F1 Demo GitHub .
Flask + Bootstrap + React.js = Plotly Dash
Сообщество Python не объединилось вокруг одного проекта с открытым исходным кодом так же, как сообщество R с Shiny , но Dash чувствует себя схожим по своему дизайну и целям. Удобные для пользователя функции, такие как шаблоны и параметры сетки, доступны через компоненты Dash Bootstrap, а интерактивность доступна по умолчанию, поскольку Dash построен на React.js . Самым большим преимуществом для меня при выборе Dash является то, что он основан на веб-фреймворке Python Flask , с которым у меня уже был опыт работы.
Разметка панели инструментов F1 занимает всего несколько строк кода, аналогично простому приложению Flask:
питон
1
app = dash.Dash(__name__, external_stylesheets = [dbc.themes.DARKLY])
2
app.title = "OmniSci Grand Prix | Converge 2019"
3
app.config['suppress_callback_exceptions'] = True
4
server = app.server
5
body = dbc.Container([
7
dbc.Row([track, leaderboard]),
8
dbc.Row([telemetry, menubox])
9
],
10
className="mt-4",
11
fluid=True
12
)
13
app.layout = html.Div([navbar, body])
Панель инструментов F1 можно рассматривать как две секции:
- Navbar, который содержит название приложения и фирменный стиль и
- Тело, состоящее из двух строк, каждая из которых имеет два столбца для хранения визуализаций.
Вам также может понравиться:
Панель инструментов в реальном времени с MongoDB .
Я решил использовать переменные-заполнители для navbar и body вместо встраивания фактических виджетов в макет. Это позволило мне работать над четырьмя разделами отдельно, вместо того, чтобы весь код Dash был вложен в одно и то же app.layout
назначение.
Вызов OmniSciDB для заполнения визуализаций
В то время как Dash предоставляет все функциональные возможности для создания и размещения визуализаций на приборной панели, вычислительная часть приложения обрабатывается OmniSciDB. Существует четыре основных запроса для панели управления, по одному для каждого из виджетов (не включая Metric
выпадающий виджет, который заполнен статически):
- Заполнение таблицы лидеров с 10 самыми быстрыми кругами.
- Получение позиции трека (диаграмма рассеяния).
- Получение телеметрии автомобиля (линейный график).
- Раскрывающийся список контрольных кругов (50 самых быстрых кругов).
Каждый из этих запросов написан как функции Python, использующие pymapd для связи с бэкэндом OmniSciDB. Давайте рассмотрим get_telemetry_data () :
питон
xxxxxxxxxx
1
def get_telemetry_data(sessionuid, lapstarttime, lapendtime, playercarindex, metric):
2
conn = pymapd.connect(host = host, user = user, password = password, dbname = dbname, port = port)
4
## get telemetry data
6
## by specifying the timestamps and sessionuid, it implies a single track
7
tele = f"""select
8
packettime,
9
max({metric}) as {metric}
10
from gtc_cartelemetry_v2
11
where sessionuid = '{sessionuid}' and
12
packettime between '{lapstarttime}' and '{lapendtime}' and
13
playercarindex = {playercarindex}
14
group by 1
15
order by 1
16
"""
17
telemetry_ref = pd.read_sql(tele, conn)
19
return telemetry_ref
Эта функция имеет несколько входов, которые помогают однозначно идентифицировать и получать данные телеметрии с определенного круга. В теле функции вы заметите, что я встраиваю вызов pymapd.connect () внутри функции вместо передачи Connection
объекта в качестве аргумента функции. Хотя создание базы данных с новым соединением каждые несколько секунд не является лучшей практикой разработки, оно гарантирует «свежее» соединение при каждой отправке нового запроса.
Я расскажу больше о соображениях производительности всего этого приложения позже в этом посте, но для предполагаемого использования приложения в качестве отдельного дисплея в кабине событий создание нового соединения для каждого запроса более чем достаточно эффективно.
Реактивное программирование с использованием Dash Callbacks
Интерактивные возможности панели мониторинга F1 создаются с помощью обратных вызовов в Dash , которые реализованы как декораторы Python . Декораторы позволяют модифицировать функции Python с помощью дополнительного поведения, фактически не изменяя основную функцию Python. Декораторы обратного вызова Dash имеют входы и выходы; изменение ввода (обычно элемента меню) перезапустит функцию Python, модифицируя конкретный элемент на странице (также определенный как часть обратного вызова).
Обратные вызовы по требованию
Первый способ, которым обратные вызовы используются на приборной панели, - это выпадающие меню для настройки Reference Lap
сравнения текущих характеристик вождения и Metric
выбора метрики для телеметрии для мониторинга. Вы можете увидеть как работают оба входа в функции build_telemetry_chart () . Чтобы объяснение не было слишком громоздким, мы просто рассмотрим сигнатуры декоратора и функции здесь:
питон
xxxxxxxxxx
1
#### THIS IS A SUMMARY, THIS FILE WILL NOT RUN AS-IS ####
2
#### INPUTS to build_telemetry_chart ####
4
#https://github.com/omnisci/F1-demo/blob/726c56ba4e8c878abeef81b7cf680bd4d36a4bd2/f1dash/controls.py#L9-L32
5
#### reference lap: build list dynamically with callback
6
reflapmenu = dcc.Dropdown(
7
id='reflapmenu',
8
searchable=False,
9
clearable=False,
10
style=dict(minWidth = '250px', paddingRight = '15px', borderRadius = 0)
11
)
12
#### telemetry metrics: these are the reasonable values from the table
14
metricmenu = dcc.Dropdown(
15
id='metricmenu',
16
options=[
17
{'label': 'speed', 'value': 'speed'},
18
{'label': 'enginerpm', 'value': 'enginerpm'},
19
{'label': 'brake', 'value': 'brake'},
20
{'label': 'gear', 'value': 'gear'},
21
{'label': 'steer', 'value': 'steer'},
22
{'label': 'throttle', 'value': 'throttle'}
23
],
24
value="speed",
25
searchable=False,
26
clearable=False,
27
style=dict(minWidth = '250px', paddingRight = '15px', borderRadius = 0, color="#FFFFFF")
28
)
29
#### Listens for the 'value' field of 'reflapmenu' and 'value' field of 'metricmenu' to change
31
#### Runs build_telemetry_chart function, which sends its output to 'telemetry-graph' id
32
#https://github.com/omnisci/F1-demo/blob/726c56ba4e8c878abeef81b7cf680bd4d36a4bd2/f1dash/app.py#L182-L185
33
#callback decorator
34
callback(Output('telemetry-graph', 'figure'), .
35
[Input('track-interval', 'n_intervals'), Input('reflapmenu', 'value'), Input('metricmenu', 'value')])
36
def build_telemetry_chart(notused, reflapvalue, metric):
37
...
38
#### This code has 'telemetry-graph' as its id, so the results of build_telemetry_chart modifies the 'figure' field of 'telgraph'
41
#https://github.com/omnisci/F1-demo/blob/726c56ba4e8c878abeef81b7cf680bd4d36a4bd2/f1dash/telemetry.py#L31-L35
42
telgraph = dcc.Graph(id='telemetry-graph',
43
config={
44
'displayModeBar': True
45
}
46
)
Строки с 6 по 28 приведенного выше фрагмента кода определяют раскрывающиеся меню. Важная вещь, на которую следует обратить внимание в этих строках, это поля id. Эти метки определяют, что будет наблюдать общее приложение Dash на предмет изменений состояния. Когда любое из этих раскрывающихся меню меняет свое поле значений (что происходит, когда пользователь взаимодействует с интерфейсом), они посылают сигнал о build_telemetry_chart()
том, что его необходимо выполнить повторно.
По build_telemetry_chart()
окончании работы он возвращает свой вывод в соответствии с положением вывода, определенным в обратном вызове (который для этого примера является идентификатором telemetry-graph
в telgraph
объекте Python). Важная концепция для понимания обратных вызовов в Dash заключается в том, что id
значение - это та же концепция идентификатора, что и в HTML, уникальный идентификатор на веб-странице. Несмотря на то, что Dash работает через Python и telgraph
является нашим объектом Python , ссылка на обратный вызов по идентификатору является промежуточным звеном для React.js, который сообщает ему, какую часть веб-страницы обновлять.
Основанные на времени обратные вызовы
Обратные вызовы по требованию добавляют интерактивность к панелям мониторинга, но для создания панели мониторинга в режиме реального времени нам необходимо периодически обновлять данные, которые поддерживают диаграммы без вмешательства пользователя. Используя класс Interval из библиотеки Dash Core Components, мы можем установить любой период времени, который мы выберем для запуска обратного вызова :
питон
xxxxxxxxxx
1
#https://github.com/omnisci/F1-demo/blob/726c56ba4e8c878abeef81b7cf680bd4d36a4bd2/f1dash/track.py#L56-L60
2
#dcc.Interval creates an object with no display characteristics, triggering a callback every 'interval' seconds
3
#for anything listening for the id 'track-interval'
4
trackgraph = html.Div([
5
dcc.Graph(id='track-graph',
6
config={
7
'displayModeBar': True
8
}
9
),
10
#controls when track and telemetry updates
11
dcc.Interval(
12
id='track-interval',
13
interval=7*1000, # in milliseconds
14
n_intervals=0
15
)
16
])
В trackgraph
объекте Python я добавил Interval
обновление, которое обновляется каждые 7000 миллисекунд для обновления track-interval
идентификатора. Вы заметите , в функции обратного вызова встроенного фрагмента выше, первое значение в массиве ввода является Input('track-interval', 'n_intervals')
; каждые 7 секунд dcc.Interval()
запускается код, который сообщает build_telemetry_chart()
функции, которую нужно запустить, и затем обновляет telemetry-graph
идентификатор.
Такое управление элементами пользовательского интерфейса может показаться запутанным, поскольку это намного больше JavaScript, чем Python! Но обратные вызовы - это то, что позволило нам встроить в приложение следующую логику: «Обновите график телеметрии, если 1) эталонный круг изменился и / или 2) метрика изменилась и / или 3) прошло 7 секунд». Таким образом, хотя обратные вызовы в Dash могут сбивать с толку, обратные вызовы являются очень мощными и их стоит потратить на изучение.
Управление порядком загрузки Dash CSS
Когда у вас есть макет панели мониторинга и добавлена интерактивность, то, что на самом деле отображается в виджетах панели мониторинга, зависит от истории, которую вы пытаетесь рассказать, и от особенностей вашего дизайна. Я не собираюсь освещать каждое принятое мной дизайнерское решение или то, как я изменил тему Darkly, чтобы иметь фирменные цвета OmniSci, но документация Dash и пользовательские форумы должны предоставить все ответы, которые вы ищете.
Одна вещь, которая поразила меня, когда я пытался стилизовать панель инструментов, заключалась в том, что мои изменения CSS продолжали перезаписываться базовыми стилями CSS из компонентов React, загружаемых Dash. То, как я преодолел это, было использовать app.index_string
для управления порядком загрузки файлов:
питон
xxxxxxxxxx
1
#https://github.com/omnisci/F1-demo/blob/726c56ba4e8c878abeef81b7cf680bd4d36a4bd2/f1dash/app.py#L36-L55
2
# helps load css at the footer, to avoid having default react css overwrite
3
app.index_string = '''
4
<!DOCTYPE html>
5
<html>
6
<head>
7
{%metas%}
8
<title>{%title%}</title>
9
{%favicon%}
10
</head>
11
<body>
12
{%app_entry%}
13
<footer>
14
{%config%}
15
{%scripts%}
16
{%renderer%}
17
{%css%}
18
</footer>
19
</body>
20
</html>
21
'''
Поместив {%css%}
последний тег в нижний колонтитул, я смог убедиться, что мои изменения CSS не были перезаписаны ни одним из классов CSS по умолчанию для Dash. Мой пользовательский CSS, как часть файла assets / external.css , теперь является последним загруженным на страницу, и, таким образом, эти определения CSS - это то, что показано в приложении.
Переход с Flask Dev Server на рабочий сервер
Завершив приложение Dash, я решил обратить внимание на предупреждение Flask о том, что сервер разработки не используется для производственной работы. Я использовал gunicorn и 8 потоков, но так как это приложение работало только в одном месте за раз (то есть на стенде событий), вполне вероятно, что я мог бы использовать меньше.
Этапы установки для запуска приложения Dask / Flask выходят за рамки этого поста, но официальная документация для Flask, а также руководство по DigitalOcean Flask сделали этот процесс довольно легким.
Одним из критических решений для этого конкретного приложения Dash было запускать приложение на том же сервере, что и OmniSciDB, и использовать несколько графических процессоров. Используя сервер GPU большего размера (экземпляр ND24s в Microsoft Azure, 448 ГБ ОЗУ ЦП / 96 ГБ ОЗУ) и запуская приложение Dash с того же сервера, он одновременно удалял сетевую задержку, а также предоставил OmniSciDB значительный буфер памяти графического процессора для хранения данных. "горячей".
Независимо от того, какую комбинацию эталонных кругов и метрик телеметрии исследовали пользователи, максимальный объем ОЗУ графического процессора, используемый приложением, составлял примерно 30 ГБ, оставляя достаточную емкость сервера, поскольку дополнительные круги расширяли наборы данных телеметрии.
Давайте посмотрим ваши пользовательские панели мониторинга!
В двух моих демонстрационных постах OmniSci F1 я показал, что можно вставить поток данных в реальном времени в OmniSciDB с помощью StreamSets, а также использовать OmniSciDB вместе с Dash для создания панели мониторинга в реальном времени. Возможности как обработки данных, так и использования панели мониторинга в режиме реального времени возможны благодаря архитектуре OmniSciDB , которая не требует создания индексов для получения эффективных запросов. Как только данные поступают в OmniSciDB и эти данные могут быть переданы из ОЗУ ЦП в ОЗУ графического процессора, эти данные становятся доступными для использования.
Для пользователей с открытым исходным кодом OmniSciDB такая среда, как Dash, может обеспечить почти полный погружения в аналитику. Единственные вещи, которые вы можете упустить, - это некоторые из расширенных функций Immerse, такие как автоматическая перекрестная фильтрация между диаграммами и рендерингом бэкэнда (и, конечно, отсутствие кода при создании панели мониторинга).