autoforge 1.8.2__tar.gz → 1.8.4__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-1.8.2 → autoforge-1.8.4}/PKG-INFO +3 -3
- {autoforge-1.8.2 → autoforge-1.8.4}/README.md +2 -2
- {autoforge-1.8.2 → autoforge-1.8.4}/pyproject.toml +1 -1
- {autoforge-1.8.2 → autoforge-1.8.4}/src/AutoForge.egg-info/PKG-INFO +3 -3
- {autoforge-1.8.2 → autoforge-1.8.4}/src/autoforge/Helper/Heightmaps/ChristofidesHeightMap.py +4 -4
- {autoforge-1.8.2 → autoforge-1.8.4}/src/autoforge/Helper/OptimizerHelper.py +101 -2
- {autoforge-1.8.2 → autoforge-1.8.4}/src/autoforge/Helper/PruningHelper.py +111 -18
- autoforge-1.8.4/src/autoforge/Init_comparer/auto_forge_init_check.py +141 -0
- autoforge-1.8.4/src/autoforge/Init_comparer/plot_results.py +116 -0
- {autoforge-1.8.2 → autoforge-1.8.4}/src/autoforge/Modules/Optimizer.py +17 -12
- {autoforge-1.8.2 → autoforge-1.8.4}/src/autoforge/auto_forge.py +97 -11
- autoforge-1.8.2/src/autoforge/Init_comparer/auto_forge_init_check.py +0 -264
- autoforge-1.8.2/src/autoforge/Init_comparer/plot_results.py +0 -126
- {autoforge-1.8.2 → autoforge-1.8.4}/setup.cfg +0 -0
- {autoforge-1.8.2 → autoforge-1.8.4}/src/AutoForge.egg-info/SOURCES.txt +0 -0
- {autoforge-1.8.2 → autoforge-1.8.4}/src/AutoForge.egg-info/dependency_links.txt +0 -0
- {autoforge-1.8.2 → autoforge-1.8.4}/src/AutoForge.egg-info/entry_points.txt +0 -0
- {autoforge-1.8.2 → autoforge-1.8.4}/src/AutoForge.egg-info/requires.txt +0 -0
- {autoforge-1.8.2 → autoforge-1.8.4}/src/AutoForge.egg-info/top_level.txt +0 -0
- {autoforge-1.8.2 → autoforge-1.8.4}/src/autoforge/Gui/__init__.py +0 -0
- {autoforge-1.8.2 → autoforge-1.8.4}/src/autoforge/Gui/browser_gui/Views/__init__.py +0 -0
- {autoforge-1.8.2 → autoforge-1.8.4}/src/autoforge/Gui/browser_gui/Views/backend_database.py +0 -0
- {autoforge-1.8.2 → autoforge-1.8.4}/src/autoforge/Gui/browser_gui/Views/filament_editor.py +0 -0
- {autoforge-1.8.2 → autoforge-1.8.4}/src/autoforge/Gui/browser_gui/app.py +0 -0
- {autoforge-1.8.2 → autoforge-1.8.4}/src/autoforge/Gui/browser_gui/main.py +0 -0
- {autoforge-1.8.2 → autoforge-1.8.4}/src/autoforge/Helper/CAdamW.py +0 -0
- {autoforge-1.8.2 → autoforge-1.8.4}/src/autoforge/Helper/FilamentHelper.py +0 -0
- {autoforge-1.8.2 → autoforge-1.8.4}/src/autoforge/Helper/Heightmaps/DepthEstimateHeightMap.py +0 -0
- {autoforge-1.8.2 → autoforge-1.8.4}/src/autoforge/Helper/Heightmaps/OldHeightmap.py +0 -0
- {autoforge-1.8.2 → autoforge-1.8.4}/src/autoforge/Helper/Heightmaps/__init__.py +0 -0
- {autoforge-1.8.2 → autoforge-1.8.4}/src/autoforge/Helper/ImageHelper.py +0 -0
- {autoforge-1.8.2 → autoforge-1.8.4}/src/autoforge/Helper/OutputHelper.py +0 -0
- {autoforge-1.8.2 → autoforge-1.8.4}/src/autoforge/Helper/__init__.py +0 -0
- {autoforge-1.8.2 → autoforge-1.8.4}/src/autoforge/Helper/filamentcolors_library.py +0 -0
- {autoforge-1.8.2 → autoforge-1.8.4}/src/autoforge/Init_comparer/__init__.py +0 -0
- {autoforge-1.8.2 → autoforge-1.8.4}/src/autoforge/Loss/LossFunctions.py +0 -0
- {autoforge-1.8.2 → autoforge-1.8.4}/src/autoforge/Loss/PerceptionLoss.py +0 -0
- {autoforge-1.8.2 → autoforge-1.8.4}/src/autoforge/Loss/__init__.py +0 -0
- {autoforge-1.8.2 → autoforge-1.8.4}/src/autoforge/Misc/__init__.py +0 -0
- {autoforge-1.8.2 → autoforge-1.8.4}/src/autoforge/Misc/color_fit_all.py +0 -0
- {autoforge-1.8.2 → autoforge-1.8.4}/src/autoforge/Modules/__init__.py +0 -0
- {autoforge-1.8.2 → autoforge-1.8.4}/src/autoforge/__init__.py +0 -0
- {autoforge-1.8.2 → autoforge-1.8.4}/src/autoforge/__main__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: AutoForge
|
|
3
|
-
Version: 1.8.
|
|
3
|
+
Version: 1.8.4
|
|
4
4
|
Summary: AutoForge is a Python tool for generating 3D printed layered models from an input image.
|
|
5
5
|
Author-email: Hendric Voss <hvoss@techfak.uni-bielefeld.de>
|
|
6
6
|
License: CC BY-NC-SA 4.0
|
|
@@ -133,7 +133,7 @@ autoforge --input_image path/to/input_image.jpg --csv_file path/to/materials.csv
|
|
|
133
133
|
- `--perform_pruning` Perform pruning after optimization (default: True).
|
|
134
134
|
**Note:** This is highly recommended even if you don't have a color/color swap limit, as it actually increases the quality of the output.
|
|
135
135
|
- `--fast_pruning` Perform pruning in chunks. 10-15x speedup compared to accurate method (default: False).
|
|
136
|
-
- `--fast_pruning_percent` Size of fast pruning chunks in percent (default: 0.
|
|
136
|
+
- `--fast_pruning_percent` Size of fast pruning chunks in percent (default: 0.5) (50%).
|
|
137
137
|
- `--pruning_max_colors` Max number of colors allowed after pruning (default: 100).
|
|
138
138
|
- `--pruning_max_swaps` Max number of swaps allowed after pruning (default: 100).
|
|
139
139
|
- `--pruning_max_layer` Max number of layers allowed after pruning (default: 75).
|
|
@@ -145,7 +145,7 @@ autoforge --input_image path/to/input_image.jpg --csv_file path/to/materials.csv
|
|
|
145
145
|
- `--num_init_rounds` Number of rounds to choose the starting height map from (default: 128).
|
|
146
146
|
- `--num_init_cluster_layers` Number of layers to cluster the image into (default: -1).
|
|
147
147
|
- `--disable_visualization_for_gradio` Simple switch to disable the matplotlib render window for gradio rendering (default: 0).
|
|
148
|
-
|
|
148
|
+
- `--best_of` Run the entire program multiple times and output the best result (default: 1)
|
|
149
149
|
|
|
150
150
|
## Outputs
|
|
151
151
|
|
|
@@ -107,7 +107,7 @@ autoforge --input_image path/to/input_image.jpg --csv_file path/to/materials.csv
|
|
|
107
107
|
- `--perform_pruning` Perform pruning after optimization (default: True).
|
|
108
108
|
**Note:** This is highly recommended even if you don't have a color/color swap limit, as it actually increases the quality of the output.
|
|
109
109
|
- `--fast_pruning` Perform pruning in chunks. 10-15x speedup compared to accurate method (default: False).
|
|
110
|
-
- `--fast_pruning_percent` Size of fast pruning chunks in percent (default: 0.
|
|
110
|
+
- `--fast_pruning_percent` Size of fast pruning chunks in percent (default: 0.5) (50%).
|
|
111
111
|
- `--pruning_max_colors` Max number of colors allowed after pruning (default: 100).
|
|
112
112
|
- `--pruning_max_swaps` Max number of swaps allowed after pruning (default: 100).
|
|
113
113
|
- `--pruning_max_layer` Max number of layers allowed after pruning (default: 75).
|
|
@@ -119,7 +119,7 @@ autoforge --input_image path/to/input_image.jpg --csv_file path/to/materials.csv
|
|
|
119
119
|
- `--num_init_rounds` Number of rounds to choose the starting height map from (default: 128).
|
|
120
120
|
- `--num_init_cluster_layers` Number of layers to cluster the image into (default: -1).
|
|
121
121
|
- `--disable_visualization_for_gradio` Simple switch to disable the matplotlib render window for gradio rendering (default: 0).
|
|
122
|
-
|
|
122
|
+
- `--best_of` Run the entire program multiple times and output the best result (default: 1)
|
|
123
123
|
|
|
124
124
|
## Outputs
|
|
125
125
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "AutoForge"
|
|
7
|
-
version="1.8.
|
|
7
|
+
version="1.8.4"
|
|
8
8
|
description = "AutoForge is a Python tool for generating 3D printed layered models from an input image."
|
|
9
9
|
authors = [{ name = "Hendric Voss", email = "hvoss@techfak.uni-bielefeld.de" }]
|
|
10
10
|
readme = "README.md"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: AutoForge
|
|
3
|
-
Version: 1.8.
|
|
3
|
+
Version: 1.8.4
|
|
4
4
|
Summary: AutoForge is a Python tool for generating 3D printed layered models from an input image.
|
|
5
5
|
Author-email: Hendric Voss <hvoss@techfak.uni-bielefeld.de>
|
|
6
6
|
License: CC BY-NC-SA 4.0
|
|
@@ -133,7 +133,7 @@ autoforge --input_image path/to/input_image.jpg --csv_file path/to/materials.csv
|
|
|
133
133
|
- `--perform_pruning` Perform pruning after optimization (default: True).
|
|
134
134
|
**Note:** This is highly recommended even if you don't have a color/color swap limit, as it actually increases the quality of the output.
|
|
135
135
|
- `--fast_pruning` Perform pruning in chunks. 10-15x speedup compared to accurate method (default: False).
|
|
136
|
-
- `--fast_pruning_percent` Size of fast pruning chunks in percent (default: 0.
|
|
136
|
+
- `--fast_pruning_percent` Size of fast pruning chunks in percent (default: 0.5) (50%).
|
|
137
137
|
- `--pruning_max_colors` Max number of colors allowed after pruning (default: 100).
|
|
138
138
|
- `--pruning_max_swaps` Max number of swaps allowed after pruning (default: 100).
|
|
139
139
|
- `--pruning_max_layer` Max number of layers allowed after pruning (default: 75).
|
|
@@ -145,7 +145,7 @@ autoforge --input_image path/to/input_image.jpg --csv_file path/to/materials.csv
|
|
|
145
145
|
- `--num_init_rounds` Number of rounds to choose the starting height map from (default: 128).
|
|
146
146
|
- `--num_init_cluster_layers` Number of layers to cluster the image into (default: -1).
|
|
147
147
|
- `--disable_visualization_for_gradio` Simple switch to disable the matplotlib render window for gradio rendering (default: 0).
|
|
148
|
-
|
|
148
|
+
- `--best_of` Run the entire program multiple times and output the best result (default: 1)
|
|
149
149
|
|
|
150
150
|
## Outputs
|
|
151
151
|
|
{autoforge-1.8.2 → autoforge-1.8.4}/src/autoforge/Helper/Heightmaps/ChristofidesHeightMap.py
RENAMED
|
@@ -440,7 +440,7 @@ def init_height_map(
|
|
|
440
440
|
background_tuple,
|
|
441
441
|
eps=1e-6,
|
|
442
442
|
random_seed=None,
|
|
443
|
-
lab_weights=(
|
|
443
|
+
lab_weights=(1.0, 1.0, 1.0),
|
|
444
444
|
init_method="quantize_maxcoverage",
|
|
445
445
|
cluster_layers=None,
|
|
446
446
|
lab_space=True,
|
|
@@ -461,7 +461,7 @@ def init_height_map(
|
|
|
461
461
|
import random
|
|
462
462
|
|
|
463
463
|
if cluster_layers is None:
|
|
464
|
-
cluster_layers = max_layers
|
|
464
|
+
cluster_layers = max_layers // 2
|
|
465
465
|
|
|
466
466
|
if random_seed is not None:
|
|
467
467
|
np.random.seed(random_seed)
|
|
@@ -482,7 +482,7 @@ def init_height_map(
|
|
|
482
482
|
target_lab_reshaped,
|
|
483
483
|
H,
|
|
484
484
|
W,
|
|
485
|
-
overcluster_k=
|
|
485
|
+
overcluster_k=500,
|
|
486
486
|
final_k=cluster_layers,
|
|
487
487
|
beta_distinct=4.0,
|
|
488
488
|
random_state=random_seed,
|
|
@@ -578,7 +578,7 @@ def init_height_map(
|
|
|
578
578
|
ordering_metric,
|
|
579
579
|
cluster_layers,
|
|
580
580
|
sil_score,
|
|
581
|
-
|
|
581
|
+
labels.reshape(H, W),
|
|
582
582
|
)
|
|
583
583
|
|
|
584
584
|
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
from contextlib import contextmanager
|
|
2
|
+
|
|
1
3
|
import torch
|
|
2
4
|
import torch.nn.functional as F
|
|
3
5
|
|
|
@@ -95,6 +97,41 @@ def deterministic_gumbel_softmax(
|
|
|
95
97
|
return y
|
|
96
98
|
|
|
97
99
|
|
|
100
|
+
@torch.jit.script
|
|
101
|
+
def bleed_layer_effect(mask: torch.Tensor, strength: float = 0.1) -> torch.Tensor:
|
|
102
|
+
"""
|
|
103
|
+
Applies a simple 2D 3x3 average blur to simulate edge bleeding.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
mask (torch.Tensor): [H,W] or [L,H,W] tensor of masks.
|
|
107
|
+
strength (float): Amount of the bleed to spread to neighbors.
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
torch.Tensor: Mask with neighboring bleed added.
|
|
111
|
+
"""
|
|
112
|
+
if mask.dim() == 2:
|
|
113
|
+
mask = mask.unsqueeze(0) # [1,H,W]
|
|
114
|
+
L, H, W = mask.shape
|
|
115
|
+
|
|
116
|
+
# 3x3 average kernel
|
|
117
|
+
kernel = (
|
|
118
|
+
torch.tensor(
|
|
119
|
+
[[1, 1, 1], [1, 0, 1], [1, 1, 1]], dtype=mask.dtype, device=mask.device
|
|
120
|
+
)
|
|
121
|
+
/ 8.0
|
|
122
|
+
) # 8 neighbors
|
|
123
|
+
|
|
124
|
+
kernel = kernel.view(1, 1, 3, 3)
|
|
125
|
+
|
|
126
|
+
# Apply conv2d to each layer independently
|
|
127
|
+
blurred = F.conv2d(mask.unsqueeze(1), kernel, padding=1, groups=1).squeeze(
|
|
128
|
+
1
|
|
129
|
+
) # [L,H,W]
|
|
130
|
+
|
|
131
|
+
# Combine original mask with bleed from neighbors
|
|
132
|
+
return mask + strength * blurred
|
|
133
|
+
|
|
134
|
+
|
|
98
135
|
@torch.jit.script
|
|
99
136
|
def composite_image_cont(
|
|
100
137
|
pixel_height_logits: torch.Tensor, # [H,W]
|
|
@@ -131,7 +168,8 @@ def composite_image_cont(
|
|
|
131
168
|
) # [L,H,W]
|
|
132
169
|
|
|
133
170
|
# 4. thickness and opacity
|
|
134
|
-
|
|
171
|
+
p_print_bleed = bleed_layer_effect(p_print, strength=0.1) # [L,H,W]
|
|
172
|
+
eff_thick = torch.clamp(p_print_bleed, 0.0, 1.0) * h
|
|
135
173
|
thick_ratio = eff_thick / layer_TDs.view(-1, 1, 1) # [L,H,W]
|
|
136
174
|
|
|
137
175
|
o, A, k, b = -1.2416557e-02, 9.6407950e-01, 3.4103447e01, -4.1554203e00
|
|
@@ -252,7 +290,8 @@ def composite_image_disc(
|
|
|
252
290
|
) # [L,H,W]
|
|
253
291
|
|
|
254
292
|
# 4. Thickness, opacity and the rest exactly as in the continuous version.
|
|
255
|
-
|
|
293
|
+
p_print_bleed = bleed_layer_effect(p_print, strength=0.1) # [L,H,W]
|
|
294
|
+
eff_thick = torch.clamp(p_print_bleed, 0.0, 1.0) * h
|
|
256
295
|
thick_ratio: torch.Tensor = eff_thick / layer_TDs.view(-1, 1, 1) # [L,H,W]
|
|
257
296
|
|
|
258
297
|
o, A, k, b = -1.2416557e-02, 9.6407950e-01, 3.4103447e01, -4.1554203e00
|
|
@@ -277,3 +316,63 @@ def composite_image_disc(
|
|
|
277
316
|
comp = comp + rem_after.unsqueeze(-1) * background # [H,W,3]
|
|
278
317
|
|
|
279
318
|
return comp * 255.0
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def _gpu_capability(device):
|
|
322
|
+
major, minor = torch.cuda.get_device_capability(device)
|
|
323
|
+
return major * 10 + minor # 80, 61, …
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def _has_fp16(device):
|
|
327
|
+
return _gpu_capability(device) >= 53 # CC 5.3 or newer
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
class PrecisionManager:
|
|
331
|
+
"""
|
|
332
|
+
Usage
|
|
333
|
+
-----
|
|
334
|
+
prec = PrecisionManager(device)
|
|
335
|
+
with prec.autocast():
|
|
336
|
+
loss = model(...)
|
|
337
|
+
prec.backward_and_step(loss, optimizer)
|
|
338
|
+
"""
|
|
339
|
+
|
|
340
|
+
def __init__(self, device):
|
|
341
|
+
self.device = device
|
|
342
|
+
self.scaler = None
|
|
343
|
+
self.autocast_dtype = None
|
|
344
|
+
self.enabled = False
|
|
345
|
+
|
|
346
|
+
if device.type == "cuda":
|
|
347
|
+
if torch.cuda.is_bf16_supported():
|
|
348
|
+
print("Using bfloat16 autocast for CUDA.")
|
|
349
|
+
self.autocast_dtype = torch.bfloat16 # fastest if available
|
|
350
|
+
self.scaler = torch.cuda.amp.GradScaler()
|
|
351
|
+
self.enabled = True
|
|
352
|
+
elif _has_fp16(device):
|
|
353
|
+
print("Using float16 autocast for CUDA.")
|
|
354
|
+
self.autocast_dtype = torch.float16
|
|
355
|
+
self.scaler = torch.cuda.amp.GradScaler()
|
|
356
|
+
self.enabled = True
|
|
357
|
+
else:
|
|
358
|
+
print("Using float32 autocast for CUDA.")
|
|
359
|
+
# TF32 fallback for very old GPUs
|
|
360
|
+
torch.backends.cuda.matmul.allow_tf32 = True
|
|
361
|
+
torch.backends.cudnn.allow_tf32 = True
|
|
362
|
+
|
|
363
|
+
@contextmanager
|
|
364
|
+
def autocast(self):
|
|
365
|
+
if self.enabled:
|
|
366
|
+
with torch.cuda.amp.autocast(dtype=self.autocast_dtype):
|
|
367
|
+
yield
|
|
368
|
+
else:
|
|
369
|
+
yield # FP32 path
|
|
370
|
+
|
|
371
|
+
def backward_and_step(self, loss, optimizer):
|
|
372
|
+
if self.scaler is not None: # FP16 path
|
|
373
|
+
self.scaler.scale(loss).backward()
|
|
374
|
+
self.scaler.step(optimizer)
|
|
375
|
+
self.scaler.update()
|
|
376
|
+
else:
|
|
377
|
+
loss.backward()
|
|
378
|
+
optimizer.step()
|
|
@@ -414,24 +414,7 @@ def prune_redundant_layers(
|
|
|
414
414
|
optimizer.max_layers = current_max_layers # keep optimiser in sync
|
|
415
415
|
|
|
416
416
|
# Baseline loss with current best parameters
|
|
417
|
-
|
|
418
|
-
eff_logits = optimizer._apply_height_offset(
|
|
419
|
-
optimizer.best_params["pixel_height_logits"],
|
|
420
|
-
optimizer.best_params["height_offsets"],
|
|
421
|
-
)
|
|
422
|
-
ref_comp = composite_image_disc(
|
|
423
|
-
eff_logits,
|
|
424
|
-
optimizer.best_params["global_logits"],
|
|
425
|
-
optimizer.vis_tau,
|
|
426
|
-
optimizer.vis_tau,
|
|
427
|
-
optimizer.h,
|
|
428
|
-
current_max_layers,
|
|
429
|
-
optimizer.material_colors,
|
|
430
|
-
optimizer.material_TDs,
|
|
431
|
-
optimizer.background,
|
|
432
|
-
rng_seed=optimizer.best_seed,
|
|
433
|
-
)
|
|
434
|
-
best_loss = compute_loss(ref_comp, optimizer.target).item()
|
|
417
|
+
best_loss = get_initial_loss(current_max_layers, optimizer)
|
|
435
418
|
|
|
436
419
|
tbar = tqdm(
|
|
437
420
|
desc=f"Layer pruning | Loss {best_loss:.4f}",
|
|
@@ -570,6 +553,28 @@ def prune_redundant_layers(
|
|
|
570
553
|
return optimizer.best_params, best_loss, current_max_layers
|
|
571
554
|
|
|
572
555
|
|
|
556
|
+
def get_initial_loss(current_max_layers, optimizer):
|
|
557
|
+
with _gpu_lock, torch.no_grad():
|
|
558
|
+
eff_logits = optimizer._apply_height_offset(
|
|
559
|
+
optimizer.best_params["pixel_height_logits"],
|
|
560
|
+
optimizer.best_params["height_offsets"],
|
|
561
|
+
)
|
|
562
|
+
ref_comp = composite_image_disc(
|
|
563
|
+
eff_logits,
|
|
564
|
+
optimizer.best_params["global_logits"],
|
|
565
|
+
optimizer.vis_tau,
|
|
566
|
+
optimizer.vis_tau,
|
|
567
|
+
optimizer.h,
|
|
568
|
+
current_max_layers,
|
|
569
|
+
optimizer.material_colors,
|
|
570
|
+
optimizer.material_TDs,
|
|
571
|
+
optimizer.background,
|
|
572
|
+
rng_seed=optimizer.best_seed,
|
|
573
|
+
)
|
|
574
|
+
best_loss = compute_loss(ref_comp, optimizer.target).item()
|
|
575
|
+
return best_loss
|
|
576
|
+
|
|
577
|
+
|
|
573
578
|
def remove_outlier_pixels(
|
|
574
579
|
height_logits: torch.Tensor, threshold: float
|
|
575
580
|
) -> torch.Tensor:
|
|
@@ -771,3 +776,91 @@ def smooth_coplanar_faces(
|
|
|
771
776
|
smoothed_height_logits = coplanar_sum / count.clamp(min=1)
|
|
772
777
|
|
|
773
778
|
return smoothed_height_logits
|
|
779
|
+
|
|
780
|
+
|
|
781
|
+
def optimise_swap_positions(
|
|
782
|
+
optimizer: FilamentOptimizer,
|
|
783
|
+
n_jobs: int | None = -1,
|
|
784
|
+
*,
|
|
785
|
+
allowed_loss_increase_percent: float = 0.0,
|
|
786
|
+
) -> torch.Tensor:
|
|
787
|
+
"""
|
|
788
|
+
Exhaustively move each swap boundary to every admissible layer and keep the
|
|
789
|
+
best configuration. Uses a progress bar to display changes.
|
|
790
|
+
|
|
791
|
+
Returns
|
|
792
|
+
-------
|
|
793
|
+
torch.Tensor
|
|
794
|
+
The discrete global assignment with optimised swap positions.
|
|
795
|
+
"""
|
|
796
|
+
|
|
797
|
+
num_materials = optimizer.material_colors.shape[0]
|
|
798
|
+
|
|
799
|
+
def disc_loss(dg_test: torch.Tensor) -> float:
|
|
800
|
+
logits_for_disc = disc_to_logits(dg_test, num_materials, big_pos=1e5)
|
|
801
|
+
with _gpu_lock, torch.no_grad():
|
|
802
|
+
out = optimizer.get_best_discretized_image(
|
|
803
|
+
custom_global_logits=logits_for_disc
|
|
804
|
+
)
|
|
805
|
+
return compute_loss(comp=out, target=optimizer.target).item()
|
|
806
|
+
|
|
807
|
+
best_dg, _ = optimizer.get_discretized_solution(best=True)
|
|
808
|
+
best_loss = disc_loss(best_dg)
|
|
809
|
+
|
|
810
|
+
outer_tbar = tqdm(desc="Optimising swap positions", total=100, leave=False)
|
|
811
|
+
improved = True
|
|
812
|
+
while improved:
|
|
813
|
+
improved = False
|
|
814
|
+
outer_tbar.update(1)
|
|
815
|
+
|
|
816
|
+
bands = find_color_bands(best_dg)
|
|
817
|
+
num_swaps = len(bands) - 1
|
|
818
|
+
if num_swaps == 0:
|
|
819
|
+
break
|
|
820
|
+
|
|
821
|
+
for swap_idx in range(num_swaps):
|
|
822
|
+
band_a = bands[swap_idx]
|
|
823
|
+
band_b = bands[swap_idx + 1]
|
|
824
|
+
|
|
825
|
+
lower_limit = bands[swap_idx - 1][1] + 1 if swap_idx > 0 else 0
|
|
826
|
+
upper_limit = (
|
|
827
|
+
bands[swap_idx + 2][0] - 1
|
|
828
|
+
if swap_idx + 2 < len(bands)
|
|
829
|
+
else best_dg.size(0) - 1
|
|
830
|
+
)
|
|
831
|
+
|
|
832
|
+
if lower_limit >= upper_limit:
|
|
833
|
+
continue
|
|
834
|
+
|
|
835
|
+
def candidate_loss(new_boundary: int):
|
|
836
|
+
if new_boundary == band_a[1]:
|
|
837
|
+
return float("inf"), None, None
|
|
838
|
+
dg_new = best_dg.clone()
|
|
839
|
+
dg_new[lower_limit : new_boundary + 1] = band_a[2]
|
|
840
|
+
dg_new[new_boundary + 1 : upper_limit + 1] = band_b[2]
|
|
841
|
+
return disc_loss(dg_new), dg_new, new_boundary
|
|
842
|
+
|
|
843
|
+
results = Parallel(n_jobs=n_jobs, backend="threading", prefer="threads")(
|
|
844
|
+
delayed(candidate_loss)(b) for b in range(lower_limit, upper_limit + 1)
|
|
845
|
+
)
|
|
846
|
+
|
|
847
|
+
cand_loss, cand_dg, new_pos = min(results, key=lambda x: x[0])
|
|
848
|
+
|
|
849
|
+
if cand_loss < best_loss * (1 + allowed_loss_increase_percent):
|
|
850
|
+
old_pos = band_a[1]
|
|
851
|
+
best_dg = cand_dg
|
|
852
|
+
best_loss = cand_loss
|
|
853
|
+
optimizer.best_params["global_logits"] = disc_to_logits(
|
|
854
|
+
best_dg, num_materials=num_materials, big_pos=1e5
|
|
855
|
+
)
|
|
856
|
+
outer_tbar.set_description(
|
|
857
|
+
f"Swap {swap_idx} moved {old_pos}->{new_pos} | Loss {best_loss:.4f}"
|
|
858
|
+
)
|
|
859
|
+
print(
|
|
860
|
+
f"Swap {swap_idx} moved {old_pos}->{new_pos} | Loss {best_loss:.4f}"
|
|
861
|
+
)
|
|
862
|
+
improved = True
|
|
863
|
+
break
|
|
864
|
+
|
|
865
|
+
outer_tbar.close()
|
|
866
|
+
return best_dg
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Grid-runner for autoforge.py
|
|
4
|
+
|
|
5
|
+
Usage example (inline defaults):
|
|
6
|
+
python run_autoforge_grid.py
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import subprocess
|
|
11
|
+
import sys
|
|
12
|
+
from concurrent.futures import ProcessPoolExecutor, as_completed
|
|
13
|
+
from itertools import product
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from time import strftime
|
|
16
|
+
|
|
17
|
+
from tqdm import tqdm
|
|
18
|
+
|
|
19
|
+
# ---------- editable section ---------- #
|
|
20
|
+
# Any CLI flag accepted by autoforge.py can be placed here.
|
|
21
|
+
DEFAULT_ARGS: dict[str, object] = {
|
|
22
|
+
"--csv_file": "../../../bambulab.csv",
|
|
23
|
+
"--iterations": 6000,
|
|
24
|
+
"--num_init_rounds": 8,
|
|
25
|
+
"--stl_output_size": 50,
|
|
26
|
+
"--disable_visualization_for_gradio": 1,
|
|
27
|
+
"--warmup_fraction": 1.0,
|
|
28
|
+
"--fast_pruning_percent": 0.05,
|
|
29
|
+
"--learning_rate_warmup_fraction": 0.01,
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
SWEEP_PARAM = "--learning_rate" # param to overwrite per run
|
|
33
|
+
SWEEP_VALUES = [
|
|
34
|
+
0.01,
|
|
35
|
+
0.02,
|
|
36
|
+
0.03,
|
|
37
|
+
] # ,0.04,0.05,0.06,0.07,0.08,0.09,0.1] # values to sweep over
|
|
38
|
+
|
|
39
|
+
IMAGES_DIR = Path("/home/scsadmin/AutoForge/images/test_images")
|
|
40
|
+
BASE_OUTPUT_DIR = Path("output_grid") # all run folders are created inside here
|
|
41
|
+
MAX_WORKERS = 16 # parallel jobs
|
|
42
|
+
# ---------- end editable section ------ #
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def make_cmd(image_path: Path, param_value, run_dir: Path, idx: int) -> list[str]:
|
|
46
|
+
"""Assemble the command line for one run."""
|
|
47
|
+
cmd: list[str] = [
|
|
48
|
+
sys.executable,
|
|
49
|
+
"../auto_forge.py",
|
|
50
|
+
"--input_image",
|
|
51
|
+
str(image_path),
|
|
52
|
+
"--output_folder",
|
|
53
|
+
str(run_dir),
|
|
54
|
+
"--random_seed",
|
|
55
|
+
str(idx + 1),
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
# default args
|
|
59
|
+
for flag, val in DEFAULT_ARGS.items():
|
|
60
|
+
if isinstance(val, bool):
|
|
61
|
+
if val: # store_true flag
|
|
62
|
+
cmd.append(flag)
|
|
63
|
+
else:
|
|
64
|
+
cmd.extend([flag, str(val)])
|
|
65
|
+
|
|
66
|
+
# swept parameter
|
|
67
|
+
cmd.extend([SWEEP_PARAM, str(param_value)])
|
|
68
|
+
return cmd
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def run_single(image_path: Path, param_value, idx) -> dict:
|
|
72
|
+
"""Worker: launch subprocess, parse loss, return result dict."""
|
|
73
|
+
run_dir = BASE_OUTPUT_DIR / (
|
|
74
|
+
f"{image_path.stem}_{idx}_{SWEEP_PARAM.lstrip('-')}={param_value}"
|
|
75
|
+
)
|
|
76
|
+
run_dir.mkdir(parents=True, exist_ok=True)
|
|
77
|
+
|
|
78
|
+
cmd = make_cmd(image_path, param_value, run_dir, idx)
|
|
79
|
+
|
|
80
|
+
proc = subprocess.run(cmd, capture_output=True, text=True)
|
|
81
|
+
# print output
|
|
82
|
+
if proc.returncode != 0:
|
|
83
|
+
print(f"Running: {' '.join(cmd)}")
|
|
84
|
+
print(f"Return code: {proc.returncode}")
|
|
85
|
+
print(f"Stdout: {proc.stdout.strip()}")
|
|
86
|
+
print(f"Stderr: {proc.stderr.strip()}")
|
|
87
|
+
|
|
88
|
+
loss_file = run_dir / "final_loss.txt"
|
|
89
|
+
loss = None
|
|
90
|
+
if loss_file.exists():
|
|
91
|
+
try:
|
|
92
|
+
loss = float(loss_file.read_text().strip())
|
|
93
|
+
except ValueError:
|
|
94
|
+
pass
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
"image": str(image_path),
|
|
98
|
+
"param_value": param_value,
|
|
99
|
+
"loss": loss,
|
|
100
|
+
"output_folder": str(run_dir),
|
|
101
|
+
"returncode": proc.returncode,
|
|
102
|
+
"stdout": proc.stdout,
|
|
103
|
+
"stderr": proc.stderr,
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def main():
|
|
108
|
+
values = SWEEP_VALUES
|
|
109
|
+
|
|
110
|
+
images = [
|
|
111
|
+
p for p in IMAGES_DIR.iterdir() if p.suffix.lower() in {".png", ".jpg", ".jpeg"}
|
|
112
|
+
]
|
|
113
|
+
|
|
114
|
+
BASE_OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
|
|
115
|
+
ts = strftime("%Y%m%d_%H%M%S")
|
|
116
|
+
# run grid
|
|
117
|
+
results = []
|
|
118
|
+
with ProcessPoolExecutor(max_workers=MAX_WORKERS) as pool:
|
|
119
|
+
futures = {
|
|
120
|
+
pool.submit(run_single, img, val, idx): (img, val)
|
|
121
|
+
for idx in range(1)
|
|
122
|
+
for img, val in product(images, values)
|
|
123
|
+
}
|
|
124
|
+
for fut in tqdm(as_completed(futures), total=len(futures), desc="Running grid"):
|
|
125
|
+
res = fut.result()
|
|
126
|
+
results.append(res)
|
|
127
|
+
print(
|
|
128
|
+
f"✓ finished {Path(res['image']).name} "
|
|
129
|
+
f"{SWEEP_PARAM}={res['param_value']} loss={res['loss']}"
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
# write summary
|
|
133
|
+
|
|
134
|
+
out_path = BASE_OUTPUT_DIR / f"out_dict_{ts}.json"
|
|
135
|
+
with open(out_path, "w") as fp:
|
|
136
|
+
json.dump(results, fp, indent=2)
|
|
137
|
+
print(f"Results saved to {out_path}")
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
if __name__ == "__main__":
|
|
141
|
+
main()
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
import traceback
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
import plotly.graph_objects as go
|
|
7
|
+
from scipy.optimize import curve_fit
|
|
8
|
+
|
|
9
|
+
json_file = [f for f in os.listdir("output_grid") if f.endswith(".json")][0]
|
|
10
|
+
# Load the JSON file with arbitrary keys
|
|
11
|
+
with open(f"output_grid/{json_file}", "r") as f:
|
|
12
|
+
data = json.load(f)
|
|
13
|
+
|
|
14
|
+
# Calculate means, standard deviations, and store the arrays for each condition.
|
|
15
|
+
# We store as a tuple: (key, mean, std, data array)
|
|
16
|
+
stats = {}
|
|
17
|
+
for run in data:
|
|
18
|
+
param_value = run["param_value"]
|
|
19
|
+
loss = run["loss"]
|
|
20
|
+
stats.setdefault(param_value, []).append(loss)
|
|
21
|
+
|
|
22
|
+
# Sort the list of tuples based on the mean (ascending order)
|
|
23
|
+
# stats.sort(key=lambda x: x[1])
|
|
24
|
+
|
|
25
|
+
# Extract sorted lists of keys, means, stds, and store the sorted arrays in a dictionary
|
|
26
|
+
sorted_keys = sorted(list(stats.keys()))
|
|
27
|
+
means = [np.mean(stats[key]) for key in sorted_keys]
|
|
28
|
+
stds = [np.std(stats[key]) for key in sorted_keys]
|
|
29
|
+
n_keys = len(sorted_keys)
|
|
30
|
+
|
|
31
|
+
# print number of samples for every key:
|
|
32
|
+
for key in sorted_keys:
|
|
33
|
+
print(f"{key}: {len(stats[key])} samples")
|
|
34
|
+
|
|
35
|
+
# sprt all by lowest mean value
|
|
36
|
+
# sorted_indices = np.argsort(means)
|
|
37
|
+
# sorted_keys = [sorted_keys[i] for i in sorted_indices]
|
|
38
|
+
# means = [means[i] for i in sorted_indices]
|
|
39
|
+
# stds = [stds[i] for i in sorted_indices]
|
|
40
|
+
|
|
41
|
+
# Create x-positions for the bars (one per key)
|
|
42
|
+
x_positions = list(range(n_keys))
|
|
43
|
+
|
|
44
|
+
# Create a Plotly bar chart with error bars
|
|
45
|
+
fig = go.Figure()
|
|
46
|
+
for i, key in enumerate(sorted_keys):
|
|
47
|
+
fig.add_trace(
|
|
48
|
+
go.Bar(
|
|
49
|
+
name=key,
|
|
50
|
+
x=[x_positions[i]],
|
|
51
|
+
y=[means[i]],
|
|
52
|
+
error_y=dict(type="data", array=[stds[i]], visible=True),
|
|
53
|
+
)
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
# Define an exponential decay function
|
|
58
|
+
def exp_decay(x, a, b, c):
|
|
59
|
+
return a * np.exp(-b * x) + c
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
# Convert x_positions and means to numpy arrays for curve fitting
|
|
63
|
+
x_data = np.array(x_positions)
|
|
64
|
+
y_data = np.array(means)
|
|
65
|
+
try:
|
|
66
|
+
# Fit the curve
|
|
67
|
+
popt, _ = curve_fit(exp_decay, x_data, y_data, p0=(1, 0.1, 0)) # initial guesses
|
|
68
|
+
|
|
69
|
+
# Generate smooth x values for the fitted curve
|
|
70
|
+
x_fit = np.linspace(min(x_data), max(x_data), 500)
|
|
71
|
+
y_fit = exp_decay(x_fit, *popt)
|
|
72
|
+
|
|
73
|
+
# Add the fitted curve to the figure
|
|
74
|
+
fig.add_trace(
|
|
75
|
+
go.Scatter(
|
|
76
|
+
x=x_fit,
|
|
77
|
+
y=y_fit,
|
|
78
|
+
mode="lines",
|
|
79
|
+
name="Exponential Fit",
|
|
80
|
+
line=dict(dash="dash", width=3),
|
|
81
|
+
)
|
|
82
|
+
)
|
|
83
|
+
except Exception:
|
|
84
|
+
traceback.print_exc()
|
|
85
|
+
|
|
86
|
+
threshold = 0.05 # 1% improvement
|
|
87
|
+
for i in range(1, len(means)):
|
|
88
|
+
rel_change = (means[i - 1] - means[i]) / means[i - 1]
|
|
89
|
+
print(
|
|
90
|
+
f"{sorted_keys[i - 1]} -> {sorted_keys[i]}: relative change = {rel_change:.4f}"
|
|
91
|
+
)
|
|
92
|
+
if rel_change < threshold:
|
|
93
|
+
print(
|
|
94
|
+
f"Diminishing returns start at x = {sorted_keys[i]} (relative change = {rel_change:.4f})"
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
fig.update_layout(
|
|
98
|
+
title="Comparison of Conditions with Mean ± STD (Sorted by Mean)",
|
|
99
|
+
xaxis=dict(
|
|
100
|
+
tickmode="array",
|
|
101
|
+
tickvals=x_positions,
|
|
102
|
+
ticktext=sorted_keys,
|
|
103
|
+
tickangle=45, # Rotate labels for better readability
|
|
104
|
+
),
|
|
105
|
+
yaxis_title="Value",
|
|
106
|
+
barmode="group",
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
fig.update_layout(
|
|
111
|
+
autosize=False,
|
|
112
|
+
width=1920,
|
|
113
|
+
height=800,
|
|
114
|
+
)
|
|
115
|
+
# save image
|
|
116
|
+
fig.write_image("out.png")
|