plannerarena 1.0__tar.gz
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.
- plannerarena-1.0/LICENSE +32 -0
- plannerarena-1.0/PKG-INFO +60 -0
- plannerarena-1.0/README.md +33 -0
- plannerarena-1.0/plannerarena/__init__.py +0 -0
- plannerarena-1.0/plannerarena/app.py +149 -0
- plannerarena-1.0/plannerarena/database.py +120 -0
- plannerarena-1.0/plannerarena/performance.py +298 -0
- plannerarena-1.0/plannerarena/progress.py +176 -0
- plannerarena-1.0/plannerarena/regression.py +140 -0
- plannerarena-1.0/plannerarena/widgets.py +139 -0
- plannerarena-1.0/plannerarena/www/ga.js +6 -0
- plannerarena-1.0/plannerarena/www/help.md +252 -0
- plannerarena-1.0/plannerarena/www/plannerarena.css +172 -0
- plannerarena-1.0/plannerarena.egg-info/PKG-INFO +60 -0
- plannerarena-1.0/plannerarena.egg-info/SOURCES.txt +19 -0
- plannerarena-1.0/plannerarena.egg-info/dependency_links.txt +1 -0
- plannerarena-1.0/plannerarena.egg-info/entry_points.txt +2 -0
- plannerarena-1.0/plannerarena.egg-info/requires.txt +8 -0
- plannerarena-1.0/plannerarena.egg-info/top_level.txt +1 -0
- plannerarena-1.0/pyproject.toml +52 -0
- plannerarena-1.0/setup.cfg +4 -0
plannerarena-1.0/LICENSE
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
Planner Arena is released as Open Source
|
|
2
|
+
under the terms of a 3-clause BSD license.
|
|
3
|
+
|
|
4
|
+
Copyright (c) 2025, Mark Moll
|
|
5
|
+
All rights reserved.
|
|
6
|
+
|
|
7
|
+
Redistribution and use in source and binary forms, with or without
|
|
8
|
+
modification, are permitted provided that the following conditions
|
|
9
|
+
are met:
|
|
10
|
+
|
|
11
|
+
* Redistributions of source code must retain the above copyright
|
|
12
|
+
notice, this list of conditions and the following disclaimer.
|
|
13
|
+
* Redistributions in binary form must reproduce the above
|
|
14
|
+
copyright notice, this list of conditions and the following
|
|
15
|
+
disclaimer in the documentation and/or other materials provided
|
|
16
|
+
with the distribution.
|
|
17
|
+
* Neither the name of Mark Moll nor the names of its contributors
|
|
18
|
+
may be used to endorse or promote products derived from this
|
|
19
|
+
software without specific prior written permission.
|
|
20
|
+
|
|
21
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
|
22
|
+
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
|
23
|
+
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
|
|
24
|
+
FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
|
|
25
|
+
COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
|
|
26
|
+
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
|
27
|
+
BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
|
28
|
+
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
29
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
|
30
|
+
LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
|
|
31
|
+
ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
|
32
|
+
POSSIBILITY OF SUCH DAMAGE.
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: plannerarena
|
|
3
|
+
Version: 1.0
|
|
4
|
+
Summary: Planner Arena
|
|
5
|
+
Author-email: Mark Moll <mark@moll.ai>
|
|
6
|
+
Maintainer-email: Mark Moll <mark@moll.ai>
|
|
7
|
+
License-Expression: BSD-3-Clause
|
|
8
|
+
Project-URL: Homepage, https://plannerarena.org
|
|
9
|
+
Project-URL: Repository, https://github.com/ompl/plannerarena.git
|
|
10
|
+
Project-URL: Issues, https://github.com/ompl/plannerarena/issues
|
|
11
|
+
Keywords: planning,benchmarking,ompl
|
|
12
|
+
Classifier: Framework :: Robot Framework :: Tool
|
|
13
|
+
Classifier: Intended Audience :: Science/Research
|
|
14
|
+
Classifier: Topic :: Scientific/Engineering :: Visualization
|
|
15
|
+
Requires-Python: >=3.10
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
License-File: LICENSE
|
|
18
|
+
Requires-Dist: htmltools>=0.6.0
|
|
19
|
+
Requires-Dist: shiny>=1.5.0
|
|
20
|
+
Requires-Dist: polars>=1.35.1
|
|
21
|
+
Requires-Dist: plotnine>=0.15.1
|
|
22
|
+
Requires-Dist: pyarrow>=22.0.0
|
|
23
|
+
Requires-Dist: faicons>=0.2.2
|
|
24
|
+
Requires-Dist: scikit-misc>=0.5.2
|
|
25
|
+
Requires-Dist: Jinja2>=3.1.6
|
|
26
|
+
Dynamic: license-file
|
|
27
|
+
|
|
28
|
+
# Planner Arena
|
|
29
|
+
|
|
30
|
+
Planner Arena is a web app for interactively exploring benchmark databases created by the [Open Motion Planning Library (OMPL)](https://ompl.kavrakilab.org). A publicly accessible version of this code is running on <http://plannerarena.org>.
|
|
31
|
+
|
|
32
|
+
See `plannerarena/www/help.md` for details.
|
|
33
|
+
|
|
34
|
+
## Run directly from cloned repository
|
|
35
|
+
|
|
36
|
+
Run this code like so from this directory:
|
|
37
|
+
|
|
38
|
+
pip3 install -r requirements.txt
|
|
39
|
+
shiny run plannerarena/app.py
|
|
40
|
+
|
|
41
|
+
## Build/run docker image
|
|
42
|
+
|
|
43
|
+
Build a docker image and run it like so:
|
|
44
|
+
|
|
45
|
+
docker build -t plannerarena:latest .
|
|
46
|
+
docker run -p 8888:8888 plannerarena:latest
|
|
47
|
+
|
|
48
|
+
Planner Arena can then be accessed in your browser at <http://127.0.0.1:8888>.
|
|
49
|
+
|
|
50
|
+
## Build/run as a Python package
|
|
51
|
+
|
|
52
|
+
To build and install a Python package yourself, run:
|
|
53
|
+
|
|
54
|
+
python3 -m build && pip3 install -U plannerarena-1.0-py3-none-any.whl
|
|
55
|
+
|
|
56
|
+
To download and install the version from PyPI, run:
|
|
57
|
+
|
|
58
|
+
pip3 install -U plannerarena
|
|
59
|
+
|
|
60
|
+
Once `plannerarena` is installed, simply type `plannerarena` in the terminal and direct your browser to <http://127.0.0.1:8888>.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Planner Arena
|
|
2
|
+
|
|
3
|
+
Planner Arena is a web app for interactively exploring benchmark databases created by the [Open Motion Planning Library (OMPL)](https://ompl.kavrakilab.org). A publicly accessible version of this code is running on <http://plannerarena.org>.
|
|
4
|
+
|
|
5
|
+
See `plannerarena/www/help.md` for details.
|
|
6
|
+
|
|
7
|
+
## Run directly from cloned repository
|
|
8
|
+
|
|
9
|
+
Run this code like so from this directory:
|
|
10
|
+
|
|
11
|
+
pip3 install -r requirements.txt
|
|
12
|
+
shiny run plannerarena/app.py
|
|
13
|
+
|
|
14
|
+
## Build/run docker image
|
|
15
|
+
|
|
16
|
+
Build a docker image and run it like so:
|
|
17
|
+
|
|
18
|
+
docker build -t plannerarena:latest .
|
|
19
|
+
docker run -p 8888:8888 plannerarena:latest
|
|
20
|
+
|
|
21
|
+
Planner Arena can then be accessed in your browser at <http://127.0.0.1:8888>.
|
|
22
|
+
|
|
23
|
+
## Build/run as a Python package
|
|
24
|
+
|
|
25
|
+
To build and install a Python package yourself, run:
|
|
26
|
+
|
|
27
|
+
python3 -m build && pip3 install -U plannerarena-1.0-py3-none-any.whl
|
|
28
|
+
|
|
29
|
+
To download and install the version from PyPI, run:
|
|
30
|
+
|
|
31
|
+
pip3 install -U plannerarena
|
|
32
|
+
|
|
33
|
+
Once `plannerarena` is installed, simply type `plannerarena` in the terminal and direct your browser to <http://127.0.0.1:8888>.
|
|
File without changes
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from shiny import App, Inputs, Outputs, Session, reactive, ui
|
|
3
|
+
from shiny.types import FileInfo
|
|
4
|
+
import faicons as fa
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from plannerarena.database import database_info_ui, database_info_server, load_database
|
|
7
|
+
from plannerarena.performance import performance_ui, performance_server
|
|
8
|
+
from plannerarena.progress import progress_ui, progress_server
|
|
9
|
+
from plannerarena.regression import regression_ui, regression_server
|
|
10
|
+
import pandas as pd
|
|
11
|
+
|
|
12
|
+
pd.options.mode.copy_on_write = True
|
|
13
|
+
|
|
14
|
+
ASSET_DIR = Path(__file__).parent / "www"
|
|
15
|
+
|
|
16
|
+
# default database and max upload size are configurable via env vars
|
|
17
|
+
DATABASE = os.getenv("DATABASE", ASSET_DIR / "benchmark.db")
|
|
18
|
+
MAX_DB_SIZE = int(os.getenv("MAX_DB_SIZE", "50000000"))
|
|
19
|
+
|
|
20
|
+
app_ui = ui.page_navbar(
|
|
21
|
+
ui.head_content(ui.include_css(ASSET_DIR / "plannerarena.css")),
|
|
22
|
+
ui.nav_panel(
|
|
23
|
+
"Overall performance",
|
|
24
|
+
performance_ui("performance"),
|
|
25
|
+
value="performance",
|
|
26
|
+
icon=fa.icon_svg("chart-bar"),
|
|
27
|
+
),
|
|
28
|
+
ui.nav_panel(
|
|
29
|
+
"Progress",
|
|
30
|
+
progress_ui("progress"),
|
|
31
|
+
value="progress",
|
|
32
|
+
icon=fa.icon_svg("chart-area"),
|
|
33
|
+
),
|
|
34
|
+
ui.nav_panel(
|
|
35
|
+
"Regression",
|
|
36
|
+
regression_ui("regression"),
|
|
37
|
+
value="regression",
|
|
38
|
+
icon=fa.icon_svg("chart-bar"),
|
|
39
|
+
),
|
|
40
|
+
ui.nav_panel(
|
|
41
|
+
"Database info",
|
|
42
|
+
database_info_ui("database_info"),
|
|
43
|
+
value="database_info",
|
|
44
|
+
icon=fa.icon_svg("circle-info"),
|
|
45
|
+
),
|
|
46
|
+
ui.nav_panel(
|
|
47
|
+
"Change database",
|
|
48
|
+
ui.div(
|
|
49
|
+
ui.div(
|
|
50
|
+
ui.h2("Upload benchmark database"),
|
|
51
|
+
ui.input_file(
|
|
52
|
+
"database",
|
|
53
|
+
label="",
|
|
54
|
+
accept=["application/x-sqlite3", ".db"],
|
|
55
|
+
),
|
|
56
|
+
ui.h2("Default benchmark database"),
|
|
57
|
+
ui.tags.ul(
|
|
58
|
+
ui.tags.li(
|
|
59
|
+
ui.a(
|
|
60
|
+
"Reset to default database", href="javascript:history.go(0)"
|
|
61
|
+
)
|
|
62
|
+
),
|
|
63
|
+
ui.tags.li(ui.a("Download default database", href="benchmark.db")),
|
|
64
|
+
),
|
|
65
|
+
class_="col-sm-10 offset-sm-1",
|
|
66
|
+
),
|
|
67
|
+
class_="row",
|
|
68
|
+
),
|
|
69
|
+
value="database",
|
|
70
|
+
icon=fa.icon_svg("database"),
|
|
71
|
+
),
|
|
72
|
+
ui.nav_panel(
|
|
73
|
+
"Help",
|
|
74
|
+
ui.div(
|
|
75
|
+
ui.div(
|
|
76
|
+
ui.markdown(open(ASSET_DIR / "help.md", "r", encoding="utf-8").read()),
|
|
77
|
+
class_="col-sm-10 offset-sm-1",
|
|
78
|
+
),
|
|
79
|
+
class_="row",
|
|
80
|
+
),
|
|
81
|
+
value="help",
|
|
82
|
+
icon=fa.icon_svg("circle-question"),
|
|
83
|
+
),
|
|
84
|
+
id="navbar",
|
|
85
|
+
footer=ui.div(
|
|
86
|
+
ui.div(
|
|
87
|
+
"Created by ",
|
|
88
|
+
ui.a("Mark Moll", href="https://moll.ai"),
|
|
89
|
+
ui.HTML(" • "),
|
|
90
|
+
"Hosted by the ",
|
|
91
|
+
ui.a("Kavraki Lab", href="https://kavrakilab.org"),
|
|
92
|
+
" at ",
|
|
93
|
+
ui.a("Rice University", href="https://www.rice.edu"),
|
|
94
|
+
ui.HTML(" • "),
|
|
95
|
+
"Repository hosted on ",
|
|
96
|
+
ui.a(
|
|
97
|
+
ui.span(fa.icon_svg("github"), "GitHub"),
|
|
98
|
+
href="https://github.com/ompl/plannerarena",
|
|
99
|
+
),
|
|
100
|
+
ui.br(),
|
|
101
|
+
"Funded in part by the ",
|
|
102
|
+
ui.a("National Science Foundation", href="https://www.nsf.gov"),
|
|
103
|
+
class_="container",
|
|
104
|
+
),
|
|
105
|
+
ui.include_js(ASSET_DIR / "ga.js"),
|
|
106
|
+
class_="footer",
|
|
107
|
+
),
|
|
108
|
+
title="Planner Arena",
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def app_server(input: Inputs, output: Outputs, session: Session):
|
|
113
|
+
@reactive.calc
|
|
114
|
+
def data():
|
|
115
|
+
file: list[FileInfo] | None = input.database()
|
|
116
|
+
if file is None or file[0]["size"] > MAX_DB_SIZE:
|
|
117
|
+
if not Path(DATABASE).exists():
|
|
118
|
+
ui.update_nav_panel("navbar", "database", "show")
|
|
119
|
+
ui.update_navset("navbar", "database")
|
|
120
|
+
ui.notification_show(
|
|
121
|
+
"No default database found. Upload a database first.",
|
|
122
|
+
duration=5,
|
|
123
|
+
type="warning",
|
|
124
|
+
)
|
|
125
|
+
return load_database(DATABASE)
|
|
126
|
+
return load_database(file[0]["datapath"])
|
|
127
|
+
|
|
128
|
+
# after a new database is uploaded switch to the "performance" tab
|
|
129
|
+
@reactive.effect
|
|
130
|
+
@reactive.event(input.database)
|
|
131
|
+
def _():
|
|
132
|
+
ui.update_nav_panel("navbar", "performance", "show")
|
|
133
|
+
ui.update_navset("navbar", "performance")
|
|
134
|
+
|
|
135
|
+
# create all the different tabs
|
|
136
|
+
performance_server("performance", data)
|
|
137
|
+
progress_server("progress", data)
|
|
138
|
+
regression_server("regression", data)
|
|
139
|
+
database_info_server("database_info", data)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
# create the Shiny app. The shiny command line app looks for this variable
|
|
143
|
+
app = App(app_ui, app_server, static_assets=ASSET_DIR)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def run():
|
|
147
|
+
from shiny._main import run_app
|
|
148
|
+
|
|
149
|
+
run_app(app, host="127.0.0.1", port=8888)
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import re
|
|
2
|
+
import sqlite3
|
|
3
|
+
import polars.selectors as cs
|
|
4
|
+
import polars as pl
|
|
5
|
+
from shiny import Inputs, Outputs, Session, module, reactive, render, ui, req
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def get_table(conn: sqlite3.Connection, table: str) -> pl.DataFrame:
|
|
10
|
+
"""read an entire table from an SQLite3 database"""
|
|
11
|
+
df = pl.read_database(
|
|
12
|
+
f"SELECT * from {table}", connection=conn, infer_schema_length=None
|
|
13
|
+
)
|
|
14
|
+
return df.rename({name: name.replace("_", " ") for name in df.columns})
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _version_key(version_string):
|
|
18
|
+
# Split the version string into numerical and non-numerical parts
|
|
19
|
+
# e.g., "1.2.3b" -> ['1', '.', '2', '.', '3', 'b']
|
|
20
|
+
parts = re.split(r"(\d+)", version_string)
|
|
21
|
+
# Convert numerical parts to integers, keep others as strings
|
|
22
|
+
return tuple(int(p) if p.isdigit() else p for p in parts)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def load_database(dbname: str | Path) -> dict:
|
|
26
|
+
"""Read a Planner Arena database and return the parsed tables"""
|
|
27
|
+
if not Path(dbname).exists():
|
|
28
|
+
return {
|
|
29
|
+
"experiments": pl.DataFrame(),
|
|
30
|
+
"problem_names": [],
|
|
31
|
+
"parameters": [],
|
|
32
|
+
"planner_configs": pl.DataFrame(),
|
|
33
|
+
"enums": pl.DataFrame(),
|
|
34
|
+
"runs": pl.DataFrame(),
|
|
35
|
+
"attributes": [],
|
|
36
|
+
"progress": pl.DataFrame(),
|
|
37
|
+
}
|
|
38
|
+
conn = sqlite3.connect(dbname)
|
|
39
|
+
experiments = get_table(conn, "experiments").rename({"name": "experiment"})
|
|
40
|
+
version_enum = pl.Enum(
|
|
41
|
+
sorted(experiments["version"].unique().to_list(), key=_version_key)
|
|
42
|
+
)
|
|
43
|
+
experiments = experiments.with_columns(pl.col("version").cast(version_enum))
|
|
44
|
+
problem_names = (
|
|
45
|
+
experiments.get_column("experiment").unique(maintain_order=True).to_list()
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
planner_configs = (
|
|
49
|
+
get_table(conn, "plannerConfigs")
|
|
50
|
+
.rename({"name": "planner"})
|
|
51
|
+
.with_columns(
|
|
52
|
+
pl.col("planner")
|
|
53
|
+
.str.replace("geometric_|control_", "")
|
|
54
|
+
.cast(pl.Categorical)
|
|
55
|
+
)
|
|
56
|
+
)
|
|
57
|
+
enums = get_table(conn, "enums").with_columns(
|
|
58
|
+
pl.col("name").cast(pl.Categorical),
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
runs = get_table(conn, "runs")
|
|
62
|
+
attributes = runs.columns[3:]
|
|
63
|
+
|
|
64
|
+
exp_exclude_cols = [
|
|
65
|
+
"totaltime",
|
|
66
|
+
"timelimit",
|
|
67
|
+
"memorylimit",
|
|
68
|
+
"runcount",
|
|
69
|
+
"hostname",
|
|
70
|
+
"cpuinfo",
|
|
71
|
+
"date",
|
|
72
|
+
"seed",
|
|
73
|
+
"setup",
|
|
74
|
+
]
|
|
75
|
+
# augment runs table with experiment name as well as any experiment parameters
|
|
76
|
+
runs = runs.join(
|
|
77
|
+
planner_configs.select("id", "planner"), left_on="plannerid", right_on="id"
|
|
78
|
+
).join(
|
|
79
|
+
experiments.select(cs.exclude(exp_exclude_cols)),
|
|
80
|
+
left_on="experimentid",
|
|
81
|
+
right_on="id",
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
progress = get_table(conn, "progress")
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
"experiments": experiments,
|
|
88
|
+
"problem_names": problem_names,
|
|
89
|
+
"parameters": experiments.columns[12:],
|
|
90
|
+
"planner_configs": planner_configs,
|
|
91
|
+
"enums": enums,
|
|
92
|
+
"runs": runs,
|
|
93
|
+
"attributes": attributes,
|
|
94
|
+
"progress": progress,
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@module.ui
|
|
99
|
+
def database_info_ui():
|
|
100
|
+
return ui.navset_tab(
|
|
101
|
+
ui.nav_panel("Benchmark setup", ui.output_data_frame("benchmark_info")),
|
|
102
|
+
ui.nav_panel("Planner Configuration", ui.output_data_frame("planner_configs")),
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@module.server
|
|
107
|
+
def database_info_server(
|
|
108
|
+
input: Inputs, output: Outputs, session: Session, data: reactive.Value
|
|
109
|
+
):
|
|
110
|
+
@output
|
|
111
|
+
@render.data_frame
|
|
112
|
+
def benchmark_info():
|
|
113
|
+
req(not data()["experiments"].is_empty())
|
|
114
|
+
return data()["experiments"].transpose(include_header=True)
|
|
115
|
+
|
|
116
|
+
@output
|
|
117
|
+
@render.data_frame
|
|
118
|
+
def planner_configs():
|
|
119
|
+
req(not data()["planner_configs"].is_empty())
|
|
120
|
+
return data()["planner_configs"].select("planner", "settings").unique()
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
import io
|
|
2
|
+
import pickle
|
|
3
|
+
import polars as pl
|
|
4
|
+
import plotnine as p9
|
|
5
|
+
from shiny import Inputs, Outputs, Session, module, reactive, render, ui, req
|
|
6
|
+
from plannerarena.widgets import (
|
|
7
|
+
problem_widget,
|
|
8
|
+
problem_parameter_filter,
|
|
9
|
+
problem_parameter_values,
|
|
10
|
+
problem_parameter_groups,
|
|
11
|
+
problem_parameter_widgets,
|
|
12
|
+
attribute_widget,
|
|
13
|
+
version_widget,
|
|
14
|
+
planner_widget,
|
|
15
|
+
download_buttons,
|
|
16
|
+
DataTuple,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@module.ui
|
|
21
|
+
def performance_ui() -> ui.Tag:
|
|
22
|
+
return ui.page_sidebar(
|
|
23
|
+
ui.sidebar(
|
|
24
|
+
ui.TagList(
|
|
25
|
+
ui.output_ui("problem_ui"),
|
|
26
|
+
ui.output_ui("problem_parameter_ui"),
|
|
27
|
+
ui.output_ui("attribute_ui"),
|
|
28
|
+
ui.input_checkbox("advanced_options", "Show advanced options"),
|
|
29
|
+
ui.panel_conditional(
|
|
30
|
+
"input.advanced_options",
|
|
31
|
+
ui.card(
|
|
32
|
+
ui.input_checkbox(
|
|
33
|
+
"show_as_cdf", "Show as cumulative distribution function"
|
|
34
|
+
),
|
|
35
|
+
ui.input_checkbox(
|
|
36
|
+
"show_simplified", "Include results after simplification"
|
|
37
|
+
),
|
|
38
|
+
ui.input_checkbox(
|
|
39
|
+
"hide_outliers", "Hide outliers in box plots"
|
|
40
|
+
),
|
|
41
|
+
ui.input_checkbox("y_log_scale", "Use log scale for Y-axis"),
|
|
42
|
+
),
|
|
43
|
+
),
|
|
44
|
+
ui.output_ui("version_ui"),
|
|
45
|
+
ui.output_ui("planner_ui"),
|
|
46
|
+
),
|
|
47
|
+
width="350px",
|
|
48
|
+
),
|
|
49
|
+
ui.HTML(
|
|
50
|
+
"""<div class="alert alert-info alert-dismissible fade show" role="alert">
|
|
51
|
+
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
|
52
|
+
If you use Planner Arena or the OMPL benchmarking facilities, then we kindly ask you to include the following citation in your publications:
|
|
53
|
+
<blockquote>
|
|
54
|
+
Mark Moll, Ioan A. Șucan, Lydia E. Kavraki, <a href=\"https://moll.ai/publications/moll2015benchmarking-motion-planning-algorithms.pdf\">Benchmarking Motion Planning Algorithms: An Extensible Infrastructure for Analysis and Visualization</a>, <em>IEEE Robotics & Automation Magazine,</em> 22(3):96–102, September 2015. doi: <a href=\"https://dx.doi.org/10.1109/MRA.2015.2448276\">10.1109/MRA.2015.2448276</a>.
|
|
55
|
+
</blockquote>
|
|
56
|
+
</div>"""
|
|
57
|
+
),
|
|
58
|
+
download_buttons(),
|
|
59
|
+
ui.output_plot("plot"),
|
|
60
|
+
ui.output_data_frame("missing_data_table"),
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def enums_plot(df: pl.DataFrame, enum: pl.DataFrame, grouping: str) -> p9.ggplot:
|
|
65
|
+
"""Create a stacked bar chart for enum types (e.g., "status").
|
|
66
|
+
|
|
67
|
+
If grouping is not empty, facetting is used (one plot per group variable value)
|
|
68
|
+
"""
|
|
69
|
+
df = df.join(enum, left_on="status", right_on="value")
|
|
70
|
+
plot = p9.ggplot(df, p9.aes(x="planner", fill="description")) + p9.geom_bar()
|
|
71
|
+
if grouping:
|
|
72
|
+
return plot + p9.facet_grid(grouping)
|
|
73
|
+
else:
|
|
74
|
+
return plot
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def ecdf_plot(df: pl.DataFrame, attr: str, grouping: str) -> p9.ggplot:
|
|
78
|
+
"""Create a plot of the empirical cumulative distribution function for the specified attribute."""
|
|
79
|
+
plot = (
|
|
80
|
+
p9.ggplot(df, p9.aes(x=attr, color="planner"))
|
|
81
|
+
+ p9.xlab(attr)
|
|
82
|
+
+ p9.ylab("cumulative probability")
|
|
83
|
+
+ p9.stat_ecdf()
|
|
84
|
+
)
|
|
85
|
+
if grouping:
|
|
86
|
+
return plot + p9.scale_linetype(name=grouping)
|
|
87
|
+
else:
|
|
88
|
+
return plot
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def ecdf_plot_with_simplified(df: pl.DataFrame, attr: str) -> p9.ggplot:
|
|
92
|
+
"""Create a plot of the empirical cumulative distribution function for the specified attribute
|
|
93
|
+
and the value of the attribute after path simplification."""
|
|
94
|
+
return (
|
|
95
|
+
p9.ggplot(
|
|
96
|
+
df.with_columns(
|
|
97
|
+
pl.col("planner").cast(pl.Utf8).alias("plannerstr"),
|
|
98
|
+
pl.col("key").cast(pl.Utf8).alias("keystr"),
|
|
99
|
+
),
|
|
100
|
+
p9.aes(
|
|
101
|
+
x="value",
|
|
102
|
+
color="planner",
|
|
103
|
+
group="plannerstr+'.'+keystr",
|
|
104
|
+
linetype="key",
|
|
105
|
+
),
|
|
106
|
+
)
|
|
107
|
+
+ p9.xlab(attr)
|
|
108
|
+
+ p9.ylab("cumulative probability")
|
|
109
|
+
+ p9.stat_ecdf()
|
|
110
|
+
+ p9.scale_linetype_discrete(
|
|
111
|
+
name=" ",
|
|
112
|
+
labels=["before simplification", "after simplification"],
|
|
113
|
+
)
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def boxplot(
|
|
118
|
+
df: pl.DataFrame, attr: str, grouping: str, outlier_shape: str, ylogscale: bool
|
|
119
|
+
) -> p9.ggplot:
|
|
120
|
+
"""Create a box plot for the specified attribute for each selected planner."""
|
|
121
|
+
|
|
122
|
+
if grouping:
|
|
123
|
+
plot = p9.ggplot(
|
|
124
|
+
df, p9.aes(x="planner", y=attr, fill=grouping)
|
|
125
|
+
) + p9.geom_boxplot(
|
|
126
|
+
position=p9.position_dodge2(width=0.8), outlier_shape=outlier_shape
|
|
127
|
+
)
|
|
128
|
+
else:
|
|
129
|
+
plot = p9.ggplot(df, p9.aes(x="planner", y=attr)) + p9.geom_boxplot(
|
|
130
|
+
color="#3073ba", fill="#99c9eb", outlier_shape=outlier_shape
|
|
131
|
+
)
|
|
132
|
+
if ylogscale:
|
|
133
|
+
return plot + p9.scale_y_log10()
|
|
134
|
+
else:
|
|
135
|
+
return plot
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def boxplot_with_simplified(
|
|
139
|
+
df: pl.DataFrame, attr: str, outlier_shape: str, ylogscale: bool
|
|
140
|
+
) -> p9.ggplot:
|
|
141
|
+
"""Create a box plot for the specified attribute and the value of the attribute after path
|
|
142
|
+
simplification for each selected planner."""
|
|
143
|
+
plot = (
|
|
144
|
+
p9.ggplot(df, p9.aes(x="planner", y="value", color="key", fill="key"))
|
|
145
|
+
+ p9.ylab(attr)
|
|
146
|
+
+ p9.geom_boxplot(outlier_shape=outlier_shape)
|
|
147
|
+
+ p9.scale_fill_manual(
|
|
148
|
+
["#99c9eb", "#ebc999"],
|
|
149
|
+
name=" ",
|
|
150
|
+
labels=["before simplification", "after simplification"],
|
|
151
|
+
)
|
|
152
|
+
+ p9.scale_color_manual(
|
|
153
|
+
["#3073ba", "#ba7330"],
|
|
154
|
+
name=" ",
|
|
155
|
+
labels=["before simplification", "after simplification"],
|
|
156
|
+
na_value="#7F7F7F",
|
|
157
|
+
)
|
|
158
|
+
)
|
|
159
|
+
if ylogscale:
|
|
160
|
+
return plot + p9.scale_y_log10()
|
|
161
|
+
else:
|
|
162
|
+
return plot
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
@module.server
|
|
166
|
+
def performance_server(
|
|
167
|
+
input: Inputs, output: Outputs, session: Session, raw_data: reactive.Value
|
|
168
|
+
):
|
|
169
|
+
@reactive.calc
|
|
170
|
+
def exp_data() -> pl.DataFrame:
|
|
171
|
+
"""Return data for the selected experiment"""
|
|
172
|
+
req(not raw_data()["runs"].is_empty())
|
|
173
|
+
return raw_data()["runs"].filter(pl.col("experiment") == input.problem())
|
|
174
|
+
|
|
175
|
+
@reactive.calc
|
|
176
|
+
def data() -> DataTuple:
|
|
177
|
+
"""Return data for the selected OMPL version, the selected planners, and selected experiment
|
|
178
|
+
parameters (if present)"""
|
|
179
|
+
param_values = problem_parameter_values(raw_data()["parameters"], input)
|
|
180
|
+
grouping = problem_parameter_groups(param_values)
|
|
181
|
+
df = problem_parameter_filter(
|
|
182
|
+
exp_data().filter(
|
|
183
|
+
(pl.col("version") == input.version())
|
|
184
|
+
& (pl.col("planner").is_in(input.planners()))
|
|
185
|
+
),
|
|
186
|
+
param_values,
|
|
187
|
+
)
|
|
188
|
+
if grouping:
|
|
189
|
+
# hacky way to create enum type from numerically sorted experiment parameters
|
|
190
|
+
grouping_enum = pl.Enum(
|
|
191
|
+
[str(v) for v in sorted(df[grouping].unique().to_list())]
|
|
192
|
+
)
|
|
193
|
+
df = df.with_columns(pl.col(grouping).cast(pl.String).cast(grouping_enum))
|
|
194
|
+
return DataTuple(df, grouping)
|
|
195
|
+
|
|
196
|
+
@output
|
|
197
|
+
@render.ui
|
|
198
|
+
def problem_ui() -> ui.Tag:
|
|
199
|
+
req(raw_data()["problem_names"])
|
|
200
|
+
return problem_widget(raw_data()["problem_names"])
|
|
201
|
+
|
|
202
|
+
@output
|
|
203
|
+
@render.ui
|
|
204
|
+
def problem_parameter_ui() -> ui.Tag | None:
|
|
205
|
+
req(not raw_data()["experiments"].is_empty())
|
|
206
|
+
return problem_parameter_widgets(
|
|
207
|
+
raw_data()["experiments"].filter(
|
|
208
|
+
(pl.col("experiment") == input.problem())
|
|
209
|
+
& (pl.col("version") == input.version())
|
|
210
|
+
),
|
|
211
|
+
raw_data()["parameters"],
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
@output
|
|
215
|
+
@render.ui
|
|
216
|
+
def attribute_ui() -> ui.Tag:
|
|
217
|
+
req(raw_data()["attributes"])
|
|
218
|
+
return attribute_widget(raw_data()["attributes"])
|
|
219
|
+
|
|
220
|
+
@output
|
|
221
|
+
@render.ui
|
|
222
|
+
def version_ui() -> ui.Tag | None:
|
|
223
|
+
return version_widget(exp_data()["version"].unique().to_list())
|
|
224
|
+
|
|
225
|
+
@output
|
|
226
|
+
@render.ui
|
|
227
|
+
def planner_ui() -> ui.Tag:
|
|
228
|
+
return planner_widget(exp_data()["planner"].unique().to_list())
|
|
229
|
+
|
|
230
|
+
@reactive.calc
|
|
231
|
+
def plot_object() -> p9.ggplot:
|
|
232
|
+
attr = input.attribute()
|
|
233
|
+
# use bar charts for enum types
|
|
234
|
+
enums = raw_data()["enums"].filter(pl.col("name") == attr)
|
|
235
|
+
grouping = data().grouping
|
|
236
|
+
if len(enums) > 0:
|
|
237
|
+
return enums_plot(data().df, enums, grouping)
|
|
238
|
+
|
|
239
|
+
outlier_shape = "" if input.hide_outliers() else "o"
|
|
240
|
+
simplified_attr = "simplified " + attr
|
|
241
|
+
include_simplified_attr = (
|
|
242
|
+
input.show_simplified() and simplified_attr in raw_data()["attributes"]
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
if include_simplified_attr:
|
|
246
|
+
df = (
|
|
247
|
+
data()
|
|
248
|
+
.df.unpivot(
|
|
249
|
+
index=["planner"],
|
|
250
|
+
on=[attr, simplified_attr],
|
|
251
|
+
variable_name="key",
|
|
252
|
+
value_name="value",
|
|
253
|
+
)
|
|
254
|
+
.with_columns(pl.col("key").cast(pl.Categorical))
|
|
255
|
+
)
|
|
256
|
+
if input.show_as_cdf():
|
|
257
|
+
return ecdf_plot_with_simplified(df, attr)
|
|
258
|
+
return boxplot_with_simplified(df, attr, outlier_shape, input.y_log_scale())
|
|
259
|
+
else:
|
|
260
|
+
df = data().df
|
|
261
|
+
if input.show_as_cdf():
|
|
262
|
+
return ecdf_plot(df, attr, grouping)
|
|
263
|
+
|
|
264
|
+
return boxplot(df, attr, grouping, outlier_shape, input.y_log_scale())
|
|
265
|
+
|
|
266
|
+
@output
|
|
267
|
+
@render.plot
|
|
268
|
+
def plot():
|
|
269
|
+
return plot_object()
|
|
270
|
+
|
|
271
|
+
@render.download(filename="performance_plot.pdf")
|
|
272
|
+
def download_pdf():
|
|
273
|
+
buffer = io.BytesIO()
|
|
274
|
+
plot_object().save(buffer, format="pdf")
|
|
275
|
+
yield buffer.getvalue()
|
|
276
|
+
|
|
277
|
+
@render.download(filename="performance_plot.pkl")
|
|
278
|
+
def download_pkl():
|
|
279
|
+
buffer = io.BytesIO()
|
|
280
|
+
pickle.dump(plot_object(), buffer)
|
|
281
|
+
yield buffer.getvalue()
|
|
282
|
+
|
|
283
|
+
@output
|
|
284
|
+
@render.data_frame
|
|
285
|
+
def missing_data_table():
|
|
286
|
+
req(input.attribute)
|
|
287
|
+
grouping = data().grouping
|
|
288
|
+
if grouping:
|
|
289
|
+
grouping = ["planner", grouping]
|
|
290
|
+
else:
|
|
291
|
+
grouping = ["planner"]
|
|
292
|
+
return (
|
|
293
|
+
data()
|
|
294
|
+
.df.with_columns(pl.col(input.attribute()).is_null().alias("missing"))
|
|
295
|
+
.group_by(grouping)
|
|
296
|
+
.agg(pl.col("missing").sum(), pl.len().alias("total"))
|
|
297
|
+
.sort(grouping)
|
|
298
|
+
)
|