autoforge 1.9.2.2__tar.gz → 1.9.2.3__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.9.2.2 → autoforge-1.9.2.3}/PKG-INFO +3 -3
- {autoforge-1.9.2.2 → autoforge-1.9.2.3}/pyproject.toml +3 -3
- {autoforge-1.9.2.2 → autoforge-1.9.2.3}/src/AutoForge.egg-info/PKG-INFO +3 -3
- {autoforge-1.9.2.2 → autoforge-1.9.2.3}/src/AutoForge.egg-info/requires.txt +1 -1
- {autoforge-1.9.2.2 → autoforge-1.9.2.3}/src/autoforge/Helper/OutputHelper.py +49 -16
- autoforge-1.9.2.3/src/autoforge/Misc/color_fit_all.py +448 -0
- autoforge-1.9.2.2/src/autoforge/Misc/color_fit_all.py +0 -1892
- {autoforge-1.9.2.2 → autoforge-1.9.2.3}/README.md +0 -0
- {autoforge-1.9.2.2 → autoforge-1.9.2.3}/setup.cfg +0 -0
- {autoforge-1.9.2.2 → autoforge-1.9.2.3}/src/AutoForge.egg-info/SOURCES.txt +0 -0
- {autoforge-1.9.2.2 → autoforge-1.9.2.3}/src/AutoForge.egg-info/dependency_links.txt +0 -0
- {autoforge-1.9.2.2 → autoforge-1.9.2.3}/src/AutoForge.egg-info/entry_points.txt +0 -0
- {autoforge-1.9.2.2 → autoforge-1.9.2.3}/src/AutoForge.egg-info/top_level.txt +0 -0
- {autoforge-1.9.2.2 → autoforge-1.9.2.3}/src/autoforge/Helper/AmpUtils.py +0 -0
- {autoforge-1.9.2.2 → autoforge-1.9.2.3}/src/autoforge/Helper/CAdamW.py +0 -0
- {autoforge-1.9.2.2 → autoforge-1.9.2.3}/src/autoforge/Helper/FilamentHelper.py +0 -0
- {autoforge-1.9.2.2 → autoforge-1.9.2.3}/src/autoforge/Helper/Heightmaps/ChristofidesHeightMap.py +0 -0
- {autoforge-1.9.2.2 → autoforge-1.9.2.3}/src/autoforge/Helper/Heightmaps/DepthEstimateHeightMap.py +0 -0
- {autoforge-1.9.2.2 → autoforge-1.9.2.3}/src/autoforge/Helper/Heightmaps/__init__.py +0 -0
- {autoforge-1.9.2.2 → autoforge-1.9.2.3}/src/autoforge/Helper/ImageHelper.py +0 -0
- {autoforge-1.9.2.2 → autoforge-1.9.2.3}/src/autoforge/Helper/OptimizerHelper.py +0 -0
- {autoforge-1.9.2.2 → autoforge-1.9.2.3}/src/autoforge/Helper/OtherHelper.py +0 -0
- {autoforge-1.9.2.2 → autoforge-1.9.2.3}/src/autoforge/Helper/PruningHelper.py +0 -0
- {autoforge-1.9.2.2 → autoforge-1.9.2.3}/src/autoforge/Helper/__init__.py +0 -0
- {autoforge-1.9.2.2 → autoforge-1.9.2.3}/src/autoforge/Helper/filamentcolors_library.py +0 -0
- {autoforge-1.9.2.2 → autoforge-1.9.2.3}/src/autoforge/Init_comparer/__init__.py +0 -0
- {autoforge-1.9.2.2 → autoforge-1.9.2.3}/src/autoforge/Init_comparer/auto_forge_init_check.py +0 -0
- {autoforge-1.9.2.2 → autoforge-1.9.2.3}/src/autoforge/Init_comparer/plot_results.py +0 -0
- {autoforge-1.9.2.2 → autoforge-1.9.2.3}/src/autoforge/Loss/LossFunctions.py +0 -0
- {autoforge-1.9.2.2 → autoforge-1.9.2.3}/src/autoforge/Loss/PerceptionLoss.py +0 -0
- {autoforge-1.9.2.2 → autoforge-1.9.2.3}/src/autoforge/Loss/__init__.py +0 -0
- {autoforge-1.9.2.2 → autoforge-1.9.2.3}/src/autoforge/Misc/__init__.py +0 -0
- {autoforge-1.9.2.2 → autoforge-1.9.2.3}/src/autoforge/Modules/Optimizer.py +0 -0
- {autoforge-1.9.2.2 → autoforge-1.9.2.3}/src/autoforge/Modules/__init__.py +0 -0
- {autoforge-1.9.2.2 → autoforge-1.9.2.3}/src/autoforge/__init__.py +0 -0
- {autoforge-1.9.2.2 → autoforge-1.9.2.3}/src/autoforge/__main__.py +0 -0
- {autoforge-1.9.2.2 → autoforge-1.9.2.3}/src/autoforge/auto_forge.py +0 -0
- {autoforge-1.9.2.2 → autoforge-1.9.2.3}/src/autoforge/priority_mask_editor.py +0 -0
- {autoforge-1.9.2.2 → autoforge-1.9.2.3}/tests/test_amp_autocast_probe.py +0 -0
- {autoforge-1.9.2.2 → autoforge-1.9.2.3}/tests/test_cadamw.py +0 -0
- {autoforge-1.9.2.2 → autoforge-1.9.2.3}/tests/test_christofides_height_map.py +0 -0
- {autoforge-1.9.2.2 → autoforge-1.9.2.3}/tests/test_christofides_heightmap.py +0 -0
- {autoforge-1.9.2.2 → autoforge-1.9.2.3}/tests/test_depth_adjusted_height_map.py +0 -0
- {autoforge-1.9.2.2 → autoforge-1.9.2.3}/tests/test_depth_estimate_height_map.py +0 -0
- {autoforge-1.9.2.2 → autoforge-1.9.2.3}/tests/test_end_to_end_optimizer.py +0 -0
- {autoforge-1.9.2.2 → autoforge-1.9.2.3}/tests/test_filament_and_output.py +0 -0
- {autoforge-1.9.2.2 → autoforge-1.9.2.3}/tests/test_filament_helper.py +0 -0
- {autoforge-1.9.2.2 → autoforge-1.9.2.3}/tests/test_flatforge.py +0 -0
- {autoforge-1.9.2.2 → autoforge-1.9.2.3}/tests/test_heightmap_init.py +0 -0
- {autoforge-1.9.2.2 → autoforge-1.9.2.3}/tests/test_image_helper.py +0 -0
- {autoforge-1.9.2.2 → autoforge-1.9.2.3}/tests/test_loss_functions.py +0 -0
- {autoforge-1.9.2.2 → autoforge-1.9.2.3}/tests/test_optimizer_helper.py +0 -0
- {autoforge-1.9.2.2 → autoforge-1.9.2.3}/tests/test_optimizer_module.py +0 -0
- {autoforge-1.9.2.2 → autoforge-1.9.2.3}/tests/test_optimizer_smoke.py +0 -0
- {autoforge-1.9.2.2 → autoforge-1.9.2.3}/tests/test_output_helper.py +0 -0
- {autoforge-1.9.2.2 → autoforge-1.9.2.3}/tests/test_output_helper_extended.py +0 -0
- {autoforge-1.9.2.2 → autoforge-1.9.2.3}/tests/test_perception_and_pruning.py +0 -0
- {autoforge-1.9.2.2 → autoforge-1.9.2.3}/tests/test_property_based.py +0 -0
- {autoforge-1.9.2.2 → autoforge-1.9.2.3}/tests/test_pruning_helper.py +0 -0
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: AutoForge
|
|
3
|
-
Version: 1.9.2.
|
|
3
|
+
Version: 1.9.2.3
|
|
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
|
|
7
|
-
Requires-Python: >=3.
|
|
7
|
+
Requires-Python: >=3.12
|
|
8
8
|
Description-Content-Type: text/markdown
|
|
9
9
|
Requires-Dist: numpy
|
|
10
|
-
Requires-Dist: torch>=2.
|
|
10
|
+
Requires-Dist: torch>=2.9.1
|
|
11
11
|
Requires-Dist: torchvision>=0.21.0
|
|
12
12
|
Requires-Dist: transformers>=4.48.1
|
|
13
13
|
Requires-Dist: opencv-python>=4.2.0.32
|
|
@@ -4,15 +4,15 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "AutoForge"
|
|
7
|
-
version="1.9.2.
|
|
7
|
+
version="1.9.2.3"
|
|
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"
|
|
11
|
-
requires-python = ">=3.
|
|
11
|
+
requires-python = ">=3.12"
|
|
12
12
|
license = { text = "CC BY-NC-SA 4.0" }
|
|
13
13
|
dependencies = [
|
|
14
14
|
"numpy",
|
|
15
|
-
"torch>=2.
|
|
15
|
+
"torch>=2.9.1",
|
|
16
16
|
"torchvision>=0.21.0",
|
|
17
17
|
"transformers>=4.48.1",
|
|
18
18
|
"opencv-python>=4.2.0.32",
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: AutoForge
|
|
3
|
-
Version: 1.9.2.
|
|
3
|
+
Version: 1.9.2.3
|
|
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
|
|
7
|
-
Requires-Python: >=3.
|
|
7
|
+
Requires-Python: >=3.12
|
|
8
8
|
Description-Content-Type: text/markdown
|
|
9
9
|
Requires-Dist: numpy
|
|
10
|
-
Requires-Dist: torch>=2.
|
|
10
|
+
Requires-Dist: torch>=2.9.1
|
|
11
11
|
Requires-Dist: torchvision>=0.21.0
|
|
12
12
|
Requires-Dist: transformers>=4.48.1
|
|
13
13
|
Requires-Dist: opencv-python>=4.2.0.32
|
|
@@ -906,19 +906,52 @@ def _save_stl_with_manifold_fix(triangles, filename):
|
|
|
906
906
|
buffer.write(struct.pack("<I", num_triangles))
|
|
907
907
|
buffer.write(stl_data.tobytes())
|
|
908
908
|
buffer.seek(0)
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
909
|
+
|
|
910
|
+
with open(filename, "wb") as f:
|
|
911
|
+
buffer.seek(0)
|
|
912
|
+
f.write(buffer.read())
|
|
913
|
+
|
|
914
|
+
# # Load with trimesh and fix manifold issues
|
|
915
|
+
# try:
|
|
916
|
+
# mesh = trimesh.load(buffer, file_type="stl")
|
|
917
|
+
# mesh.merge_vertices()
|
|
918
|
+
#
|
|
919
|
+
# # Try available degenerate-face removal utilities across trimesh versions
|
|
920
|
+
# try:
|
|
921
|
+
# if hasattr(mesh, "remove_degenerate_faces"):
|
|
922
|
+
# mesh.remove_degenerate_faces()
|
|
923
|
+
# elif hasattr(trimesh.repair, "remove_degenerate_faces"):
|
|
924
|
+
# trimesh.repair.remove_degenerate_faces(mesh)
|
|
925
|
+
# elif hasattr(trimesh.repair, "remove_degenerate_triangles"):
|
|
926
|
+
# trimesh.repair.remove_degenerate_triangles(mesh)
|
|
927
|
+
# except Exception as repair_err:
|
|
928
|
+
# print(f"Warning: Skipped degenerate-face removal for {filename}: {repair_err}")
|
|
929
|
+
#
|
|
930
|
+
# # Remove duplicate faces if the method exists
|
|
931
|
+
# try:
|
|
932
|
+
# if hasattr(mesh, "remove_duplicate_faces"):
|
|
933
|
+
# mesh.remove_duplicate_faces()
|
|
934
|
+
# elif hasattr(trimesh.repair, "remove_duplicate_faces"):
|
|
935
|
+
# trimesh.repair.remove_duplicate_faces(mesh)
|
|
936
|
+
# except Exception as repair_err:
|
|
937
|
+
# print(f"Warning: Skipped duplicate-face removal for {filename}: {repair_err}")
|
|
938
|
+
#
|
|
939
|
+
# # Fill holes and fix normals when available
|
|
940
|
+
# try:
|
|
941
|
+
# if hasattr(mesh, "fill_holes"):
|
|
942
|
+
# mesh.fill_holes()
|
|
943
|
+
# except Exception as repair_err:
|
|
944
|
+
# print(f"Warning: Skipped hole filling for {filename}: {repair_err}")
|
|
945
|
+
#
|
|
946
|
+
# try:
|
|
947
|
+
# mesh.fix_normals()
|
|
948
|
+
# except Exception as repair_err:
|
|
949
|
+
# print(f"Warning: Skipped normal fixing for {filename}: {repair_err}")
|
|
950
|
+
#
|
|
951
|
+
# mesh.export(filename)
|
|
952
|
+
# except Exception as e:
|
|
953
|
+
# print(f"Warning: Error fixing manifold for {filename}: {e}")
|
|
954
|
+
# # Fall back to saving without manifold fix
|
|
955
|
+
# with open(filename, "wb") as f:
|
|
956
|
+
# buffer.seek(0)
|
|
957
|
+
# f.write(buffer.read())
|
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import pandas as pd
|
|
3
|
+
from typing import List, Tuple, Dict
|
|
4
|
+
|
|
5
|
+
import pysindy as ps
|
|
6
|
+
from pysindy.feature_library import PolynomialLibrary
|
|
7
|
+
from pysindy.optimizers import TorchOptimizer
|
|
8
|
+
import matplotlib.pyplot as plt
|
|
9
|
+
from sklearn.preprocessing import StandardScaler
|
|
10
|
+
|
|
11
|
+
"""
|
|
12
|
+
Opacity SINDy Estimator (scalar opacity per layer)
|
|
13
|
+
-------------------------------------------------
|
|
14
|
+
- Extracts a single scalar opacity o_l per layer via RGB least squares using
|
|
15
|
+
the Porter–Duff over compositing model.
|
|
16
|
+
- Fits a PySINDy continuous-time model with control inputs u = [TD_I, layer_height].
|
|
17
|
+
- Simulates opacity sequences and composites predicted colors; plots measured vs predicted.
|
|
18
|
+
|
|
19
|
+
Expected CSV columns:
|
|
20
|
+
- "Transmission Distance" (TD_I)
|
|
21
|
+
- "Background Material" (hex color)
|
|
22
|
+
- "Layer Material" (hex color)
|
|
23
|
+
- "Layer 1" ... "Layer N" (hex color per layer composite measurement)
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
# ---------------------
|
|
27
|
+
# Utilities
|
|
28
|
+
# ---------------------
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def hex_to_rgb(hex_str: str) -> np.ndarray:
|
|
32
|
+
s = hex_str.lstrip("#")
|
|
33
|
+
r = int(s[0:2], 16)
|
|
34
|
+
g = int(s[2:4], 16)
|
|
35
|
+
b = int(s[4:6], 16)
|
|
36
|
+
return np.array([r, g, b], dtype=np.float32)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# sRGB (0-1) <-> linear RGB (0-1)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _srgb01_to_linear01(c: np.ndarray) -> np.ndarray:
|
|
43
|
+
c = np.asarray(c, dtype=np.float32)
|
|
44
|
+
return np.where(
|
|
45
|
+
c <= 0.04045,
|
|
46
|
+
c / 12.92,
|
|
47
|
+
((c + 0.055) / 1.055) ** 2.4,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _linear01_to_srgb01(c_lin: np.ndarray) -> np.ndarray:
|
|
52
|
+
c_lin = np.asarray(c_lin, dtype=np.float32)
|
|
53
|
+
return np.where(
|
|
54
|
+
c_lin <= 0.0031308,
|
|
55
|
+
12.92 * c_lin,
|
|
56
|
+
1.055 * (np.clip(c_lin, 0.0, None) ** (1.0 / 2.4)) - 0.055,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def hex_to_linear_rgb01(hex_str: str) -> np.ndarray:
|
|
61
|
+
rgb255 = hex_to_rgb(hex_str)
|
|
62
|
+
srgb01 = np.clip(rgb255 / 255.0, 0.0, 1.0)
|
|
63
|
+
return np.clip(_srgb01_to_linear01(srgb01), 0.0, 1.0)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def linear_rgb01_to_srgb255(rgb_lin01: np.ndarray) -> np.ndarray:
|
|
67
|
+
srgb01 = _linear01_to_srgb01(np.clip(rgb_lin01, 0.0, 1.0))
|
|
68
|
+
return np.clip(srgb01 * 255.0, 0.0, 255.0)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
# Forward compositing from scalar opacity series (in linear RGB domain)
|
|
72
|
+
# Returns list of per-layer composite sRGB (float32 in [0,255]) and final color.
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def composite_from_opacity_series(
|
|
76
|
+
bg_lin01: np.ndarray,
|
|
77
|
+
fg_lin01: np.ndarray,
|
|
78
|
+
o_series: np.ndarray,
|
|
79
|
+
) -> Tuple[List[np.ndarray], np.ndarray]:
|
|
80
|
+
comp_lin = np.zeros(3, dtype=np.float32)
|
|
81
|
+
remaining = 1.0
|
|
82
|
+
per_layer_colors_srgb255: List[np.ndarray] = []
|
|
83
|
+
for o in o_series:
|
|
84
|
+
o = float(np.clip(o, 0.0, 1.0))
|
|
85
|
+
comp_lin = comp_lin + (remaining * o) * fg_lin01
|
|
86
|
+
remaining = remaining * (1.0 - o)
|
|
87
|
+
color_lin = comp_lin + remaining * bg_lin01
|
|
88
|
+
color_srgb255 = linear_rgb01_to_srgb255(color_lin)
|
|
89
|
+
per_layer_colors_srgb255.append(color_srgb255)
|
|
90
|
+
final_color = (
|
|
91
|
+
per_layer_colors_srgb255[-1]
|
|
92
|
+
if per_layer_colors_srgb255
|
|
93
|
+
else linear_rgb01_to_srgb255(bg_lin01)
|
|
94
|
+
)
|
|
95
|
+
return per_layer_colors_srgb255, final_color
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def extract_scalar_opacity_series(
|
|
99
|
+
df: pd.DataFrame,
|
|
100
|
+
num_layers: int,
|
|
101
|
+
layer_height: float,
|
|
102
|
+
) -> Tuple[List[np.ndarray], List[np.ndarray]]:
|
|
103
|
+
"""
|
|
104
|
+
Invert compositing to recover a scalar opacity per layer using RGB least squares.
|
|
105
|
+
Operates in linear RGB domain for physical correctness.
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
- o_list: list of (num_layers,) arrays per sample
|
|
109
|
+
- t_list: list of (num_layers,) time arrays per sample (t = idx * layer_height)
|
|
110
|
+
"""
|
|
111
|
+
o_list: List[np.ndarray] = []
|
|
112
|
+
t_list: List[np.ndarray] = []
|
|
113
|
+
|
|
114
|
+
eps = 1e-8
|
|
115
|
+
for _, row in df.iterrows():
|
|
116
|
+
bg_lin = hex_to_linear_rgb01(str(row["Background Material"]))
|
|
117
|
+
fg_lin = hex_to_linear_rgb01(str(row["Layer Material"]))
|
|
118
|
+
|
|
119
|
+
comp_lin = np.zeros(3, dtype=np.float32)
|
|
120
|
+
remaining = 1.0
|
|
121
|
+
|
|
122
|
+
o_series = np.zeros(num_layers, dtype=np.float32)
|
|
123
|
+
for layer in range(1, num_layers + 1):
|
|
124
|
+
meas_lin = hex_to_linear_rgb01(str(row[f"Layer {layer}"]))
|
|
125
|
+
# d = meas - (comp + remaining*bg) = remaining * o * (fg - bg)
|
|
126
|
+
d = meas_lin - (comp_lin + remaining * bg_lin)
|
|
127
|
+
v = remaining * (fg_lin - bg_lin)
|
|
128
|
+
denom = float(np.dot(v, v))
|
|
129
|
+
if denom < eps:
|
|
130
|
+
o = 0.0
|
|
131
|
+
else:
|
|
132
|
+
o = float(np.dot(d, v) / denom)
|
|
133
|
+
o = float(np.clip(o, 0.0, 1.0))
|
|
134
|
+
o_series[layer - 1] = o
|
|
135
|
+
|
|
136
|
+
# Update state in linear
|
|
137
|
+
comp_lin = comp_lin + (remaining * o) * fg_lin
|
|
138
|
+
remaining = remaining * (1.0 - o)
|
|
139
|
+
|
|
140
|
+
t = np.arange(1, num_layers + 1, dtype=np.float32) * layer_height
|
|
141
|
+
o_list.append(o_series)
|
|
142
|
+
t_list.append(t)
|
|
143
|
+
|
|
144
|
+
return o_list, t_list
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def build_control_list(
|
|
148
|
+
df: pd.DataFrame, num_layers: int, layer_height: float
|
|
149
|
+
) -> List[np.ndarray]:
|
|
150
|
+
"""
|
|
151
|
+
Build control input list U per trajectory with two controls per layer:
|
|
152
|
+
u = [TD_I, layer_height]
|
|
153
|
+
Returns a list of arrays shape (num_layers, 2).
|
|
154
|
+
"""
|
|
155
|
+
U_list: List[np.ndarray] = []
|
|
156
|
+
for _, row in df.iterrows():
|
|
157
|
+
TD_I = float(row.at["Transmission Distance"]) # scalar
|
|
158
|
+
U = np.stack(
|
|
159
|
+
[
|
|
160
|
+
np.full(num_layers, TD_I, dtype=np.float32),
|
|
161
|
+
np.full(num_layers, float(layer_height), dtype=np.float32),
|
|
162
|
+
],
|
|
163
|
+
axis=1,
|
|
164
|
+
)
|
|
165
|
+
U_list.append(U)
|
|
166
|
+
return U_list
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
# ---------------------
|
|
170
|
+
# SINDy training (scalar state with controls)
|
|
171
|
+
# ---------------------
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def fit_sindy_with_control(
|
|
175
|
+
trajectories: List[np.ndarray],
|
|
176
|
+
t_list: List[np.ndarray],
|
|
177
|
+
U_list_scaled: List[np.ndarray],
|
|
178
|
+
) -> ps.SINDy:
|
|
179
|
+
"""
|
|
180
|
+
Fit continuous-time SINDy-with-control for a scalar state using multiple trajectories.
|
|
181
|
+
Controls are already scaled.
|
|
182
|
+
"""
|
|
183
|
+
feature_library = PolynomialLibrary(
|
|
184
|
+
degree=3, include_interaction=True, include_bias=True
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
model = ps.SINDy(
|
|
188
|
+
feature_library=feature_library,
|
|
189
|
+
differentiation_method=ps.FiniteDifference(),
|
|
190
|
+
optimizer=TorchOptimizer(),
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
x_list = [traj.reshape(-1, 1) for traj in trajectories]
|
|
194
|
+
model.fit(x=x_list, t=t_list, u=U_list_scaled)
|
|
195
|
+
return model
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
# ---------------------
|
|
199
|
+
# Public API
|
|
200
|
+
# ---------------------
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def run_sindy_opacity(
|
|
204
|
+
csv_path: str = "printed_colors.csv",
|
|
205
|
+
num_layers: int = 16,
|
|
206
|
+
layer_height: float = 0.04,
|
|
207
|
+
plot_examples: bool = True,
|
|
208
|
+
n_plot_samples: int = 6,
|
|
209
|
+
random_seed: int | None = 123,
|
|
210
|
+
) -> Dict[str, object]:
|
|
211
|
+
"""
|
|
212
|
+
Load CSV, invert compositing to get per-layer scalar opacities, fit SINDy-with-control
|
|
213
|
+
model, print equation, and return the model, scaler, and other artifacts.
|
|
214
|
+
"""
|
|
215
|
+
df = pd.read_csv(csv_path)
|
|
216
|
+
|
|
217
|
+
# Extract scalar opacity trajectories (in linear space)
|
|
218
|
+
o_list, t_list = extract_scalar_opacity_series(df, num_layers, layer_height)
|
|
219
|
+
|
|
220
|
+
# Build controls and scale them
|
|
221
|
+
U_list = build_control_list(df, num_layers, layer_height)
|
|
222
|
+
scaler = StandardScaler()
|
|
223
|
+
U_stacked = np.vstack(U_list)
|
|
224
|
+
scaler.fit(U_stacked)
|
|
225
|
+
U_list_scaled = [scaler.transform(U) for U in U_list]
|
|
226
|
+
|
|
227
|
+
# Fit SINDy model (scalar state)
|
|
228
|
+
sindy_model = fit_sindy_with_control(o_list, t_list, U_list_scaled)
|
|
229
|
+
|
|
230
|
+
print("\nDiscovered opacity dynamics (with controls TD_I & layer_height):")
|
|
231
|
+
sindy_model.print()
|
|
232
|
+
|
|
233
|
+
# Smoke simulate first trajectory
|
|
234
|
+
try:
|
|
235
|
+
sim_t = t_list[0]
|
|
236
|
+
sim_u = U_list_scaled[0]
|
|
237
|
+
x0 = np.array([o_list[0][0]], dtype=np.float32)
|
|
238
|
+
x_sim = np.array(sindy_model.simulate(x0, sim_t, u=sim_u)).reshape(-1)
|
|
239
|
+
x_sim = np.clip(x_sim, 0.0, 1.0)
|
|
240
|
+
print("\nSample simulated opacity (first trajectory):", x_sim[:8])
|
|
241
|
+
except Exception as e:
|
|
242
|
+
print("Simulation failed:", e)
|
|
243
|
+
|
|
244
|
+
# Evaluate simulation error across all trajectories (opacity + final RGB)
|
|
245
|
+
metrics = evaluate_simulation_and_color_error(
|
|
246
|
+
sindy_model, df, o_list, t_list, U_list, U_list_scaled
|
|
247
|
+
)
|
|
248
|
+
print("\nOpacity and color simulation error (averaged across trajectories):")
|
|
249
|
+
print(
|
|
250
|
+
f"Opacity: MAE={metrics['opacity']['MAE']:.4f}, RMSE={metrics['opacity']['RMSE']:.4f}"
|
|
251
|
+
)
|
|
252
|
+
print(
|
|
253
|
+
f"Final RGB: MAE={metrics['rgb']['MAE']:.4f}, RMSE={metrics['rgb']['RMSE']:.4f}"
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
artifacts: Dict[str, object] = {
|
|
257
|
+
"model": sindy_model,
|
|
258
|
+
"scaler": scaler,
|
|
259
|
+
"controls": ["Transmission Distance", "Layer Height"],
|
|
260
|
+
"num_layers": num_layers,
|
|
261
|
+
"layer_height": layer_height,
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if plot_examples:
|
|
265
|
+
try:
|
|
266
|
+
plot_random_color_comparison(
|
|
267
|
+
sindy_model,
|
|
268
|
+
scaler,
|
|
269
|
+
df,
|
|
270
|
+
o_list,
|
|
271
|
+
t_list,
|
|
272
|
+
U_list,
|
|
273
|
+
num_layers=num_layers,
|
|
274
|
+
n_samples=n_plot_samples,
|
|
275
|
+
seed=random_seed,
|
|
276
|
+
)
|
|
277
|
+
except Exception as e:
|
|
278
|
+
print("Plotting failed:", e)
|
|
279
|
+
|
|
280
|
+
return artifacts
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def evaluate_simulation_and_color_error(
|
|
284
|
+
model: ps.SINDy,
|
|
285
|
+
df: pd.DataFrame,
|
|
286
|
+
o_list: List[np.ndarray],
|
|
287
|
+
t_list: List[np.ndarray],
|
|
288
|
+
U_list: List[np.ndarray],
|
|
289
|
+
U_list_scaled: List[np.ndarray],
|
|
290
|
+
) -> Dict[str, Dict[str, float]]:
|
|
291
|
+
"""
|
|
292
|
+
Simulate each trajectory and compute errors vs extracted opacity and final RGB.
|
|
293
|
+
Returns a dict with MAE and RMSE for opacity (scalar) and RGB (final color).
|
|
294
|
+
"""
|
|
295
|
+
mae_o_all, rmse_o_all = [], []
|
|
296
|
+
mae_rgb_all, rmse_rgb_all = [], []
|
|
297
|
+
|
|
298
|
+
for i, o_series in enumerate(o_list):
|
|
299
|
+
t = t_list[i]
|
|
300
|
+
u_scaled = U_list_scaled[i]
|
|
301
|
+
x0 = np.array([o_series[0]], dtype=np.float32)
|
|
302
|
+
try:
|
|
303
|
+
sim = np.array(model.simulate(x0, t, u=u_scaled)).reshape(-1)
|
|
304
|
+
except Exception:
|
|
305
|
+
continue
|
|
306
|
+
sim = np.clip(sim, 0.0, 1.0)
|
|
307
|
+
n = min(len(sim), len(o_series))
|
|
308
|
+
err = sim[:n] - o_series[:n]
|
|
309
|
+
mae_o_all.append(float(np.mean(np.abs(err))))
|
|
310
|
+
rmse_o_all.append(float(np.sqrt(np.mean(err**2))))
|
|
311
|
+
|
|
312
|
+
# Compare final RGB (work in linear domain for compositing)
|
|
313
|
+
row = df.iloc[i]
|
|
314
|
+
bg_lin = hex_to_linear_rgb01(str(row["Background Material"]))
|
|
315
|
+
fg_lin = hex_to_linear_rgb01(str(row["Layer Material"]))
|
|
316
|
+
|
|
317
|
+
# Measured final color (sRGB for comparison)
|
|
318
|
+
try:
|
|
319
|
+
meas_hex = str(row[f"Layer {len(o_series)}"])
|
|
320
|
+
meas_rgb = hex_to_rgb(meas_hex)
|
|
321
|
+
except Exception:
|
|
322
|
+
last_layer_col = [c for c in df.columns if c.startswith("Layer ")][-1]
|
|
323
|
+
meas_rgb = hex_to_rgb(str(row[last_layer_col]))
|
|
324
|
+
|
|
325
|
+
_, pred_rgb_srgb = composite_from_opacity_series(bg_lin, fg_lin, sim[:n])
|
|
326
|
+
# Color error in 0-1 space averaged across channels
|
|
327
|
+
e_rgb = (pred_rgb_srgb - meas_rgb) / 255.0
|
|
328
|
+
mae_rgb_all.append(float(np.mean(np.abs(e_rgb))))
|
|
329
|
+
rmse_rgb_all.append(float(np.sqrt(np.mean(e_rgb**2))))
|
|
330
|
+
print(
|
|
331
|
+
f"Sample {i}: Average RGB error (MAE) = {np.mean(mae_rgb_all):.4f}, Average Opacity error (MAE) = {np.mean(mae_o_all):.4f}"
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
return {
|
|
335
|
+
"opacity": {
|
|
336
|
+
"MAE": float(np.mean(mae_o_all)) if mae_o_all else float("nan"),
|
|
337
|
+
"RMSE": float(np.mean(rmse_o_all)) if rmse_o_all else float("nan"),
|
|
338
|
+
},
|
|
339
|
+
"rgb": {
|
|
340
|
+
"MAE": float(np.mean(mae_rgb_all)) if mae_rgb_all else float("nan"),
|
|
341
|
+
"RMSE": float(np.mean(rmse_rgb_all)) if rmse_rgb_all else float("nan"),
|
|
342
|
+
},
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
# ---------------------
|
|
347
|
+
# Visualization: measured vs predicted composite colors
|
|
348
|
+
# ---------------------
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def _rgb_to_patch(rgb: np.ndarray, size: int = 40) -> np.ndarray:
|
|
352
|
+
rgb01 = np.clip(rgb / 255.0, 0.0, 1.0)
|
|
353
|
+
arr = np.ones((size, size, 3), dtype=np.float32)
|
|
354
|
+
arr[..., 0] = rgb01[0]
|
|
355
|
+
arr[..., 1] = rgb01[1]
|
|
356
|
+
arr[..., 2] = rgb01[2]
|
|
357
|
+
return arr
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def plot_random_color_comparison(
|
|
361
|
+
model: ps.SINDy,
|
|
362
|
+
scaler: StandardScaler,
|
|
363
|
+
df: pd.DataFrame,
|
|
364
|
+
o_list: List[np.ndarray],
|
|
365
|
+
t_list: List[np.ndarray],
|
|
366
|
+
U_list: List[np.ndarray],
|
|
367
|
+
num_layers: int,
|
|
368
|
+
n_samples: int = 6,
|
|
369
|
+
seed: int | None = None,
|
|
370
|
+
save_path: str | None = None,
|
|
371
|
+
):
|
|
372
|
+
"""
|
|
373
|
+
Pick 5-10 random rows from df, simulate opacity with the discovered
|
|
374
|
+
ODE and composite predicted colors. Plot measured final color vs predicted
|
|
375
|
+
final color side by side per sample with MAE in the title.
|
|
376
|
+
"""
|
|
377
|
+
rng = np.random.default_rng(seed)
|
|
378
|
+
N = len(df)
|
|
379
|
+
n = max(1, min(n_samples, N))
|
|
380
|
+
idxs = rng.choice(N, size=n, replace=False)
|
|
381
|
+
|
|
382
|
+
rows = [df.iloc[int(i)] for i in idxs]
|
|
383
|
+
|
|
384
|
+
fig, axes = plt.subplots(nrows=n, ncols=2, figsize=(6, 2.2 * n))
|
|
385
|
+
if n == 1:
|
|
386
|
+
axes = np.array([axes])
|
|
387
|
+
|
|
388
|
+
for row_idx, (ax_left, ax_right) in enumerate(axes):
|
|
389
|
+
i = int(idxs[row_idx])
|
|
390
|
+
row = rows[row_idx]
|
|
391
|
+
bg_lin = hex_to_linear_rgb01(str(row["Background Material"]))
|
|
392
|
+
fg_lin = hex_to_linear_rgb01(str(row["Layer Material"]))
|
|
393
|
+
|
|
394
|
+
# Measured final composite color from CSV (Layer num_layers, sRGB space)
|
|
395
|
+
try:
|
|
396
|
+
meas_hex = str(row[f"Layer {num_layers}"])
|
|
397
|
+
meas_rgb = hex_to_rgb(meas_hex)
|
|
398
|
+
except Exception:
|
|
399
|
+
last_layer_col = [c for c in df.columns if c.startswith("Layer ")][-1]
|
|
400
|
+
meas_rgb = hex_to_rgb(str(row[last_layer_col]))
|
|
401
|
+
|
|
402
|
+
# Controls
|
|
403
|
+
U = U_list[i]
|
|
404
|
+
U_scaled = scaler.transform(U)
|
|
405
|
+
|
|
406
|
+
# Simulate predicted opacity sequence
|
|
407
|
+
t = t_list[i]
|
|
408
|
+
x0 = np.array([o_list[i][0]], dtype=np.float32)
|
|
409
|
+
try:
|
|
410
|
+
sim = np.array(model.simulate(x0, t, u=U_scaled)).reshape(-1)
|
|
411
|
+
except Exception:
|
|
412
|
+
sim = np.zeros_like(t)
|
|
413
|
+
sim = np.clip(sim, 0.0, 1.0)
|
|
414
|
+
|
|
415
|
+
# Forward composite predicted final color (linear -> sRGB conversion inside)
|
|
416
|
+
_, pred_rgb = composite_from_opacity_series(
|
|
417
|
+
bg_lin01=bg_lin, fg_lin01=fg_lin, o_series=sim
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
# Build small color patches
|
|
421
|
+
left_img = _rgb_to_patch(meas_rgb)
|
|
422
|
+
right_img = _rgb_to_patch(pred_rgb)
|
|
423
|
+
|
|
424
|
+
ax_left.imshow(left_img)
|
|
425
|
+
ax_left.set_title("Measured")
|
|
426
|
+
ax_left.axis("off")
|
|
427
|
+
|
|
428
|
+
ax_right.imshow(right_img)
|
|
429
|
+
err = np.mean(np.abs((pred_rgb - meas_rgb) / 255.0))
|
|
430
|
+
ax_right.set_title(f"Predicted (MAE={err:.3f})")
|
|
431
|
+
ax_right.axis("off")
|
|
432
|
+
|
|
433
|
+
plt.tight_layout()
|
|
434
|
+
if save_path:
|
|
435
|
+
plt.savefig(save_path, dpi=160)
|
|
436
|
+
print(f"Saved comparison plot to {save_path}")
|
|
437
|
+
else:
|
|
438
|
+
plt.show()
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
if __name__ == "__main__":
|
|
442
|
+
# Run with default CSV name if available; show comparison.
|
|
443
|
+
try:
|
|
444
|
+
run_sindy_opacity(plot_examples=True, n_plot_samples=12)
|
|
445
|
+
except FileNotFoundError:
|
|
446
|
+
print(
|
|
447
|
+
"CSV not found for SINDy opacity run. Please provide a valid csv_path or run within the project that contains printed_colors.csv."
|
|
448
|
+
)
|