API reference¶
The public API is everything importable from planviz. Every figure function
takes dark: bool = False, takes an optional ax=, returns what it drew, and
saves nothing.
Style¶
planviz.style ¶
Applying the Frontier matplotlib style, in light and dark.
The stylesheet ships inside the wheel (planviz/styles/frontier.mplstyle), so
nothing here touches the network and no consumer has to locate the branding
repository at runtime.
Importing :mod:planviz changes no global state. The style is applied only when
you ask for it — either permanently with :func:use_style, or for the duration
of a block with :func:style_context. Every figure function in the library
draws inside a :func:style_context and additionally stamps the resolved
colours onto the artists it creates, so a returned figure keeps its appearance
after the context has closed.
rc_params ¶
rc_params(dark: bool = False) -> dict[str, Any]
Return the rcParams that turn the light stylesheet into dark.
The vendored stylesheet is the light scheme — light-first is the brand's position, because papers, READMEs and notebooks all default to a light ground. Dark mode is that stylesheet plus this overlay.
>>> import planviz
>>> planviz.style.rc_params(dark=True)["axes.facecolor"]
'#1a2126'
>>> planviz.style.rc_params(dark=False)
{}
Source code in planviz/style.py
32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 | |
use_style ¶
use_style(dark: bool = False) -> Tokens
Apply the Frontier style to matplotlib's global rcParams; return tokens.
Use this once at the top of a notebook or a script, so figures you draw by
hand match the ones this library draws. It is a deliberate, explicit call:
importing planviz on its own leaves matplotlib untouched.
>>> import matplotlib
>>> matplotlib.use("Agg")
>>> import matplotlib.pyplot as plt, planviz
>>> t = planviz.use_style(dark=True)
>>> t.mode
'dark'
>>> plt.rcParams["axes.facecolor"]
'#1a2126'
>>> plt.rcdefaults()
Source code in planviz/style.py
66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 | |
style_context ¶
style_context(dark: bool = False) -> Iterator[Tokens]
Apply the Frontier style for the duration of a block; yield its tokens.
rcParams are restored on exit, which is what keeps a library call from reaching into a caller's notebook.
>>> import matplotlib
>>> matplotlib.use("Agg")
>>> import matplotlib.pyplot as plt, planviz
>>> before = plt.rcParams["axes.facecolor"]
>>> with planviz.style_context() as t:
... figure_facecolor = plt.rcParams["figure.facecolor"]
>>> t.path
'#c2472c'
>>> plt.rcParams["axes.facecolor"] == before
True
Source code in planviz/style.py
90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 | |
Tokens¶
planviz.tokens ¶
Frontier design tokens — the palette every planviz figure draws in.
Generated by scripts/sync_tokens.py from the openplan-labs/branding
repository (tokens/tokens.json). Do not edit by hand: CI regenerates
this file and fails the build if it has drifted from the brand.
The three semantic values are the figure legend, not a chart palette. path
is the solution and the only warm colour in the system; frontier is the open
list; expanded is the closed list. A fourth or fifth series in a chart takes
:data:AGENT_RAMP, never one of those three.
>>> from planviz import tokens
>>> tokens.LIGHT.path
'#c2472c'
>>> tokens.get(dark=True).path
'#e87a5c'
>>> tokens.LIGHT.agent(2)
'#7a6f9c'
Tokens
dataclass
¶
One resolved colour scheme — light or dark.
Every field is a CSS/matplotlib colour string. Prefer the semantic names over literals: a figure drawn in these values is on-brand by construction.
>>> from planviz import tokens
>>> t = tokens.get(dark=False)
>>> t.mode, t.path
('light', '#c2472c')
Source code in planviz/tokens.py
76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 | |
agent ¶
agent(index: int) -> str
Return the ramp colour for agent index, wrapping past eight.
from planviz import tokens tokens.LIGHT.agent(0) == tokens.AGENT_RAMP[0] True
Source code in planviz/tokens.py
116 117 118 119 120 121 122 123 | |
marker ¶
marker(index: int) -> str
Return the marker for agent index — the shape channel.
Changes only once the hue has wrapped, so agent 8 shares agent 0's colour but not its mark.
>>> from planviz import tokens
>>> tokens.LIGHT.marker(0), tokens.LIGHT.marker(8)
('o', 's')
Source code in planviz/tokens.py
125 126 127 128 129 130 131 132 133 134 135 | |
agent_colors ¶
agent_colors(names: Sequence[str]) -> dict
Map names to ramp colours by position — stable across re-renders.
from planviz import tokens tokens.LIGHT.agent_colors(["a", "b"])["a"] == tokens.AGENT_RAMP[0] True
Source code in planviz/tokens.py
137 138 139 140 141 142 143 144 | |
sequential ¶
sequential(name: str = 'frontier_seq')
Return the single-hue magnitude colormap: expanded → path.
Warm end is expensive, which agrees with the rest of the system. Never a rainbow.
>>> from planviz import tokens
>>> tokens.LIGHT.sequential().N
256
Source code in planviz/tokens.py
146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 | |
get ¶
get(dark: bool = False) -> Tokens
Return :data:DARK when dark is true, else :data:LIGHT.
from planviz import tokens tokens.get(dark=True).mode 'dark'
Source code in planviz/tokens.py
209 210 211 212 213 214 215 216 | |
Grids and agents¶
planviz.grids ¶
Grid maps, multi-agent routes, and the canonical search figure.
Three pictures, in the order a reader meets them:
- :func:
draw_grid— the problem. Obstacles are structure, so they takeline, the same value as a table rule. - :func:
draw_search— what the search did. The three-mark legend (filled dot, hollow ring, connected stroke) is the brand's canonical figure and the one most worth copying. - :func:
draw_paths— what it returned, for many agents at once. - :func:
draw_heatmap— a magnitude per cell: congestion, visit counts, cost.
Grids are indexed [row][col] with row 0 at the top, and cells are drawn at
integer coordinates, so (0, 0) is the top-left cell centre. Nothing here
knows about pymapf, cuplan or jupyddl types: a grid is any 2-D
array-like where truthy means blocked, and a path is any sequence of
(row, col) pairs.
draw_heatmap ¶
draw_heatmap(values: Any, grid: Any = None, *, ax: Axes | None = None, dark: bool = False, label: str | None = None, colorbar: bool = True, vmax: float | None = None, title: str | None = None, figsize: tuple[float, float] | None = None) -> Axes
Shade each cell of a grid by a magnitude — congestion, visits, cost.
The ramp is the single-hue expanded → path sequential map: warm is
expensive, which is the same claim the accent makes everywhere else in the
system. Never a rainbow, and never a diverging map for a quantity that has
no meaningful midpoint.
Blocked cells are drawn as obstacles, not as zero. A cell no agent could enter and a cell no agent chose to enter are different facts.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
values
|
Any
|
2-D array-like of magnitudes, same shape as |
required |
grid
|
Any
|
occupancy grid; blocked cells are masked out of the ramp. |
None
|
ax
|
Axes | None
|
draw into this axes instead of creating one. |
None
|
dark
|
bool
|
use the dark scheme. |
False
|
label
|
str | None
|
colourbar label — say the unit. |
None
|
colorbar
|
bool
|
draw the colourbar. |
True
|
vmax
|
float | None
|
top of the ramp; defaults to the largest value present. |
None
|
title
|
str | None
|
axes title. |
None
|
figsize
|
tuple[float, float] | None
|
overrides the size derived from the grid's aspect. |
None
|
Returns:
| Name | Type | Description |
|---|---|---|
The |
Axes
|
class: |
Example
import matplotlib; matplotlib.use("Agg") import planviz ax = planviz.draw_heatmap( ... [[0, 2, 0], [1, 5, 1], [0, 3, 0]], ... grid=[[0, 0, 0], [0, 0, 0], [1, 0, 0]], ... label="agent-timesteps", ... ) len(ax.images) 1
Source code in planviz/grids.py
142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 | |
draw_grid ¶
draw_grid(grid: Any = None, *, shape: tuple[int, int] | None = None, ax: Axes | None = None, dark: bool = False, title: str | None = None, lattice: bool = False, figsize: tuple[float, float] | None = None) -> Axes
Draw an occupancy grid: blocked cells filled, nothing else.
No lattice is drawn by default. The cells are the grid, and a lattice
under an occupancy map doubles the line count for no information — see
brand/figures.md. Pass lattice=True for a small teaching figure
where the cell boundaries are the point.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
grid
|
Any
|
2-D array-like indexed |
None
|
shape
|
tuple[int, int] | None
|
|
None
|
ax
|
Axes | None
|
draw into this axes instead of creating one. |
None
|
dark
|
bool
|
use the dark scheme. |
False
|
title
|
str | None
|
axes title. |
None
|
lattice
|
bool
|
draw hairline cell boundaries. |
False
|
figsize
|
tuple[float, float] | None
|
overrides the size derived from the grid's aspect. |
None
|
Returns:
| Name | Type | Description |
|---|---|---|
The |
Axes
|
class: |
Example
import matplotlib; matplotlib.use("Agg") import planviz ax = planviz.draw_grid([[0, 0, 1], [0, 1, 0], [0, 0, 0]]) tuple(float(v) for v in ax.get_xlim()) (-0.5, 2.5)
Source code in planviz/grids.py
222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 | |
draw_paths ¶
draw_paths(paths: Any, grid: Any = None, *, shape: tuple[int, int] | None = None, ax: Axes | None = None, dark: bool = False, highlight: str | None = None, labels: bool = True, offsets: bool = True, endpoints: bool = True, title: str | None = None, figsize: tuple[float, float] | None = None) -> Axes
Draw multi-agent routes over a grid — one stroke per agent.
Colour comes from the agent ramp by stable index, so re-rendering with
a different agent order does not reshuffle the figure. Three rules from
brand/figures.md are enforced here:
- past eight agents, individual hues stop being readable, so the figure switches to one colour at reduced opacity;
- when
highlightnames an agent, that agent takespath— the solution accent — and every other agent drops tofaint; - each route starts on a hollow ring and ends on a filled disc, so direction is carried without arrowheads.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
paths
|
Any
|
|
required |
grid
|
Any
|
optional occupancy grid to draw underneath. |
None
|
shape
|
tuple[int, int] | None
|
|
None
|
ax
|
Axes | None
|
draw into this axes instead of creating one. |
None
|
dark
|
bool
|
use the dark scheme. |
False
|
highlight
|
str | None
|
name of the one agent this figure is about. |
None
|
labels
|
bool
|
annotate each route with its agent name. |
True
|
offsets
|
bool
|
draw routes on slightly offset rails so a shared corridor still shows how many agents are in it. |
True
|
endpoints
|
bool
|
draw the start ring and goal disc. |
True
|
title
|
str | None
|
axes title. |
None
|
figsize
|
tuple[float, float] | None
|
overrides the size derived from the grid's aspect. |
None
|
Returns:
| Name | Type | Description |
|---|---|---|
The |
Axes
|
class: |
Example
import matplotlib; matplotlib.use("Agg") import planviz ax = planviz.draw_paths( ... {"a": [(0, 0), (0, 1), (1, 1)], "b": [(2, 0), (1, 0), (1, 1)]}, ... shape=(3, 3), ... ) len(ax.lines) > 0 True
Source code in planviz/grids.py
274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 | |
draw_search ¶
draw_search(expanded: Iterable | None = None, frontier: Iterable | None = None, path: Sequence | None = None, *, grid: Any = None, shape: tuple[int, int] | None = None, start: tuple[int, int] | None = None, goal: tuple[int, int] | None = None, unvisited: bool = True, ax: Axes | None = None, dark: bool = False, legend: bool = True, title: str | None = None, figsize: tuple[float, float] | None = None) -> Axes
Draw the three sets of a search: closed list, open list, and solution.
This is the organisation's canonical figure. The three sets differ by shape as well as hue — small filled dot, hollow ring, connected stroke — which is the channel that survives greyscale printing and red/green colour blindness. The path is drawn last and is the only continuous element.
One idea per figure: this draws the search and its result at full
strength, which is right for a legend plate and for the final frame of an
animation. For a figure about expansion order alone, pass path=None.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
expanded
|
Iterable | None
|
cells in the closed list, as |
None
|
frontier
|
Iterable | None
|
cells in the open list. |
None
|
path
|
Sequence | None
|
the returned solution, in order. |
None
|
grid
|
Any
|
optional occupancy grid to draw underneath. |
None
|
shape
|
tuple[int, int] | None
|
|
None
|
start
|
tuple[int, int] | None
|
start cell — a hollow ring. Defaults to the path's first cell. |
None
|
goal
|
tuple[int, int] | None
|
goal cell — a filled disc. Defaults to the path's last cell. |
None
|
unvisited
|
bool
|
stipple the never-touched free cells in |
True
|
ax
|
Axes | None
|
draw into this axes instead of creating one. |
None
|
dark
|
bool
|
use the dark scheme. |
False
|
legend
|
bool
|
draw the three-mark legend. Worth keeping the first time a reader meets the figure; drop it once they know the mapping. |
True
|
title
|
str | None
|
axes title. |
None
|
figsize
|
tuple[float, float] | None
|
overrides the size derived from the grid's aspect. |
None
|
Returns:
| Name | Type | Description |
|---|---|---|
The |
Axes
|
class: |
Example
import matplotlib; matplotlib.use("Agg") import planviz ax = planviz.draw_search( ... expanded=[(0, 0), (0, 1), (1, 0)], ... frontier=[(1, 1), (2, 0)], ... path=[(0, 0), (1, 0), (2, 0)], ... shape=(3, 3), ... ) ax.get_title() ''
Source code in planviz/grids.py
401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 | |
Animations¶
planviz.animate ¶
Animations: agents executing a plan, and the search that produced it.
Both build a :class:~matplotlib.animation.FuncAnimation and neither writes a
file — use :func:save_animation for that, or :func:to_jshtml to embed one
in a notebook.
:func:animate_search follows the brand's one rule for search animations: the
frontier moves, the expanded set accumulates, and the path appears once at
the end and stays. The accumulated closed list is the cost of the search, so
fading it out hides the thing the figure is arguing about.
GIFs are capped at 12 fps and 800 px wide by :func:save_animation, because
they are read in a GitHub README on a train.
animate_paths ¶
animate_paths(paths: Any, grid: Any = None, *, shape: tuple[int, int] | None = None, dark: bool = False, substeps: int = SUBSTEPS, trail: int = 8, hold: int = 12, title: str | None = None, figsize: tuple[float, float] | None = None, interval: int = 1000 // GIF_FPS) -> FuncAnimation
Animate agents executing a multi-agent plan over a grid.
Each agent glides between cells with an eased step, drags a short trail,
and has a static goal ring drawn once. Colours come from the agent ramp by
stable index, exactly as in :func:planviz.draw_paths.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
paths
|
Any
|
|
required |
grid
|
Any
|
optional occupancy grid to draw underneath. |
None
|
shape
|
tuple[int, int] | None
|
|
None
|
dark
|
bool
|
use the dark scheme. |
False
|
substeps
|
int
|
interpolation frames per timestep; higher is smoother. |
SUBSTEPS
|
trail
|
int
|
how many timesteps of history stay visible behind each agent. |
8
|
hold
|
int
|
extra frames at the end so the final state is readable before a looping GIF restarts. |
12
|
title
|
str | None
|
axes title. |
None
|
figsize
|
tuple[float, float] | None
|
overrides the size derived from the grid's aspect. |
None
|
interval
|
int
|
milliseconds between frames in an interactive backend. |
1000 // GIF_FPS
|
Returns:
| Name | Type | Description |
|---|---|---|
A |
FuncAnimation
|
class: |
FuncAnimation
|
or it is garbage-collected before it renders. |
Example
import matplotlib; matplotlib.use("Agg") import planviz anim = planviz.animate_paths( ... {"a": [(0, 0), (0, 1), (0, 2)], "b": [(2, 2), (1, 2), (0, 2)]}, ... shape=(3, 3), ... ) sum(1 for _ in anim.new_frame_seq()) > 0 True
Source code in planviz/animate.py
65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 | |
animate_search ¶
animate_search(expansions: Sequence, *, frontiers: Sequence[Sequence] | None = None, path: Sequence | None = None, grid: Any = None, shape: tuple[int, int] | None = None, start: tuple[int, int] | None = None, goal: tuple[int, int] | None = None, frames: int = 90, hold: int = 12, dark: bool = False, title: str | None = None, figsize: tuple[float, float] | None = None, interval: int = 1000 // GIF_FPS) -> FuncAnimation
Animate a search: an accumulating closed list and a moving frontier.
The expanded set never fades — it is the cost of the search. The path is drawn once, in the last frames, and stays.
expansions is resampled to at most frames frames, so a 20-node
search and a 200,000-node search produce clips of the same length.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
expansions
|
Sequence
|
cells in the order they left the open list. |
required |
frontiers
|
Sequence[Sequence] | None
|
optional open-list snapshot after each expansion, aligned
with |
None
|
path
|
Sequence | None
|
the solution, revealed in the final frames. |
None
|
grid
|
Any
|
optional occupancy grid to draw underneath. |
None
|
shape
|
tuple[int, int] | None
|
|
None
|
start
|
tuple[int, int] | None
|
start cell — a hollow ring, drawn from the first frame. |
None
|
goal
|
tuple[int, int] | None
|
goal cell — a filled disc, drawn from the first frame. |
None
|
frames
|
int
|
maximum number of search frames before the hold. |
90
|
hold
|
int
|
frames the solved state is held for. |
12
|
dark
|
bool
|
use the dark scheme. |
False
|
title
|
str | None
|
axes title. |
None
|
figsize
|
tuple[float, float] | None
|
overrides the size derived from the grid's aspect. |
None
|
interval
|
int
|
milliseconds between frames in an interactive backend. |
1000 // GIF_FPS
|
Returns:
| Name | Type | Description |
|---|---|---|
A |
FuncAnimation
|
class: |
Example
import matplotlib; matplotlib.use("Agg") import planviz anim = planviz.animate_search( ... [(0, 0), (0, 1), (1, 1)], path=[(0, 0), (0, 1), (1, 1)], ... shape=(3, 3), frames=4, ... ) sum(1 for _ in anim.new_frame_seq()) > 0 True
Source code in planviz/animate.py
191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 | |
save_animation ¶
save_animation(animation: FuncAnimation, path: str | Path, *, fps: int = GIF_FPS, width_px: int = GIF_WIDTH_PX, dpi: int | None = None, bitrate: int = 3200) -> Path
Write an animation to .gif (pillow) or .mp4 (ffmpeg).
For a GIF the defaults are the brand's README cap — 12 fps, 800 px wide —
and dpi is derived from width_px so the cap holds whatever figure
size produced the animation. Pass dpi to override.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
animation
|
FuncAnimation
|
a :class: |
required |
path
|
str | Path
|
destination; the suffix picks the writer. |
required |
fps
|
int
|
frames per second. |
GIF_FPS
|
width_px
|
int
|
target pixel width, used to derive |
GIF_WIDTH_PX
|
dpi
|
int | None
|
explicit dots per inch, overriding |
None
|
bitrate
|
int
|
MP4 bitrate. |
3200
|
Returns:
| Name | Type | Description |
|---|---|---|
The |
Path
|
class: |
Example
import matplotlib; matplotlib.use("Agg") import planviz, os, tempfile anim = planviz.animate_search([(0, 0), (1, 1)], shape=(2, 2), frames=2) out = planviz.save_animation( ... anim, os.path.join(tempfile.mkdtemp(), "search.gif") ... ) out.suffix '.gif'
Source code in planviz/animate.py
327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 | |
to_jshtml ¶
to_jshtml(animation: FuncAnimation) -> str
Return an HTML/JS player for the animation — what a notebook shows.
Example
import matplotlib; matplotlib.use("Agg") import planviz anim = planviz.animate_search([(0, 0), (1, 1)], shape=(2, 2), frames=2) planviz.to_jshtml(anim).lstrip().startswith("<") True
Source code in planviz/animate.py
394 395 396 397 398 399 400 401 402 403 404 | |
Search progress¶
planviz.search ¶
Search progress, the radial wavefront, and plan timelines.
These are the pictures a single-agent planner needs — jupyddl's
--plot, --tree and --plan-plot in library form:
- :func:
search_progress/ :func:search_panels— howf,g,h, the open-list size and the node counters moved as the search ran. - :func:
radial_wavefront— the search tree in polar coordinates: radius is depth, so a breadth-first flood is a disc and a greedy dive is a spoke. - :func:
plan_timeline— the returned plan as a Gantt, with waiting drawn as its own mark rather than as an absence.
None of them import a planner. A trace is a mapping of named series; a plan is
a sequence of steps; a tree is a flat sequence of (node, parent, depth)
records where a parent may be a node that was never itself expanded.
Step
dataclass
¶
One segment of a plan timeline.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
label
|
str
|
what to write on (or beside) the bar. |
required |
start
|
float
|
when the step begins, in whatever unit the axis is in. |
0.0
|
duration
|
float
|
how long it lasts. Unit-duration steps give a staircase. |
1.0
|
row
|
str | int | None
|
the lane this step belongs to — an agent name, an object, or
|
None
|
kind
|
str
|
|
'action'
|
Example
from planviz import Step Step("move(a, b)", 0.0, 2.5, row="robot1").kind 'action'
Source code in planviz/search.py
463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 | |
search_progress ¶
search_progress(series: Mapping[str, Sequence[float]], *, x: Sequence[float] | None = None, ax: Axes | None = None, dark: bool = False, highlight: str | None = None, marks: Sequence[float] | None = None, fill: bool = False, log_y: bool = False, end_labels: bool = True, legend: bool = False, xlabel: str = 'nodes expanded', ylabel: str | None = None, title: str | None = None, figsize: tuple[float, float] | None = None) -> Axes
Plot one line per named series against search progress.
Use it for f/g/h over expansions, for the open-list size, for
the cost of each expanded node, or for one line per planner. Colours come
from the agent ramp; highlight promotes one series to path, which
is how the brand says a comparison should make its argument — one loud
line, the rest supporting.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
series
|
Mapping[str, Sequence[float]]
|
|
required |
x
|
Sequence[float] | None
|
shared x values; defaults to |
None
|
ax
|
Axes | None
|
draw into this axes instead of creating one. |
None
|
dark
|
bool
|
use the dark scheme. |
False
|
highlight
|
str | None
|
the series being argued for; it takes the |
None
|
marks
|
Sequence[float] | None
|
x positions to draw as dashed vertical rules — an IDA* bound restart, a replanning event, a timeout. |
None
|
fill
|
bool
|
shade under each line. Sensible for a single frontier-size series, noisy for several. |
False
|
log_y
|
bool
|
log-scale the y axis. It is labelled, because a silent log axis is a way to be misleading by accident. |
False
|
end_labels
|
bool
|
annotate each line at its end instead of in a legend. |
True
|
legend
|
bool
|
draw a legend box as well. |
False
|
xlabel
|
str
|
x axis label. |
'nodes expanded'
|
ylabel
|
str | None
|
y axis label. |
None
|
title
|
str | None
|
axes title. |
None
|
figsize
|
tuple[float, float] | None
|
figure size when creating the axes. |
None
|
Returns:
| Name | Type | Description |
|---|---|---|
The |
Axes
|
class: |
Example
import matplotlib; matplotlib.use("Agg") import planviz ax = planviz.search_progress( ... {"f": [4, 4, 5, 5, 6], "g": [0, 1, 2, 3, 4], "h": [4, 3, 3, 2, 2]}, ... ylabel="cost", ... ) len(ax.lines) 3
Source code in planviz/search.py
86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 | |
search_panels ¶
search_panels(panels: Mapping[str, Mapping[str, Sequence[float]]], *, x: Sequence[float] | None = None, dark: bool = False, ncols: int = 2, highlight: str | None = None, marks: Sequence[float] | None = None, fill: Sequence[str] = (), log_y: Sequence[str] = (), xlabel: str = 'nodes expanded', ylabels: Mapping[str, str] | None = None, suptitle: str | None = None, figsize: tuple[float, float] | None = None) -> Figure
Lay several :func:search_progress panels on one figure.
One idea per panel, one figure per search. The panels share a colour assignment, so a series named the same way in two panels is the same colour in both.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
panels
|
Mapping[str, Mapping[str, Sequence[float]]]
|
|
required |
x
|
Sequence[float] | None
|
shared x values for every panel. |
None
|
dark
|
bool
|
use the dark scheme. |
False
|
ncols
|
int
|
panels per row. |
2
|
highlight
|
str | None
|
series promoted to the |
None
|
marks
|
Sequence[float] | None
|
x positions drawn as dashed rules in every panel. |
None
|
fill
|
Sequence[str]
|
titles of the panels to shade under. |
()
|
log_y
|
Sequence[str]
|
titles of the panels to log-scale. |
()
|
xlabel
|
str
|
x axis label, applied to the bottom row. |
'nodes expanded'
|
ylabels
|
Mapping[str, str] | None
|
|
None
|
suptitle
|
str | None
|
figure title. |
None
|
figsize
|
tuple[float, float] | None
|
figure size; derived from the panel count when omitted. |
None
|
Returns:
| Name | Type | Description |
|---|---|---|
The |
Figure
|
class: |
Example
import matplotlib; matplotlib.use("Agg") import planviz fig = planviz.search_panels({ ... "f, g and h": {"f": [4, 5, 6], "g": [0, 1, 2], "h": [4, 4, 4]}, ... "open list": {"|open|": [1, 4, 7]}, ... }) len(fig.axes) 2
Source code in planviz/search.py
165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 | |
radial_wavefront ¶
radial_wavefront(nodes: Iterable, *, frontier: Iterable[int] | None = None, goal: int | None = None, max_nodes: int = 4000, edges: bool = True, ax: Axes | None = None, dark: bool = False, value_label: str = 'heuristic', title: str | None = None, figsize: tuple[float, float] | None = None) -> Axes
Draw the search tree in polar coordinates — depth is radius.
Nodes at the same depth are spread evenly around their ring, so the shape of the figure is the shape of the search: a uniform-cost flood fills a disc, a greedy best-first dive is a single spoke, and an A* with a good heuristic is a wedge aimed at the goal.
Colour along the expanded → path ramp encodes value (the
heuristic by default), warm meaning expensive. Nodes still on the open
list take the frontier's hollow ring, so the legend still reads.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
nodes
|
Iterable
|
flat records of expanded nodes, in expansion order. Each is a
|
required |
frontier
|
Iterable[int] | None
|
ids of nodes still on the open list. |
None
|
goal
|
int | None
|
id of the goal node, marked with a filled disc. |
None
|
max_nodes
|
int
|
cap; beyond a few thousand rings the ink stops resolving. |
4000
|
edges
|
bool
|
draw parent→child edges. Turn them off for a dense search. |
True
|
ax
|
Axes | None
|
a polar axes to draw into; one is created when omitted. |
None
|
dark
|
bool
|
use the dark scheme. |
False
|
value_label
|
str
|
colourbar label. |
'heuristic'
|
title
|
str | None
|
axes title. |
None
|
figsize
|
tuple[float, float] | None
|
figure size when creating the axes. |
None
|
Returns:
| Type | Description |
|---|---|
Axes
|
The polar :class: |
Example
import matplotlib; matplotlib.use("Agg") import planviz ax = planviz.radial_wavefront( ... [(0, -1, 0, 3.0), (1, 0, 1, 2.0), (2, 0, 1, 2.5), (3, 1, 2, 1.0)], ... goal=3, ... ) ax.name 'polar'
Source code in planviz/search.py
309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 | |
plan_timeline ¶
plan_timeline(plan: Iterable, *, ax: Axes | None = None, dark: bool = False, highlight: str | int | None = None, max_steps: int = 60, annotate: bool = True, xlabel: str = 'time', title: str | None = None, figsize: tuple[float, float] | None = None) -> Axes
Draw a plan as a timeline — one bar per step, one lane per actor.
Two shapes fall out of the same function. Give every step its own lane
(the default for a plain list of action names) and you get a sequential
plan read top to bottom. Give steps a row and you get a Gantt: one
lane per agent, per resource, or per object.
Waiting is drawn hatched rather than left blank, because waiting is where coordination cost shows up and an empty gap reads as "nothing happened".
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
plan
|
Iterable
|
:class: |
required |
ax
|
Axes | None
|
draw into this axes instead of creating one. |
None
|
dark
|
bool
|
use the dark scheme. |
False
|
highlight
|
str | int | None
|
the lane this figure is about; it takes the |
None
|
max_steps
|
int
|
truncate longer plans, with a note saying so. |
60
|
annotate
|
bool
|
write each step's label on its bar when it has its own lane. |
True
|
xlabel
|
str
|
x axis label. |
'time'
|
title
|
str | None
|
axes title. |
None
|
figsize
|
tuple[float, float] | None
|
figure size; derived from the lane count when omitted. |
None
|
Returns:
| Name | Type | Description |
|---|---|---|
The |
Axes
|
class: |
Example
import matplotlib; matplotlib.use("Agg") import planviz ax = planviz.plan_timeline(["pick(a)", "move(a, b)", "drop(a)"]) len(ax.patches) 3 gantt = planviz.plan_timeline([ ... planviz.Step("move", 0, 2, row="r1"), ... planviz.Step("wait", 2, 1, row="r1", kind="wait"), ... planviz.Step("move", 0, 3, row="r2"), ... ]) len(gantt.get_yticks()) 2
Source code in planviz/search.py
537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 | |
timeline_from_paths ¶
timeline_from_paths(paths: Any, *, wait_label: str = 'wait') -> list[Step]
Turn multi-agent grid paths into :class:Step objects for a timeline.
Contiguous runs of movement become "action" steps, runs where an agent
stayed in place become "wait" steps, and the tail an agent spends
parked on its goal after arriving becomes an "idle" step. That last
distinction matters: a short path in a long plan is not a gap in the
figure, it is an agent that finished early.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
paths
|
Any
|
|
required |
wait_label
|
str
|
label written on wait segments. |
'wait'
|
Returns:
| Type | Description |
|---|---|
list[Step]
|
A list of :class: |
Example
import planviz steps = planviz.timeline_from_paths( ... {"a": [(0, 0), (0, 1), (0, 1), (0, 2)], "b": [(1, 0), (1, 1)]} ... ) for step in steps: ... if step.row == "a": ... print(step.kind, step.start, step.duration) action 0.0 1.0 wait 1.0 1.0 action 2.0 1.0
Source code in planviz/search.py
684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 | |
Benchmarks¶
planviz.benchmarks ¶
Benchmark charts: scaling, coverage, where the time goes, and crossovers.
A benchmark plot is an argument, so the ink goes into the comparison. Three
rules from brand/figures.md are built in rather than left to the caller:
- One loud line.
highlightgives the series being argued for thepathaccent; everything else takes the agent ramp as a supporting neutral. A chart where every series is loud makes no argument. - Spread, not just the middle. Curves draw the median over seeds with a min–max band, because a median alone hides a bimodal solver.
- Log axes say so. Planner runtimes span four orders of magnitude, and a
silent log axis is a way to be misleading by accident, so
(log)is appended to the label.
Every function takes plain mappings — no benchmark harness type is imported — and returns the axes or figure without saving it.
scaling_curve ¶
scaling_curve(series: Mapping[str, Any], *, x: Sequence[float] | None = None, ax: Axes | None = None, dark: bool = False, highlight: str | None = None, band: bool = True, timeouts: Mapping[str, Sequence[float]] | None = None, cap: float | None = None, log_x: bool = False, log_y: bool = True, x_base: int = 2, marker: str = 'o', legend: bool = True, xlabel: str = 'agents', ylabel: str = 'wall time (s)', title: str | None = None, figsize: tuple[float, float] | None = None) -> Axes
Plot cost against problem size — one line per backend or planner.
Each line is the median over seeds with a min–max band, which is the honest summary of a stochastic benchmark: a median alone hides a solver that is fast four times in five and pathological on the fifth.
Runs that hit the time limit are drawn as their own mark — a hollow triangle at the cap — rather than extrapolated or silently dropped. A missing point and a timeout are different results and the figure should say which one it is.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
series
|
Mapping[str, Any]
|
|
required |
x
|
Sequence[float] | None
|
shared x values, when |
None
|
ax
|
Axes | None
|
draw into this axes instead of creating one. |
None
|
dark
|
bool
|
use the dark scheme. |
False
|
highlight
|
str | None
|
the series being argued for; it takes the |
None
|
band
|
bool
|
draw the min–max band. |
True
|
timeouts
|
Mapping[str, Sequence[float]] | None
|
|
None
|
cap
|
float | None
|
the time limit, where timeout marks are drawn. Defaults to the largest median in the figure. |
None
|
log_x
|
bool
|
log-scale x (base |
False
|
log_y
|
bool
|
log-scale y. |
True
|
x_base
|
int
|
base of the x log scale. |
2
|
marker
|
str
|
marker on each measured point. |
'o'
|
legend
|
bool
|
draw a legend. |
True
|
xlabel
|
str
|
x axis label. |
'agents'
|
ylabel
|
str
|
y axis label. |
'wall time (s)'
|
title
|
str | None
|
axes title. |
None
|
figsize
|
tuple[float, float] | None
|
figure size when creating the axes. |
None
|
Returns:
| Name | Type | Description |
|---|---|---|
The |
Axes
|
class: |
Example
import matplotlib; matplotlib.use("Agg") import planviz ax = planviz.scaling_curve( ... { ... "cpu": {8: [0.4, 0.5], 16: [1.6, 1.9]}, ... "cuda": {8: [0.2, 0.2], 16: [0.3, 0.4]}, ... }, ... highlight="cuda", ... timeouts={"cpu": [32]}, ... cap=10.0, ... ) ax.get_ylabel() 'wall time (s) (log)'
Source code in planviz/benchmarks.py
103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 | |
success_heatmap ¶
success_heatmap(matrix: Any, *, x_labels: Sequence[str] | None = None, y_labels: Sequence[str] | None = None, ax: Axes | None = None, dark: bool = False, annotate: bool = True, vmin: float = 0.0, vmax: float = 1.0, missing: str = '—', percent: bool = True, xlabel: str = 'agents', ylabel: str = 'obstacle density', title: str | None = None, figsize: tuple[float, float] | None = None) -> Axes
Draw coverage over a two-axis sweep — how often the solver succeeded.
The ramp runs path (warm, nothing solved) to expanded (cool, all
solved), which agrees with the rest of the system: warm is expensive.
Cells with nothing to report are drawn as an em dash, not as zero. "No seed reported here" and "every seed failed here" are different claims, and a heatmap that conflates them is wrong in the direction that flatters the solver.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
matrix
|
Any
|
2-D array-like of rates in |
required |
x_labels
|
Sequence[str] | None
|
column tick labels. |
None
|
y_labels
|
Sequence[str] | None
|
row tick labels. |
None
|
ax
|
Axes | None
|
draw into this axes instead of creating one. |
None
|
dark
|
bool
|
use the dark scheme. |
False
|
annotate
|
bool
|
write the rate in each cell. |
True
|
vmin
|
float
|
value mapped to the warm end. |
0.0
|
vmax
|
float
|
value mapped to the cool end. |
1.0
|
missing
|
str
|
text drawn in unmeasured cells. |
'—'
|
percent
|
bool
|
format annotations as percentages. |
True
|
xlabel
|
str
|
x axis label. |
'agents'
|
ylabel
|
str
|
y axis label. |
'obstacle density'
|
title
|
str | None
|
axes title. |
None
|
figsize
|
tuple[float, float] | None
|
figure size when creating the axes. |
None
|
Returns:
| Name | Type | Description |
|---|---|---|
The |
Axes
|
class: |
Example
import matplotlib; matplotlib.use("Agg") import planviz ax = planviz.success_heatmap( ... [[1.0, 1.0, 0.66], [1.0, 0.33, None]], ... x_labels=["8", "16", "32"], y_labels=["5%", "15%"], ... ) ax.get_xlabel() 'agents'
Source code in planviz/benchmarks.py
213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 | |
phase_breakdown ¶
phase_breakdown(phases: Mapping[Any, Mapping[str, float]], *, order: Sequence[str] | None = None, accent: str | None = None, neutral: Iterable[str] = (), ax: Axes | None = None, dark: bool = False, normalize: bool = True, totals: bool = True, total_format: str = '{:.2f}s', legend: bool = True, xlabel: str = 'batch size', ylabel: str | None = None, title: str | None = None, figsize: tuple[float, float] | None = None) -> Axes
Stack where the wall time went, one bar per configuration.
Bands are separated by hatch as well as hue. Four cool blues in a stack read identically in greyscale and to a colour-blind reader — this is the failure cuda-planning hit with its host-to-device and device-to-host bands — so the second channel is not optional here.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
phases
|
Mapping[Any, Mapping[str, float]]
|
|
required |
order
|
Sequence[str] | None
|
phases bottom to top; defaults to first-seen order. |
None
|
accent
|
str | None
|
the phase the figure is about — the useful work. It takes the
|
None
|
neutral
|
Iterable[str]
|
phases drawn in |
()
|
ax
|
Axes | None
|
draw into this axes instead of creating one. |
None
|
dark
|
bool
|
use the dark scheme. |
False
|
normalize
|
bool
|
stack fractions of each column's total rather than seconds. |
True
|
totals
|
bool
|
write each column's absolute total above its bar. Keep this
on with |
True
|
total_format
|
str
|
format string for those totals. |
'{:.2f}s'
|
legend
|
bool
|
draw a legend below the axes. |
True
|
xlabel
|
str
|
x axis label. |
'batch size'
|
ylabel
|
str | None
|
y axis label; derived from |
None
|
title
|
str | None
|
axes title. |
None
|
figsize
|
tuple[float, float] | None
|
figure size when creating the axes. |
None
|
Returns:
| Name | Type | Description |
|---|---|---|
The |
Axes
|
class: |
Example
import matplotlib; matplotlib.use("Agg") import planviz ax = planviz.phase_breakdown( ... { ... "64": {"kernel": 0.4, "h2d": 0.1, "d2h": 0.1, "host": 0.2}, ... "128": {"kernel": 1.1, "h2d": 0.2, "d2h": 0.2, "host": 0.3}, ... }, ... accent="kernel", neutral=["host"], ... ) ax.get_ylabel() 'fraction of wall time'
Source code in planviz/benchmarks.py
329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 | |
throughput_curve ¶
throughput_curve(series: Mapping[str, Any], *, x: Sequence[float] | None = None, ax: Axes | None = None, dark: bool = False, highlight: str | None = None, band: bool = True, log_x: bool = True, log_y: bool = True, x_base: int = 2, legend: bool = True, xlabel: str = 'batch size', ylabel: str = 'items / s', title: str | None = None, note: str | None = 'a rising line means the device is not yet saturated', figsize: tuple[float, float] | None = None) -> Axes
Plot throughput against batch size — the saturation picture.
Same data shape as :func:scaling_curve, different question. Wall time
always rises with the batch; throughput is what says whether the device is
working harder or just working longer, and a line that is still climbing
at the right edge means the sweep stopped before saturation.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
series
|
Mapping[str, Any]
|
|
required |
x
|
Sequence[float] | None
|
shared x values, when |
None
|
ax
|
Axes | None
|
draw into this axes instead of creating one. |
None
|
dark
|
bool
|
use the dark scheme. |
False
|
highlight
|
str | None
|
the series being argued for; it takes the |
None
|
band
|
bool
|
draw the min–max band. |
True
|
log_x
|
bool
|
log-scale x (base |
True
|
log_y
|
bool
|
log-scale y. |
True
|
x_base
|
int
|
base of the x log scale. |
2
|
legend
|
bool
|
draw a legend. |
True
|
xlabel
|
str
|
x axis label. |
'batch size'
|
ylabel
|
str
|
y axis label. |
'items / s'
|
title
|
str | None
|
axes title. |
None
|
note
|
str | None
|
caption under the axes; pass |
'a rising line means the device is not yet saturated'
|
figsize
|
tuple[float, float] | None
|
figure size when creating the axes. |
None
|
Returns:
| Name | Type | Description |
|---|---|---|
The |
Axes
|
class: |
Example
import matplotlib; matplotlib.use("Agg") import planviz ax = planviz.throughput_curve( ... {"cuda": {64: [1.2e5], 128: [2.1e5], 256: [2.4e5]}}, ... highlight="cuda", ... ) ax.get_xlabel() 'batch size (log)'
Source code in planviz/benchmarks.py
469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 | |
crossover_plot ¶
crossover_plot(ratios: Mapping[str, Any], *, x: Sequence[float] | None = None, ax: Axes | None = None, dark: bool = False, highlight: str | None = None, baseline: float = 1.0, baseline_label: str = 'parity', log_x: bool = True, log_y: bool = True, x_base: int = 2, legend: bool = True, xlabel: str = 'agents', ylabel: str = 'baseline time ÷ candidate time', title: str | None = None, figsize: tuple[float, float] | None = None) -> Axes
Plot speedup ratios and the size at which the ranking flips.
A crossover chart is the only honest way to answer "is the GPU faster?" —
the answer is a size, not a number. The parity line is drawn in faint
because it is a reference, not a result; above it the denominator wins.
Ratios must come from instances both sides solved. Averaging a fast solver's successes against a slow solver's timeouts produces a speedup number that means nothing.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
ratios
|
Mapping[str, Any]
|
|
required |
x
|
Sequence[float] | None
|
shared x values, when |
None
|
ax
|
Axes | None
|
draw into this axes instead of creating one. |
None
|
dark
|
bool
|
use the dark scheme. |
False
|
highlight
|
str | None
|
the series being argued for; it takes the |
None
|
baseline
|
float
|
y value of the parity rule. |
1.0
|
baseline_label
|
str
|
label annotated on that rule. |
'parity'
|
log_x
|
bool
|
log-scale x (base |
True
|
log_y
|
bool
|
log-scale y — a ratio axis should be symmetric about parity, and only a log axis is. |
True
|
x_base
|
int
|
base of the x log scale. |
2
|
legend
|
bool
|
draw a legend. |
True
|
xlabel
|
str
|
x axis label. |
'agents'
|
ylabel
|
str
|
y axis label. |
'baseline time ÷ candidate time'
|
title
|
str | None
|
axes title. |
None
|
figsize
|
tuple[float, float] | None
|
figure size when creating the axes. |
None
|
Returns:
| Name | Type | Description |
|---|---|---|
The |
Axes
|
class: |
Example
import matplotlib; matplotlib.use("Agg") import planviz ax = planviz.crossover_plot( ... {"prioritized": {8: [0.6], 16: [1.4], 32: [3.1]}}, ... highlight="prioritized", ... ) len(ax.lines) >= 2 True
Source code in planviz/benchmarks.py
546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 | |
Output¶
planviz._common.save ¶
save(figure_or_ax: Figure | Axes, path: str | Path, dpi: int = 200) -> Path
Save a figure (or the figure owning an axes) and return the path.
Figure functions never save on their own — this is the explicit call.
>>> import matplotlib; matplotlib.use("Agg")
>>> import planviz, tempfile, os
>>> ax = planviz.draw_grid([[0, 1], [0, 0]])
>>> out = planviz.save(ax, os.path.join(tempfile.mkdtemp(), "grid.png"))
>>> os.path.getsize(out) > 0
True
Source code in planviz/_common.py
188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 | |