Files
2026-hypn0-site/hypn0/hypn0_site/services/halftone.py
T
2026-09-06 18:55:46 +03:00

575 lines
26 KiB
Python
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.
import io
import math
import random
import re
from collections import defaultdict
from typing import BinaryIO, Optional, Union
from PIL import Image
def encode_to_base36(num: int) -> str:
"""Кодирует неотрицательное число в компактный строковый идентификатор."""
if num < 0:
raise ValueError("Число должно быть неотрицательным")
charset = "HYPN0xyzabcdefghijklmnopqrstuvwABCDEFGIJKLMOQRSTUVWXZ123456789"
if num < len(charset):
return charset[num]
result = []
while num > 0:
result.append(charset[num % len(charset)])
num //= len(charset)
return "".join(reversed(result))
def generate_shape_def(
shape: str, radius: int, shape_id: str, max_radius: int = 8, char: Optional[str] = None
) -> str:
"""Генерирует SVG-элемент для тега <defs> в зависимости от типа фигуры."""
r = radius
match shape:
case "ring":
inner_r = max(1, round(r * 0.55))
if inner_r >= r:
inner_r = max(0, r - 1)
if inner_r == 0:
return f'<circle id="{shape_id}" r="{r}"/>'
return (
f'<path id="{shape_id}" fill-rule="evenodd" '
f'd="M0,{-r}A{r},{r} 0 100,{r}A{r},{r} 0 100,{-r}'
f'M0,{-inner_r} A{inner_r},{inner_r} 0 100,{inner_r}A{inner_r},{inner_r} 0 100,{-inner_r}Z"/>'
)
case "square":
size = r * 2
rx = max(0.5, r * 0.1)
return f'<rect id="{shape_id}" x="{-r}" y="{-r}" width="{size}" height="{size}" rx="{rx:.1f}"/>'
case "diamond":
return f'<polygon id="{shape_id}" points="0,{-r} {-r},0 0,{r} {r},0"/>'
case "triangle":
return f'<polygon id="{shape_id}" points="0,{-r} {-r},{r} {r},{r}"/>'
case "hexagon":
w = round(r * 0.866)
h_half = round(r * 0.5)
return f'<polygon id="{shape_id}" points="0,{-r} {w},{-h_half} {w},{h_half} 0,{r} {-w},{h_half} {-w},{-h_half}"/>'
case "star":
points = []
inner_r = r * 0.45
for i in range(10):
angle = i * math.pi / 5 - math.pi / 2
curr_r = r if i % 2 == 0 else inner_r
px = round(curr_r * math.cos(angle), 1)
py = round(curr_r * math.sin(angle), 1)
points.append(f"{px},{py}")
pts_str = " ".join(points)
return f'<polygon id="{shape_id}" points="{pts_str}"/>'
case "cross":
arm = max(1, round(r * 0.35))
return f'<path id="{shape_id}" d="M{-arm},{-r}H{arm}V{-arm}H{r}V{arm}H{arm}V{r}H{-arm}V{arm}H{-r}V{-arm}H{-arm}Z"/>'
case "line":
th = max(1, round(r * 0.25))
return f'<rect id="{shape_id}" x="{-r}" y="{-th}" width="{2*r}" height="{2*th}" rx="1"/>'
case "snowflake":
points = []
inner_r = max(0.5, r * 0.25)
for i in range(16):
angle = i * math.pi / 8 - math.pi / 2
curr_r = r if i % 2 == 0 else inner_r
px = round(curr_r * math.cos(angle), 1)
py = round(curr_r * math.sin(angle), 1)
points.append(f"{px},{py}")
pts_str = " ".join(points)
return f'<polygon id="{shape_id}" points="{pts_str}"/>'
case "binary":
if char is None:
# Вероятностная функция: вероятность "0" пропорциональна оптической плотности
# В тенях преобладают '0' (~85%), но с шансом ~15% выскакивают '1'
# В светах преобладают '1' (~85%), но с шансом ~15% выскакивают '0'
prob_zero = min(0.85, max(0.15, (r / max_radius) if max_radius else 0.5))
char = "0" if random.random() < prob_zero else "1"
font_size = max(4, round(r * 2.0))
return (
f'<text id="{shape_id}" font-family="ui-monospace,monospace" font-weight="900" font-size="{font_size}">{char}</text>'
)
case "heart":
r_top = round(r * 0.3)
r_mid = round(r * 0.4)
return (
f'<path id="{shape_id}" '
f'd="M0,{r}C{-r},{r_mid} {-r},{-r} 0,{-r_top}C{r},{-r} {r},{r_mid} 0,{r}Z"/>'
)
case "circle" | _:
return f'<circle id="{shape_id}" r="{r}"/>'
def generate_halftone_svg(
image: Union[Image.Image, BinaryIO, bytes, str],
*,
cols: int = 35,
max_radius: int = 8,
shape: str = "circle",
color: str = "#a855ff",
opacity: float = 0.9,
blink: int = 6,
rotation: int = 0,
scale: int = 980,
angle: int = 0,
animation_variants: int = 12,
seed: int = 42,
) -> str:
"""
Генерирует чистый оптимизированный SVG-халфтон из растрового изображения.
Параметры:
- image: PIL Image, файловый объект (BytesIO), байты или путь к файлу
- cols: число точек по наибольшей стороне сетки (10-200)
- max_radius: максимальный радиус/размер точки (3-20)
- shape: тип фигуры ('circle', 'ring', 'square', 'diamond', 'triangle', 'hexagon', 'star', 'cross', 'line', 'snowflake', 'heart')
- color: основной HEX цвет (например '#a855ff')
- opacity: коэффициент непрозрачности (0.0 - 1.0)
- blink: интенсивность мерцания (0 - выключено/статичный, 1-10 - скорость)
- rotation: угол покачивания в градусах (-15 .. +15)
- scale: масштаб пульсации (800 .. 1040, где 980 = 0.98)
- angle: наклон растровой сетки (-44 .. +45)
- animation_variants: число вариантов CSS-задержек
- seed: сид для детерминированного распределения анимаций
"""
rng = random.Random(seed)
# 1. Загрузка и подготовка изображения в градациях серого
if isinstance(image, (bytes, bytearray)):
pil_img = Image.open(io.BytesIO(image))
elif hasattr(image, "read"):
if hasattr(image, "seek"):
image.seek(0)
pil_img = Image.open(image)
elif isinstance(image, str):
pil_img = Image.open(image)
elif isinstance(image, Image.Image):
pil_img = image
else:
raise ValueError("Неподдерживаемый тип входного изображения")
# Конвертируем в Grayscale
img = pil_img.convert("L")
# Вычисляем размеры базовой сетки по исходным пропорциям изображения
# Значение `cols` задает число ячеек по наибольшей стороне (ширине или высоте)
if img.width >= img.height:
grid_width = max(1, cols)
grid_height = max(1, round(cols * (img.height / img.width)))
else:
grid_height = max(1, cols)
grid_width = max(1, round(cols * (img.width / img.height)))
img_resized = img.resize((grid_width, grid_height), Image.Resampling.LANCZOS)
img_w, img_h = img_resized.width, img_resized.height
# Базовый шаг ячейки сетки (номинальный радиус касания точек R0 = 10, диаметр D0 = 20)
STEP = 20
# Расчет запаса/паспарту (margin) по краям холста:
# Учитывает увеличенный радиус точек (при наползании) и масштабирование при пульсации/повороте
scale_val = max(0.5, min(1.5, scale / 1000.0))
max_effective_radius = max_radius * max(1.0, scale_val)
max_visual_extent = math.ceil(max_effective_radius * 1.42) + 2
margin = max(4, max_visual_extent - STEP // 2 + 2)
width = grid_width * STEP + 2 * margin
height = grid_height * STEP + 2 * margin
# Функция билинейной интерполяции для сэмплирования яркости в дробных координатах сетки
def sample_brightness(fx: float, fy: float) -> float:
"""Возвращает интерполированную яркость (0-255) в нормализованных координатах img_resized."""
if fx < 0 or fx > img_w - 1 or fy < 0 or fy > img_h - 1:
# За пределами изображения считаем фон белым (255)
if fx < -0.5 or fx > img_w - 0.5 or fy < -0.5 or fy > img_h - 0.5:
return 255.0
fx = max(0.0, min(img_w - 1.0, fx))
fy = max(0.0, min(img_h - 1.0, fy))
x0 = int(math.floor(fx))
y0 = int(math.floor(fy))
x1 = min(x0 + 1, img_w - 1)
y1 = min(y0 + 1, img_h - 1)
wx = fx - x0
wy = fy - y0
p00 = img_resized.getpixel((x0, y0))
p10 = img_resized.getpixel((x1, y0))
p01 = img_resized.getpixel((x0, y1))
p11 = img_resized.getpixel((x1, y1))
top = p00 * (1.0 - wx) + p10 * wx
bottom = p01 * (1.0 - wx) + p11 * wx
return top * (1.0 - wy) + bottom * wy
# 2. Обход узлов наклонной сетки и группировка
elements_by_class = defaultdict(list)
unique_shape_keys = set()
is_animated = blink > 0
# Центр изображения на холсте (в координатах SVG без марджина)
canvas_cx = (grid_width * STEP) / 2.0
canvas_cy = (grid_height * STEP) / 2.0
rad = math.radians(angle)
cos_a = math.cos(rad)
sin_a = math.sin(rad)
# Векторы шага сетки вдоль осей U (строка) и V (столбец)
dx_u, dy_u = STEP * cos_a, STEP * sin_a
dx_v, dy_v = -STEP * sin_a, STEP * cos_a
# Диапазон охвата узлов сетки с запасом, чтобы перекрыть весь повернутый холст
max_dim = math.hypot(grid_width, grid_height)
u_range = int(math.ceil(max_dim / 2.0)) + 2
v_range = int(math.ceil(max_dim / 2.0)) + 2
# Перебираем узлы сетки вокруг центра
for v in range(-v_range, v_range + 1):
for u in range(-u_range, u_range + 1):
# Точные вещественные координаты узла относительно центра холста
rel_x = u * dx_u + v * dx_v
rel_y = u * dy_u + v * dy_v
# Координаты на видимом холсте (от 0 до grid_width * STEP)
pos_x = canvas_cx + rel_x
pos_y = canvas_cy + rel_y
# Проверяем, попадает ли узел в пределы видимой области холста
if not (0 <= pos_x <= grid_width * STEP and 0 <= pos_y <= grid_height * STEP):
continue
# Координаты в сетке img_resized
sample_fx = (pos_x / (grid_width * STEP)) * (img_w - 1)
sample_fy = (pos_y / (grid_height * STEP)) * (img_h - 1)
brightness = sample_brightness(sample_fx, sample_fy)
factor = (255.0 - brightness) / 255.0
# Отсекаем слишком светлые участки (шум фона)
if factor < 0.12:
continue
radius = int(factor * max_radius)
if radius == 0:
continue
# Итоговые координаты центра точки в SVG (с учетом паспарту-margin)
cx = round(margin + pos_x)
cy = round(margin + pos_y)
anim_index = rng.randint(0, animation_variants - 1) if is_animated else 0
encoded_class = encode_to_base36(anim_index)
if shape == "binary":
# Вероятность выпадения '0' плавно растет с оптической плотностью
prob_zero = 0.15 + 0.70 * (radius / max_radius if max_radius else factor)
char = "0" if rng.random() < prob_zero else "1"
shape_id = f"s{encode_to_base36(radius)}{char}"
unique_shape_keys.add((radius, char, shape_id))
else:
shape_id = f"s{encode_to_base36(radius)}"
unique_shape_keys.add((radius, None, shape_id))
elements_by_class[encoded_class].append((shape_id, cx, cy, anim_index))
# 3. Формирование <defs>
defs_list = ["<defs>"]
for radius, char, shape_id in sorted(unique_shape_keys, key=lambda k: (k[0], k[1] or "")):
defs_list.append(generate_shape_def(shape, radius, shape_id, max_radius=max_radius, char=char))
defs_list.append("</defs>")
defs_html = "".join(defs_list)
# 4. Формирование групп <g class="a..."> и CSS классов задержек
# Список простых чисел для создания богатой апериодической полиритмии мерцания
PRIME_DELAYS = [0, 0.1, 0.3, 0.5, 0.7, 1.1, 1.3, 1.7, 1.9, 2.3, 2.9, 3.1]
duration = max(0.4, 2.5 - (blink * 0.19)) if is_animated else 1.0
groups = []
animation_classes = []
# Сортируем группы по индексу анимации для детерминированного порядка
for encoded_class in sorted(elements_by_class.keys(), key=lambda c: elements_by_class[c][0][3]):
group_elements = elements_by_class[encoded_class]
if not group_elements:
continue
anim_index = group_elements[0][3]
if is_animated:
prime_base = PRIME_DELAYS[anim_index % len(PRIME_DELAYS)]
# Масштабируем задержку относительно длительности анимации с округлением до десятых
delay = round(prime_base * (duration / 2.0), 1)
# Вычисляем уникальный центр трансформации для каждой группы анимации.
# Центры группируются вокруг центра холста (50% 50%) с органическим разбросом,
# благодаря чему группы дышат и покачиваются вокруг слегка смещенных фокусов.
angle_jitter = (anim_index * (2 * math.pi / max(1, animation_variants))) + (seed % 7)
spread_factor = 0.02 + 0.13 * abs((scale - 1000) / 200.0) # от 2% до 15%
orig_x = round(50.0 + math.cos(angle_jitter) * spread_factor * 100)
orig_y = round(50.0 + math.sin(angle_jitter) * spread_factor * 100)
origin_rule = f";transform-origin:{orig_x}% {orig_y}%"
else:
delay = 0.0
origin_rule = ""
# Генерируем правило задержки и центра в CSS
if is_animated:
delay_str = f"{int(delay)}" if delay == int(delay) else f"{delay:.1f}".rstrip("0").rstrip(".")
animation_classes.append(f".a{encoded_class}{{--d:{delay_str}s{origin_rule}}}")
uses = "".join(
f'<use href="#{shape_id}" x="{cx}" y="{cy}"/>'
for shape_id, cx, cy, _ in group_elements
)
if uses:
groups.append(f'<g class="a{encoded_class}">{uses}</g>')
animation_css = "".join(animation_classes)
groups_html = "".join(groups)
# 6. Стилизация и Keyframes
scale_val = max(0.5, min(1.5, scale / 1000.0))
scale_str = f"{scale_val:.3f}".rstrip("0").rstrip(".")
has_scale = scale_val != 1.0
has_rotation = rotation != 0
transform_0 = []
transform_100 = []
if has_scale:
transform_0.append("scale(1)")
transform_100.append(f"scale({scale_str})")
if has_rotation:
transform_0.append("rotate(0deg)")
transform_100.append(f"rotate({rotation}deg)")
tf_0_rule = f";transform:{' '.join(transform_0)}" if transform_0 else ""
tf_100_rule = f";transform:{' '.join(transform_100)}" if transform_100 else ""
# Корректный цвет и прозрачность
clean_color = color.strip() if color else "#a855ff"
if not clean_color.startswith("#"):
clean_color = f"#{clean_color}"
has_custom_opacity = round(opacity, 2) < 1.0
opacity_rule = f"opacity:{opacity:.1f};" if has_custom_opacity else ""
fill_style = f"fill:{clean_color};stroke:{clean_color};{opacity_rule}"
if is_animated:
# Управление паузой/запуском через CSS Custom Property --hypn0-play.
# По умолчанию running (для живого превью и детального просмотра).
# В галерее значение переопределяется на paused для 0% нагрузки на процессор.
anim_rule = (
f"animation:noise {duration:.1f}s ease-in-out infinite alternate;"
f"animation-delay:var(--d,0s);"
f"animation-play-state:var(--hypn0-play,running);"
)
op_0 = f"{max(0.2, opacity * 0.7):.1f}".rstrip("0").rstrip(".")
op_50 = f"{opacity:.1f}".rstrip("0").rstrip(".")
op_100 = f"{max(0.1, opacity * 0.5):.1f}".rstrip("0").rstrip(".")
keyframes_rule = (
f"@keyframes noise{{"
f"0%{{opacity:{op_0}{tf_0_rule}}}"
f"50%{{opacity:{op_50}}}"
f"100%{{opacity:{op_100}{tf_100_rule}}}"
f"}}"
)
else:
anim_rule = "animation:none;"
keyframes_rule = ""
frozen_opacity = f";opacity:{opacity:.1f}!important" if has_custom_opacity else ""
svg_css = (
f"<style>"
f"svg{{background:transparent}}"
f"circle,rect,polygon,path,text{{{fill_style}}}"
f"g[class^=\"a\"]{{{anim_rule}transition:all .5s ease-out}}"
f"{keyframes_rule}"
f".frozen g[class^=\"a\"],.frozen circle,.frozen rect,.frozen polygon,.frozen path,.frozen text{{animation:none!important{frozen_opacity};transform:none!important}}"
f"{animation_css}"
f"</style>"
)
svg_template = (
f'<!-- Generated by hypn0.xyz -->\n'
f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {width} {height}" '
f'data-author="hypn0.xyz" data-generator="https://hypn0.xyz">'
f'{svg_css}'
f'<desc>Generated by Hypn0 (https://hypn0.xyz)</desc>'
f'{defs_html}'
f'<g id="hypn0-xyz" data-source="https://hypn0.xyz">{groups_html}</g>'
f'</svg>'
)
return svg_template
def prepare_gallery_svg(svg_content: str) -> str:
"""
Модифицирует SVG для долговременного хранения в галерее:
внедряет CSS Custom Property (--hypn0-play: paused), которое свободно наследуется
внутрь элементов и теневых деревьев <use>.
Логика работы:
1. В покое (--hypn0-play: paused): 0% потребления CPU и GPU браузера в ленте галереи.
2. При наведении курсора (:host(:hover) в Shadow DOM или svg:hover при открытии файла):
значение переключается на running, и анимация оживает.
"""
if not svg_content:
return svg_content
# Наследуемая переменная паузы и оверрайд стилей фигур
# Важно: селектор :host(:hover) работает при вставке в Shadow DOM карточки,
# а svg:hover работает, если файл открыт напрямую в браузере.
hover_css = (
"svg{--hypn0-play:paused}"
":host(:hover) svg,svg:hover{--hypn0-play:running!important}"
"g[class^=\"a\"],circle,rect,polygon,path,text{animation-play-state:var(--hypn0-play,paused)!important}"
)
if "</style>" in svg_content:
return svg_content.replace("</style>", f"{hover_css}</style>", 1)
return svg_content
def prepare_active_svg(svg_content: str) -> str:
"""
Возвращает полностью активный SVG без паузы анимации вне ховера
для страницы детального просмотра (/gallery/<hash_id>) и скачивания пользователем.
Удаляет оверрайды пауз (--hypn0-play: paused), восстанавливая оригинальные
авторские цвета, прозрачности и непрерывную живую анимацию на полной мощности.
"""
if not svg_content:
return svg_content
# Удаляем внедренные правила паузы через CSS-переменные и старые форматы
gallery_css_variants = [
"svg{--hypn0-play:paused}:host(:hover) svg,svg:hover{--hypn0-play:running!important}g[class^=\"a\"],circle,rect,polygon,path,text{animation-play-state:var(--hypn0-play,paused)!important}",
"svg{--hypn0-play:paused}:host(:hover) svg,svg:hover{--hypn0-play:running!important}.shape,circle,rect,polygon,path{animation-play-state:var(--hypn0-play,paused)!important}",
"svg{--hypn0-play:paused}:host(:hover) svg,svg:hover{--hypn0-play:running!important}circle,rect,polygon,path{animation-play-state:var(--hypn0-play,paused)!important}",
"svg{--hypn0-play:paused}:host(:hover) svg,svg:hover{--hypn0-play:running!important}circle,rect,polygon,path,text{animation-play-state:var(--hypn0-play,paused)!important}",
"svg{--hypn0-play:paused}svg:hover{--hypn0-play:running}.shape,circle,rect,polygon,path{animation-play-state:var(--hypn0-play,paused)!important}",
"svg{--hypn0-play:paused}svg:hover{--hypn0-play:running}circle,rect,polygon,path{animation-play-state:var(--hypn0-play,paused)!important}",
"svg{--hypn0-play:paused}svg:hover{--hypn0-play:running}circle,rect,polygon,path,text{animation-play-state:var(--hypn0-play,paused)!important}",
"svg{--hypn0-play:paused}svg:hover{--hypn0-play:running}g[class^=\"a\"],circle,rect,polygon,path,text{animation-play-state:var(--hypn0-play,paused)!important}",
]
cleaned = svg_content
for gcss in gallery_css_variants:
cleaned = cleaned.replace(gcss, "")
cleaned = re.sub(r"svg\s*\{[^}]*--hypn0-play:\s*paused[^}]*\}", "", cleaned)
cleaned = re.sub(r"(:host\(:hover\)\s*svg\s*,\s*)?svg:hover\s*\{[^}]*--hypn0-play:[^}]*\}", "", cleaned)
cleaned = re.sub(
r"(g\[class\^=\"a\"\],)?(\.shape,)?circle,rect,polygon,path(,text)?\s*\{animation-play-state:\s*var\(--hypn0-play,\s*paused\)!important\}",
"",
cleaned,
)
cleaned = re.sub(r"@media\s*\(\s*hover\s*:\s*hover\s*\)\s*\{[^}]*:[^}]*\}", "", cleaned)
cleaned = re.sub(r"svg:not\(:hover\)[^{]*\{[^}]*\}", "", cleaned)
old_hover_css = (
"svg:not(:hover) .shape,svg:not(:hover) circle,svg:not(:hover) rect,"
"svg:not(:hover) polygon,svg:not(:hover) path{animation-play-state:paused!important}"
)
cleaned = cleaned.replace(old_hover_css, "")
return cleaned
def analyze_svg_structure(svg_content: str) -> dict:
"""
Анализирует разметку SVG и возвращает наукообразную детальную статистику
по геометрическим примитивам, классам, анимациям и энтропии данных.
"""
if not svg_content:
return {
"total_bytes": 0,
"total_kb": 0.0,
"total_oscillators": 0,
"use_count": 0,
"circle_count": 0,
"rect_count": 0,
"polygon_count": 0,
"path_count": 0,
"defs_count": 0,
"groups_count": 0,
"keyframes_count": 0,
"classes_count": 0,
"viewbox": "0 0 1000 1000",
"width": 1000,
"height": 1000,
"aspect_ratio": "1:1",
"density": 0.0,
"bytes_per_quantum": 0.0,
}
total_bytes = len(svg_content.encode("utf-8"))
# Подсчет векторных сущностей
use_count = len(re.findall(r"<use\b", svg_content))
circle_count = len(re.findall(r"<circle\b", svg_content))
rect_count = len(re.findall(r"<rect\b", svg_content))
polygon_count = len(re.findall(r"<polygon\b", svg_content))
path_count = len(re.findall(r"<path\b", svg_content))
text_count = len(re.findall(r"<text\b", svg_content))
defs_count = circle_count + rect_count + polygon_count + path_count + text_count
# Группы и классы
groups_count = len(re.findall(r"<g\b", svg_content))
keyframes_count = len(re.findall(r"@keyframes\b", svg_content))
classes = set(re.findall(r"\.([a-zA-Z0-9_-]+)\s*\{", svg_content))
classes_count = len(classes)
# ViewBox и геометрия
vb_match = re.search(r'viewBox=["\']([^"\']+)["\']', svg_content)
viewbox = vb_match.group(1) if vb_match else "0 0 1000 1000"
vb_parts = viewbox.split()
try:
width = int(float(vb_parts[2])) if len(vb_parts) >= 3 else 1000
height = int(float(vb_parts[3])) if len(vb_parts) >= 4 else 1000
except (ValueError, IndexError):
width, height = 1000, 1000
gcd_val = math.gcd(width, height)
aspect_ratio = f"{width // gcd_val}:{height // gcd_val}" if gcd_val > 0 else "1:1"
# Наукообразные показатели
total_oscillators = use_count if use_count > 0 else defs_count
bytes_per_quantum = round(total_bytes / max(total_oscillators, 1), 1)
density = round(total_oscillators / max((width * height) / 1048576.0, 1.0), 2)
return {
"total_bytes": total_bytes,
"total_kb": round(total_bytes / 1024, 2),
"total_oscillators": total_oscillators,
"use_count": use_count,
"circle_count": circle_count,
"rect_count": rect_count,
"polygon_count": polygon_count,
"path_count": path_count,
"defs_count": defs_count,
"groups_count": groups_count,
"keyframes_count": keyframes_count,
"classes_count": classes_count,
"viewbox": viewbox,
"width": width,
"height": height,
"aspect_ratio": aspect_ratio,
"density": density,
"bytes_per_quantum": bytes_per_quantum,
}