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.
@@ -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(" &bull; "),
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(" &bull; "),
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
+ )