9.2 KiB
9.2 KiB
In [ ]:
import sqlite3
import pandas as pd
import numpy as np
from pathlib import Path
DB_PATH = "../OpenNestTraining.db" # Adjust to your database location
OUTPUT_PATH = "../../OpenNest.Engine/Models/angle_predictor.onnx"
COMPETITIVE_THRESHOLD = 0.95 # Angle is "competitive" if >= 95% of bestIn [ ]:
# Extract training data from SQLite
conn = sqlite3.connect(DB_PATH)
query = """
SELECT
p.Area, p.Convexity, p.AspectRatio, p.BBFill, p.Circularity,
p.PerimeterToAreaRatio, p.VertexCount,
r.SheetWidth, r.SheetHeight, r.Id as RunId,
a.AngleDeg, a.Direction, a.PartCount
FROM AngleResults a
JOIN Runs r ON a.RunId = r.Id
JOIN Parts p ON r.PartId = p.Id
WHERE a.PartCount > 0
"""
df = pd.read_sql_query(query, conn)
conn.close()
print(f"Loaded {len(df)} angle result rows")
print(f"Unique runs: {df['RunId'].nunique()}")
print(f"Angle range: {df['AngleDeg'].min()}-{df['AngleDeg'].max()}")In [ ]:
# For each run, find best PartCount (max of H and V per angle),
# then label angles within 95% of best as positive.
# Best count per angle per run (max of H and V)
angle_best = df.groupby(['RunId', 'AngleDeg'])['PartCount'].max().reset_index()
angle_best.columns = ['RunId', 'AngleDeg', 'BestCount']
# Best count per run (overall best angle)
run_best = angle_best.groupby('RunId')['BestCount'].max().reset_index()
run_best.columns = ['RunId', 'RunBest']
# Merge and compute labels
labels = angle_best.merge(run_best, on='RunId')
labels['IsCompetitive'] = (labels['BestCount'] >= labels['RunBest'] * COMPETITIVE_THRESHOLD).astype(int)
# Pivot to 36-column binary label matrix
label_matrix = labels.pivot_table(
index='RunId', columns='AngleDeg', values='IsCompetitive', fill_value=0
)
# Ensure all 36 angle columns exist (0, 5, 10, ..., 175)
all_angles = [i * 5 for i in range(36)]
for a in all_angles:
if a not in label_matrix.columns:
label_matrix[a] = 0
label_matrix = label_matrix[all_angles]
print(f"Label matrix: {label_matrix.shape}")
print(f"Average competitive angles per run: {label_matrix.sum(axis=1).mean():.1f}")In [ ]:
# Build feature matrix - one row per run
features_query = """
SELECT DISTINCT
r.Id as RunId, p.FileName,
p.Area, p.Convexity, p.AspectRatio, p.BBFill, p.Circularity,
p.PerimeterToAreaRatio, p.VertexCount,
r.SheetWidth, r.SheetHeight
FROM Runs r
JOIN Parts p ON r.PartId = p.Id
WHERE r.Id IN ({})
""".format(','.join(str(x) for x in label_matrix.index))
conn = sqlite3.connect(DB_PATH)
features_df = pd.read_sql_query(features_query, conn)
conn.close()
features_df = features_df.set_index('RunId')
# Derived features
features_df['SheetAspectRatio'] = features_df['SheetWidth'] / features_df['SheetHeight']
features_df['PartToSheetAreaRatio'] = features_df['Area'] / (features_df['SheetWidth'] * features_df['SheetHeight'])
# Filter outliers (title blocks, etc.)
mask = (features_df['BBFill'] >= 0.01) & (features_df['Area'] > 0.1)
print(f"Filtering: {(~mask).sum()} outlier runs removed")
features_df = features_df[mask]
label_matrix = label_matrix.loc[features_df.index]
feature_cols = ['Area', 'Convexity', 'AspectRatio', 'BBFill', 'Circularity',
'PerimeterToAreaRatio', 'VertexCount',
'SheetWidth', 'SheetHeight', 'SheetAspectRatio', 'PartToSheetAreaRatio']
X = features_df[feature_cols].values
y = label_matrix.values
print(f"Features: {X.shape}, Labels: {y.shape}")In [ ]:
from sklearn.model_selection import GroupShuffleSplit
from sklearn.multioutput import MultiOutputClassifier
import xgboost as xgb
# Split by part (all sheet sizes for a part stay in the same split)
groups = features_df['FileName']
splitter = GroupShuffleSplit(n_splits=1, test_size=0.2, random_state=42)
train_idx, test_idx = next(splitter.split(X, y, groups))
X_train, X_test = X[train_idx], X[test_idx]
y_train, y_test = y[train_idx], y[test_idx]
print(f"Train: {len(train_idx)}, Test: {len(test_idx)}")
# Train XGBoost multi-label classifier
base_clf = xgb.XGBClassifier(
n_estimators=200,
max_depth=6,
learning_rate=0.1,
use_label_encoder=False,
eval_metric='logloss',
random_state=42
)
clf = MultiOutputClassifier(base_clf, n_jobs=-1)
clf.fit(X_train, y_train)
print("Training complete")In [ ]:
from sklearn.metrics import recall_score, precision_score
import matplotlib.pyplot as plt
y_pred = clf.predict(X_test)
y_prob = np.array([est.predict_proba(X_test)[:, 1] for est in clf.estimators_]).T
# Per-angle metrics
recalls = []
precisions = []
for i in range(36):
if y_test[:, i].sum() > 0:
recalls.append(recall_score(y_test[:, i], y_pred[:, i], zero_division=0))
precisions.append(precision_score(y_test[:, i], y_pred[:, i], zero_division=0))
print(f"Mean recall: {np.mean(recalls):.3f}")
print(f"Mean precision: {np.mean(precisions):.3f}")
# Average angles predicted per run
avg_predicted = y_pred.sum(axis=1).mean()
print(f"Avg angles predicted per run: {avg_predicted:.1f}")
# Plot
fig, axes = plt.subplots(1, 2, figsize=(12, 4))
axes[0].bar(range(len(recalls)), recalls)
axes[0].set_title('Recall per Angle Bin')
axes[0].set_xlabel('Angle (5-deg bins)')
axes[0].axhline(y=0.95, color='r', linestyle='--', label='Target 95%')
axes[0].legend()
axes[1].bar(range(len(precisions)), precisions)
axes[1].set_title('Precision per Angle Bin')
axes[1].set_xlabel('Angle (5-deg bins)')
axes[1].axhline(y=0.60, color='r', linestyle='--', label='Target 60%')
axes[1].legend()
plt.tight_layout()
plt.show()In [ ]:
from skl2onnx import convert_sklearn
from skl2onnx.common.data_types import FloatTensorType
from pathlib import Path
initial_type = [('features', FloatTensorType([None, 11]))]
onnx_model = convert_sklearn(clf, initial_types=initial_type)
output_path = Path(OUTPUT_PATH)
output_path.parent.mkdir(parents=True, exist_ok=True)
with open(output_path, 'wb') as f:
f.write(onnx_model.SerializeToString())
print(f"Model saved to {output_path} ({output_path.stat().st_size / 1024:.0f} KB)")