autoforge 2.0.1__tar.gz → 2.1.0__tar.gz
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.
- {autoforge-2.0.1 → autoforge-2.1.0}/PKG-INFO +2 -2
- {autoforge-2.0.1 → autoforge-2.1.0}/pyproject.toml +2 -2
- {autoforge-2.0.1 → autoforge-2.1.0}/src/AutoForge.egg-info/PKG-INFO +2 -2
- {autoforge-2.0.1 → autoforge-2.1.0}/src/AutoForge.egg-info/SOURCES.txt +8 -0
- autoforge-2.1.0/src/autoforge/Helper/ConstraintHelper.py +279 -0
- autoforge-2.1.0/src/autoforge/Helper/FusedComposite.py +900 -0
- autoforge-2.1.0/src/autoforge/Helper/HeightAssign.py +148 -0
- {autoforge-2.0.1 → autoforge-2.1.0}/src/autoforge/Helper/Heightmaps/FastTSPHeightMap.py +19 -3
- autoforge-2.1.0/src/autoforge/Helper/OptimizerHelper.py +1432 -0
- {autoforge-2.0.1 → autoforge-2.1.0}/src/autoforge/Helper/OutputHelper.py +7 -0
- autoforge-2.1.0/src/autoforge/Helper/PixelHeightRefine.py +1053 -0
- {autoforge-2.0.1 → autoforge-2.1.0}/src/autoforge/Helper/PruningHelper.py +287 -97
- {autoforge-2.0.1 → autoforge-2.1.0}/src/autoforge/Modules/Optimizer.py +930 -52
- {autoforge-2.0.1 → autoforge-2.1.0}/src/autoforge/auto_forge.py +355 -9
- {autoforge-2.0.1 → autoforge-2.1.0}/src/autoforge/webui/api/optimization.py +65 -5
- {autoforge-2.0.1 → autoforge-2.1.0}/src/autoforge/webui/api/pruning.py +32 -14
- {autoforge-2.0.1 → autoforge-2.1.0}/src/autoforge/webui/api/ws.py +5 -0
- {autoforge-2.0.1 → autoforge-2.1.0}/src/autoforge/webui/helpers/pipeline_runner.py +79 -2
- {autoforge-2.0.1 → autoforge-2.1.0}/src/autoforge/webui/helpers/slider_render.py +19 -18
- {autoforge-2.0.1 → autoforge-2.1.0}/src/autoforge/webui/helpers/sliders.py +15 -7
- {autoforge-2.0.1 → autoforge-2.1.0}/src/autoforge/webui/models.py +5 -0
- {autoforge-2.0.1 → autoforge-2.1.0}/tests/test_bugfixes_2026_09_26.py +1 -0
- autoforge-2.1.0/tests/test_constrained_opt.py +596 -0
- autoforge-2.1.0/tests/test_graph_cache_eviction.py +117 -0
- autoforge-2.1.0/tests/test_graph_capture_threads.py +53 -0
- autoforge-2.1.0/tests/test_gumbel_nan.py +25 -0
- {autoforge-2.0.1 → autoforge-2.1.0}/tests/test_optimizer_training.py +6 -2
- autoforge-2.0.1/src/autoforge/Helper/OptimizerHelper.py +0 -822
- {autoforge-2.0.1 → autoforge-2.1.0}/README.md +0 -0
- {autoforge-2.0.1 → autoforge-2.1.0}/setup.cfg +0 -0
- {autoforge-2.0.1 → autoforge-2.1.0}/src/AutoForge.egg-info/dependency_links.txt +0 -0
- {autoforge-2.0.1 → autoforge-2.1.0}/src/AutoForge.egg-info/entry_points.txt +0 -0
- {autoforge-2.0.1 → autoforge-2.1.0}/src/AutoForge.egg-info/requires.txt +0 -0
- {autoforge-2.0.1 → autoforge-2.1.0}/src/AutoForge.egg-info/top_level.txt +0 -0
- {autoforge-2.0.1 → autoforge-2.1.0}/src/autoforge/Helper/AmpUtils.py +0 -0
- {autoforge-2.0.1 → autoforge-2.1.0}/src/autoforge/Helper/CAdamW.py +0 -0
- {autoforge-2.0.1 → autoforge-2.1.0}/src/autoforge/Helper/DeviceUtils.py +0 -0
- {autoforge-2.0.1 → autoforge-2.1.0}/src/autoforge/Helper/FilamentHelper.py +0 -0
- {autoforge-2.0.1 → autoforge-2.1.0}/src/autoforge/Helper/Heightmaps/ChristofidesHeightMap.py +0 -0
- {autoforge-2.0.1 → autoforge-2.1.0}/src/autoforge/Helper/Heightmaps/DepthEstimateHeightMap.py +0 -0
- {autoforge-2.0.1 → autoforge-2.1.0}/src/autoforge/Helper/Heightmaps/__init__.py +0 -0
- {autoforge-2.0.1 → autoforge-2.1.0}/src/autoforge/Helper/ImageHelper.py +0 -0
- {autoforge-2.0.1 → autoforge-2.1.0}/src/autoforge/Helper/OtherHelper.py +0 -0
- {autoforge-2.0.1 → autoforge-2.1.0}/src/autoforge/Helper/__init__.py +0 -0
- {autoforge-2.0.1 → autoforge-2.1.0}/src/autoforge/Helper/filamentcolors_library.py +0 -0
- {autoforge-2.0.1 → autoforge-2.1.0}/src/autoforge/Init_comparer/__init__.py +0 -0
- {autoforge-2.0.1 → autoforge-2.1.0}/src/autoforge/Init_comparer/auto_forge_init_check.py +0 -0
- {autoforge-2.0.1 → autoforge-2.1.0}/src/autoforge/Init_comparer/plot_results.py +0 -0
- {autoforge-2.0.1 → autoforge-2.1.0}/src/autoforge/Loss/LossFunctions.py +0 -0
- {autoforge-2.0.1 → autoforge-2.1.0}/src/autoforge/Loss/PerceptionLoss.py +0 -0
- {autoforge-2.0.1 → autoforge-2.1.0}/src/autoforge/Loss/__init__.py +0 -0
- {autoforge-2.0.1 → autoforge-2.1.0}/src/autoforge/Misc/__init__.py +0 -0
- {autoforge-2.0.1 → autoforge-2.1.0}/src/autoforge/Misc/color_fit_all.py +0 -0
- {autoforge-2.0.1 → autoforge-2.1.0}/src/autoforge/Modules/__init__.py +0 -0
- {autoforge-2.0.1 → autoforge-2.1.0}/src/autoforge/__init__.py +0 -0
- {autoforge-2.0.1 → autoforge-2.1.0}/src/autoforge/__main__.py +0 -0
- {autoforge-2.0.1 → autoforge-2.1.0}/src/autoforge/data/filamentcolors_catalog.json +0 -0
- {autoforge-2.0.1 → autoforge-2.1.0}/src/autoforge/priority_mask_editor.py +0 -0
- {autoforge-2.0.1 → autoforge-2.1.0}/src/autoforge/webui/__init__.py +0 -0
- {autoforge-2.0.1 → autoforge-2.1.0}/src/autoforge/webui/api/__init__.py +0 -0
- {autoforge-2.0.1 → autoforge-2.1.0}/src/autoforge/webui/api/filaments.py +0 -0
- {autoforge-2.0.1 → autoforge-2.1.0}/src/autoforge/webui/api/images.py +0 -0
- {autoforge-2.0.1 → autoforge-2.1.0}/src/autoforge/webui/api/init.py +0 -0
- {autoforge-2.0.1 → autoforge-2.1.0}/src/autoforge/webui/api/outputs.py +0 -0
- {autoforge-2.0.1 → autoforge-2.1.0}/src/autoforge/webui/api/preview.py +0 -0
- {autoforge-2.0.1 → autoforge-2.1.0}/src/autoforge/webui/api/project.py +0 -0
- {autoforge-2.0.1 → autoforge-2.1.0}/src/autoforge/webui/api/settings.py +0 -0
- {autoforge-2.0.1 → autoforge-2.1.0}/src/autoforge/webui/api/sliders.py +0 -0
- {autoforge-2.0.1 → autoforge-2.1.0}/src/autoforge/webui/api/state.py +0 -0
- {autoforge-2.0.1 → autoforge-2.1.0}/src/autoforge/webui/api/system.py +0 -0
- {autoforge-2.0.1 → autoforge-2.1.0}/src/autoforge/webui/config.py +0 -0
- {autoforge-2.0.1 → autoforge-2.1.0}/src/autoforge/webui/helpers/__init__.py +0 -0
- {autoforge-2.0.1 → autoforge-2.1.0}/src/autoforge/webui/helpers/colored_mesh.py +0 -0
- {autoforge-2.0.1 → autoforge-2.1.0}/src/autoforge/webui/helpers/gpu_memory.py +0 -0
- {autoforge-2.0.1 → autoforge-2.1.0}/src/autoforge/webui/helpers/mesh_persist.py +0 -0
- {autoforge-2.0.1 → autoforge-2.1.0}/src/autoforge/webui/helpers/telemetry.py +0 -0
- {autoforge-2.0.1 → autoforge-2.1.0}/src/autoforge/webui/server.py +0 -0
- {autoforge-2.0.1 → autoforge-2.1.0}/src/autoforge/webui/services/__init__.py +0 -0
- {autoforge-2.0.1 → autoforge-2.1.0}/src/autoforge/webui/services/catalog_service.py +0 -0
- {autoforge-2.0.1 → autoforge-2.1.0}/src/autoforge/webui/services/filament_service.py +0 -0
- {autoforge-2.0.1 → autoforge-2.1.0}/src/autoforge/webui/services/image_service.py +0 -0
- {autoforge-2.0.1 → autoforge-2.1.0}/src/autoforge/webui/services/optimization_service.py +0 -0
- {autoforge-2.0.1 → autoforge-2.1.0}/src/autoforge/webui/services/project_service.py +0 -0
- {autoforge-2.0.1 → autoforge-2.1.0}/tests/test_amp_utils.py +0 -0
- {autoforge-2.0.1 → autoforge-2.1.0}/tests/test_auto_forge_cli.py +0 -0
- {autoforge-2.0.1 → autoforge-2.1.0}/tests/test_bug_regressions.py +0 -0
- {autoforge-2.0.1 → autoforge-2.1.0}/tests/test_cadamw.py +0 -0
- {autoforge-2.0.1 → autoforge-2.1.0}/tests/test_christofides_heightmap.py +0 -0
- {autoforge-2.0.1 → autoforge-2.1.0}/tests/test_composite_image.py +0 -0
- {autoforge-2.0.1 → autoforge-2.1.0}/tests/test_depth_adjusted_height_map.py +0 -0
- {autoforge-2.0.1 → autoforge-2.1.0}/tests/test_depth_estimate_height_map.py +0 -0
- {autoforge-2.0.1 → autoforge-2.1.0}/tests/test_device_utils.py +0 -0
- {autoforge-2.0.1 → autoforge-2.1.0}/tests/test_end_to_end_optimizer.py +0 -0
- {autoforge-2.0.1 → autoforge-2.1.0}/tests/test_fasttsp_heightmap.py +0 -0
- {autoforge-2.0.1 → autoforge-2.1.0}/tests/test_filament_helper.py +0 -0
- {autoforge-2.0.1 → autoforge-2.1.0}/tests/test_filamentcolors_library.py +0 -0
- {autoforge-2.0.1 → autoforge-2.1.0}/tests/test_flatforge.py +0 -0
- {autoforge-2.0.1 → autoforge-2.1.0}/tests/test_image_helper.py +0 -0
- {autoforge-2.0.1 → autoforge-2.1.0}/tests/test_loss_functions.py +0 -0
- {autoforge-2.0.1 → autoforge-2.1.0}/tests/test_optimizer_module.py +0 -0
- {autoforge-2.0.1 → autoforge-2.1.0}/tests/test_other_helper.py +0 -0
- {autoforge-2.0.1 → autoforge-2.1.0}/tests/test_output_helper.py +0 -0
- {autoforge-2.0.1 → autoforge-2.1.0}/tests/test_perception_loss.py +0 -0
- {autoforge-2.0.1 → autoforge-2.1.0}/tests/test_pipeline_smoke.py +0 -0
- {autoforge-2.0.1 → autoforge-2.1.0}/tests/test_property_based.py +0 -0
- {autoforge-2.0.1 → autoforge-2.1.0}/tests/test_prune_repeatability.py +0 -0
- {autoforge-2.0.1 → autoforge-2.1.0}/tests/test_pruning_helper.py +0 -0
- {autoforge-2.0.1 → autoforge-2.1.0}/tests/test_remove_height_spikes.py +0 -0
- {autoforge-2.0.1 → autoforge-2.1.0}/tests/test_webui_backend.py +0 -0
- {autoforge-2.0.1 → autoforge-2.1.0}/tests/test_webui_regressions.py +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: AutoForge
|
|
3
|
-
Version: 2.0
|
|
4
|
-
Summary: AutoForge - 3D color
|
|
3
|
+
Version: 2.1.0
|
|
4
|
+
Summary: AutoForge - 3D color prints made easy
|
|
5
5
|
Author-email: Hendric Voss <hvoss@techfak.uni-bielefeld.de>
|
|
6
6
|
License: CC BY-NC-SA 4.0
|
|
7
7
|
Requires-Python: >=3.10
|
|
@@ -4,8 +4,8 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "AutoForge"
|
|
7
|
-
version="2.0
|
|
8
|
-
description = "AutoForge - 3D color
|
|
7
|
+
version="2.1.0"
|
|
8
|
+
description = "AutoForge - 3D color prints made easy"
|
|
9
9
|
authors = [{ name = "Hendric Voss", email = "hvoss@techfak.uni-bielefeld.de" }]
|
|
10
10
|
readme = "README.md"
|
|
11
11
|
requires-python = ">=3.10"
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: AutoForge
|
|
3
|
-
Version: 2.0
|
|
4
|
-
Summary: AutoForge - 3D color
|
|
3
|
+
Version: 2.1.0
|
|
4
|
+
Summary: AutoForge - 3D color prints made easy
|
|
5
5
|
Author-email: Hendric Voss <hvoss@techfak.uni-bielefeld.de>
|
|
6
6
|
License: CC BY-NC-SA 4.0
|
|
7
7
|
Requires-Python: >=3.10
|
|
@@ -12,12 +12,16 @@ src/autoforge/auto_forge.py
|
|
|
12
12
|
src/autoforge/priority_mask_editor.py
|
|
13
13
|
src/autoforge/Helper/AmpUtils.py
|
|
14
14
|
src/autoforge/Helper/CAdamW.py
|
|
15
|
+
src/autoforge/Helper/ConstraintHelper.py
|
|
15
16
|
src/autoforge/Helper/DeviceUtils.py
|
|
16
17
|
src/autoforge/Helper/FilamentHelper.py
|
|
18
|
+
src/autoforge/Helper/FusedComposite.py
|
|
19
|
+
src/autoforge/Helper/HeightAssign.py
|
|
17
20
|
src/autoforge/Helper/ImageHelper.py
|
|
18
21
|
src/autoforge/Helper/OptimizerHelper.py
|
|
19
22
|
src/autoforge/Helper/OtherHelper.py
|
|
20
23
|
src/autoforge/Helper/OutputHelper.py
|
|
24
|
+
src/autoforge/Helper/PixelHeightRefine.py
|
|
21
25
|
src/autoforge/Helper/PruningHelper.py
|
|
22
26
|
src/autoforge/Helper/__init__.py
|
|
23
27
|
src/autoforge/Helper/filamentcolors_library.py
|
|
@@ -75,6 +79,7 @@ tests/test_bugfixes_2026_09_26.py
|
|
|
75
79
|
tests/test_cadamw.py
|
|
76
80
|
tests/test_christofides_heightmap.py
|
|
77
81
|
tests/test_composite_image.py
|
|
82
|
+
tests/test_constrained_opt.py
|
|
78
83
|
tests/test_depth_adjusted_height_map.py
|
|
79
84
|
tests/test_depth_estimate_height_map.py
|
|
80
85
|
tests/test_device_utils.py
|
|
@@ -83,6 +88,9 @@ tests/test_fasttsp_heightmap.py
|
|
|
83
88
|
tests/test_filament_helper.py
|
|
84
89
|
tests/test_filamentcolors_library.py
|
|
85
90
|
tests/test_flatforge.py
|
|
91
|
+
tests/test_graph_cache_eviction.py
|
|
92
|
+
tests/test_graph_capture_threads.py
|
|
93
|
+
tests/test_gumbel_nan.py
|
|
86
94
|
tests/test_image_helper.py
|
|
87
95
|
tests/test_loss_functions.py
|
|
88
96
|
tests/test_optimizer_module.py
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
"""Colour / swap limits held by the optimizer itself (--constrained_opt).
|
|
2
|
+
|
|
3
|
+
``project_assignment`` maps per-layer material scores [L, M] to the best
|
|
4
|
+
discrete layer stack that uses at most ``max_colors`` distinct materials
|
|
5
|
+
besides the base filament (``free``: layers may reuse it at no cost) and
|
|
6
|
+
at most ``max_swaps`` material changes between consecutive layers, where
|
|
7
|
+
"best" means the highest summed per-layer score. With log-probabilities as
|
|
8
|
+
scores that is the most likely feasible stack; with Gumbel-perturbed
|
|
9
|
+
log-probabilities it is a random feasible sample (perturb-and-MAP), which is
|
|
10
|
+
what the discrete snapshots during training use.
|
|
11
|
+
"""
|
|
12
|
+
from itertools import combinations
|
|
13
|
+
from typing import Optional
|
|
14
|
+
|
|
15
|
+
import numpy as np
|
|
16
|
+
import torch
|
|
17
|
+
|
|
18
|
+
# Enumerate every palette up to this many subsets, greedy search beyond.
|
|
19
|
+
MAX_SUBSETS = 5000
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _mat(base: Optional[int]) -> Optional[int]:
|
|
23
|
+
"""The base's filament index (None when there is no base filament)."""
|
|
24
|
+
return base if base is not None and base >= 0 else None
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _viterbi_swaps(score: torch.Tensor, max_swaps: int, base: Optional[int] = None) -> torch.Tensor:
|
|
28
|
+
"""argmax over stacks with <= max_swaps changes of sum_l score[b, l, dg[l]].
|
|
29
|
+
|
|
30
|
+
With ``base`` (the base's filament, -1 for a base color that is no
|
|
31
|
+
filament) the change from the base to the first layer counts too: the
|
|
32
|
+
base is the stack's band 0.
|
|
33
|
+
|
|
34
|
+
score: [B, L, M] (use a large negative score for forbidden materials).
|
|
35
|
+
Returns [B, L] int64 on score's device.
|
|
36
|
+
|
|
37
|
+
Runs in NumPy on the CPU: the tables are tiny ([B, max_swaps+1, M] per
|
|
38
|
+
layer), and as a loop of small GPU kernels it took ~18ms per call with
|
|
39
|
+
the GPU idle in between - the bulk of the constrained snapshot cost.
|
|
40
|
+
"""
|
|
41
|
+
dev = score.device
|
|
42
|
+
# A NaN score makes every comparison false, which backtracked into a
|
|
43
|
+
# swap count of -1.
|
|
44
|
+
sc = np.nan_to_num(
|
|
45
|
+
score.detach().to("cpu", torch.float64).numpy(), nan=-1e9, posinf=1e9, neginf=-1e9
|
|
46
|
+
)
|
|
47
|
+
B, L, M = sc.shape
|
|
48
|
+
S = max_swaps + 1
|
|
49
|
+
neg = -1e300
|
|
50
|
+
dp = np.full((B, S, M), neg)
|
|
51
|
+
if base is None:
|
|
52
|
+
dp[:, 0] = sc[:, 0]
|
|
53
|
+
else:
|
|
54
|
+
# Staying in the base filament is free, anything else is a swap.
|
|
55
|
+
if 0 <= base < M:
|
|
56
|
+
dp[:, 0, base] = sc[:, 0, base]
|
|
57
|
+
if S > 1:
|
|
58
|
+
other = np.arange(M) != base
|
|
59
|
+
dp[:, 1, other] = sc[:, 0, other]
|
|
60
|
+
back_stay = np.empty((max(L - 1, 0), B, S, M), dtype=bool)
|
|
61
|
+
back_from = np.empty((max(L - 1, 0), B, S, M), dtype=np.int64)
|
|
62
|
+
ar = np.arange(M)
|
|
63
|
+
for l in range(1, L):
|
|
64
|
+
# Best previous material other than m, one swap earlier.
|
|
65
|
+
i1 = dp.argmax(axis=2) # [B,S]
|
|
66
|
+
v1 = np.take_along_axis(dp, i1[..., None], axis=2)[..., 0]
|
|
67
|
+
masked = dp.copy()
|
|
68
|
+
np.put_along_axis(masked, i1[..., None], neg, axis=2)
|
|
69
|
+
i2 = masked.argmax(axis=2)
|
|
70
|
+
v2 = np.take_along_axis(masked, i2[..., None], axis=2)[..., 0]
|
|
71
|
+
is_best = i1[..., None] == ar
|
|
72
|
+
other_v = np.where(is_best, v2[..., None], v1[..., None]) # [B,S,M]
|
|
73
|
+
other_i = np.where(is_best, i2[..., None], i1[..., None])
|
|
74
|
+
switch_v = np.full_like(dp, neg)
|
|
75
|
+
switch_v[:, 1:] = other_v[:, :-1]
|
|
76
|
+
switch_i = np.zeros_like(other_i)
|
|
77
|
+
switch_i[:, 1:] = other_i[:, :-1]
|
|
78
|
+
stay = dp >= switch_v
|
|
79
|
+
stay[:, 0] = True # no swap left to come from
|
|
80
|
+
dp = np.where(stay, dp, switch_v) + sc[:, l][:, None, :]
|
|
81
|
+
back_stay[l - 1] = stay
|
|
82
|
+
back_from[l - 1] = switch_i
|
|
83
|
+
flat = dp.reshape(B, -1).argmax(axis=1)
|
|
84
|
+
s_idx, m_idx = flat // M, flat % M
|
|
85
|
+
out = np.empty((B, L), dtype=np.int64)
|
|
86
|
+
out[:, L - 1] = m_idx
|
|
87
|
+
bi = np.arange(B)
|
|
88
|
+
for l in range(L - 1, 0, -1):
|
|
89
|
+
st = back_stay[l - 1][bi, s_idx, m_idx]
|
|
90
|
+
prev = np.where(st, m_idx, back_from[l - 1][bi, s_idx, m_idx])
|
|
91
|
+
s_idx = np.where(st, s_idx, s_idx - 1)
|
|
92
|
+
m_idx = prev
|
|
93
|
+
out[:, l - 1] = m_idx
|
|
94
|
+
return torch.from_numpy(out).to(dev)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _palette_candidates(M: int, max_colors: int, free: Optional[int], device) -> Optional[torch.Tensor]:
|
|
98
|
+
"""All palettes as [N, M] bool: ``max_colors`` materials besides
|
|
99
|
+
``free`` (the base filament, always in the palette at no cost), or None
|
|
100
|
+
when there are too many to enumerate."""
|
|
101
|
+
others = [m for m in range(M) if m != free]
|
|
102
|
+
k = min(max_colors, len(others))
|
|
103
|
+
n_sub = 1
|
|
104
|
+
for i in range(k):
|
|
105
|
+
n_sub = n_sub * (len(others) - i) // (i + 1)
|
|
106
|
+
if n_sub > MAX_SUBSETS:
|
|
107
|
+
return None
|
|
108
|
+
subs = torch.tensor(list(combinations(others, k)), device=device, dtype=torch.long).view(-1, k)
|
|
109
|
+
masks = torch.zeros((subs.shape[0], M), dtype=torch.bool, device=device)
|
|
110
|
+
masks.scatter_(1, subs, True)
|
|
111
|
+
if free is not None:
|
|
112
|
+
masks[:, free] = True
|
|
113
|
+
return masks
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def top_palettes(score: torch.Tensor, max_colors: int, k: int, free: Optional[int] = None) -> torch.Tensor:
|
|
117
|
+
"""The k best palettes for scores [L, M] by sum_l max_{m in palette}
|
|
118
|
+
score[l, m], as [k', M] bool (k' <= k; exhaustive search only)."""
|
|
119
|
+
L, M = score.shape
|
|
120
|
+
free = _mat(free)
|
|
121
|
+
masks = _palette_candidates(M, max_colors, free, score.device)
|
|
122
|
+
if masks is None:
|
|
123
|
+
return _palette_masks(score.unsqueeze(0), max_colors, free)
|
|
124
|
+
neg = torch.tensor(torch.finfo(score.dtype).min / 4, device=score.device)
|
|
125
|
+
vals = torch.where(masks.unsqueeze(1), score.unsqueeze(0), neg).amax(-1).sum(-1)
|
|
126
|
+
return masks[vals.topk(min(k, vals.shape[0])).indices]
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _palette_masks(score: torch.Tensor, max_colors: int, free: Optional[int] = None) -> torch.Tensor:
|
|
130
|
+
"""Best palette per batch item, as [B, M] bool: at most ``max_colors``
|
|
131
|
+
materials besides ``free`` (the base filament, which is always in it),
|
|
132
|
+
judged by sum_l max_{m in palette} score[b, l, m]."""
|
|
133
|
+
B, L, M = score.shape
|
|
134
|
+
dev = score.device
|
|
135
|
+
neg = torch.finfo(score.dtype).min / 4
|
|
136
|
+
masks = _palette_candidates(M, max_colors, free, dev)
|
|
137
|
+
if masks is not None:
|
|
138
|
+
# [B, N, L]: best in-palette score per layer
|
|
139
|
+
vals = torch.where(
|
|
140
|
+
masks.view(1, -1, 1, M), score.unsqueeze(1), torch.tensor(neg, device=dev)
|
|
141
|
+
).amax(dim=-1)
|
|
142
|
+
best = vals.sum(-1).argmax(dim=1) # [B]
|
|
143
|
+
return masks[best]
|
|
144
|
+
# Greedy: drop the material whose removal costs least, until small enough.
|
|
145
|
+
mask = torch.ones((B, M), dtype=torch.bool, device=dev)
|
|
146
|
+
n_other = M - (1 if free is not None else 0)
|
|
147
|
+
for _ in range(max(0, n_other - max_colors)):
|
|
148
|
+
costs = []
|
|
149
|
+
for m in range(M):
|
|
150
|
+
trial = mask.clone()
|
|
151
|
+
trial[:, m] = False
|
|
152
|
+
v = torch.where(trial.unsqueeze(1), score, torch.tensor(neg, device=dev))
|
|
153
|
+
c = v.amax(-1).sum(-1)
|
|
154
|
+
keep = mask[:, m] & (m != free)
|
|
155
|
+
c = torch.where(keep, c, torch.full_like(c, neg))
|
|
156
|
+
costs.append(c)
|
|
157
|
+
drop = torch.stack(costs, 1).argmax(1)
|
|
158
|
+
mask[torch.arange(B, device=dev), drop] = False
|
|
159
|
+
return mask
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def project_assignment(
|
|
163
|
+
score: torch.Tensor,
|
|
164
|
+
max_colors: Optional[int],
|
|
165
|
+
max_swaps: Optional[int],
|
|
166
|
+
free: Optional[int] = None,
|
|
167
|
+
) -> torch.Tensor:
|
|
168
|
+
"""Best feasible stack for scores [L, M] or [B, L, M] -> [L] / [B, L].
|
|
169
|
+
|
|
170
|
+
``free`` is the base: its filament index (a layer in it adds no colour
|
|
171
|
+
to the print, and a first layer in it is no swap), -1 for a base colour
|
|
172
|
+
that is no filament (a colour of its own, and the first layer is always
|
|
173
|
+
a swap), or None to leave the base out entirely. ``max_colors`` limits
|
|
174
|
+
the materials besides the base filament."""
|
|
175
|
+
squeeze = score.dim() == 2
|
|
176
|
+
if squeeze:
|
|
177
|
+
score = score.unsqueeze(0)
|
|
178
|
+
score = score.float()
|
|
179
|
+
M = score.shape[-1]
|
|
180
|
+
mat = _mat(free)
|
|
181
|
+
if max_colors is not None and max_colors < M - (1 if mat is not None else 0):
|
|
182
|
+
mask = _palette_masks(score, max_colors, mat)
|
|
183
|
+
score = torch.where(
|
|
184
|
+
mask.unsqueeze(1), score, torch.tensor(-1e9, device=score.device)
|
|
185
|
+
)
|
|
186
|
+
if max_swaps is not None and max_swaps < score.shape[1] - (1 if free is None else 0):
|
|
187
|
+
out = _viterbi_swaps(score, max_swaps, free)
|
|
188
|
+
else:
|
|
189
|
+
out = score.argmax(-1)
|
|
190
|
+
return out[0] if squeeze else out
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def feasible(
|
|
194
|
+
dg: torch.Tensor, max_colors: Optional[int], max_swaps: Optional[int], free: Optional[int] = None
|
|
195
|
+
) -> bool:
|
|
196
|
+
c, s = count_colors_swaps(dg, free)
|
|
197
|
+
return (max_colors is None or c <= max_colors) and (max_swaps is None or s <= max_swaps)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def neighbour_stacks(
|
|
201
|
+
dg: torch.Tensor,
|
|
202
|
+
n_mat: int,
|
|
203
|
+
max_colors: Optional[int],
|
|
204
|
+
max_swaps: Optional[int],
|
|
205
|
+
free: Optional[int] = None,
|
|
206
|
+
) -> list:
|
|
207
|
+
"""Feasible single-move neighbours of the stack ``dg`` [L]: recolour a
|
|
208
|
+
band, shift a band boundary by 1-2 layers, recolour one layer, replace one
|
|
209
|
+
palette material everywhere by any other material."""
|
|
210
|
+
L = dg.shape[0]
|
|
211
|
+
mat = _mat(free)
|
|
212
|
+
cur = dg.tolist()
|
|
213
|
+
seen = {tuple(cur)}
|
|
214
|
+
out = []
|
|
215
|
+
|
|
216
|
+
def add(cand):
|
|
217
|
+
t = tuple(cand)
|
|
218
|
+
if t in seen:
|
|
219
|
+
return
|
|
220
|
+
seen.add(t)
|
|
221
|
+
colors = len(set(cand) - {mat})
|
|
222
|
+
swaps = sum(1 for a, b in zip(cand[:-1], cand[1:]) if a != b)
|
|
223
|
+
if free is not None and cand[0] != free:
|
|
224
|
+
swaps += 1 # from the base to the first layer
|
|
225
|
+
if (max_colors is None or colors <= max_colors) and (
|
|
226
|
+
max_swaps is None or swaps <= max_swaps
|
|
227
|
+
):
|
|
228
|
+
out.append(cand)
|
|
229
|
+
|
|
230
|
+
bands = []
|
|
231
|
+
start = 0
|
|
232
|
+
for i in range(1, L + 1):
|
|
233
|
+
if i == L or cur[i] != cur[start]:
|
|
234
|
+
bands.append((start, i - 1, cur[start]))
|
|
235
|
+
start = i
|
|
236
|
+
for a, b, c in bands:
|
|
237
|
+
for m in range(n_mat):
|
|
238
|
+
if m != c:
|
|
239
|
+
add(cur[:a] + [m] * (b - a + 1) + cur[b + 1:])
|
|
240
|
+
for (a0, b0, c0), (a1, b1, c1) in zip(bands[:-1], bands[1:]):
|
|
241
|
+
for d in (1, 2):
|
|
242
|
+
if b0 - d + 1 >= a0: # upper band grows downward
|
|
243
|
+
add(cur[: b0 - d + 1] + [c1] * d + cur[b0 + 1:])
|
|
244
|
+
if a1 + d - 1 <= b1: # lower band grows upward
|
|
245
|
+
add(cur[:a1] + [c0] * d + cur[a1 + d:])
|
|
246
|
+
# The base filament is always available to a layer, at no colour cost.
|
|
247
|
+
palette = sorted(set(cur) | ({mat} if mat is not None else set()))
|
|
248
|
+
for l in range(L):
|
|
249
|
+
for m in palette if max_colors is not None else range(n_mat):
|
|
250
|
+
if m != cur[l]:
|
|
251
|
+
add(cur[:l] + [m] + cur[l + 1:])
|
|
252
|
+
for p in sorted(set(cur)):
|
|
253
|
+
for m in range(n_mat):
|
|
254
|
+
if m != p: # replace (m outside the palette) or merge (m inside, the base included)
|
|
255
|
+
add([m if v == p else v for v in cur])
|
|
256
|
+
return [torch.tensor(c, dtype=dg.dtype, device=dg.device) for c in out]
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def count_colors_swaps(dg: torch.Tensor, free: Optional[int] = None) -> tuple[int, int]:
|
|
260
|
+
"""(materials in the stack besides the base filament, swaps) - ``free``
|
|
261
|
+
as in ``project_assignment``. With a base the print's colours are the
|
|
262
|
+
first number plus one, and the swaps include the change from the base to
|
|
263
|
+
the first layer."""
|
|
264
|
+
mats = set(torch.unique(dg).tolist())
|
|
265
|
+
mats.discard(_mat(free))
|
|
266
|
+
swaps = int((dg[1:] != dg[:-1]).sum().item())
|
|
267
|
+
if free is not None and int(dg[0]) != free:
|
|
268
|
+
swaps += 1
|
|
269
|
+
return len(mats), swaps
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def base_material_index(background: torch.Tensor, material_colors: torch.Tensor) -> Optional[int]:
|
|
273
|
+
"""Index of the filament the base is printed in, or None when the base
|
|
274
|
+
color is not one of the filaments (a custom color: it then takes a
|
|
275
|
+
color slot of its own). Matched by color, which covers the automatically
|
|
276
|
+
picked base as well as a filament chosen by hand."""
|
|
277
|
+
d = (material_colors.float() - background.float().view(1, -1)).abs().amax(dim=1)
|
|
278
|
+
i = int(torch.argmin(d))
|
|
279
|
+
return i if float(d[i]) < 0.5 / 255 else None
|