tensorbored 2.21.0rc1769988660__py3-none-any.whl → 2.21.0rc1770021042__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- tensorbored/plugins/core/color_sampler.py +451 -0
- tensorbored/plugins/core/profile_writer.py +280 -0
- tensorbored/version.py +1 -1
- {tensorbored-2.21.0rc1769988660.dist-info → tensorbored-2.21.0rc1770021042.dist-info}/METADATA +1 -1
- {tensorbored-2.21.0rc1769988660.dist-info → tensorbored-2.21.0rc1770021042.dist-info}/RECORD +9 -7
- {tensorbored-2.21.0rc1769988660.dist-info → tensorbored-2.21.0rc1770021042.dist-info}/WHEEL +0 -0
- {tensorbored-2.21.0rc1769988660.dist-info → tensorbored-2.21.0rc1770021042.dist-info}/entry_points.txt +0 -0
- {tensorbored-2.21.0rc1769988660.dist-info → tensorbored-2.21.0rc1770021042.dist-info}/licenses/LICENSE +0 -0
- {tensorbored-2.21.0rc1769988660.dist-info → tensorbored-2.21.0rc1770021042.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
# Copyright 2026 The TensorFlow Authors. All Rights Reserved.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
# ==============================================================================
|
|
15
|
+
"""Perceptually uniform color sampling for TensorBoard run colors.
|
|
16
|
+
|
|
17
|
+
This module provides utilities for generating visually distinguishable colors
|
|
18
|
+
using the OKLCH color space, which is perceptually uniform. This means equal
|
|
19
|
+
steps in the color space correspond to equal perceived differences.
|
|
20
|
+
|
|
21
|
+
Example usage:
|
|
22
|
+
|
|
23
|
+
from tensorbored.plugins.core import color_sampler
|
|
24
|
+
|
|
25
|
+
# Get 5 evenly-spaced colors
|
|
26
|
+
colors = color_sampler.sample_colors(5)
|
|
27
|
+
# ['#dc8a78', '#a4b93e', '#40c4aa', '#7aa6f5', '#d898d5']
|
|
28
|
+
|
|
29
|
+
# Use with run_colors
|
|
30
|
+
run_ids = ['train', 'eval', 'test', 'baseline', 'experiment']
|
|
31
|
+
run_colors = {rid: color_sampler.sample_colors(len(run_ids))[i]
|
|
32
|
+
for i, rid in enumerate(run_ids)}
|
|
33
|
+
|
|
34
|
+
# Or use the ColorMap class for cleaner syntax
|
|
35
|
+
cm = color_sampler.ColorMap(len(run_ids))
|
|
36
|
+
run_colors = {rid: cm(i) for i, rid in enumerate(run_ids)}
|
|
37
|
+
|
|
38
|
+
# Even simpler - auto-assign from list
|
|
39
|
+
run_colors = color_sampler.colors_for_runs(run_ids)
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
import math
|
|
43
|
+
from typing import List, Tuple
|
|
44
|
+
|
|
45
|
+
# =============================================================================
|
|
46
|
+
# OKLCH Color Space Implementation
|
|
47
|
+
# =============================================================================
|
|
48
|
+
# OKLCH is a perceptually uniform color space where:
|
|
49
|
+
# L = Lightness (0 = black, 1 = white)
|
|
50
|
+
# C = Chroma (0 = gray, higher = more saturated)
|
|
51
|
+
# H = Hue angle in degrees (0-360)
|
|
52
|
+
#
|
|
53
|
+
# We convert OKLCH → OKLAB → Linear sRGB → sRGB → Hex
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _oklch_to_oklab(L: float, C: float, H: float) -> Tuple[float, float, float]:
|
|
57
|
+
"""Convert OKLCH to OKLAB."""
|
|
58
|
+
h_rad = math.radians(H)
|
|
59
|
+
a = C * math.cos(h_rad)
|
|
60
|
+
b = C * math.sin(h_rad)
|
|
61
|
+
return (L, a, b)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _oklab_to_linear_srgb(
|
|
65
|
+
L: float, a: float, b: float
|
|
66
|
+
) -> Tuple[float, float, float]:
|
|
67
|
+
"""Convert OKLAB to linear sRGB."""
|
|
68
|
+
# OKLAB to LMS (approximate)
|
|
69
|
+
l_ = L + 0.3963377774 * a + 0.2158037573 * b
|
|
70
|
+
m_ = L - 0.1055613458 * a - 0.0638541728 * b
|
|
71
|
+
s_ = L - 0.0894841775 * a - 1.2914855480 * b
|
|
72
|
+
|
|
73
|
+
# Cube the values
|
|
74
|
+
l = l_ * l_ * l_
|
|
75
|
+
m = m_ * m_ * m_
|
|
76
|
+
s = s_ * s_ * s_
|
|
77
|
+
|
|
78
|
+
# LMS to linear sRGB
|
|
79
|
+
r = +4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s
|
|
80
|
+
g = -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s
|
|
81
|
+
b = -0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s
|
|
82
|
+
|
|
83
|
+
return (r, g, b)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _linear_to_srgb(x: float) -> float:
|
|
87
|
+
"""Convert linear RGB component to sRGB (gamma correction)."""
|
|
88
|
+
if x <= 0.0031308:
|
|
89
|
+
return 12.92 * x
|
|
90
|
+
return 1.055 * (x ** (1 / 2.4)) - 0.055
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _clamp(x: float, lo: float = 0.0, hi: float = 1.0) -> float:
|
|
94
|
+
"""Clamp value to range."""
|
|
95
|
+
return max(lo, min(hi, x))
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _oklch_to_hex(L: float, C: float, H: float) -> str:
|
|
99
|
+
"""Convert OKLCH color to hex string."""
|
|
100
|
+
# OKLCH → OKLAB → Linear sRGB → sRGB
|
|
101
|
+
lab = _oklch_to_oklab(L, C, H)
|
|
102
|
+
linear = _oklab_to_linear_srgb(*lab)
|
|
103
|
+
srgb = tuple(_clamp(_linear_to_srgb(c)) for c in linear)
|
|
104
|
+
|
|
105
|
+
# Convert to 8-bit and format as hex
|
|
106
|
+
r = int(round(srgb[0] * 255))
|
|
107
|
+
g = int(round(srgb[1] * 255))
|
|
108
|
+
b = int(round(srgb[2] * 255))
|
|
109
|
+
|
|
110
|
+
return f"#{r:02x}{g:02x}{b:02x}"
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
# =============================================================================
|
|
114
|
+
# Public API
|
|
115
|
+
# =============================================================================
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def sample_colors(
|
|
119
|
+
n: int,
|
|
120
|
+
lightness: float = 0.7,
|
|
121
|
+
chroma: float = 0.15,
|
|
122
|
+
hue_start: float = 0.0,
|
|
123
|
+
hue_range: float = 360.0,
|
|
124
|
+
) -> List[str]:
|
|
125
|
+
"""Generate n perceptually uniform, evenly-spaced colors.
|
|
126
|
+
|
|
127
|
+
Uses the OKLCH color space to ensure colors are visually distinguishable.
|
|
128
|
+
Colors are spaced evenly around the hue wheel while maintaining consistent
|
|
129
|
+
lightness and chroma for uniform appearance.
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
n: Number of colors to generate.
|
|
133
|
+
lightness: OKLCH lightness (0-1). Default 0.7 works well on white
|
|
134
|
+
backgrounds. Use ~0.65 for dark backgrounds.
|
|
135
|
+
chroma: OKLCH chroma (0-0.4). Higher = more saturated. Default 0.15
|
|
136
|
+
gives vivid but not garish colors.
|
|
137
|
+
hue_start: Starting hue angle in degrees (0-360). Shifts the color
|
|
138
|
+
palette around the wheel.
|
|
139
|
+
hue_range: Range of hues to use (default 360 = full wheel). Use less
|
|
140
|
+
to restrict to a portion of the spectrum.
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
List of n hex color strings (e.g., ['#dc8a78', '#40c4aa', ...]).
|
|
144
|
+
|
|
145
|
+
Example:
|
|
146
|
+
>>> sample_colors(3)
|
|
147
|
+
['#dc8a78', '#5fba72', '#7a9ef7']
|
|
148
|
+
|
|
149
|
+
>>> sample_colors(5, lightness=0.6, chroma=0.2)
|
|
150
|
+
['#d96a5c', '#8ba600', '#00ab9e', '#4d95f2', '#c87ed4']
|
|
151
|
+
"""
|
|
152
|
+
if n <= 0:
|
|
153
|
+
return []
|
|
154
|
+
|
|
155
|
+
colors = []
|
|
156
|
+
for i in range(n):
|
|
157
|
+
# Evenly space hues, leaving a gap so first and last aren't too close
|
|
158
|
+
hue = (hue_start + (i * hue_range / n)) % 360
|
|
159
|
+
colors.append(_oklch_to_hex(lightness, chroma, hue))
|
|
160
|
+
|
|
161
|
+
return colors
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def sample_colors_varied(
|
|
165
|
+
n: int,
|
|
166
|
+
lightness_range: Tuple[float, float] = (0.55, 0.8),
|
|
167
|
+
chroma_range: Tuple[float, float] = (0.12, 0.18),
|
|
168
|
+
) -> List[str]:
|
|
169
|
+
"""Generate n colors with varied lightness and chroma for maximum distinction.
|
|
170
|
+
|
|
171
|
+
When you have many colors (>8), varying lightness and chroma in addition
|
|
172
|
+
to hue helps distinguish them. This function maximizes perceptual distance
|
|
173
|
+
between colors.
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
n: Number of colors to generate.
|
|
177
|
+
lightness_range: (min, max) lightness values.
|
|
178
|
+
chroma_range: (min, max) chroma values.
|
|
179
|
+
|
|
180
|
+
Returns:
|
|
181
|
+
List of n hex color strings optimized for visual distinction.
|
|
182
|
+
|
|
183
|
+
Example:
|
|
184
|
+
>>> sample_colors_varied(10) # Good for 10+ runs
|
|
185
|
+
"""
|
|
186
|
+
if n <= 0:
|
|
187
|
+
return []
|
|
188
|
+
|
|
189
|
+
colors = []
|
|
190
|
+
l_min, l_max = lightness_range
|
|
191
|
+
c_min, c_max = chroma_range
|
|
192
|
+
|
|
193
|
+
for i in range(n):
|
|
194
|
+
# Primary variation: hue
|
|
195
|
+
hue = (i * 360 / n) % 360
|
|
196
|
+
|
|
197
|
+
# Secondary variation: alternate lightness and chroma
|
|
198
|
+
# This creates a "zigzag" pattern in L-C space
|
|
199
|
+
t = i / max(n - 1, 1)
|
|
200
|
+
|
|
201
|
+
if i % 2 == 0:
|
|
202
|
+
lightness = l_min + (l_max - l_min) * (1 - t * 0.5)
|
|
203
|
+
chroma = c_min + (c_max - c_min) * t
|
|
204
|
+
else:
|
|
205
|
+
lightness = l_min + (l_max - l_min) * (0.5 + t * 0.5)
|
|
206
|
+
chroma = c_max - (c_max - c_min) * t * 0.5
|
|
207
|
+
|
|
208
|
+
colors.append(_oklch_to_hex(lightness, chroma, hue))
|
|
209
|
+
|
|
210
|
+
return colors
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
class ColorMap:
|
|
214
|
+
"""A callable color map that returns colors by index.
|
|
215
|
+
|
|
216
|
+
Convenient for use with enumerate() or dict comprehensions.
|
|
217
|
+
|
|
218
|
+
Example:
|
|
219
|
+
>>> cm = ColorMap(5)
|
|
220
|
+
>>> cm(0)
|
|
221
|
+
'#dc8a78'
|
|
222
|
+
>>> cm(2)
|
|
223
|
+
'#40c4aa'
|
|
224
|
+
>>> run_colors = {rid: cm(i) for i, rid in enumerate(run_ids)}
|
|
225
|
+
"""
|
|
226
|
+
|
|
227
|
+
def __init__(
|
|
228
|
+
self,
|
|
229
|
+
n: int,
|
|
230
|
+
lightness: float = 0.7,
|
|
231
|
+
chroma: float = 0.15,
|
|
232
|
+
hue_start: float = 0.0,
|
|
233
|
+
varied: bool = False,
|
|
234
|
+
):
|
|
235
|
+
"""Create a color map with n colors.
|
|
236
|
+
|
|
237
|
+
Args:
|
|
238
|
+
n: Number of colors in the palette.
|
|
239
|
+
lightness: OKLCH lightness (ignored if varied=True).
|
|
240
|
+
chroma: OKLCH chroma (ignored if varied=True).
|
|
241
|
+
hue_start: Starting hue angle.
|
|
242
|
+
varied: If True, use sample_colors_varied() for better distinction
|
|
243
|
+
with many colors (>8).
|
|
244
|
+
"""
|
|
245
|
+
if varied:
|
|
246
|
+
self._colors = sample_colors_varied(n)
|
|
247
|
+
else:
|
|
248
|
+
self._colors = sample_colors(n, lightness, chroma, hue_start)
|
|
249
|
+
|
|
250
|
+
def __call__(self, index: int) -> str:
|
|
251
|
+
"""Get color at index (wraps around if out of bounds)."""
|
|
252
|
+
if not self._colors:
|
|
253
|
+
return "#808080" # Gray fallback
|
|
254
|
+
return self._colors[index % len(self._colors)]
|
|
255
|
+
|
|
256
|
+
def __len__(self) -> int:
|
|
257
|
+
return len(self._colors)
|
|
258
|
+
|
|
259
|
+
def __iter__(self):
|
|
260
|
+
return iter(self._colors)
|
|
261
|
+
|
|
262
|
+
def __getitem__(self, index: int) -> str:
|
|
263
|
+
return self._colors[index]
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def colors_for_runs(
|
|
267
|
+
run_ids: List[str],
|
|
268
|
+
lightness: float = 0.7,
|
|
269
|
+
chroma: float = 0.15,
|
|
270
|
+
varied: bool = False,
|
|
271
|
+
) -> dict:
|
|
272
|
+
"""Generate a run_colors dict for a list of run IDs.
|
|
273
|
+
|
|
274
|
+
Convenience function that creates a complete run_colors mapping.
|
|
275
|
+
|
|
276
|
+
Args:
|
|
277
|
+
run_ids: List of run identifiers.
|
|
278
|
+
lightness: OKLCH lightness.
|
|
279
|
+
chroma: OKLCH chroma.
|
|
280
|
+
varied: Use varied lightness/chroma for many runs.
|
|
281
|
+
|
|
282
|
+
Returns:
|
|
283
|
+
Dict mapping run IDs to hex color strings.
|
|
284
|
+
|
|
285
|
+
Example:
|
|
286
|
+
>>> colors_for_runs(['train', 'eval', 'test'])
|
|
287
|
+
{'train': '#dc8a78', 'eval': '#5fba72', 'test': '#7a9ef7'}
|
|
288
|
+
"""
|
|
289
|
+
n = len(run_ids)
|
|
290
|
+
if varied or n > 8:
|
|
291
|
+
colors = sample_colors_varied(n)
|
|
292
|
+
else:
|
|
293
|
+
colors = sample_colors(n, lightness, chroma)
|
|
294
|
+
|
|
295
|
+
return {rid: colors[i] for i, rid in enumerate(run_ids)}
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
# =============================================================================
|
|
299
|
+
# Preset Palettes
|
|
300
|
+
# =============================================================================
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def palette_categorical(n: int) -> List[str]:
|
|
304
|
+
"""Generate a categorical palette optimized for charts.
|
|
305
|
+
|
|
306
|
+
Uses high chroma and medium lightness for maximum pop on white backgrounds.
|
|
307
|
+
"""
|
|
308
|
+
return sample_colors(n, lightness=0.65, chroma=0.18)
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def palette_sequential(n: int, hue: float = 250) -> List[str]:
|
|
312
|
+
"""Generate a sequential palette (light to dark) for ordered data.
|
|
313
|
+
|
|
314
|
+
All colors have the same hue but vary in lightness.
|
|
315
|
+
|
|
316
|
+
Args:
|
|
317
|
+
n: Number of colors.
|
|
318
|
+
hue: Base hue (default 250 = blue).
|
|
319
|
+
|
|
320
|
+
Returns:
|
|
321
|
+
List of colors from light to dark.
|
|
322
|
+
"""
|
|
323
|
+
if n <= 0:
|
|
324
|
+
return []
|
|
325
|
+
|
|
326
|
+
colors = []
|
|
327
|
+
for i in range(n):
|
|
328
|
+
# Lightness from 0.9 (light) to 0.35 (dark)
|
|
329
|
+
lightness = 0.9 - (i / max(n - 1, 1)) * 0.55
|
|
330
|
+
# Chroma increases slightly with darkness
|
|
331
|
+
chroma = 0.08 + (i / max(n - 1, 1)) * 0.12
|
|
332
|
+
colors.append(_oklch_to_hex(lightness, chroma, hue))
|
|
333
|
+
|
|
334
|
+
return colors
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def palette_diverging(
|
|
338
|
+
n: int, hue_low: float = 250, hue_high: float = 30
|
|
339
|
+
) -> List[str]:
|
|
340
|
+
"""Generate a diverging palette for data with a meaningful midpoint.
|
|
341
|
+
|
|
342
|
+
Goes from one hue through neutral to another hue.
|
|
343
|
+
|
|
344
|
+
Args:
|
|
345
|
+
n: Number of colors (odd numbers work best).
|
|
346
|
+
hue_low: Hue for low values (default 250 = blue).
|
|
347
|
+
hue_high: Hue for high values (default 30 = orange).
|
|
348
|
+
|
|
349
|
+
Returns:
|
|
350
|
+
List of colors diverging from center.
|
|
351
|
+
"""
|
|
352
|
+
if n <= 0:
|
|
353
|
+
return []
|
|
354
|
+
|
|
355
|
+
colors = []
|
|
356
|
+
mid = (n - 1) / 2
|
|
357
|
+
|
|
358
|
+
for i in range(n):
|
|
359
|
+
if i < mid:
|
|
360
|
+
# Low side: blue-ish
|
|
361
|
+
t = i / mid if mid > 0 else 0
|
|
362
|
+
lightness = 0.45 + t * 0.45 # Dark to light
|
|
363
|
+
chroma = 0.18 * (1 - t) # Saturated to neutral
|
|
364
|
+
hue = hue_low
|
|
365
|
+
elif i > mid:
|
|
366
|
+
# High side: orange-ish
|
|
367
|
+
t = (i - mid) / (n - 1 - mid) if n - 1 > mid else 0
|
|
368
|
+
lightness = 0.9 - t * 0.45 # Light to dark
|
|
369
|
+
chroma = 0.18 * t # Neutral to saturated
|
|
370
|
+
hue = hue_high
|
|
371
|
+
else:
|
|
372
|
+
# Midpoint: neutral
|
|
373
|
+
lightness = 0.9
|
|
374
|
+
chroma = 0.0
|
|
375
|
+
hue = 0
|
|
376
|
+
|
|
377
|
+
colors.append(_oklch_to_hex(lightness, chroma, hue))
|
|
378
|
+
|
|
379
|
+
return colors
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
# =============================================================================
|
|
383
|
+
# Color Utilities
|
|
384
|
+
# =============================================================================
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
def lighten(hex_color: str, amount: float = 0.1) -> str:
|
|
388
|
+
"""Lighten a hex color by increasing its OKLCH lightness.
|
|
389
|
+
|
|
390
|
+
Args:
|
|
391
|
+
hex_color: Input color as hex string (e.g., '#dc8a78').
|
|
392
|
+
amount: How much to lighten (0-1).
|
|
393
|
+
|
|
394
|
+
Returns:
|
|
395
|
+
Lightened hex color string.
|
|
396
|
+
"""
|
|
397
|
+
l, c, h = _hex_to_oklch(hex_color)
|
|
398
|
+
l = min(1.0, l + amount)
|
|
399
|
+
return _oklch_to_hex(l, c, h)
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
def darken(hex_color: str, amount: float = 0.1) -> str:
|
|
403
|
+
"""Darken a hex color by decreasing its OKLCH lightness.
|
|
404
|
+
|
|
405
|
+
Args:
|
|
406
|
+
hex_color: Input color as hex string.
|
|
407
|
+
amount: How much to darken (0-1).
|
|
408
|
+
|
|
409
|
+
Returns:
|
|
410
|
+
Darkened hex color string.
|
|
411
|
+
"""
|
|
412
|
+
l, c, h = _hex_to_oklch(hex_color)
|
|
413
|
+
l = max(0.0, l - amount)
|
|
414
|
+
return _oklch_to_hex(l, c, h)
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
def _hex_to_oklch(hex_color: str) -> Tuple[float, float, float]:
|
|
418
|
+
"""Convert hex color to OKLCH (approximate reverse conversion)."""
|
|
419
|
+
# Parse hex
|
|
420
|
+
hex_color = hex_color.lstrip("#")
|
|
421
|
+
r = int(hex_color[0:2], 16) / 255
|
|
422
|
+
g = int(hex_color[2:4], 16) / 255
|
|
423
|
+
b = int(hex_color[4:6], 16) / 255
|
|
424
|
+
|
|
425
|
+
# sRGB to linear
|
|
426
|
+
def to_linear(c):
|
|
427
|
+
return c / 12.92 if c <= 0.04045 else ((c + 0.055) / 1.055) ** 2.4
|
|
428
|
+
|
|
429
|
+
r_lin = to_linear(r)
|
|
430
|
+
g_lin = to_linear(g)
|
|
431
|
+
b_lin = to_linear(b)
|
|
432
|
+
|
|
433
|
+
# Linear sRGB to LMS
|
|
434
|
+
l = 0.4122214708 * r_lin + 0.5363325363 * g_lin + 0.0514459929 * b_lin
|
|
435
|
+
m = 0.2119034982 * r_lin + 0.6806995451 * g_lin + 0.1073969566 * b_lin
|
|
436
|
+
s = 0.0883024619 * r_lin + 0.2817188376 * g_lin + 0.6299787005 * b_lin
|
|
437
|
+
|
|
438
|
+
# LMS to OKLAB
|
|
439
|
+
l_ = l ** (1 / 3) if l >= 0 else -((-l) ** (1 / 3))
|
|
440
|
+
m_ = m ** (1 / 3) if m >= 0 else -((-m) ** (1 / 3))
|
|
441
|
+
s_ = s ** (1 / 3) if s >= 0 else -((-s) ** (1 / 3))
|
|
442
|
+
|
|
443
|
+
L = 0.2104542553 * l_ + 0.7936177850 * m_ - 0.0040720468 * s_
|
|
444
|
+
a = 1.9779984951 * l_ - 2.4285922050 * m_ + 0.4505937099 * s_
|
|
445
|
+
b_val = 0.0259040371 * l_ + 0.7827717662 * m_ - 0.8086757660 * s_
|
|
446
|
+
|
|
447
|
+
# OKLAB to OKLCH
|
|
448
|
+
C = math.sqrt(a * a + b_val * b_val)
|
|
449
|
+
H = math.degrees(math.atan2(b_val, a)) % 360
|
|
450
|
+
|
|
451
|
+
return (L, C, H)
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
# Copyright 2026 The TensorFlow Authors. All Rights Reserved.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
# ==============================================================================
|
|
15
|
+
"""Utility for writing TensorBoard default profiles from Python.
|
|
16
|
+
|
|
17
|
+
This module provides a simple API for training scripts to set default
|
|
18
|
+
TensorBoard dashboard configurations. When users load TensorBoard, the
|
|
19
|
+
default profile will be automatically applied.
|
|
20
|
+
|
|
21
|
+
Example usage:
|
|
22
|
+
|
|
23
|
+
from tensorbored.plugins.core import profile_writer
|
|
24
|
+
|
|
25
|
+
# Create a profile with pinned cards and run colors
|
|
26
|
+
profile = profile_writer.create_profile(
|
|
27
|
+
name="Training Dashboard",
|
|
28
|
+
pinned_cards=[
|
|
29
|
+
{"plugin": "scalars", "tag": "train/loss"},
|
|
30
|
+
{"plugin": "scalars", "tag": "train/accuracy"},
|
|
31
|
+
{"plugin": "scalars", "tag": "eval/loss"},
|
|
32
|
+
],
|
|
33
|
+
run_colors={
|
|
34
|
+
"train": "#2196F3", # Blue
|
|
35
|
+
"eval": "#4CAF50", # Green
|
|
36
|
+
},
|
|
37
|
+
tag_filter="loss|accuracy",
|
|
38
|
+
smoothing=0.8,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
# Write the profile to the logdir
|
|
42
|
+
profile_writer.write_profile(logdir, profile)
|
|
43
|
+
|
|
44
|
+
# Or use the convenience function
|
|
45
|
+
profile_writer.set_default_profile(
|
|
46
|
+
logdir,
|
|
47
|
+
pinned_cards=[{"plugin": "scalars", "tag": "train/loss"}],
|
|
48
|
+
run_colors={"train": "#ff0000"},
|
|
49
|
+
)
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
import json
|
|
53
|
+
import os
|
|
54
|
+
import time
|
|
55
|
+
from typing import Any, Dict, List, Optional
|
|
56
|
+
|
|
57
|
+
# Profile format version
|
|
58
|
+
PROFILE_VERSION = 1
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def create_profile(
|
|
62
|
+
name: str = "Default Profile",
|
|
63
|
+
pinned_cards: Optional[List[Dict[str, Any]]] = None,
|
|
64
|
+
run_colors: Optional[Dict[str, str]] = None,
|
|
65
|
+
group_colors: Optional[List[Dict[str, Any]]] = None,
|
|
66
|
+
superimposed_cards: Optional[List[Dict[str, Any]]] = None,
|
|
67
|
+
tag_filter: str = "",
|
|
68
|
+
run_filter: str = "",
|
|
69
|
+
smoothing: float = 0.6,
|
|
70
|
+
group_by: Optional[Dict[str, Any]] = None,
|
|
71
|
+
) -> Dict[str, Any]:
|
|
72
|
+
"""Create a TensorBoard profile dictionary.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
name: User-friendly name for the profile.
|
|
76
|
+
pinned_cards: List of cards to pin. Each card is a dict with:
|
|
77
|
+
- plugin: str (e.g., "scalars", "images", "histograms")
|
|
78
|
+
- tag: str (the tag name)
|
|
79
|
+
- runId: str (optional, for single-run plugins)
|
|
80
|
+
- sample: int (optional, for sampled plugins like images)
|
|
81
|
+
run_colors: Dict mapping run names/IDs to hex color strings
|
|
82
|
+
(e.g., {"run1": "#ff0000", "run2": "#00ff00"}).
|
|
83
|
+
group_colors: List of group color assignments. Each entry is a dict:
|
|
84
|
+
- groupKey: str
|
|
85
|
+
- colorId: int
|
|
86
|
+
superimposed_cards: List of superimposed card definitions. Each is:
|
|
87
|
+
- id: str (unique identifier)
|
|
88
|
+
- title: str (display title)
|
|
89
|
+
- tags: List[str] (scalar tags to combine)
|
|
90
|
+
- runId: Optional[str] (run filter, or None for all runs)
|
|
91
|
+
tag_filter: Regex pattern to filter tags.
|
|
92
|
+
run_filter: Regex pattern to filter runs.
|
|
93
|
+
smoothing: Scalar smoothing value (0.0 to 0.999).
|
|
94
|
+
group_by: Grouping configuration dict with:
|
|
95
|
+
- key: str ("RUN", "EXPERIMENT", "REGEX", or "REGEX_BY_EXP")
|
|
96
|
+
- regexString: str (optional, for REGEX/REGEX_BY_EXP)
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
A profile dictionary ready to be written to the logdir.
|
|
100
|
+
"""
|
|
101
|
+
# Convert run_colors dict to list format
|
|
102
|
+
run_color_entries = []
|
|
103
|
+
if run_colors:
|
|
104
|
+
for run_id, color in run_colors.items():
|
|
105
|
+
run_color_entries.append({"runId": run_id, "color": color})
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
"version": PROFILE_VERSION,
|
|
109
|
+
"data": {
|
|
110
|
+
"version": PROFILE_VERSION,
|
|
111
|
+
"name": name,
|
|
112
|
+
"lastModifiedTimestamp": int(time.time() * 1000),
|
|
113
|
+
"pinnedCards": pinned_cards or [],
|
|
114
|
+
"runColors": run_color_entries,
|
|
115
|
+
"groupColors": group_colors or [],
|
|
116
|
+
"superimposedCards": superimposed_cards or [],
|
|
117
|
+
"tagFilter": tag_filter,
|
|
118
|
+
"runFilter": run_filter,
|
|
119
|
+
"smoothing": smoothing,
|
|
120
|
+
"groupBy": group_by,
|
|
121
|
+
},
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def write_profile(logdir: str, profile: Dict[str, Any]) -> str:
|
|
126
|
+
"""Write a profile to the logdir.
|
|
127
|
+
|
|
128
|
+
The profile will be written to `<logdir>/.tensorboard/default_profile.json`.
|
|
129
|
+
When TensorBoard starts with this logdir, it will offer this profile
|
|
130
|
+
as the default dashboard configuration.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
logdir: The TensorBoard log directory.
|
|
134
|
+
profile: A profile dictionary (from create_profile or manually created).
|
|
135
|
+
|
|
136
|
+
Returns:
|
|
137
|
+
The path to the written profile file.
|
|
138
|
+
|
|
139
|
+
Raises:
|
|
140
|
+
ValueError: If the profile is missing required fields.
|
|
141
|
+
OSError: If unable to write to the logdir.
|
|
142
|
+
"""
|
|
143
|
+
if "version" not in profile:
|
|
144
|
+
raise ValueError("Profile must have a 'version' field")
|
|
145
|
+
|
|
146
|
+
profile_dir = os.path.join(logdir, ".tensorboard")
|
|
147
|
+
os.makedirs(profile_dir, exist_ok=True)
|
|
148
|
+
|
|
149
|
+
profile_path = os.path.join(profile_dir, "default_profile.json")
|
|
150
|
+
with open(profile_path, "w", encoding="utf-8") as f:
|
|
151
|
+
json.dump(profile, f, indent=2)
|
|
152
|
+
|
|
153
|
+
return profile_path
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def read_profile(logdir: str) -> Optional[Dict[str, Any]]:
|
|
157
|
+
"""Read the default profile from a logdir.
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
logdir: The TensorBoard log directory.
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
The profile dictionary, or None if no profile exists.
|
|
164
|
+
"""
|
|
165
|
+
profile_path = os.path.join(logdir, ".tensorboard", "default_profile.json")
|
|
166
|
+
if not os.path.exists(profile_path):
|
|
167
|
+
return None
|
|
168
|
+
|
|
169
|
+
try:
|
|
170
|
+
with open(profile_path, "r", encoding="utf-8") as f:
|
|
171
|
+
return json.load(f)
|
|
172
|
+
except (json.JSONDecodeError, OSError):
|
|
173
|
+
return None
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def set_default_profile(
|
|
177
|
+
logdir: str,
|
|
178
|
+
name: str = "Default Profile",
|
|
179
|
+
pinned_cards: Optional[List[Dict[str, Any]]] = None,
|
|
180
|
+
run_colors: Optional[Dict[str, str]] = None,
|
|
181
|
+
group_colors: Optional[List[Dict[str, Any]]] = None,
|
|
182
|
+
superimposed_cards: Optional[List[Dict[str, Any]]] = None,
|
|
183
|
+
tag_filter: str = "",
|
|
184
|
+
run_filter: str = "",
|
|
185
|
+
smoothing: float = 0.6,
|
|
186
|
+
group_by: Optional[Dict[str, Any]] = None,
|
|
187
|
+
) -> str:
|
|
188
|
+
"""Convenience function to create and write a profile in one call.
|
|
189
|
+
|
|
190
|
+
Args:
|
|
191
|
+
logdir: The TensorBoard log directory.
|
|
192
|
+
name: User-friendly name for the profile.
|
|
193
|
+
pinned_cards: List of cards to pin (see create_profile).
|
|
194
|
+
run_colors: Dict mapping run names to hex colors.
|
|
195
|
+
group_colors: List of group color assignments.
|
|
196
|
+
superimposed_cards: List of superimposed card definitions.
|
|
197
|
+
tag_filter: Regex pattern to filter tags.
|
|
198
|
+
run_filter: Regex pattern to filter runs.
|
|
199
|
+
smoothing: Scalar smoothing value.
|
|
200
|
+
group_by: Grouping configuration.
|
|
201
|
+
|
|
202
|
+
Returns:
|
|
203
|
+
The path to the written profile file.
|
|
204
|
+
"""
|
|
205
|
+
profile = create_profile(
|
|
206
|
+
name=name,
|
|
207
|
+
pinned_cards=pinned_cards,
|
|
208
|
+
run_colors=run_colors,
|
|
209
|
+
group_colors=group_colors,
|
|
210
|
+
superimposed_cards=superimposed_cards,
|
|
211
|
+
tag_filter=tag_filter,
|
|
212
|
+
run_filter=run_filter,
|
|
213
|
+
smoothing=smoothing,
|
|
214
|
+
group_by=group_by,
|
|
215
|
+
)
|
|
216
|
+
return write_profile(logdir, profile)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def pin_scalar(tag: str) -> Dict[str, str]:
|
|
220
|
+
"""Helper to create a pinned scalar card entry.
|
|
221
|
+
|
|
222
|
+
Args:
|
|
223
|
+
tag: The scalar tag name (e.g., "train/loss").
|
|
224
|
+
|
|
225
|
+
Returns:
|
|
226
|
+
A dict suitable for the pinned_cards list.
|
|
227
|
+
"""
|
|
228
|
+
return {"plugin": "scalars", "tag": tag}
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def pin_histogram(tag: str, run_id: str) -> Dict[str, str]:
|
|
232
|
+
"""Helper to create a pinned histogram card entry.
|
|
233
|
+
|
|
234
|
+
Args:
|
|
235
|
+
tag: The histogram tag name.
|
|
236
|
+
run_id: The run ID (required for histograms).
|
|
237
|
+
|
|
238
|
+
Returns:
|
|
239
|
+
A dict suitable for the pinned_cards list.
|
|
240
|
+
"""
|
|
241
|
+
return {"plugin": "histograms", "tag": tag, "runId": run_id}
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def pin_image(tag: str, run_id: str, sample: int = 0) -> Dict[str, Any]:
|
|
245
|
+
"""Helper to create a pinned image card entry.
|
|
246
|
+
|
|
247
|
+
Args:
|
|
248
|
+
tag: The image tag name.
|
|
249
|
+
run_id: The run ID (required for images).
|
|
250
|
+
sample: The sample index (default 0).
|
|
251
|
+
|
|
252
|
+
Returns:
|
|
253
|
+
A dict suitable for the pinned_cards list.
|
|
254
|
+
"""
|
|
255
|
+
return {"plugin": "images", "tag": tag, "runId": run_id, "sample": sample}
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def create_superimposed_card(
|
|
259
|
+
title: str,
|
|
260
|
+
tags: List[str],
|
|
261
|
+
run_id: Optional[str] = None,
|
|
262
|
+
) -> Dict[str, Any]:
|
|
263
|
+
"""Helper to create a superimposed card entry.
|
|
264
|
+
|
|
265
|
+
Superimposed cards combine multiple scalar tags on a single plot.
|
|
266
|
+
|
|
267
|
+
Args:
|
|
268
|
+
title: Display title for the card.
|
|
269
|
+
tags: List of scalar tag names to superimpose.
|
|
270
|
+
run_id: Optional run ID filter (None shows all runs).
|
|
271
|
+
|
|
272
|
+
Returns:
|
|
273
|
+
A dict suitable for the superimposed_cards list.
|
|
274
|
+
"""
|
|
275
|
+
return {
|
|
276
|
+
"id": f"superimposed-{int(time.time() * 1000)}",
|
|
277
|
+
"title": title,
|
|
278
|
+
"tags": tags,
|
|
279
|
+
"runId": run_id,
|
|
280
|
+
}
|
tensorbored/version.py
CHANGED
{tensorbored-2.21.0rc1769988660.dist-info → tensorbored-2.21.0rc1770021042.dist-info}/RECORD
RENAMED
|
@@ -13,7 +13,7 @@ tensorbored/manager.py,sha256=6LKijMpiKUtPTpApEJnQby113MXM55-G_guhulGQsdA,16300
|
|
|
13
13
|
tensorbored/notebook.py,sha256=zO8ruL979Lc1a6mGOW0J-o97RjbmIZ2nWci3l0y1wLA,14698
|
|
14
14
|
tensorbored/plugin_util.py,sha256=DLJzEy9_50Iq-o14JGz1Q0DwaND9b64Q7BmDROyqZyQ,8376
|
|
15
15
|
tensorbored/program.py,sha256=GHDG5FqtdJtAknzQekWGTYFXwpEnHXWHlAZoidN8Kmo,35500
|
|
16
|
-
tensorbored/version.py,sha256=
|
|
16
|
+
tensorbored/version.py,sha256=gIqOTSQDg2IAuXrWCI90-ktmVw68maIXrNjnEo1zqBQ,804
|
|
17
17
|
tensorbored/webfiles.zip,sha256=1bIiTAQHXahMebNZdnQdctde_hd9Ws8oF69o-dFT_1g,4999505
|
|
18
18
|
tensorbored/_vendor/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
19
19
|
tensorbored/_vendor/bleach/__init__.py,sha256=T_iQHpeDCQpz55IVNEsktpOzw3QHvwj7rfTj34bWWKc,3689
|
|
@@ -156,7 +156,9 @@ tensorbored/plugins/audio/plugin_data_pb2.py,sha256=YwUPbBSQhslWWQKbUINEX8PSitC5
|
|
|
156
156
|
tensorbored/plugins/audio/summary.py,sha256=w1xrRP-GFXWzgrY1MS9xxKWiAD0tBe8xYWZZ7dXM7A8,8572
|
|
157
157
|
tensorbored/plugins/audio/summary_v2.py,sha256=LxtBrrU3qvHjmD7UjylFTIZHhcaSNrkdhzL9LaZX3oY,5453
|
|
158
158
|
tensorbored/plugins/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
159
|
+
tensorbored/plugins/core/color_sampler.py,sha256=5xq5IFr9-n3azp044qAH4QwdLUiS0feuZT9puBZUDak,14035
|
|
159
160
|
tensorbored/plugins/core/core_plugin.py,sha256=ID72vqZrJ--Ww8MIH4jHmJdWFQt1lxC7QAyQbKHG9Ag,35920
|
|
161
|
+
tensorbored/plugins/core/profile_writer.py,sha256=y4x8D1nzsFxwgizD9exjsv-RB2RSulMJ3rwVtUx6-AE,9320
|
|
160
162
|
tensorbored/plugins/custom_scalar/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
161
163
|
tensorbored/plugins/custom_scalar/custom_scalars_plugin.py,sha256=vMJS3hBPBXxpd5zw4-LS1HxkHgB12-18cHZf3TXKmuM,11753
|
|
162
164
|
tensorbored/plugins/custom_scalar/layout_pb2.py,sha256=9VfNLBWhLxEQvoOS-r0hgc9VabMBMr4hPiy1oVqwJgE,4340
|
|
@@ -263,9 +265,9 @@ tensorbored/util/platform_util.py,sha256=5jr42kvi6GqRzFx6YponSDOFYg7_RL-DTlk6dDe
|
|
|
263
265
|
tensorbored/util/tb_logging.py,sha256=LNfFN1qZqAoZd6OaBWE_L_bqOqA_i1m9xj8lIZGQ_G4,780
|
|
264
266
|
tensorbored/util/tensor_util.py,sha256=KfR_OMxxUjb8mp7yUH-x8CnEd0C8yxwHZ-vBQYDrdyE,21752
|
|
265
267
|
tensorbored/util/timing.py,sha256=DduPKf8izD47D-UJ6D3xlcLDqI5WTAXK2d3WDmb3Abg,3734
|
|
266
|
-
tensorbored-2.21.
|
|
267
|
-
tensorbored-2.21.
|
|
268
|
-
tensorbored-2.21.
|
|
269
|
-
tensorbored-2.21.
|
|
270
|
-
tensorbored-2.21.
|
|
271
|
-
tensorbored-2.21.
|
|
268
|
+
tensorbored-2.21.0rc1770021042.dist-info/licenses/LICENSE,sha256=1Y0t0nxgtnadUpTCqZlfVLWaaCju6MmbbODuxXOy-Rg,37111
|
|
269
|
+
tensorbored-2.21.0rc1770021042.dist-info/METADATA,sha256=fqNWOS3n1BB93t6wk1ENAR-EniFsdgEFR8c7gB6YnGg,1781
|
|
270
|
+
tensorbored-2.21.0rc1770021042.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
271
|
+
tensorbored-2.21.0rc1770021042.dist-info/entry_points.txt,sha256=WiMjmdrTDkZXBlZmKWt1Kz0FCymATWUZjyTHEW-v3LU,196
|
|
272
|
+
tensorbored-2.21.0rc1770021042.dist-info/top_level.txt,sha256=F6oc1TBDSM0Hq_5vyIWijSnNewbXEBnBMS15t2S1jaY,12
|
|
273
|
+
tensorbored-2.21.0rc1770021042.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tensorbored-2.21.0rc1769988660.dist-info → tensorbored-2.21.0rc1770021042.dist-info}/top_level.txt
RENAMED
|
File without changes
|