flopscope 0.2.0__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.
- benchmarks/__init__.py +1 -0
- benchmarks/__main__.py +6 -0
- benchmarks/_baseline.py +171 -0
- benchmarks/_bitwise.py +231 -0
- benchmarks/_complex.py +176 -0
- benchmarks/_contractions.py +291 -0
- benchmarks/_fft.py +198 -0
- benchmarks/_impl_urls.py +139 -0
- benchmarks/_linalg.py +197 -0
- benchmarks/_linalg_delegates.py +407 -0
- benchmarks/_metadata.py +141 -0
- benchmarks/_misc.py +653 -0
- benchmarks/_perf.py +321 -0
- benchmarks/_perm_group_calibration.py +175 -0
- benchmarks/_pointwise.py +372 -0
- benchmarks/_polynomial.py +193 -0
- benchmarks/_random.py +209 -0
- benchmarks/_reductions.py +136 -0
- benchmarks/_sorting.py +289 -0
- benchmarks/_stats.py +137 -0
- benchmarks/_window.py +92 -0
- benchmarks/accumulation/__init__.py +0 -0
- benchmarks/accumulation/bench_cost_compute.py +138 -0
- benchmarks/dashboard.py +312 -0
- benchmarks/runner.py +636 -0
- flopscope/__init__.py +273 -0
- flopscope/_accumulation/__init__.py +13 -0
- flopscope/_accumulation/_bipartite.py +121 -0
- flopscope/_accumulation/_burnside.py +51 -0
- flopscope/_accumulation/_cache.py +146 -0
- flopscope/_accumulation/_components.py +153 -0
- flopscope/_accumulation/_cost.py +1414 -0
- flopscope/_accumulation/_cost_descriptions.py +63 -0
- flopscope/_accumulation/_detection.py +318 -0
- flopscope/_accumulation/_ladder.py +191 -0
- flopscope/_accumulation/_output_orbit.py +104 -0
- flopscope/_accumulation/_partition.py +290 -0
- flopscope/_accumulation/_path_info.py +211 -0
- flopscope/_accumulation/_public.py +169 -0
- flopscope/_accumulation/_reduction.py +310 -0
- flopscope/_accumulation/_regimes.py +303 -0
- flopscope/_accumulation/_shape.py +33 -0
- flopscope/_accumulation/_wreath.py +209 -0
- flopscope/_budget.py +1027 -0
- flopscope/_config.py +118 -0
- flopscope/_counting_ops.py +451 -0
- flopscope/_display.py +478 -0
- flopscope/_docstrings.py +59 -0
- flopscope/_dtypes.py +20 -0
- flopscope/_einsum.py +717 -0
- flopscope/_errstate.py +25 -0
- flopscope/_flops.py +282 -0
- flopscope/_free_ops.py +2654 -0
- flopscope/_ndarray.py +1126 -0
- flopscope/_opt_einsum/LICENSE +21 -0
- flopscope/_opt_einsum/NOTICE +59 -0
- flopscope/_opt_einsum/__init__.py +209 -0
- flopscope/_opt_einsum/_contract.py +1478 -0
- flopscope/_opt_einsum/_helpers.py +164 -0
- flopscope/_opt_einsum/_hsluv.py +273 -0
- flopscope/_opt_einsum/_path_random.py +462 -0
- flopscope/_opt_einsum/_paths.py +1653 -0
- flopscope/_opt_einsum/_subgraph_symmetry.py +544 -0
- flopscope/_opt_einsum/_symmetry.py +140 -0
- flopscope/_opt_einsum/_typing.py +37 -0
- flopscope/_perm_group.py +717 -0
- flopscope/_pointwise.py +2522 -0
- flopscope/_polynomial.py +278 -0
- flopscope/_registry.py +3216 -0
- flopscope/_sorting_ops.py +571 -0
- flopscope/_symmetric.py +812 -0
- flopscope/_symmetry_transport.py +510 -0
- flopscope/_symmetry_utils.py +669 -0
- flopscope/_type_info.py +12 -0
- flopscope/_unwrap.py +70 -0
- flopscope/_validation.py +83 -0
- flopscope/_version_check.py +46 -0
- flopscope/_weights.py +195 -0
- flopscope/_window.py +177 -0
- flopscope/accounting.py +565 -0
- flopscope/data/default_weights.json +462 -0
- flopscope/data/weights.csv +509 -0
- flopscope/errors.py +197 -0
- flopscope/numpy/__init__.py +878 -0
- flopscope/numpy/fft/__init__.py +55 -0
- flopscope/numpy/fft/_free.py +51 -0
- flopscope/numpy/fft/_transforms.py +695 -0
- flopscope/numpy/linalg/__init__.py +105 -0
- flopscope/numpy/linalg/_aliases.py +126 -0
- flopscope/numpy/linalg/_compound.py +161 -0
- flopscope/numpy/linalg/_decompositions.py +353 -0
- flopscope/numpy/linalg/_properties.py +533 -0
- flopscope/numpy/linalg/_solvers.py +444 -0
- flopscope/numpy/linalg/_svd.py +122 -0
- flopscope/numpy/random/__init__.py +684 -0
- flopscope/numpy/random/_cost_formulas.py +115 -0
- flopscope/numpy/random/_counted_classes.py +241 -0
- flopscope/numpy/testing/__init__.py +13 -0
- flopscope/numpy/typing/__init__.py +30 -0
- flopscope/py.typed +0 -0
- flopscope/stats/__init__.py +84 -0
- flopscope/stats/_base.py +77 -0
- flopscope/stats/_cauchy.py +146 -0
- flopscope/stats/_erf.py +190 -0
- flopscope/stats/_expon.py +146 -0
- flopscope/stats/_laplace.py +150 -0
- flopscope/stats/_logistic.py +148 -0
- flopscope/stats/_lognorm.py +160 -0
- flopscope/stats/_ndtri.py +133 -0
- flopscope/stats/_norm.py +149 -0
- flopscope/stats/_truncnorm.py +186 -0
- flopscope/stats/_uniform.py +141 -0
- flopscope-0.2.0.dist-info/METADATA +23 -0
- flopscope-0.2.0.dist-info/RECORD +115 -0
- flopscope-0.2.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"""Contains helper functions for opt_einsum testing scripts."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Collection, Iterable, Sequence
|
|
4
|
+
from typing import Any, overload
|
|
5
|
+
|
|
6
|
+
# Inline type aliases (formerly from ._typing, deleted in Task 7+8).
|
|
7
|
+
ArrayIndexType = frozenset # frozenset[str]
|
|
8
|
+
ArrayType = object # Any
|
|
9
|
+
|
|
10
|
+
__all__ = ["compute_size_by_dict", "find_contraction", "flop_count"]
|
|
11
|
+
|
|
12
|
+
_valid_chars = "abcdefghijklmopqABC"
|
|
13
|
+
_sizes = [2, 3, 4, 5, 4, 3, 2, 6, 5, 4, 3, 2, 5, 7, 4, 3, 2, 3, 4]
|
|
14
|
+
_default_dim_dict = dict(zip(_valid_chars, _sizes, strict=False))
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@overload
|
|
18
|
+
def compute_size_by_dict(indices: Iterable[int], idx_dict: list[int]) -> int: ...
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@overload
|
|
22
|
+
def compute_size_by_dict(indices: Collection[str], idx_dict: dict[str, int]) -> int: ...
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def compute_size_by_dict(indices: Any, idx_dict: Any) -> int:
|
|
26
|
+
"""Computes the product of the elements in indices based on the dictionary
|
|
27
|
+
idx_dict.
|
|
28
|
+
|
|
29
|
+
Parameters
|
|
30
|
+
----------
|
|
31
|
+
indices : iterable
|
|
32
|
+
Indices to base the product on.
|
|
33
|
+
idx_dict : dictionary
|
|
34
|
+
Dictionary of index _sizes
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
-------
|
|
38
|
+
ret : int
|
|
39
|
+
The resulting product.
|
|
40
|
+
|
|
41
|
+
Examples:
|
|
42
|
+
--------
|
|
43
|
+
>>> compute_size_by_dict('abbc', {'a': 2, 'b':3, 'c':5})
|
|
44
|
+
90
|
|
45
|
+
|
|
46
|
+
"""
|
|
47
|
+
ret = 1
|
|
48
|
+
for i in indices: # lgtm [py/iteration-string-and-sequence]
|
|
49
|
+
ret *= idx_dict[i]
|
|
50
|
+
return ret
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def find_contraction(
|
|
54
|
+
positions: Collection[int],
|
|
55
|
+
input_sets: list[ArrayIndexType],
|
|
56
|
+
output_set: ArrayIndexType,
|
|
57
|
+
) -> tuple[frozenset[str], list[ArrayIndexType], ArrayIndexType, ArrayIndexType]:
|
|
58
|
+
"""Finds the contraction for a given set of input and output sets.
|
|
59
|
+
|
|
60
|
+
Parameters
|
|
61
|
+
----------
|
|
62
|
+
positions : iterable
|
|
63
|
+
Integer positions of terms used in the contraction.
|
|
64
|
+
input_sets : list
|
|
65
|
+
List of sets that represent the lhs side of the einsum subscript
|
|
66
|
+
output_set : set
|
|
67
|
+
Set that represents the rhs side of the overall einsum subscript
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
-------
|
|
71
|
+
new_result : set
|
|
72
|
+
The indices of the resulting contraction
|
|
73
|
+
remaining : list
|
|
74
|
+
List of sets that have not been contracted, the new set is appended to
|
|
75
|
+
the end of this list
|
|
76
|
+
idx_removed : set
|
|
77
|
+
Indices removed from the entire contraction
|
|
78
|
+
idx_contraction : set
|
|
79
|
+
The indices used in the current contraction
|
|
80
|
+
|
|
81
|
+
Examples:
|
|
82
|
+
--------
|
|
83
|
+
# A simple dot product test case
|
|
84
|
+
>>> pos = (0, 1)
|
|
85
|
+
>>> isets = [set('ab'), set('bc')]
|
|
86
|
+
>>> oset = set('ac')
|
|
87
|
+
>>> find_contraction(pos, isets, oset)
|
|
88
|
+
({'a', 'c'}, [{'a', 'c'}], {'b'}, {'a', 'b', 'c'})
|
|
89
|
+
|
|
90
|
+
# A more complex case with additional terms in the contraction
|
|
91
|
+
>>> pos = (0, 2)
|
|
92
|
+
>>> isets = [set('abd'), set('ac'), set('bdc')]
|
|
93
|
+
>>> oset = set('ac')
|
|
94
|
+
>>> find_contraction(pos, isets, oset)
|
|
95
|
+
({'a', 'c'}, [{'a', 'c'}, {'a', 'c'}], {'b', 'd'}, {'a', 'b', 'c', 'd'})
|
|
96
|
+
"""
|
|
97
|
+
remaining = list(input_sets)
|
|
98
|
+
inputs = (remaining.pop(i) for i in sorted(positions, reverse=True))
|
|
99
|
+
idx_contract = frozenset.union(*inputs)
|
|
100
|
+
idx_remain = output_set.union(*remaining)
|
|
101
|
+
|
|
102
|
+
new_result = idx_remain & idx_contract
|
|
103
|
+
idx_removed = idx_contract - new_result
|
|
104
|
+
remaining.append(new_result)
|
|
105
|
+
|
|
106
|
+
return new_result, remaining, idx_removed, idx_contract
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def flop_count(
|
|
110
|
+
idx_contraction: Collection[str],
|
|
111
|
+
inner: bool,
|
|
112
|
+
num_terms: int,
|
|
113
|
+
size_dictionary: dict[str, int],
|
|
114
|
+
*,
|
|
115
|
+
input_subscripts: Sequence[str] | None = None,
|
|
116
|
+
output_subscript: str | None = None,
|
|
117
|
+
input_shapes: Sequence[Sequence[int]] | None = None,
|
|
118
|
+
) -> int:
|
|
119
|
+
"""Per-step dense FLOP count (no declared symmetry).
|
|
120
|
+
|
|
121
|
+
Routes through compute_accumulation_cost so the result is consistent
|
|
122
|
+
with what ``einsum_accumulation_cost`` would return for an einsum with
|
|
123
|
+
no declared input symmetry. The α/M formula produces textbook FMA=2
|
|
124
|
+
scalar-op counts (multiplies and adds counted separately, with the
|
|
125
|
+
off-by-one accumulator-init credit applied).
|
|
126
|
+
|
|
127
|
+
Legacy fallback (when subscripts/shapes are not provided): the old
|
|
128
|
+
overall_size * op_factor formula with FMA=1 (inner does not add an extra
|
|
129
|
+
op).
|
|
130
|
+
|
|
131
|
+
Examples:
|
|
132
|
+
--------
|
|
133
|
+
>>> flop_count('abc', False, 1, {'a': 2, 'b':3, 'c':5})
|
|
134
|
+
30
|
|
135
|
+
|
|
136
|
+
>>> flop_count('abc', True, 2, {'a': 2, 'b':3, 'c':5})
|
|
137
|
+
30
|
|
138
|
+
|
|
139
|
+
"""
|
|
140
|
+
if input_subscripts is None or input_shapes is None:
|
|
141
|
+
overall_size = compute_size_by_dict(idx_contraction, size_dictionary)
|
|
142
|
+
op_factor = max(1, num_terms - 1)
|
|
143
|
+
return overall_size * op_factor
|
|
144
|
+
|
|
145
|
+
from flopscope._accumulation._cost import compute_accumulation_cost
|
|
146
|
+
|
|
147
|
+
canonical = ",".join(input_subscripts) + "->" + (output_subscript or "")
|
|
148
|
+
per_op_syms = (None,) * len(input_subscripts)
|
|
149
|
+
cost = compute_accumulation_cost(
|
|
150
|
+
canonical_subscripts=canonical,
|
|
151
|
+
input_parts=tuple(input_subscripts),
|
|
152
|
+
output_subscript=output_subscript or "",
|
|
153
|
+
shapes=tuple(tuple(s) for s in input_shapes),
|
|
154
|
+
per_op_symmetries=per_op_syms,
|
|
155
|
+
identity_pattern=None,
|
|
156
|
+
)
|
|
157
|
+
return max(cost.total, 1)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def has_array_interface(array: ArrayType) -> ArrayType:
|
|
161
|
+
if hasattr(array, "__array_interface__"):
|
|
162
|
+
return True
|
|
163
|
+
else:
|
|
164
|
+
return False
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
"""Minimal HSLuv helpers for terminal label palettes.
|
|
2
|
+
|
|
3
|
+
Adapted from the MIT-licensed ``hsluv-python`` reference implementation:
|
|
4
|
+
https://github.com/hsluv/hsluv-python
|
|
5
|
+
|
|
6
|
+
The palette construction idea mirrors seaborn's ``husl_palette`` API, but
|
|
7
|
+
this module does not vendor seaborn code or add a seaborn dependency.
|
|
8
|
+
See NOTICE in this package for attribution details.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import math
|
|
14
|
+
from functools import cache
|
|
15
|
+
|
|
16
|
+
_M = (
|
|
17
|
+
(3.240969941904521, -1.537383177570093, -0.498610760293),
|
|
18
|
+
(-0.96924363628087, 1.87596750150772, 0.041555057407175),
|
|
19
|
+
(0.055630079696993, -0.20397695888897, 1.056971514242878),
|
|
20
|
+
)
|
|
21
|
+
_M_INV = (
|
|
22
|
+
(0.41239079926595, 0.35758433938387, 0.18048078840183),
|
|
23
|
+
(0.21263900587151, 0.71516867876775, 0.072192315360733),
|
|
24
|
+
(0.019330818715591, 0.11919477979462, 0.95053215224966),
|
|
25
|
+
)
|
|
26
|
+
_REF_Y = 1.0
|
|
27
|
+
_REF_U = 0.19783000664283
|
|
28
|
+
_REF_V = 0.46831999493879
|
|
29
|
+
_KAPPA = 903.2962962
|
|
30
|
+
_EPSILON = 0.0088564516
|
|
31
|
+
|
|
32
|
+
# Frozen to preserve the current Rich label colors. Keep this in sync with
|
|
33
|
+
# qualitative_hsluv_palette(64, lightness=0.5, saturation=0.9).
|
|
34
|
+
PRECOMPUTED_QUALITATIVE_HSLUV_PALETTE_64 = (
|
|
35
|
+
"#E3245A",
|
|
36
|
+
"#2F70EF",
|
|
37
|
+
"#228921",
|
|
38
|
+
"#B92FED",
|
|
39
|
+
"#987022",
|
|
40
|
+
"#268389",
|
|
41
|
+
"#7B5EF0",
|
|
42
|
+
"#D529A6",
|
|
43
|
+
"#D14923",
|
|
44
|
+
"#5E8222",
|
|
45
|
+
"#238755",
|
|
46
|
+
"#2A7CBC",
|
|
47
|
+
"#E7232C",
|
|
48
|
+
"#9D4AF0",
|
|
49
|
+
"#5569F0",
|
|
50
|
+
"#DD2680",
|
|
51
|
+
"#C82CCA",
|
|
52
|
+
"#B75F22",
|
|
53
|
+
"#7C7B22",
|
|
54
|
+
"#408722",
|
|
55
|
+
"#24856F",
|
|
56
|
+
"#22893B",
|
|
57
|
+
"#2C76D5",
|
|
58
|
+
"#2880A3",
|
|
59
|
+
"#E62443",
|
|
60
|
+
"#DF3623",
|
|
61
|
+
"#6864F0",
|
|
62
|
+
"#D92893",
|
|
63
|
+
"#8D55F0",
|
|
64
|
+
"#AC3DF0",
|
|
65
|
+
"#426DF0",
|
|
66
|
+
"#E1256D",
|
|
67
|
+
"#CF2AB8",
|
|
68
|
+
"#C12EDC",
|
|
69
|
+
"#A86822",
|
|
70
|
+
"#C55522",
|
|
71
|
+
"#6D7E22",
|
|
72
|
+
"#4F8422",
|
|
73
|
+
"#318822",
|
|
74
|
+
"#8A7622",
|
|
75
|
+
"#2E73E2",
|
|
76
|
+
"#25847C",
|
|
77
|
+
"#278196",
|
|
78
|
+
"#228848",
|
|
79
|
+
"#238662",
|
|
80
|
+
"#22892E",
|
|
81
|
+
"#2B79C8",
|
|
82
|
+
"#297EAF",
|
|
83
|
+
"#E42B23",
|
|
84
|
+
"#D84023",
|
|
85
|
+
"#E5244F",
|
|
86
|
+
"#E72337",
|
|
87
|
+
"#A544F0",
|
|
88
|
+
"#845AF0",
|
|
89
|
+
"#C52DD3",
|
|
90
|
+
"#CC2BC1",
|
|
91
|
+
"#DC278A",
|
|
92
|
+
"#5F67F0",
|
|
93
|
+
"#7161F0",
|
|
94
|
+
"#D22AAF",
|
|
95
|
+
"#9550F0",
|
|
96
|
+
"#D7289D",
|
|
97
|
+
"#DF2676",
|
|
98
|
+
"#4B6BF0",
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _distance_line_from_origin(line: tuple[float, float]) -> float:
|
|
103
|
+
slope, intercept = line
|
|
104
|
+
return abs(intercept) / math.sqrt(slope**2 + 1)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _length_of_ray_until_intersect(theta: float, line: tuple[float, float]) -> float:
|
|
108
|
+
slope, intercept = line
|
|
109
|
+
return intercept / (math.sin(theta) - slope * math.cos(theta))
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _get_bounds(lightness: float) -> list[tuple[float, float]]:
|
|
113
|
+
result = []
|
|
114
|
+
sub1 = ((lightness + 16) ** 3) / 1560896
|
|
115
|
+
sub2 = sub1 if sub1 > _EPSILON else lightness / _KAPPA
|
|
116
|
+
|
|
117
|
+
for channel in range(3):
|
|
118
|
+
m1, m2, m3 = _M[channel]
|
|
119
|
+
for t in range(2):
|
|
120
|
+
top1 = (284517 * m1 - 94839 * m3) * sub2
|
|
121
|
+
top2 = (
|
|
122
|
+
838422 * m3 + 769860 * m2 + 731718 * m1
|
|
123
|
+
) * lightness * sub2 - 769860 * t * lightness
|
|
124
|
+
bottom = (632260 * m3 - 126452 * m2) * sub2 + 126452 * t
|
|
125
|
+
result.append((top1 / bottom, top2 / bottom))
|
|
126
|
+
|
|
127
|
+
return result
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _max_chroma_for_lh(lightness: float, hue: float) -> float:
|
|
131
|
+
hue_radians = math.radians(hue)
|
|
132
|
+
lengths = [
|
|
133
|
+
_length_of_ray_until_intersect(hue_radians, bound)
|
|
134
|
+
for bound in _get_bounds(lightness)
|
|
135
|
+
]
|
|
136
|
+
return min(length for length in lengths if length >= 0)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _dot_product(
|
|
140
|
+
left: tuple[float, float, float], right: tuple[float, float, float]
|
|
141
|
+
) -> float:
|
|
142
|
+
return sum(
|
|
143
|
+
left_value * right_value
|
|
144
|
+
for left_value, right_value in zip(left, right, strict=False)
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _from_linear(channel: float) -> float:
|
|
149
|
+
if channel <= 0.0031308:
|
|
150
|
+
return 12.92 * channel
|
|
151
|
+
return 1.055 * math.pow(channel, 5 / 12) - 0.055
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _l_to_y(lightness: float) -> float:
|
|
155
|
+
if lightness <= 8:
|
|
156
|
+
return _REF_Y * lightness / _KAPPA
|
|
157
|
+
return _REF_Y * (((lightness + 16) / 116) ** 3)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _lch_to_luv(color: tuple[float, float, float]) -> tuple[float, float, float]:
|
|
161
|
+
lightness, chroma, hue = color
|
|
162
|
+
hue_radians = math.radians(hue)
|
|
163
|
+
return (
|
|
164
|
+
lightness,
|
|
165
|
+
math.cos(hue_radians) * chroma,
|
|
166
|
+
math.sin(hue_radians) * chroma,
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _luv_to_xyz(color: tuple[float, float, float]) -> tuple[float, float, float]:
|
|
171
|
+
lightness, u_value, v_value = color
|
|
172
|
+
if lightness == 0:
|
|
173
|
+
return (0.0, 0.0, 0.0)
|
|
174
|
+
|
|
175
|
+
var_u = u_value / (13 * lightness) + _REF_U
|
|
176
|
+
var_v = v_value / (13 * lightness) + _REF_V
|
|
177
|
+
y_value = _l_to_y(lightness)
|
|
178
|
+
x_value = y_value * 9 * var_u / (4 * var_v)
|
|
179
|
+
z_value = y_value * (12 - 3 * var_u - 20 * var_v) / (4 * var_v)
|
|
180
|
+
return (x_value, y_value, z_value)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _xyz_to_rgb(color: tuple[float, float, float]) -> tuple[float, float, float]:
|
|
184
|
+
return tuple(_from_linear(_dot_product(row, color)) for row in _M) # type: ignore[return-value]
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _hsluv_to_lch(color: tuple[float, float, float]) -> tuple[float, float, float]:
|
|
188
|
+
hue, saturation, lightness = color
|
|
189
|
+
if lightness > 100 - 1e-7:
|
|
190
|
+
return (100.0, 0.0, hue)
|
|
191
|
+
if lightness < 1e-8:
|
|
192
|
+
return (0.0, 0.0, hue)
|
|
193
|
+
max_chroma = _max_chroma_for_lh(lightness, hue)
|
|
194
|
+
return (lightness, max_chroma * saturation / 100, hue)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def hsluv_to_rgb(color: tuple[float, float, float]) -> tuple[float, float, float]:
|
|
198
|
+
return _xyz_to_rgb(_luv_to_xyz(_lch_to_luv(_hsluv_to_lch(color))))
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def rgb_to_hex(color: tuple[float, float, float]) -> str:
|
|
202
|
+
channels = []
|
|
203
|
+
for channel in color:
|
|
204
|
+
channel = min(max(channel, 0.0), 1.0)
|
|
205
|
+
channels.append(int(math.floor(channel * 255 + 0.5)))
|
|
206
|
+
return "#{:02X}{:02X}{:02X}".format(*channels)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def hsluv_to_hex(color: tuple[float, float, float]) -> str:
|
|
210
|
+
return rgb_to_hex(hsluv_to_rgb(color))
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _hex_to_rgb(color: str) -> tuple[int, int, int]:
|
|
214
|
+
return tuple(int(color[index : index + 2], 16) for index in (1, 3, 5)) # type: ignore[return-value]
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def rgb_distance_hex(left: str, right: str) -> float:
|
|
218
|
+
left_rgb = _hex_to_rgb(left)
|
|
219
|
+
right_rgb = _hex_to_rgb(right)
|
|
220
|
+
return math.dist(left_rgb, right_rgb)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
@cache
|
|
224
|
+
def rich_label_palette(slot_count: int) -> tuple[str, ...]:
|
|
225
|
+
if slot_count <= 64:
|
|
226
|
+
return PRECOMPUTED_QUALITATIVE_HSLUV_PALETTE_64
|
|
227
|
+
return tuple(qualitative_hsluv_palette(slot_count, lightness=0.5, saturation=0.9))
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def qualitative_hsluv_palette(
|
|
231
|
+
n_colors: int,
|
|
232
|
+
*,
|
|
233
|
+
hue: float = 0.01,
|
|
234
|
+
saturation: float = 0.9,
|
|
235
|
+
lightness: float = 0.5,
|
|
236
|
+
hue_samples: int | None = None,
|
|
237
|
+
) -> list[str]:
|
|
238
|
+
"""Build a deterministic, high-separation qualitative HSLuv palette.
|
|
239
|
+
|
|
240
|
+
Parameters mirror seaborn's ``husl_palette`` convention, using normalized
|
|
241
|
+
floats in ``[0, 1]`` for hue, saturation, and lightness.
|
|
242
|
+
"""
|
|
243
|
+
if n_colors <= 0:
|
|
244
|
+
return []
|
|
245
|
+
|
|
246
|
+
if hue_samples is None:
|
|
247
|
+
hue_samples = max(48, n_colors * 32)
|
|
248
|
+
|
|
249
|
+
start_degrees = (hue % 1.0) * 360.0
|
|
250
|
+
candidates = [
|
|
251
|
+
hsluv_to_hex(
|
|
252
|
+
(
|
|
253
|
+
(start_degrees + 360.0 * index / hue_samples) % 360.0,
|
|
254
|
+
saturation * 100.0,
|
|
255
|
+
lightness * 100.0,
|
|
256
|
+
)
|
|
257
|
+
)
|
|
258
|
+
for index in range(hue_samples)
|
|
259
|
+
]
|
|
260
|
+
candidates = list(dict.fromkeys(candidates))
|
|
261
|
+
|
|
262
|
+
selected = [candidates[0]]
|
|
263
|
+
while len(selected) < min(n_colors, len(candidates)):
|
|
264
|
+
remaining = [candidate for candidate in candidates if candidate not in selected]
|
|
265
|
+
best = max(
|
|
266
|
+
remaining,
|
|
267
|
+
key=lambda candidate: min(
|
|
268
|
+
rgb_distance_hex(candidate, chosen) for chosen in selected
|
|
269
|
+
),
|
|
270
|
+
)
|
|
271
|
+
selected.append(best)
|
|
272
|
+
|
|
273
|
+
return selected[:n_colors]
|