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.
Files changed (43) hide show
  1. {autoforge-1.8.2 → autoforge-1.8.4}/PKG-INFO +3 -3
  2. {autoforge-1.8.2 → autoforge-1.8.4}/README.md +2 -2
  3. {autoforge-1.8.2 → autoforge-1.8.4}/pyproject.toml +1 -1
  4. {autoforge-1.8.2 → autoforge-1.8.4}/src/AutoForge.egg-info/PKG-INFO +3 -3
  5. {autoforge-1.8.2 → autoforge-1.8.4}/src/autoforge/Helper/Heightmaps/ChristofidesHeightMap.py +4 -4
  6. {autoforge-1.8.2 → autoforge-1.8.4}/src/autoforge/Helper/OptimizerHelper.py +101 -2
  7. {autoforge-1.8.2 → autoforge-1.8.4}/src/autoforge/Helper/PruningHelper.py +111 -18
  8. autoforge-1.8.4/src/autoforge/Init_comparer/auto_forge_init_check.py +141 -0
  9. autoforge-1.8.4/src/autoforge/Init_comparer/plot_results.py +116 -0
  10. {autoforge-1.8.2 → autoforge-1.8.4}/src/autoforge/Modules/Optimizer.py +17 -12
  11. {autoforge-1.8.2 → autoforge-1.8.4}/src/autoforge/auto_forge.py +97 -11
  12. autoforge-1.8.2/src/autoforge/Init_comparer/auto_forge_init_check.py +0 -264
  13. autoforge-1.8.2/src/autoforge/Init_comparer/plot_results.py +0 -126
  14. {autoforge-1.8.2 → autoforge-1.8.4}/setup.cfg +0 -0
  15. {autoforge-1.8.2 → autoforge-1.8.4}/src/AutoForge.egg-info/SOURCES.txt +0 -0
  16. {autoforge-1.8.2 → autoforge-1.8.4}/src/AutoForge.egg-info/dependency_links.txt +0 -0
  17. {autoforge-1.8.2 → autoforge-1.8.4}/src/AutoForge.egg-info/entry_points.txt +0 -0
  18. {autoforge-1.8.2 → autoforge-1.8.4}/src/AutoForge.egg-info/requires.txt +0 -0
  19. {autoforge-1.8.2 → autoforge-1.8.4}/src/AutoForge.egg-info/top_level.txt +0 -0
  20. {autoforge-1.8.2 → autoforge-1.8.4}/src/autoforge/Gui/__init__.py +0 -0
  21. {autoforge-1.8.2 → autoforge-1.8.4}/src/autoforge/Gui/browser_gui/Views/__init__.py +0 -0
  22. {autoforge-1.8.2 → autoforge-1.8.4}/src/autoforge/Gui/browser_gui/Views/backend_database.py +0 -0
  23. {autoforge-1.8.2 → autoforge-1.8.4}/src/autoforge/Gui/browser_gui/Views/filament_editor.py +0 -0
  24. {autoforge-1.8.2 → autoforge-1.8.4}/src/autoforge/Gui/browser_gui/app.py +0 -0
  25. {autoforge-1.8.2 → autoforge-1.8.4}/src/autoforge/Gui/browser_gui/main.py +0 -0
  26. {autoforge-1.8.2 → autoforge-1.8.4}/src/autoforge/Helper/CAdamW.py +0 -0
  27. {autoforge-1.8.2 → autoforge-1.8.4}/src/autoforge/Helper/FilamentHelper.py +0 -0
  28. {autoforge-1.8.2 → autoforge-1.8.4}/src/autoforge/Helper/Heightmaps/DepthEstimateHeightMap.py +0 -0
  29. {autoforge-1.8.2 → autoforge-1.8.4}/src/autoforge/Helper/Heightmaps/OldHeightmap.py +0 -0
  30. {autoforge-1.8.2 → autoforge-1.8.4}/src/autoforge/Helper/Heightmaps/__init__.py +0 -0
  31. {autoforge-1.8.2 → autoforge-1.8.4}/src/autoforge/Helper/ImageHelper.py +0 -0
  32. {autoforge-1.8.2 → autoforge-1.8.4}/src/autoforge/Helper/OutputHelper.py +0 -0
  33. {autoforge-1.8.2 → autoforge-1.8.4}/src/autoforge/Helper/__init__.py +0 -0
  34. {autoforge-1.8.2 → autoforge-1.8.4}/src/autoforge/Helper/filamentcolors_library.py +0 -0
  35. {autoforge-1.8.2 → autoforge-1.8.4}/src/autoforge/Init_comparer/__init__.py +0 -0
  36. {autoforge-1.8.2 → autoforge-1.8.4}/src/autoforge/Loss/LossFunctions.py +0 -0
  37. {autoforge-1.8.2 → autoforge-1.8.4}/src/autoforge/Loss/PerceptionLoss.py +0 -0
  38. {autoforge-1.8.2 → autoforge-1.8.4}/src/autoforge/Loss/__init__.py +0 -0
  39. {autoforge-1.8.2 → autoforge-1.8.4}/src/autoforge/Misc/__init__.py +0 -0
  40. {autoforge-1.8.2 → autoforge-1.8.4}/src/autoforge/Misc/color_fit_all.py +0 -0
  41. {autoforge-1.8.2 → autoforge-1.8.4}/src/autoforge/Modules/__init__.py +0 -0
  42. {autoforge-1.8.2 → autoforge-1.8.4}/src/autoforge/__init__.py +0 -0
  43. {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.2
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.2) (20%).
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.2) (20%).
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.2"
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.2
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.2) (20%).
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
 
@@ -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=(2.0, 1.0, 1.0),
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=150,
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
- new_labels * cluster_layers,
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
- eff_thick = p_print * h # [L,H,W]
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
- eff_thick: torch.Tensor = p_print * h # [L,H,W]
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
- with _gpu_lock, torch.no_grad():
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")