Files
littlefs/scripts/treemap.py
Christopher Haster 1c92b7e892 scripts: treemap[d3].py: Squared --squarify, added --rectify
This adds --rectify for a parent-aspect-ratio-preserving --squarify
variant, reverting squarify to try to match the aspect ratio of a
square (1:1).

I can see arguments for both of these. On one hand --squarify makes the
squarest squares, which according to Mark Bruls et al's paper on the
topic is easier visually compare. On the other hand --rectify may be
more visually pleasing and fit into parent tiles better.

d3 allows for any ratio, but at the moment I'm not seeing a strong
reason for the extra parameter.
2025-03-11 15:19:04 -05:00

958 lines
29 KiB
Python
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
#
# Inspired by d3:
# https://d3js.org
#
# prevent local imports
if __name__ == "__main__":
__import__('sys').path.pop(0)
import bisect
import collections as co
import csv
import itertools as it
import math as mt
import shutil
# we don't actually need that many chars/colors thanks to the
# 4-colorability of all 2d maps
CHARS = ['.']
COLORS = [34, 31, 32, 35, 33, 36]
CHARS_DOTS = " .':"
CHARS_BRAILLE = (
'⠀⢀⡀⣀⠠⢠⡠⣠⠄⢄⡄⣄⠤⢤⡤⣤' '⠐⢐⡐⣐⠰⢰⡰⣰⠔⢔⡔⣔⠴⢴⡴⣴'
'⠂⢂⡂⣂⠢⢢⡢⣢⠆⢆⡆⣆⠦⢦⡦⣦' '⠒⢒⡒⣒⠲⢲⡲⣲⠖⢖⡖⣖⠶⢶⡶⣶'
'⠈⢈⡈⣈⠨⢨⡨⣨⠌⢌⡌⣌⠬⢬⡬⣬' '⠘⢘⡘⣘⠸⢸⡸⣸⠜⢜⡜⣜⠼⢼⡼⣼'
'⠊⢊⡊⣊⠪⢪⡪⣪⠎⢎⡎⣎⠮⢮⡮⣮' '⠚⢚⡚⣚⠺⢺⡺⣺⠞⢞⡞⣞⠾⢾⡾⣾'
'⠁⢁⡁⣁⠡⢡⡡⣡⠅⢅⡅⣅⠥⢥⡥⣥' '⠑⢑⡑⣑⠱⢱⡱⣱⠕⢕⡕⣕⠵⢵⡵⣵'
'⠃⢃⡃⣃⠣⢣⡣⣣⠇⢇⡇⣇⠧⢧⡧⣧' '⠓⢓⡓⣓⠳⢳⡳⣳⠗⢗⡗⣗⠷⢷⡷⣷'
'⠉⢉⡉⣉⠩⢩⡩⣩⠍⢍⡍⣍⠭⢭⡭⣭' '⠙⢙⡙⣙⠹⢹⡹⣹⠝⢝⡝⣝⠽⢽⡽⣽'
'⠋⢋⡋⣋⠫⢫⡫⣫⠏⢏⡏⣏⠯⢯⡯⣯' '⠛⢛⡛⣛⠻⢻⡻⣻⠟⢟⡟⣟⠿⢿⡿⣿')
def openio(path, mode='r', buffering=-1):
# allow '-' for stdin/stdout
if path == '-':
if 'r' in mode:
return os.fdopen(os.dup(sys.stdin.fileno()), mode, buffering)
else:
return os.fdopen(os.dup(sys.stdout.fileno()), mode, buffering)
else:
return open(path, mode, buffering)
# parse different data representations
def dat(x):
# allow the first part of an a/b fraction
if '/' in x:
x, _ = x.split('/', 1)
# first try as int
try:
return int(x, 0)
except ValueError:
pass
# then try as float
try:
return float(x)
# just don't allow infinity or nan
if mt.isinf(x) or mt.isnan(x):
raise ValueError("invalid dat %r" % x)
except ValueError:
pass
# else give up
raise ValueError("invalid dat %r" % x)
def collect(csv_paths, defines=[]):
# collect results from CSV files
fields = []
results = []
for path in csv_paths:
try:
with openio(path) as f:
reader = csv.DictReader(f, restval='')
fields.extend(
k for k in reader.fieldnames
if k not in fields)
for r in reader:
# filter by matching defines
if not all(k in r and r[k] in vs for k, vs in defines):
continue
results.append(r)
except FileNotFoundError:
pass
return fields, results
def fold(results, by=None, fields=None, labels=None, defines=[]):
# filter by matching defines
if defines:
results_ = []
for r in results:
if all(k in r and r[k] in vs for k, vs in defines):
results_.append(r)
results = results_
if by:
# find all 'by' values
keys = set()
for r in results:
keys.add(tuple(r.get(k, '') for k in by))
keys = sorted(keys)
# collect dataset
datasets = co.OrderedDict()
labels_ = co.OrderedDict()
for key in (keys if by else [()]):
for field in fields:
# organize by 'by' and field
for r in results:
# filter by 'by'
if by and not all(
k in r and r[k] == v
for k, v in zip(by, key)):
continue
# find field
if field is not None:
if field not in r:
continue
try:
v = dat(r[field])
except ValueError:
continue
else:
v = None
# hide 'field' if there is only one field
key_ = key
if len(fields or []) > 1 or not key_:
key_ += (field,)
# do _not_ sum v here, it's tempting but risks
# incorrect and misleading results
datasets[key_] = v
# also find label?
if labels is not None:
for label_ in labels:
if label_ not in r:
continue
labels_[key_] = r[label_]
return datasets, labels_
# a little ascii renderer
class Canvas:
def __init__(self, width, height, *,
color=False,
dots=False,
braille=False):
# scale if we're printing with dots or braille
if braille:
self.width = 2*width
self.height = 4*height
elif dots:
self.width = width
self.height = 2*height
else:
self.width = width
self.height = height
self.color_ = color
self.dots = dots
self.braille = braille
# create initial canvas
self.grid = [False] * (self.width*self.height)
self.colors = [''] * (self.width*self.height)
def __getitem__(self, xy):
x, y = xy
# ignore out of bounds
if x < 0 or y < 0 or x >= self.width or y >= self.height:
return
return self.grid[x + y*self.width]
def __setitem__(self, xy, char):
x, y = xy
# ignore out of bounds
if x < 0 or y < 0 or x >= self.width or y >= self.height:
return
self.grid[x + y*self.width] = char
def color(self, x, y, color=None):
# ignore out of bounds
if x < 0 or y < 0 or x >= self.width or y >= self.height:
return
if color is not None:
self.colors[x + y*self.width] = color
else:
return self.colors[x + y*self.width]
def point(self, x, y, *,
char=True,
color=''):
# scale if needed
if self.braille and char is not True and char is not False:
xscale, yscale = 2, 4
elif self.dots and char is not True and char is not False:
xscale, yscale = 1, 2
else:
xscale, yscale = 1, 1
for i in range(xscale*yscale):
x_ = x-(x%xscale) + (xscale-1-(i%xscale))
y_ = y-(y%yscale) + (i//xscale)
self[x_, y_] = char
self.color(x_, y_, color)
def line(self, x1, y1, x2, y2, *,
char=True,
color=''):
# incremental error line algorithm
ex = abs(x2 - x1)
ey = -abs(y2 - y1)
dx = +1 if x1 < x2 else -1
dy = +1 if y1 < y2 else -1
e = ex + ey
while True:
self.point(x1, y1, color=color, char=char)
e2 = 2*e
if x1 == x2 and y1 == y2:
break
if e2 > ey:
e += ey
x1 += dx
if x1 == x2 and y1 == y2:
break
if e2 < ex:
e += ex
y1 += dy
self.point(x2, y2, color=color, char=char)
def rect(self, x, y, w, h, *,
char=True,
color=''):
for j in range(h):
for i in range(w):
self.point(x+i, y+j, char=char, color=color)
def label(self, x, y, label, *,
color=''):
# scale if needed
if self.braille:
xscale, yscale = 2, 4
elif self.dots:
xscale, yscale = 1, 2
else:
xscale, yscale = 1, 1
for i, char in enumerate(label):
self.point(x+i*xscale, y, char=char, color=color)
def draw(self, row):
# scale if needed
if self.braille:
xscale, yscale = 2, 4
elif self.dots:
xscale, yscale = 1, 2
else:
xscale, yscale = 1, 1
y = self.height//yscale-1 - row
row_ = []
for x in range(self.width//xscale):
color = ''
char = False
byte = 0
for i in range(xscale*yscale):
x_ = x*xscale + (xscale-1-(i%xscale))
y_ = y*yscale + (i//xscale)
# calculate char
char_ = self[x_, y_]
if char_:
byte |= 1 << i
if char_ is not True and char_ is not False:
char = char_
# keep track of best color
color_ = self.color(x_, y_)
if color_:
color = color_
# figure out winning char
if byte:
if char is not True and char is not False:
pass
elif self.braille:
char = CHARS_BRAILLE[byte]
else:
char = CHARS_DOTS[byte]
else:
char = ' '
# color?
if byte and self.color_ and color:
char = '\x1b[%sm%s\x1b[m' % (color, char)
row_.append(char)
return ''.join(row_)
# a type to represent tiles
class Tile:
def __init__(self, key, children,
x=None, y=None, width=None, height=None, *,
depth=None,
label=None,
color=None):
self.key = key
if isinstance(children, list):
self.children = children
self.value = sum(c.value for c in children)
else:
self.children = []
self.value = children
self.x = x
self.y = y
self.width = width
self.height = height
self.depth = depth
self.label = label
self.color = color
def __repr__(self):
return 'Tile(%r, %r, %r, %r, %r, %r)' % (
','.join(self.key), self.value,
self.x, self.y, self.width, self.height)
# recursively build heirarchy
@staticmethod
def merge(tiles, prefix=()):
# organize by 'by' field
tiles_ = co.OrderedDict()
for t in tiles:
if len(prefix)+1 >= len(t.key):
tiles_[t.key] = t
else:
key = prefix + (t.key[len(prefix)],)
if key not in tiles_:
tiles_[key] = []
tiles_[key].append(t)
tiles__ = []
for key, t in tiles_.items():
if isinstance(t, Tile):
tiles__.append(t)
else:
tiles__.append(Tile.merge(t, key))
tiles_ = tiles__
return Tile(prefix, tiles_, depth=len(prefix))
def __lt__(self, other):
return self.value < other.value
# recursive traversals
def tiles(self):
yield self
for child in self.children:
yield from child.tiles()
def leaves(self):
for t in self.tiles():
if not t.children:
yield t
# sort recursively
def sort(self):
self.children.sort(reverse=True)
for t in self.children:
t.sort()
# recursive align to int boundaries
def align(self):
# this extra +0.1 and using points instead of width/height is
# to help minimize rounding errors
x0 = int(self.x+0.1)
y0 = int(self.y+0.1)
x1 = int(self.x+self.width+0.1)
y1 = int(self.y+self.height+0.1)
self.x = x0
self.y = y0
self.width = x1 - x0
self.height = y1 - y0
# recurse
for t in self.children:
t.align()
# return some interesting info about these tiles
def stat(self):
leaves = list(self.leaves())
mean = self.value / max(len(leaves), 1)
stddev = mt.sqrt(sum((t.value - mean)**2 for t in leaves)
/ max(len(leaves), 1))
min_ = min((t.value for t in leaves), default=0)
max_ = max((t.value for t in leaves), default=0)
return {
'total': self.value,
'mean': mean,
'stddev': stddev,
'min': min_,
'max': max_,
}
# our parititioning schemes
def partition_binary(children, total, x, y, width, height):
sums = [0]
for t in children:
sums.append(sums[-1] + t.value)
# recursively partition into a roughly weight-balanced binary tree
def partition_(i, j, value, x, y, width, height):
# no child? guess we're done
if i == j:
return
# single child? assign the partition
elif i == j-1:
children[i].x = x
children[i].y = y
children[i].width = width
children[i].height = height
return
# binary search to find best split index
target = sums[i] + (value / 2)
k = bisect.bisect(sums, target, i+1, j-1)
# nudge split index if it results in less error
if k > i+1 and (sums[k] - target) > (target - sums[k-1]):
k -= 1
l = sums[k] - sums[i]
r = value - l
# split horizontally?
if width > height:
dx = ((sums[k] - sums[i]) / value) * width
partition_(i, k, l, x, y, dx, height)
partition_(k, j, r, x+dx, y, width-dx, height)
# split vertically?
else:
dy = ((sums[k] - sums[i]) / value) * height
partition_(i, k, l, x, y, width, dy)
partition_(k, j, r, x, y+dy, width, height-dy)
partition_(0, len(children), total, x, y, width, height)
def partition_slice(children, total, x, y, width, height):
# give each child a slice
x_ = x
for t in children:
t.x = x_
t.y = y
t.width = (t.value / total) * width
t.height = height
x_ += t.width
def partition_dice(children, total, x, y, width, height):
# give each child a slice
y_ = y
for t in children:
t.x = x
t.y = y_
t.width = width
t.height = (t.value / total) * height
y_ += t.height
def partition_squarify(children, total, x, y, width, height, *,
aspect_ratio=(1,1)):
if width == 0 or height == 0:
for t in children:
t.x = x
t.y = y
t.width = width
t.height = height
return
# this algorithm is described here:
# https://www.win.tue.nl/~vanwijk/stm.pdf
i = 0
x_ = x
y_ = y
total_ = total
width_ = width
height_ = height
# note we don't really care about width vs height until
# actually slicing
ratio = max(aspect_ratio[0]/aspect_ratio[1],
aspect_ratio[1]/aspect_ratio[0])
while i < len(children):
# calculate initial aspect ratio
sum_ = children[i].value
min_ = children[i].value
max_ = children[i].value
w = total_ * (ratio / max(width_/height_, height_/width_))
ratio_ = max((max_*w)/(sum_**2), (sum_**2)/(min_*w))
# keep adding children to this row/col until it starts to hurt
# our aspect ratio
j = i + 1
while j < len(children):
sum__ = sum_ + children[j].value
min__ = min(min_, children[j].value)
max__ = max(max_, children[j].value)
ratio__ = max((max__*w)/(sum__**2), (sum__**2)/(min__*w))
if ratio__ > ratio_:
break
sum_ = sum__
min_ = min__
max_ = max__
ratio_ = ratio__
j += 1
# vertical col? dice horizontally?
if width_ > height_:
dx = (sum_ / total_) * width_
partition_dice(children[i:j], sum_, x_, y_, dx, height_)
x_ += dx
width_ -= dx
# horizontal row? slice vertically?
else:
dy = (sum_ / total_) * height_
partition_slice(children[i:j], sum_, x_, y_, width_, dy)
y_ += dy
height_ -= dy
# start partitioning the other direction
total_ -= sum_
i = j
def main(csv_paths, *,
by=None,
fields=None,
labels=None,
defines=[],
color=False,
dots=False,
braille=False,
chars=None,
colors=None,
width=None,
height=None,
no_header=False,
to_scale=None,
aspect_ratio=(1,1),
title=None,
padding=0,
**args):
# figure out what color should be
if color == 'auto':
color = sys.stdout.isatty()
elif color == 'always':
color = True
else:
color = False
# figure out chars/colors
if chars is not None:
chars_ = chars
else:
chars_ = CHARS
if colors is not None:
colors_ = colors
else:
colors_ = COLORS
# figure out width/height
if width is None:
width_ = min(80, shutil.get_terminal_size((80, 5))[0])
elif width:
width_ = width
else:
width_ = shutil.get_terminal_size((80, 5))[0]
if height is None:
height_ = 2 if title is not None or not no_header else 1
elif height:
height_ = height
else:
height_ = shutil.get_terminal_size((80, 5))[1]
# first collect results from CSV files
fields_, results = collect(csv_paths, defines)
if not by and not fields:
print("error: needs --by or --fields to figure out fields",
file=sys.stderr)
sys.exit(-1)
# if by not specified, guess it's anything not in fields/labels/defines
if not by:
by = [k for k in fields_
if k not in (fields or [])
and k not in (labels or [])
and not any(k == k_ for k_, _ in defines)]
# if fields not specified, guess it's anything not in by/labels/defines
if not fields:
fields = [k for k in fields_
if k not in (by or [])
and k not in (labels or [])
and not any(k == k_ for k_, _ in defines)]
# then extract the requested dataset
datasets, labels_ = fold(results, by, fields, labels, defines)
# build tile heirarchy
tile = Tile.merge([
Tile(k, v, label=labels_.get(k))
for k, v in datasets.items()
# discard anything with the value 0 early, otherwise these
# cause a lot of problems
if v != 0])
# sort
tile.sort()
# assign colors/chars after sorting to try to minimize touching
# colors, while keeping things somewhat reproducible
# use colors for top of tree
for i, t in enumerate(tile.children):
for t_ in t.tiles():
t_.color = colors_[i % len(colors_)]
# and chars for bottom of tree
for i, t in enumerate(tile.leaves()):
t.char = chars_[i % len(chars_)]
# scale width/height if requested now that we have our data
if to_scale and (width is None or height is None) and tile.value:
# scale if needed
if braille:
xscale, yscale = 2, 4
elif dots:
xscale, yscale = 1, 2
else:
xscale, yscale = 1, 1
# scale width only
if height is not None:
width_ = mt.ceil(
((tile.value * to_scale) / (height_*yscale))
/ xscale)
# scale height only
elif width is not None:
height_ = mt.ceil(
((tile.value * to_scale) / (width_*xscale))
/ yscale)
# scale based on aspect-ratio
else:
width_ = mt.ceil(
(mt.sqrt(tile.value * to_scale)
* (aspect_ratio[0] / aspect_ratio[1]))
/ xscale)
height_ = mt.ceil(
((tile.value * to_scale) / (width_*xscale))
/ yscale)
# create a canvas
canvas = Canvas(width_, height_,
color=color,
dots=dots,
braille=braille)
# recursively partition tiles
tile.x = 0
tile.y = 0
tile.width = canvas.width
tile.height = canvas.height
def partition(tile):
# apply top padding
if tile.depth == 0:
tile.x += padding
tile.y += padding
tile.width -= min(padding, tile.width)
tile.height -= min(padding, tile.height)
x__ = tile.x
y__ = tile.y
width__ = tile.width
height__ = tile.height
# create space for header
if title is not None or not no_header:
y__ += 1
height__ -= min(1, height__)
else:
# apply bottom padding
if not tile.children:
tile.width -= min(padding, tile.width)
tile.height -= min(padding, tile.height)
x__ = tile.x
y__ = tile.y
width__ = tile.width
height__ = tile.height
# partition via requested scheme
if tile.children:
if args.get('binary'):
partition_binary(tile.children, tile.value,
x__, y__, width__, height__)
elif (args.get('slice')
or (args.get('slice_and_dice') and (tile.depth & 1) == 0)
or (args.get('dice_and_slice') and (tile.depth & 1) == 1)):
partition_slice(tile.children, tile.value,
x__, y__, width__, height__)
elif (args.get('dice')
or (args.get('slice_and_dice') and (tile.depth & 1) == 1)
or (args.get('dice_and_slice') and (tile.depth & 1) == 0)):
partition_dice(tile.children, tile.value,
x__, y__, width__, height__)
elif args.get('squarify'):
partition_squarify(tile.children, tile.value,
x__, y__, width__, height__)
elif args.get('rectify'):
partition_squarify(tile.children, tile.value,
x__, y__, width__, height__,
aspect_ratio=(width_, height_))
else:
# default to binary partitioning
partition_binary(tile.children, tile.value,
x__, y__, width__, height__)
# recursively partition
for t in tile.children:
partition(t)
partition(tile)
# align to pixel boundaries
tile.align()
# render to canvas
labels_ = []
for t in tile.leaves():
x__ = t.x
y__ = t.y
width__ = t.width
height__ = t.height
# skip anything with zero weight/height after aligning things
if width__ == 0 or height__ == 0:
continue
# flip y
y__ = canvas.height - (y__+height__)
canvas.rect(x__, y__, width__, height__,
# default to first letter in each label/key
char=(True if braille or dots
else t.label[0]
if chars is None
and t.label is not None
else t.key[-1][0]
if chars is None
and t.key
and t.key[-1]
else t.char if t.char is not None else chars_[0]),
color=t.color if t.color is not None else colors_[0])
if labels:
if t.label is not None:
label__ = t.label
else:
label__ = ','.join(t.key)
# render these later so they get priority
labels_.append((x__, y__+height__-1, label__[:width__]))
for x__, y__, label__ in labels_:
canvas.label(x__, y__, label__)
# print some summary info
if not no_header:
stat = tile.stat()
stat_ = 'total %d, avg %d +-%dσ, min %d, max %d' % (
stat['total'],
stat['mean'], stat['stddev'],
stat['min'], stat['max'])
if title and not no_header:
print('%s%*s%s' % (title, width_-len(stat_)-len(title), '', stat_))
elif title:
print(title)
elif not no_header:
print(stat_)
# draw canvas
for row in range(1 if title or not no_header else 0, height_):
line = canvas.draw(row)
print(line)
if __name__ == "__main__":
import argparse
import sys
parser = argparse.ArgumentParser(
description="Render CSV files as a treemap.",
allow_abbrev=False)
parser.add_argument(
'csv_paths',
nargs='*',
help="Input *.csv files.")
parser.add_argument(
'-b', '--by',
action='append',
help="Group by this field.")
parser.add_argument(
'-f', '--field',
dest='fields',
action='append',
help="Field to use for tile sizes.")
parser.add_argument(
'-l', '--label',
nargs='?',
dest='labels',
action='append',
help="Field to use as tile label.")
parser.add_argument(
'-D', '--define',
dest='defines',
action='append',
type=lambda x: (
lambda k, vs: (
k.strip(),
{v.strip() for v in vs.split(',')})
)(*x.split('=', 1)),
help="Only include results where this field is this value.")
parser.add_argument(
'--color',
choices=['never', 'always', 'auto'],
default='auto',
help="When to use terminal colors. Defaults to 'auto'.")
parser.add_argument(
'-:', '--dots',
action='store_true',
help="Use 1x2 ascii dot characters.")
parser.add_argument(
'-⣿', '--braille',
action='store_true',
help="Use 2x4 unicode braille characters. Note that braille "
"characters sometimes suffer from inconsistent widths.")
parser.add_argument(
'--chars',
help="Characters to use for tiles.")
parser.add_argument(
'--colors',
type=lambda x: [x.strip() for x in x.split(',')],
help="Colors to use for tiles.")
parser.add_argument(
'-W', '--width',
nargs='?',
type=lambda x: int(x, 0),
const=0,
help="Width in columns. 0 uses the terminal width. Defaults to "
"min(terminal, 80).")
parser.add_argument(
'-H', '--height',
nargs='?',
type=lambda x: int(x, 0),
const=0,
help="Height in rows. 0 uses the terminal height. Defaults to 1.")
parser.add_argument(
'-N', '--no-header',
action='store_true',
help="Don't show the header.")
parser.add_argument(
'--binary',
action='store_true',
help="Use the binary partitioning scheme. This attempts to "
"recursively subdivide the tiles into a roughly "
"weight-balanced binary tree. This is the default.")
parser.add_argument(
'--slice',
action='store_true',
help="Use the slice partitioning scheme. This simply slices "
"tiles vertically.")
parser.add_argument(
'--dice',
action='store_true',
help="Use the dice partitioning scheme. This simply slices "
"tiles horizontally.")
parser.add_argument(
'--slice-and-dice',
action='store_true',
help="Use the slice-and-dice partitioning scheme. This "
"alternates between slicing and dicing each layer.")
parser.add_argument(
'--dice-and-slice',
action='store_true',
help="Use the dice-and-slice partitioning scheme. This is like "
"slice-and-dice, but flipped.")
parser.add_argument(
'--squarify',
action='store_true',
help="Use the squarify partitioning scheme. This is a greedy "
"algorithm created by Mark Bruls et al that tries to "
"minimize tile aspect ratios.")
parser.add_argument(
'--rectify',
action='store_true',
help="Use the rectify partitioning scheme. This is like "
"squarify, but tries to match the aspect ratio of the "
"window.")
parser.add_argument(
'--to-scale',
nargs='?',
type=float,
const=1,
help="Scale the resulting treemap such that 1 pixel ~= 1/scale "
"units. Defaults to scale=1. ")
parser.add_argument(
'-R', '--aspect-ratio',
type=lambda x: tuple(float(v) for v in x.split(':', 1)),
default=(1, 1),
help="Aspect ratio to use with --to-scale. Defaults to 1:1.")
parser.add_argument(
'--title',
help="Add a title.")
parser.add_argument(
'--padding',
type=float,
default=0,
help="Padding to add to each level of the treemap. Defaults to 0.")
sys.exit(main(**{k: v
for k, v in vars(parser.parse_intermixed_args()).items()
if v is not None}))