feat: add knowledge-driven joint design studio

This commit is contained in:
Jerry
2026-08-14 13:53:51 +08:00
parent 7d12459a32
commit 281afdfd9e
57 changed files with 269659 additions and 1152 deletions
+2
View File
@@ -0,0 +1,2 @@
*.STEP binary
*.step binary
+289 -11
View File
@@ -1,6 +1,7 @@
import { createOpenAI } from "@ai-sdk/openai";
import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
import { streamText, type ModelMessage } from "ai";
import { generateText, Output, streamText, type ModelMessage } from "ai";
import { z } from "zod";
import { loadLlmConfig, resolveProviderApiKey, selectedModelId, type LlmConfig } from "./config";
type ChatMessage = {
@@ -8,6 +9,11 @@ type ChatMessage = {
content: string;
};
export type CadOptions = {
kind?: "joint_module" | "reducer";
mode?: "auto" | "requirement";
};
function messageContent(value: unknown) {
if (!value || typeof value !== "object") return "";
const record = value as Record<string, unknown>;
@@ -122,14 +128,15 @@ function compactJson(value: unknown, maxLength: number) {
function buildSystemPrompt(viewerContext: unknown[], attachments: unknown[]) {
return [
"You are the CAD Agent Studio assistant inside Orbit Joint Studio.",
"You are the intelligent mechanical design assistant inside 灵心造物.",
"Follow the CAD Agent Studio Agent Rules from cadSet.",
"You may answer ordinary questions, clarify requirements, and explain engineering tradeoffs.",
"Strict honesty rule: never claim that a CAD file, model, STEP, URDF, or preview artifact was generated unless a real server-side CAD generation tool has succeeded and returned artifact URLs.",
"This Orbit integration exposes real server-side CAD generation, task storage, artifact download, and upload endpoints. The UI has a separate explicit Generate action; ordinary chat must remain ordinary chat.",
"This product exposes real server-side design generation, task storage, artifact download, and upload endpoints. The UI has a separate explicit Generate action; ordinary chat must remain ordinary chat.",
"White-label rule: in user-facing replies, do not mention model providers, third-party product names, or internal file-format names unless the user explicitly asks for technical export details. Use 灵心造物, 设计模型, 零件, 拼装, and 验证 as the product vocabulary.",
"If the user asks you to generate or modify CAD inside chat, clarify the requirements and tell them to use the explicit Generate action when they are ready. Do not invent files, task ids, generated variants, or viewer state.",
"Do not rely on hardcoded templates, canned examples, mock data, fake filenames, or imaginary viewer state.",
"普通聊天、确认、说明、提问不得自动生成 CAD。只有真实 CAD 工具接通并成功写出 artifact 后,才允许说已经生成。",
"普通聊天、确认、说明、提问不得自动生成设计模型。只有真实设计工具成功生成并保存模型文件后,才允许说已经生成。",
`Viewer context count: ${viewerContext.length}`,
viewerContext.length ? compactJson(viewerContext, 10000) : "",
`Attachment count: ${attachments.length}`,
@@ -152,7 +159,7 @@ function messageText(message: Record<string, unknown> | undefined) {
}
function hasCadIntent(text: string) {
return /(生成|创建|设计|建模|重建|导出|修改|调整|改成).*(减速器|行星|关节|模组|cad|step|urdf|mjcf|模数|传动比|行星轮|齿宽|压力角)|\b(generate|create|design|reconstruct|export|modify|edit)\b.*\b(cad|step|urdf|mjcf|reducer|joint|planetary|module|ratio)\b/i.test(text);
return /(生成|创建|设计|建模|重建|导出|修改|调整|改成).*(减速器|行星|关节|模组|cad|step|urdf|mjcf|模数|传动比|行星轮|齿宽|压力角)|(?:减速器|行星|关节|模组).*(?:扭矩|传动比|减速比|尺寸|外径|紧凑|噪音|模数|齿宽|压力角)|\b(generate|create|design|reconstruct|export|modify|edit)\b.*\b(cad|step|urdf|mjcf|reducer|joint|planetary|module|ratio)\b/i.test(text);
}
export type RequirementOverrides = {
@@ -169,6 +176,41 @@ export type RequirementOverrides = {
max_outer_diameter_mm?: number;
};
const cadCandidateSchema = z.object({
title: z.string().min(1).max(80),
rationale: z.string().min(1).max(240),
topology_family: z.enum(["simple_2k_h", "simple_2k_h_cascade", "ferguson_wolfrom"]),
target_ratio: z.number().positive(),
tooth_form: z.enum(["spur", "helical"]),
module_mm: z.number().positive().optional(),
planet_count: z.number().int().min(3).max(4).optional(),
helix_angle_deg: z.number().min(0).max(35).optional(),
pressure_angle_deg: z.number().min(14.5).max(25).optional(),
face_width_mm: z.number().positive().max(30).optional(),
backlash_mm: z.number().min(0).max(1).optional(),
ring_rim_thickness_mm: z.number().positive().max(20).optional(),
max_outer_diameter_mm: z.number().positive().optional(),
reuse_design_id: z.string().nullable().optional(),
});
const cadDesignPlanSchema = z.object({
interpreted_requirement: z.string().min(1).max(400),
assumptions: z.array(z.string().max(200)).max(6),
candidates: z.array(cadCandidateSchema).min(1).max(3),
});
type CadCandidate = z.infer<typeof cadCandidateSchema>;
type CatalogDesign = {
id: string;
kind: "joint_module" | "reducer";
topology: CadCandidate["topology_family"];
ratio: number;
parameters?: Record<string, unknown>;
eligible: boolean;
catalogClass?: string;
};
function firstNumber(text: string, patterns: RegExp[], integer = false) {
for (const pattern of patterns) {
const match = text.match(pattern);
@@ -242,12 +284,219 @@ async function backendJson(pathname: string, init: RequestInit = {}) {
return payload;
}
function candidateOverrides(candidate: CadCandidate): RequirementOverrides {
return Object.fromEntries(Object.entries({
topology_family: candidate.topology_family,
target_ratio: candidate.target_ratio,
tooth_form: candidate.tooth_form,
module_mm: candidate.module_mm,
planet_count: candidate.planet_count,
helix_angle_deg: candidate.tooth_form === "helical" ? candidate.helix_angle_deg || 15 : 0,
pressure_angle_deg: candidate.pressure_angle_deg,
face_width_mm: candidate.face_width_mm,
backlash_mm: candidate.backlash_mm,
ring_rim_thickness_mm: candidate.ring_rim_thickness_mm,
max_outer_diameter_mm: candidate.max_outer_diameter_mm,
}).filter(([, value]) => value !== undefined)) as RequirementOverrides;
}
function closeEnough(actual: unknown, expected: unknown, tolerance = 0.001) {
const actualNumber = Number(actual);
const expectedNumber = Number(expected);
return Number.isFinite(actualNumber)
&& Number.isFinite(expectedNumber)
&& Math.abs(actualNumber - expectedNumber) <= tolerance;
}
export function findReusableDesign(
candidate: CadCandidate,
catalog: CatalogDesign[],
kind: "joint_module" | "reducer",
usedIds = new Set<string>(),
) {
const compatible = catalog.filter((design) => {
if (!design.eligible || design.catalogClass === "test_artifact" || usedIds.has(design.id)) return false;
if (design.kind !== kind || design.topology !== candidate.topology_family) return false;
const ratioTolerance = Math.max(0.02, candidate.target_ratio * 0.002);
if (!closeEnough(design.ratio, candidate.target_ratio, ratioTolerance)) return false;
const parameters = design.parameters || {};
if (String(parameters.tooth_form || "spur") !== candidate.tooth_form) return false;
if (candidate.module_mm !== undefined && !closeEnough(parameters.module_mm, candidate.module_mm)) return false;
if (candidate.planet_count !== undefined && !closeEnough(parameters.planet_count, candidate.planet_count)) return false;
if (candidate.face_width_mm !== undefined && !closeEnough(parameters.face_width_mm, candidate.face_width_mm)) return false;
return true;
});
if (candidate.reuse_design_id) {
const requested = compatible.find((design) => design.id === candidate.reuse_design_id);
if (requested) return requested;
}
return compatible[0] || null;
}
async function planCadCandidates({
text,
kind,
provider,
model,
catalog,
}: {
text: string;
kind: "joint_module" | "reducer";
provider?: string;
model?: string;
catalog: CatalogDesign[];
}) {
const config = loadLlmConfig();
const selected = selectedModelId(provider, model, config);
const apiKey = resolveProviderApiKey(selected.providerConfig);
if (!apiKey) throw new Error("智能设计模型尚未配置访问密钥。");
const languageModel = buildLanguageModel({
apiKey,
provider: selected.provider,
providerConfig: selected.providerConfig,
model: selected.model,
});
const supportedTopologies = kind === "joint_module"
? ["simple_2k_h", "simple_2k_h_cascade"]
: ["simple_2k_h", "simple_2k_h_cascade", "ferguson_wolfrom"];
const verifiedCatalog = catalog
.filter((design) => design.eligible && design.catalogClass !== "test_artifact" && design.kind === kind)
.map((design) => ({
id: design.id,
topology: design.topology,
ratio: design.ratio,
parameters: design.parameters,
}));
const result = await generateText({
model: languageModel,
output: Output.object({
schema: cadDesignPlanSchema,
name: "mechanical_design_plan",
description: "A constrained plan that can be executed by the mechanical CAD toolchain.",
}),
system: [
"You are the mechanical design planner inside 灵心造物.",
"Convert the user's request into executable candidates for the real CAD solver; never invent unsupported mechanisms or claim validation.",
`The selected product kind is ${kind}. Supported topology families: ${supportedTopologies.join(", ")}.`,
"Return up to three meaningfully different candidates. Use only module values 0.25, 0.3, or 0.5 mm and 3 or 4 planets.",
"For helical gears use a helix angle from 12 to 20 degrees. Pressure angle should normally be 20 degrees.",
"A verified existing design may be reused only when its kind, topology, ratio, tooth form, module, planet count and explicit user constraints match.",
"Set reuse_design_id only to an id from the verified catalog. Otherwise leave it null so the CAD program generates a new design.",
"Do not output prose outside the structured result.",
].join("\n"),
prompt: [
`User requirement: ${text}`,
`Verified generated-design catalog: ${compactJson(verifiedCatalog, 12000)}`,
"Produce the strongest feasible candidate set for this request.",
].join("\n\n"),
temperature: 0.2,
maxOutputTokens: 1800,
});
const explicit = parseRequirementOverrides(text);
const allowed = new Set(supportedTopologies);
const candidates = result.output.candidates.map((candidate) => {
const merged = { ...candidate, ...explicit } as CadCandidate;
if (!allowed.has(merged.topology_family)) {
throw new Error(`当前${kind === "joint_module" ? "关节模组" : "减速器"}不支持 ${merged.topology_family}`);
}
return merged;
});
return {
plan: { ...result.output, candidates },
provider: selected.provider,
model: selected.model,
};
}
async function runCadDesignBatch({
text,
cadOptions,
provider,
model,
}: {
text: string;
cadOptions?: CadOptions;
provider?: string;
model?: string;
}) {
const kind = cadOptions?.kind || (/(关节|模组|joint|actuator)/i.test(text) ? "joint_module" : "reducer");
const catalogPayload = await backendJson("/api/designs");
const catalog = (Array.isArray(catalogPayload?.designs) ? catalogPayload.designs : []) as CatalogDesign[];
const planned = await planCadCandidates({ text, kind, provider, model, catalog });
const planId = `plan_${Date.now().toString(36)}`;
const designIds: string[] = [];
const usedIds = new Set<string>();
const failures: string[] = [];
let generatedCount = 0;
let reusedCount = 0;
for (const [index, candidate] of planned.plan.candidates.entries()) {
const reusable = findReusableDesign(candidate, catalog, kind, usedIds);
if (reusable) {
designIds.push(reusable.id);
usedIds.add(reusable.id);
reusedCount += 1;
continue;
}
try {
const generated = await backendJson("/api/generate", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
kind,
request: `${text} · ${candidate.title}`,
mode: cadOptions?.mode || "auto",
requirementOverrides: candidateOverrides(candidate),
planner: {
planId,
provider: planned.provider,
model: planned.model,
candidateIndex: index,
candidate,
},
}),
});
if (generated?.status !== "success" || !generated?.taskId) {
failures.push(`${candidate.title}:生成或验证未通过`);
continue;
}
const exported = await backendJson(`/api/tasks/${encodeURIComponent(generated.taskId)}/exports/urdf`, {
method: "POST",
});
if (exported?.status !== "success") {
failures.push(`${candidate.title}:装配模型导出失败`);
continue;
}
designIds.push(String(generated.taskId));
usedIds.add(String(generated.taskId));
generatedCount += 1;
} catch (error) {
failures.push(`${candidate.title}${error instanceof Error ? error.message : "执行失败"}`);
}
}
if (!designIds.length) {
throw new Error(`没有方案通过真实生成与验证。${failures.length ? ` ${failures.join("")}` : ""}`);
}
return {
planId,
provider: planned.provider,
model: planned.model,
interpretedRequirement: planned.plan.interpreted_requirement,
designIds,
generatedCount,
reusedCount,
failures,
};
}
async function runExplicitCadAction({
text,
selectedTaskId,
cadOptions,
}: {
text: string;
selectedTaskId?: string;
cadOptions?: CadOptions;
}) {
if (/(重建|reconstruct).*(step|stp|上传)/i.test(text) || /(step|stp|上传).*(重建|reconstruct)/i.test(text)) {
if (!selectedTaskId) throw new Error("请先选择包含 STEP 上传文件的任务。");
@@ -282,25 +531,32 @@ async function runExplicitCadAction({
if (!selectedTaskId) throw new Error("请先在右侧选择一个已生成任务,再导出 URDF。");
return backendJson(`/api/tasks/${encodeURIComponent(selectedTaskId)}/exports/urdf`, { method: "POST" });
}
const kind = /(关节|模组|joint|actuator)/i.test(text) ? "joint_module" : "reducer";
return backendJson("/api/generate", {
const kind = cadOptions?.kind || (/(关节|模组|joint|actuator)/i.test(text) ? "joint_module" : "reducer");
const generated = await backendJson("/api/generate", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
kind,
request: text,
mode: "auto",
mode: cadOptions?.mode || "auto",
requirementOverrides: parseRequirementOverrides(text),
}),
});
if (generated?.status !== "success" || !generated?.taskId) {
return generated;
}
return backendJson(`/api/tasks/${encodeURIComponent(generated.taskId)}/exports/urdf`, {
method: "POST",
});
}
function textResponse(text: string) {
function textResponse(text: string, extraHeaders: Record<string, string> = {}) {
return new Response(text, {
status: 200,
headers: {
"Content-Type": "text/plain; charset=utf-8",
"Cache-Control": "no-store",
...extraHeaders,
},
});
}
@@ -312,6 +568,7 @@ export async function streamAgentText({
provider,
model,
selectedTaskId,
cadOptions,
}: {
messages: unknown[];
attachments: unknown[];
@@ -319,13 +576,34 @@ export async function streamAgentText({
provider?: string;
model?: string;
selectedTaskId?: string;
cadOptions?: CadOptions;
}) {
const userText = messageText(lastUserText(messages));
if (hasCadIntent(userText)) {
const result = await runExplicitCadAction({ text: userText, selectedTaskId });
const isExistingTaskAction = Boolean(selectedTaskId) && /(重建|修改|调整|改成|导出|reconstruct|modify|edit|export)/i.test(userText);
if (isExistingTaskAction) {
const result = await runExplicitCadAction({ text: userText, selectedTaskId, cadOptions });
const artifact = result?.robotExport?.packageUrl || result?.artifacts?.find((item: Record<string, unknown>) => item.role === "primary")?.url;
const suffix = artifact ? `\n产物:${artifact}` : "";
return textResponse(`${result?.summary || "CAD 工具链执行完成。"}\n任务 ID${result?.taskId || selectedTaskId || "-"}${suffix}`);
return textResponse(`${result?.summary || "设计工具链执行完成。"}\n任务 ID${result?.taskId || selectedTaskId || "-"}${suffix}`);
}
const batch = await runCadDesignBatch({
text: userText,
cadOptions,
provider,
model,
});
const failureText = batch.failures.length ? `,另有 ${batch.failures.length} 个候选未通过工程验证` : "";
return textResponse(
`已理解目标:${batch.interpretedRequirement}\n已得到 ${batch.designIds.length} 个真实可查看方案:本次新生成 ${batch.generatedCount} 个,复用已验证结果 ${batch.reusedCount}${failureText}`,
{
"X-Lingxin-Design-Ids": batch.designIds.join(","),
"X-Lingxin-Plan-Id": batch.planId,
"X-Lingxin-Generated-Count": String(batch.generatedCount),
"X-Lingxin-Reused-Count": String(batch.reusedCount),
"X-Lingxin-Planner": batch.provider,
},
);
}
const config = loadLlmConfig();
const selected = selectedModelId(provider, model, config);
+10
View File
@@ -103,6 +103,15 @@ const server = http.createServer(async (request, response) => {
try {
const body = await readJson(request);
const record = body && typeof body === "object" ? body as Record<string, unknown> : {};
const cadOptionsRecord = record.cadOptions && typeof record.cadOptions === "object"
? record.cadOptions as Record<string, unknown>
: {};
const cadKind = cadOptionsRecord.kind === "joint_module" || cadOptionsRecord.kind === "reducer"
? cadOptionsRecord.kind
: undefined;
const cadMode = cadOptionsRecord.mode === "auto" || cadOptionsRecord.mode === "requirement"
? cadOptionsRecord.mode
: undefined;
const streamResponse = await streamAgentText({
messages: Array.isArray(record.messages) ? record.messages : [],
attachments: Array.isArray(record.attachments) ? record.attachments : [],
@@ -110,6 +119,7 @@ const server = http.createServer(async (request, response) => {
provider: typeof record.provider === "string" ? record.provider : undefined,
model: typeof record.model === "string" ? record.model : undefined,
selectedTaskId: typeof record.selectedTaskId === "string" ? record.selectedTaskId : undefined,
cadOptions: { kind: cadKind, mode: cadMode },
});
await writeWebResponse(response, streamResponse);
} catch (error) {
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,211 @@
ISO-10303-21;
HEADER;
FILE_DESCRIPTION (( 'STEP AP214' ),
'1' );
FILE_NAME ('3500-motor.STEP',
'2026-08-05T07:41:56',
( '' ),
( '' ),
'SwSTEP 2.0',
'SolidWorks 2024',
'' );
FILE_SCHEMA (( 'AUTOMOTIVE_DESIGN' ));
ENDSEC;
DATA;
#1 =( NAMED_UNIT ( * ) PLANE_ANGLE_UNIT ( ) SI_UNIT ( $, .RADIAN. ) );
#2 =( NAMED_UNIT ( * ) SI_UNIT ( $, .STERADIAN. ) SOLID_ANGLE_UNIT ( ) );
#3 = ROLE_ASSOCIATION ( #77, #156 ) ;
#4 = OBJECT_ROLE ('mandatory',$ ) ;
#5 = PRODUCT_DEFINITION_SHAPE ( 'NONE', 'NONE', #191 ) ;
#6 = SHAPE_DEFINITION_REPRESENTATION ( #161, #140 ) ;
#7 = SHAPE_REPRESENTATION ( '3500-motor', ( #24, #121, #16, #21, #138 ), #133 ) ;
#8 = SHAPE_DEFINITION_REPRESENTATION ( #97, #129 ) ;
#9 = PRODUCT_DEFINITION_CONTEXT ( 'detailed design', #147, 'design' ) ;
#10 = PRODUCT_CONTEXT ( 'NONE', #64, 'mechanical' ) ;
#11 = APPLICATION_CONTEXT ( 'automotive_design' ) ;
#12 = DIRECTION ( 'NONE', ( 0.000000000000000000, 0.000000000000000000, 1.000000000000000000 ) ) ;
#13 = PROPERTY_DEFINITION_REPRESENTATION ( #51, #129 ) ;
#14 = ROLE_ASSOCIATION ( #54, #57 ) ;
#15 = CARTESIAN_POINT ( 'NONE', ( 0.000000000000000000, 0.000000000000000000, 0.000000000000000000 ) ) ;
#16 = AXIS2_PLACEMENT_3D ( 'NONE', #178, #179, #164 ) ;
#17 = APPLICATION_CONTEXT ( 'automotive_design' ) ;
#18 = PRODUCT_DEFINITION_SHAPE ( 'NONE', 'NONE', #190 ) ;
#19 = PRODUCT_RELATED_PRODUCT_CATEGORY ( 'part', '', ( #70 ) ) ;
#20 = DIRECTION ( 'NONE', ( 0.000000000000000000, 0.000000000000000000, 1.000000000000000000 ) ) ;
#21 = AXIS2_PLACEMENT_3D ( 'NONE', #166, #35, #85 ) ;
#22 = DOCUMENT_FILE ( '3500-\X2\8f6c5b50591658f3\X0\.STEP' , '', '', #142, '' ,$ ) ;
#23 = PRODUCT ( '\X2\8f9351fa8f74\X0\', '\X2\8f9351fa8f74\X0\', '', ( #141 ) ) ;
#24 = AXIS2_PLACEMENT_3D ( 'NONE', #15, #115, #37 ) ;
#25 = PRODUCT_DEFINITION_FORMATION_WITH_SPECIFIED_SOURCE ( 'å¯', '', #23, .NOT_KNOWN. ) ;
#26 = CONTEXT_DEPENDENT_SHAPE_REPRESENTATION ( #114, #149 ) ;
#27 = PROPERTY_DEFINITION_REPRESENTATION ( #143, #150 ) ;
#28 = DIRECTION ( 'NONE', ( 1.000000000000000000, 0.000000000000000000, 0.000000000000000000 ) ) ;
#29 = DIRECTION ( 'NONE', ( 0.000000000000000000, 0.000000000000000000, 1.000000000000000000 ) ) ;
#30 = UNCERTAINTY_MEASURE_WITH_UNIT (LENGTH_MEASURE( 1.000000000000000082E-05 ), #93, 'distance_accuracy_value', 'NONE');
#31 = PROPERTY_DEFINITION_REPRESENTATION ( #175, #129 ) ;
#32 = APPLICATION_CONTEXT ( 'automotive_design' ) ;
#33 = DIRECTION ( 'NONE', ( 0.1357859094218650531, 2.918564516451405225E-17, -0.9907382029590243722 ) ) ;
#34 = PROPERTY_DEFINITION ('document property','', #58 ) ;
#35 = DIRECTION ( 'NONE', ( 0.6045425643510958791, -5.541274120980522856E-17, 0.7965728390346994425 ) ) ;
#36 = SHAPE_DEFINITION_REPRESENTATION ( #84, #150 ) ;
#37 = DIRECTION ( 'NONE', ( 1.000000000000000000, 0.000000000000000000, 0.000000000000000000 ) ) ;
#38 = EXTERNAL_SOURCE (IDENTIFIER('') ) ;
#39 = CONTEXT_DEPENDENT_SHAPE_REPRESENTATION ( #172, #5 ) ;
#40 = SHAPE_DEFINITION_REPRESENTATION ( #18, #165 ) ;
#41 = IDENTIFICATION_ROLE ( 'external document id and location', $) ;
#42 = PRODUCT_DEFINITION ( 'æ™', '', #136, #131 ) ;
#43 = APPLIED_DOCUMENT_REFERENCE ( #58,'',( #104) ) ;
#44 = CARTESIAN_POINT ( 'NONE', ( 0.000000000000000000, 0.000000000000000000, 0.000000000000000000 ) ) ;
#45 = APPLICATION_CONTEXT ( 'automotive_design' ) ;
#46 = DOCUMENT_FILE ( '\X2\7ebf5708\X0\.STEP' , '', '', #176, '' ,$ ) ;
#47 = PRODUCT_RELATED_PRODUCT_CATEGORY ( 'part', '', ( #98 ) ) ;
#48 = AXIS2_PLACEMENT_3D ( 'NONE', #44, #20, #151 ) ;
#49 = CARTESIAN_POINT ( 'NONE', ( -20.69409224678082992, -0.1521863292730761541, 0.000000000000000000 ) ) ;
#50 = EXTERNAL_SOURCE (IDENTIFIER('') ) ;
#51 = PROPERTY_DEFINITION ('external definition','', #120 ) ;
#52 = PRODUCT_DEFINITION_FORMATION_WITH_SPECIFIED_SOURCE ( 'å¯', '', #170, .NOT_KNOWN. ) ;
#53 = APPLICATION_PROTOCOL_DEFINITION ( 'draft international standard', 'automotive_design', 1998, #147 ) ;
#54 = OBJECT_ROLE ('mandatory',$ ) ;
#55 = PRODUCT_DEFINITION ( 'æ™', '', #144, #9 ) ;
#56 = ITEM_DEFINED_TRANSFORMATION ( 'NONE', 'NONE', #121, #127 ) ;
#57 = APPLIED_DOCUMENT_REFERENCE ( #46,'',( #42) ) ;
#58 = DOCUMENT_FILE ( '3500-\X2\5b9a5b50591658f3\X0\.STEP' , '', '', #87, '' ,$ ) ;
#59 = UNCERTAINTY_MEASURE_WITH_UNIT (LENGTH_MEASURE( 1.000000000000000082E-05 ), #78, 'distance_accuracy_value', 'NONE');
#60 = APPLIED_EXTERNAL_IDENTIFICATION_ASSIGNMENT ( '\X2\8f9351fa8f74\X0\.STEP' , #79, #38,( #120) ) ;
#61 =( LENGTH_UNIT ( ) NAMED_UNIT ( * ) SI_UNIT ( .MILLI., .METRE. ) );
#62 = DIRECTION ( 'NONE', ( 0.000000000000000000, 0.000000000000000000, 1.000000000000000000 ) ) ;
#63 =( NAMED_UNIT ( * ) SI_UNIT ( $, .STERADIAN. ) SOLID_ANGLE_UNIT ( ) );
#64 = APPLICATION_CONTEXT ( 'automotive_design' ) ;
#65 = PRODUCT_DEFINITION_SHAPE ( 'NONE', 'NONE', #192 ) ;
#66 = AXIS2_PLACEMENT_3D ( 'NONE', #95, #29, #28 ) ;
#67 =( LENGTH_UNIT ( ) NAMED_UNIT ( * ) SI_UNIT ( .MILLI., .METRE. ) );
#68 = APPLICATION_PROTOCOL_DEFINITION ( 'draft international standard', 'automotive_design', 1998, #17 ) ;
#69 =( REPRESENTATION_RELATIONSHIP ('NONE','NONE', #7, #150 ) REPRESENTATION_RELATIONSHIP_WITH_TRANSFORMATION ( #89 )SHAPE_REPRESENTATION_RELATIONSHIP( ) );
#70 = PRODUCT ( '\X2\7ebf5708\X0\', '\X2\7ebf5708\X0\', '', ( #72 ) ) ;
#71 = PRODUCT_DEFINITION_CONTEXT ( 'detailed design', #32, 'design' ) ;
#72 = PRODUCT_CONTEXT ( 'NONE', #75, 'mechanical' ) ;
#73 = UNCERTAINTY_MEASURE_WITH_UNIT (LENGTH_MEASURE( 1.000000000000000082E-05 ), #67, 'distance_accuracy_value', 'NONE');
#74 = SHAPE_DEFINITION_REPRESENTATION ( #110, #7 ) ;
#75 = APPLICATION_CONTEXT ( 'automotive_design' ) ;
#76 = DIRECTION ( 'NONE', ( 1.000000000000000000, 0.000000000000000000, 0.000000000000000000 ) ) ;
#77 = OBJECT_ROLE ('mandatory',$ ) ;
#78 =( LENGTH_UNIT ( ) NAMED_UNIT ( * ) SI_UNIT ( .MILLI., .METRE. ) );
#79 = IDENTIFICATION_ROLE ( 'external document id and location', $) ;
#80 = PROPERTY_DEFINITION_REPRESENTATION ( #137, #150 ) ;
#81 =( NAMED_UNIT ( * ) PLANE_ANGLE_UNIT ( ) SI_UNIT ( $, .RADIAN. ) );
#82 = APPLICATION_CONTEXT ( 'automotive_design' ) ;
#83 = DIRECTION ( 'NONE', ( 1.000000000000000000, 0.000000000000000000, 0.000000000000000000 ) ) ;
#84 = PRODUCT_DEFINITION_SHAPE ( 'NONE', 'NONE', #42 ) ;
#85 = DIRECTION ( 'NONE', ( 0.7965728390346994425, 4.907188895435571839E-17, -0.6045425643510958791 ) ) ;
#86 = PRODUCT_DEFINITION_SHAPE ( 'NONE', 'NONE', #194 ) ;
#87 = DOCUMENT_TYPE ( 'geometry' ) ;
#88 =( NAMED_UNIT ( * ) SI_UNIT ( $, .STERADIAN. ) SOLID_ANGLE_UNIT ( ) );
#89 = ITEM_DEFINED_TRANSFORMATION ( 'NONE', 'NONE', #16, #66 ) ;
#90 = UNCERTAINTY_MEASURE_WITH_UNIT (LENGTH_MEASURE( 1.000000000000000082E-05 ), #61, 'distance_accuracy_value', 'NONE');
#91 = EXTERNAL_SOURCE (IDENTIFIER('') ) ;
#92 =( GEOMETRIC_REPRESENTATION_CONTEXT ( 3 ) GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT ( ( #135 ) ) GLOBAL_UNIT_ASSIGNED_CONTEXT ( ( #181, #139, #162 ) ) REPRESENTATION_CONTEXT ( 'NONE', 'WORKASPACE' ) );
#93 =( LENGTH_UNIT ( ) NAMED_UNIT ( * ) SI_UNIT ( .MILLI., .METRE. ) );
#94 = APPLIED_EXTERNAL_IDENTIFICATION_ASSIGNMENT ( '3500-\X2\5b9a5b50591658f3\X0\.STEP' , #186, #50,( #58) ) ;
#95 = CARTESIAN_POINT ( 'NONE', ( 0.000000000000000000, 0.000000000000000000, 0.000000000000000000 ) ) ;
#96 = PROPERTY_DEFINITION_REPRESENTATION ( #34, #140 ) ;
#97 = PRODUCT_DEFINITION_SHAPE ( 'NONE', 'NONE', #123 ) ;
#98 = PRODUCT ( '3500-\X2\5b9a5b50591658f3\X0\', '3500-\X2\5b9a5b50591658f3\X0\', '', ( #10 ) ) ;
#99 = APPLIED_EXTERNAL_IDENTIFICATION_ASSIGNMENT ( '\X2\7ebf5708\X0\.STEP' , #41, #91,( #46) ) ;
#100 = APPLICATION_PROTOCOL_DEFINITION ( 'draft international standard', 'automotive_design', 1998, #32 ) ;
#101 = APPLICATION_PROTOCOL_DEFINITION ( 'draft international standard', 'automotive_design', 1998, #103 ) ;
#102 = PROPERTY_DEFINITION_REPRESENTATION ( #112, #165 ) ;
#103 = APPLICATION_CONTEXT ( 'automotive_design' ) ;
#104 = PRODUCT_DEFINITION ( 'æ™', '', #155, #146 ) ;
#105 = CONTEXT_DEPENDENT_SHAPE_REPRESENTATION ( #152, #86 ) ;
#106 = ITEM_DEFINED_TRANSFORMATION ( 'NONE', 'NONE', #138, #48 ) ;
#107 = CARTESIAN_POINT ( 'NONE', ( 0.000000000000000000, 0.000000000000000000, 0.000000000000000000 ) ) ;
#108 = EXTERNAL_SOURCE (IDENTIFIER('') ) ;
#109 = DIRECTION ( 'NONE', ( 1.000000000000000000, 0.000000000000000000, 0.000000000000000000 ) ) ;
#110 = PRODUCT_DEFINITION_SHAPE ( 'NONE', 'NONE', #55 ) ;
#111 = DIRECTION ( 'NONE', ( 0.000000000000000000, 0.000000000000000000, 1.000000000000000000 ) ) ;
#112 = PROPERTY_DEFINITION ('document property','', #22 ) ;
#113 =( GEOMETRIC_REPRESENTATION_CONTEXT ( 3 ) GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT ( ( #59 ) ) GLOBAL_UNIT_ASSIGNED_CONTEXT ( ( #78, #81, #63 ) ) REPRESENTATION_CONTEXT ( 'NONE', 'WORKASPACE' ) );
#114 =( REPRESENTATION_RELATIONSHIP ('NONE','NONE', #7, #129 ) REPRESENTATION_RELATIONSHIP_WITH_TRANSFORMATION ( #160 )SHAPE_REPRESENTATION_RELATIONSHIP( ) );
#115 = DIRECTION ( 'NONE', ( 0.000000000000000000, 0.000000000000000000, 1.000000000000000000 ) ) ;
#116 = APPLICATION_PROTOCOL_DEFINITION ( 'draft international standard', 'automotive_design', 1998, #158 ) ;
#117 = ROLE_ASSOCIATION ( #4, #43 ) ;
#118 =( NAMED_UNIT ( * ) PLANE_ANGLE_UNIT ( ) SI_UNIT ( $, .RADIAN. ) );
#119 = CARTESIAN_POINT ( 'NONE', ( 0.000000000000000000, 0.000000000000000000, 0.000000000000000000 ) ) ;
#120 = DOCUMENT_FILE ( '\X2\8f9351fa8f74\X0\.STEP' , '', '', #153, '' ,$ ) ;
#121 = AXIS2_PLACEMENT_3D ( 'NONE', #177, #148, #33 ) ;
#122 = PRODUCT ( '3500-motor', '3500-motor', '', ( #126 ) ) ;
#123 = PRODUCT_DEFINITION ( 'æ™', '', #25, #174 ) ;
#124 = PROPERTY_DEFINITION ('external definition','', #58 ) ;
#125 = APPLIED_EXTERNAL_IDENTIFICATION_ASSIGNMENT ( '3500-\X2\8f6c5b50591658f3\X0\.STEP' , #180, #108,( #22) ) ;
#126 = PRODUCT_CONTEXT ( 'NONE', #17, 'mechanical' ) ;
#127 = AXIS2_PLACEMENT_3D ( 'NONE', #107, #62, #109 ) ;
#128 = APPLICATION_PROTOCOL_DEFINITION ( 'draft international standard', 'automotive_design', 1998, #75 ) ;
#129 = SHAPE_REPRESENTATION ( '\X2\8f9351fa8f74\X0\', ( #184 ), #167 ) ;
#130 = APPLICATION_PROTOCOL_DEFINITION ( 'draft international standard', 'automotive_design', 1998, #82 ) ;
#131 = PRODUCT_DEFINITION_CONTEXT ( 'detailed design', #11, 'design' ) ;
#132 = APPLICATION_PROTOCOL_DEFINITION ( 'draft international standard', 'automotive_design', 1998, #64 ) ;
#133 =( GEOMETRIC_REPRESENTATION_CONTEXT ( 3 ) GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT ( ( #73 ) ) GLOBAL_UNIT_ASSIGNED_CONTEXT ( ( #67, #157, #88 ) ) REPRESENTATION_CONTEXT ( 'NONE', 'WORKASPACE' ) );
#134 = PROPERTY_DEFINITION_REPRESENTATION ( #124, #140 ) ;
#135 = UNCERTAINTY_MEASURE_WITH_UNIT (LENGTH_MEASURE( 1.000000000000000082E-05 ), #181, 'distance_accuracy_value', 'NONE');
#136 = PRODUCT_DEFINITION_FORMATION_WITH_SPECIFIED_SOURCE ( 'å¯', '', #70, .NOT_KNOWN. ) ;
#137 = PROPERTY_DEFINITION ('external definition','', #46 ) ;
#138 = AXIS2_PLACEMENT_3D ( 'NONE', #49, #111, #83 ) ;
#139 =( NAMED_UNIT ( * ) PLANE_ANGLE_UNIT ( ) SI_UNIT ( $, .RADIAN. ) );
#140 = SHAPE_REPRESENTATION ( '3500-\X2\5b9a5b50591658f3\X0\', ( #48 ), #92 ) ;
#141 = PRODUCT_CONTEXT ( 'NONE', #158, 'mechanical' ) ;
#142 = DOCUMENT_TYPE ( 'geometry' ) ;
#143 = PROPERTY_DEFINITION ('document property','', #46 ) ;
#144 = PRODUCT_DEFINITION_FORMATION_WITH_SPECIFIED_SOURCE ( 'å¯', '', #122, .NOT_KNOWN. ) ;
#145 = ROLE_ASSOCIATION ( #169, #187 ) ;
#146 = PRODUCT_DEFINITION_CONTEXT ( 'detailed design', #82, 'design' ) ;
#147 = APPLICATION_CONTEXT ( 'automotive_design' ) ;
#148 = DIRECTION ( 'NONE', ( -0.9907382029590243722, 1.300329224549426624E-16, -0.1357859094218650531 ) ) ;
#149 = PRODUCT_DEFINITION_SHAPE ( 'NONE', 'NONE', #193 ) ;
#150 = SHAPE_REPRESENTATION ( '\X2\7ebf5708\X0\', ( #66 ), #113 ) ;
#151 = DIRECTION ( 'NONE', ( 1.000000000000000000, 0.000000000000000000, 0.000000000000000000 ) ) ;
#152 =( REPRESENTATION_RELATIONSHIP ('NONE','NONE', #7, #140 ) REPRESENTATION_RELATIONSHIP_WITH_TRANSFORMATION ( #106 )SHAPE_REPRESENTATION_RELATIONSHIP( ) );
#153 = DOCUMENT_TYPE ( 'geometry' ) ;
#154 = PRODUCT_RELATED_PRODUCT_CATEGORY ( 'part', '', ( #170 ) ) ;
#155 = PRODUCT_DEFINITION_FORMATION_WITH_SPECIFIED_SOURCE ( 'å¯', '', #98, .NOT_KNOWN. ) ;
#156 = APPLIED_DOCUMENT_REFERENCE ( #22,'',( #190) ) ;
#157 =( NAMED_UNIT ( * ) PLANE_ANGLE_UNIT ( ) SI_UNIT ( $, .RADIAN. ) );
#158 = APPLICATION_CONTEXT ( 'automotive_design' ) ;
#159 = PRODUCT_RELATED_PRODUCT_CATEGORY ( 'part', '', ( #122 ) ) ;
#160 = ITEM_DEFINED_TRANSFORMATION ( 'NONE', 'NONE', #21, #184 ) ;
#161 = PRODUCT_DEFINITION_SHAPE ( 'NONE', 'NONE', #104 ) ;
#162 =( NAMED_UNIT ( * ) SI_UNIT ( $, .STERADIAN. ) SOLID_ANGLE_UNIT ( ) );
#163 = PROPERTY_DEFINITION ('external definition','', #22 ) ;
#164 = DIRECTION ( 'NONE', ( 0.9392666069019323105, -2.661893422904825989E-16, -0.3431883464789138860 ) ) ;
#165 = SHAPE_REPRESENTATION ( '3500-\X2\8f6c5b50591658f3\X0\', ( #127 ), #173 ) ;
#166 = CARTESIAN_POINT ( 'NONE', ( -20.69409224678082282, -29.20025735031341441, -1.980024217501652473E-14 ) ) ;
#167 =( GEOMETRIC_REPRESENTATION_CONTEXT ( 3 ) GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT ( ( #30 ) ) GLOBAL_UNIT_ASSIGNED_CONTEXT ( ( #93, #1, #2 ) ) REPRESENTATION_CONTEXT ( 'NONE', 'WORKASPACE' ) );
#168 = APPLICATION_PROTOCOL_DEFINITION ( 'draft international standard', 'automotive_design', 1998, #45 ) ;
#169 = OBJECT_ROLE ('mandatory',$ ) ;
#170 = PRODUCT ( '3500-\X2\8f6c5b50591658f3\X0\', '3500-\X2\8f6c5b50591658f3\X0\', '', ( #185 ) ) ;
#171 = CONTEXT_DEPENDENT_SHAPE_REPRESENTATION ( #69, #65 ) ;
#172 =( REPRESENTATION_RELATIONSHIP ('NONE','NONE', #7, #165 ) REPRESENTATION_RELATIONSHIP_WITH_TRANSFORMATION ( #56 )SHAPE_REPRESENTATION_RELATIONSHIP( ) );
#173 =( GEOMETRIC_REPRESENTATION_CONTEXT ( 3 ) GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT ( ( #90 ) ) GLOBAL_UNIT_ASSIGNED_CONTEXT ( ( #61, #118, #188 ) ) REPRESENTATION_CONTEXT ( 'NONE', 'WORKASPACE' ) );
#174 = PRODUCT_DEFINITION_CONTEXT ( 'detailed design', #45, 'design' ) ;
#175 = PROPERTY_DEFINITION ('document property','', #120 ) ;
#176 = DOCUMENT_TYPE ( 'geometry' ) ;
#177 = CARTESIAN_POINT ( 'NONE', ( -20.69409224678083703, -15.20025735031341974, 3.306816626080788524E-15 ) ) ;
#178 = CARTESIAN_POINT ( 'NONE', ( -9.562283266018031469, -5.722672541680885239, 30.50916516287898972 ) ) ;
#179 = DIRECTION ( 'NONE', ( -2.929481626803014992E-16, -0.9999999999999998890, -2.612875571213605436E-17 ) ) ;
#180 = IDENTIFICATION_ROLE ( 'external document id and location', $) ;
#181 =( LENGTH_UNIT ( ) NAMED_UNIT ( * ) SI_UNIT ( .MILLI., .METRE. ) );
#182 = PROPERTY_DEFINITION_REPRESENTATION ( #163, #165 ) ;
#183 = APPLICATION_PROTOCOL_DEFINITION ( 'draft international standard', 'automotive_design', 1998, #11 ) ;
#184 = AXIS2_PLACEMENT_3D ( 'NONE', #119, #12, #76 ) ;
#185 = PRODUCT_CONTEXT ( 'NONE', #103, 'mechanical' ) ;
#186 = IDENTIFICATION_ROLE ( 'external document id and location', $) ;
#187 = APPLIED_DOCUMENT_REFERENCE ( #120,'',( #123) ) ;
#188 =( NAMED_UNIT ( * ) SI_UNIT ( $, .STERADIAN. ) SOLID_ANGLE_UNIT ( ) );
#189 = PRODUCT_RELATED_PRODUCT_CATEGORY ( 'part', '', ( #23 ) ) ;
#190 = PRODUCT_DEFINITION ( 'æ™', '', #52, #71 ) ;
#191 = NEXT_ASSEMBLY_USAGE_OCCURRENCE ( 'NAUO1', ' ', ' ', #55, #190, $ ) ;
#192 = NEXT_ASSEMBLY_USAGE_OCCURRENCE ( 'NAUO2', ' ', ' ', #55, #42, $ ) ;
#193 = NEXT_ASSEMBLY_USAGE_OCCURRENCE ( 'NAUO3', ' ', ' ', #55, #123, $ ) ;
#194 = NEXT_ASSEMBLY_USAGE_OCCURRENCE ( 'NAUO4', ' ', ' ', #55, #104, $ ) ;
ENDSEC;
END-ISO-10303-21;
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -1,6 +1,11 @@
{
"platform_id": "actuator_v2_fixed_platform",
"motor_step": "../motors/3500_motor_part2_standard.step",
"motor_reference_assembly_step": "../motors/3500_split/3500_motor_reference.step",
"motor_stator_housing_step": "../motors/3500_split/3500-定子外壳.STEP",
"motor_rotor_housing_step": "../motors/3500_split/3500-转子外壳.STEP",
"motor_output_shaft_step": "../motors/3500_split/输出轴.STEP",
"motor_windings_step": "../motors/3500_split/线圈.STEP",
"housing_step": "../housings/actuator_v2_knee_abd_main_case_2.step",
"reference_assembly_step": "../references/actuator_v2_knee_abd_reference.step"
}
@@ -6,7 +6,7 @@
"motor_to_reducer_relation": "motor_output_to_sun_keyed_input",
"housing_relation": "ring_fixed_to_housing",
"joint_output": "carrier_output_shaft",
"motor_placement_policy": "external_flush_input_end",
"motor_placement_policy": "reference_pose",
"output_extension_mm": 2.0,
"coupling_clearance_mm": 0.5,
"input_shaft_insertion_depth_mm": 4.0,
@@ -6,7 +6,7 @@
"motor_to_reducer_relation": "motor_output_to_s1_sun_input",
"housing_relation": "stage_rings_fixed_to_housing",
"joint_output": "s2_carrier_output",
"motor_placement_policy": "external_flush_input_end",
"motor_placement_policy": "reference_pose",
"output_extension_mm": 2.0,
"coupling_clearance_mm": 0.5,
"input_shaft_insertion_depth_mm": 4.0,
+53
View File
@@ -0,0 +1,53 @@
# Mechanical Transmission Knowledge Base
This directory is the detached, read-only knowledge source for future
knowledge-assisted generation. It does not participate in the current CLI,
solver, CAD, validation, or joint-module execution paths.
## Safety boundary
- `knowledge_enabled` is conceptually `false` until an explicit integration
layer is added later.
- Existing code under `src/` must not import this directory.
- Knowledge entries describe facts and bind stable capability IDs to existing
implementations; they do not execute Python code.
- A mechanism is not considered supported merely because it has a topology
entry. Its `maturity` and capability bindings define how far it can run.
- Generated experiments must eventually use `output/experiments/`, never the
existing `output/runs/` production path.
## Layers
```text
schemas/ machine-readable contracts for every entry kind
ontology/ common vocabulary: ports, relations, units, maturity
components/ reusable parts and virtual references
relations/ local physical and kinematic connections
mechanisms/ executable mechanism units/topologies
compositions/ legal ways to connect mechanism units
capabilities/ bindings to solvers, layouts, CAD, and validators
evidence/ regression baselines and validation provenance
tools/ read-only consistency checks
```
The initial catalog only records capabilities already present in the project:
- `simple_2k_h`
- `simple_2k_h_cascade`
- `ferguson_wolfrom`
- the six currently registered topology relations
No new transmission structure is claimed to be executable in this first
version.
## Validate
From `backend/`:
```bash
python3 knowledge/tools/validate_knowledge.py
```
The validator uses the Python standard library. It checks entry identity,
cross-references, mechanism graph integrity, capability source locations, and
the hashes of the three legacy topology templates.
@@ -0,0 +1,187 @@
{
"schema_version": "1.0",
"id": "capability.registry.legacy",
"version": "0.1.0",
"entries": [
{
"schema_version": "1.0",
"id": "capability.parameter_solver.simple_2kh",
"version": "1.0.0",
"kind": "parameter_solver",
"implementation": {"mode": "legacy_binding", "source_path": "src/parameter_solver.py", "symbol": "solve_parameters"},
"supports": ["mechanism.planetary.simple_2k_h"],
"limitations": ["Dispatch is currently selected by legacy topology_family."],
"maturity": "production"
},
{
"schema_version": "1.0",
"id": "capability.parameter_solver.simple_2kh_cascade",
"version": "1.0.0",
"kind": "parameter_solver",
"implementation": {"mode": "legacy_binding", "source_path": "src/parameter_solver.py", "symbol": "solve_parameters"},
"supports": ["mechanism.planetary.simple_2k_h_cascade", "composition.coaxial_serial.two_simple_2kh"],
"limitations": ["Exactly two stages are supported."],
"maturity": "production"
},
{
"schema_version": "1.0",
"id": "capability.parameter_solver.ferguson_wolfrom",
"version": "1.0.0",
"kind": "parameter_solver",
"implementation": {"mode": "legacy_binding", "source_path": "src/parameter_solver.py", "symbol": "solve_parameters"},
"supports": ["mechanism.planetary.ferguson_wolfrom"],
"limitations": ["Required gear tooth counts are supplied explicitly by the requirement."],
"maturity": "parameter_solved"
},
{
"schema_version": "1.0",
"id": "capability.kinematics.graph",
"version": "1.0.0",
"kind": "kinematic_solver",
"implementation": {"mode": "legacy_binding", "source_path": "src/kinematics.py", "symbol": "solve_instance"},
"supports": ["relation.mesh.external_cylindrical", "relation.mesh.internal_cylindrical", "relation.joint.fixed", "relation.joint.rigid"],
"limitations": ["Only registered legacy relation equations are executable."],
"maturity": "production"
},
{
"schema_version": "1.0",
"id": "capability.layout.simple_2kh",
"version": "1.0.0",
"kind": "layout_solver",
"implementation": {"mode": "legacy_binding", "source_path": "src/placement/simple_2k_h.py", "symbol": "solve_simple_2kh_placements"},
"supports": ["mechanism.planetary.simple_2k_h"],
"limitations": [],
"maturity": "production"
},
{
"schema_version": "1.0",
"id": "capability.layout.simple_2kh_cascade",
"version": "1.0.0",
"kind": "layout_solver",
"implementation": {"mode": "legacy_binding", "source_path": "src/placement/simple_2k_h_cascade.py", "symbol": "solve_cascade_placements"},
"supports": ["mechanism.planetary.simple_2k_h_cascade", "composition.coaxial_serial.two_simple_2kh"],
"limitations": ["Exactly two stages are supported."],
"maturity": "production"
},
{
"schema_version": "1.0",
"id": "capability.layout.ferguson_wolfrom",
"version": "1.0.0",
"kind": "layout_solver",
"implementation": {"mode": "legacy_binding", "source_path": "src/placement/ferguson_wolfrom.py", "symbol": "solve_ferguson_wolfrom_placements"},
"supports": ["mechanism.planetary.ferguson_wolfrom"],
"limitations": [],
"maturity": "geometry_validated"
},
{
"schema_version": "1.0",
"id": "capability.cad.simplecad_parts",
"version": "1.0.0",
"kind": "component_cad_generator",
"implementation": {"mode": "legacy_binding", "source_path": "src/cad/simplecad_parts.py", "symbol": null},
"supports": ["component.gear.cylindrical.external", "component.gear.cylindrical.internal", "component.carrier.planetary"],
"limitations": ["Component generation is orchestrated by family-specific assembly generators."],
"maturity": "production"
},
{
"schema_version": "1.0",
"id": "capability.cad.simple_2kh",
"version": "1.0.0",
"kind": "assembly_cad_generator",
"implementation": {"mode": "legacy_binding", "source_path": "src/cad/simplecad_generator.py", "symbol": "generate_simplecad"},
"supports": ["mechanism.planetary.simple_2k_h"],
"limitations": ["Spur and helical tooth forms only."],
"maturity": "production"
},
{
"schema_version": "1.0",
"id": "capability.cad.simple_2kh_cascade",
"version": "1.0.0",
"kind": "assembly_cad_generator",
"implementation": {"mode": "legacy_binding", "source_path": "src/cad/simplecad_cascade_generator.py", "symbol": "generate_simplecad_cascade"},
"supports": ["mechanism.planetary.simple_2k_h_cascade", "composition.coaxial_serial.two_simple_2kh"],
"limitations": ["Exactly two stages are supported."],
"maturity": "production"
},
{
"schema_version": "1.0",
"id": "capability.cad.ferguson_wolfrom",
"version": "1.0.0",
"kind": "assembly_cad_generator",
"implementation": {"mode": "legacy_binding", "source_path": "src/cad/simplecad_ferguson_wolfrom_generator.py", "symbol": "generate_simplecad_ferguson_wolfrom"},
"supports": ["mechanism.planetary.ferguson_wolfrom"],
"limitations": ["Kinematic-demo detail only; not supported by the coaxial joint-module adapter."],
"maturity": "geometry_validated"
},
{
"schema_version": "1.0",
"id": "capability.validation.external_mesh",
"version": "1.0.0",
"kind": "relation_validator",
"implementation": {"mode": "legacy_binding", "source_path": "src/relation_validators/mesh.py", "symbol": "ExternalMeshValidator"},
"supports": ["relation.mesh.external_cylindrical"],
"limitations": [],
"maturity": "production"
},
{
"schema_version": "1.0",
"id": "capability.validation.internal_mesh",
"version": "1.0.0",
"kind": "relation_validator",
"implementation": {"mode": "legacy_binding", "source_path": "src/relation_validators/mesh.py", "symbol": "InternalMeshValidator"},
"supports": ["relation.mesh.internal_cylindrical"],
"limitations": [],
"maturity": "production"
},
{
"schema_version": "1.0",
"id": "capability.validation.revolute_joint",
"version": "1.0.0",
"kind": "relation_validator",
"implementation": {"mode": "legacy_binding", "source_path": "src/relation_validators/joints.py", "symbol": "RevoluteJointValidator"},
"supports": ["relation.joint.revolute"],
"limitations": [],
"maturity": "production"
},
{
"schema_version": "1.0",
"id": "capability.validation.fixed",
"version": "1.0.0",
"kind": "relation_validator",
"implementation": {"mode": "legacy_binding", "source_path": "src/relation_validators/joints.py", "symbol": "FixedValidator"},
"supports": ["relation.joint.fixed", "component.reference.housing"],
"limitations": [],
"maturity": "production"
},
{
"schema_version": "1.0",
"id": "capability.validation.rigid",
"version": "1.0.0",
"kind": "relation_validator",
"implementation": {"mode": "legacy_binding", "source_path": "src/relation_validators/joints.py", "symbol": "RigidValidator"},
"supports": ["relation.joint.rigid"],
"limitations": [],
"maturity": "production"
},
{
"schema_version": "1.0",
"id": "capability.validation.coaxial",
"version": "1.0.0",
"kind": "relation_validator",
"implementation": {"mode": "legacy_binding", "source_path": "src/relation_validators/joints.py", "symbol": "CoaxialValidator"},
"supports": ["relation.alignment.coaxial", "component.reference.main_axis"],
"limitations": ["Current topology validator assumes the project main Z axis."],
"maturity": "production"
},
{
"schema_version": "1.0",
"id": "capability.validation.reducer",
"version": "1.0.0",
"kind": "validation_pipeline",
"implementation": {"mode": "legacy_binding", "source_path": "src/validators.py", "symbol": "validate_from_files"},
"supports": ["mechanism.planetary.simple_2k_h", "mechanism.planetary.simple_2k_h_cascade", "mechanism.planetary.ferguson_wolfrom"],
"limitations": ["Validation coverage remains family dependent."],
"maturity": "production"
}
]
}
+23
View File
@@ -0,0 +1,23 @@
{
"schema_version": "1.0",
"id": "catalog.mechanical_transmission",
"version": "0.1.0",
"mode": "detached_read_only",
"integration_state": "not_connected",
"default_enabled": false,
"scope": "general_mechanical_transmission",
"initial_domain": "planetary_reducers",
"entry_roots": [
"components",
"relations",
"mechanisms",
"compositions",
"capabilities",
"evidence"
],
"stable_kernel": {
"path": "src",
"may_import_knowledge": false,
"knowledge_may_reference_kernel": true
}
}
@@ -0,0 +1,29 @@
{
"schema_version": "1.0",
"id": "component.carrier.planetary",
"version": "1.0.0",
"kind": "carrier",
"virtual": false,
"roles": ["carrier", "static_carrier", "output_carrier"],
"parameters": {
"planet_count": {"type": "integer", "minimum": 1},
"planet_orbit_radius_mm": {"type": "number", "exclusive_minimum": 0},
"plate_thickness_mm": {"type": "number", "exclusive_minimum": 0}
},
"ports": [
{"id": "rotation_axis", "type": "port.rotation.coaxial", "multiplicity": "one"},
{"id": "planet_pin_joint", "type": "port.joint.revolute", "multiplicity": "many"},
{"id": "stationary_mount", "type": "port.mount.stationary", "multiplicity": "one"}
],
"capability_refs": [
"capability.cad.simplecad_parts",
"capability.validation.revolute_joint"
],
"applicability": {
"current_mechanisms": [
"mechanism.planetary.simple_2k_h",
"mechanism.planetary.ferguson_wolfrom"
]
},
"maturity": "production"
}
@@ -0,0 +1,32 @@
{
"schema_version": "1.0",
"id": "component.gear.cylindrical.external",
"version": "1.0.0",
"kind": "gear",
"virtual": false,
"roles": ["sun", "planet", "parallel_axis_gear"],
"parameters": {
"teeth": {"type": "integer", "minimum_exclusive": 17},
"module_mm": {"type": "number", "exclusive_minimum": 0},
"pressure_angle_deg": {"type": "number", "exclusive_minimum": 0, "maximum": 45},
"face_width_mm": {"type": "number", "exclusive_minimum": 0},
"tooth_form": {"enum": ["spur", "helical"]},
"helix_angle_deg": {"type": "number", "minimum": 0, "maximum": 35},
"helix_hand": {"enum": ["left", "right"]}
},
"ports": [
{"id": "rotation_axis", "type": "port.rotation.coaxial", "multiplicity": "one"},
{"id": "external_teeth", "type": "port.mesh.external_cylindrical", "multiplicity": "many"},
{"id": "journal_rotation", "type": "port.joint.revolute", "multiplicity": "one"},
{"id": "radial_support", "type": "port.support.radial", "multiplicity": "many"}
],
"capability_refs": [
"capability.cad.simplecad_parts",
"capability.validation.external_mesh"
],
"applicability": {
"current_project_tooth_forms": ["spur", "helical"],
"note": "Sun and planet are mechanism roles, not separate component families."
},
"maturity": "production"
}
@@ -0,0 +1,31 @@
{
"schema_version": "1.0",
"id": "component.gear.cylindrical.internal",
"version": "1.0.0",
"kind": "gear",
"virtual": false,
"roles": ["ring", "internal_gear"],
"parameters": {
"teeth": {"type": "integer", "minimum_exclusive": 17},
"module_mm": {"type": "number", "exclusive_minimum": 0},
"pressure_angle_deg": {"type": "number", "exclusive_minimum": 0, "maximum": 45},
"face_width_mm": {"type": "number", "exclusive_minimum": 0},
"rim_thickness_mm": {"type": "number", "exclusive_minimum": 0},
"tooth_form": {"enum": ["spur", "helical"]},
"helix_angle_deg": {"type": "number", "minimum": 0, "maximum": 35},
"helix_hand": {"enum": ["left", "right"]}
},
"ports": [
{"id": "rotation_axis", "type": "port.rotation.coaxial", "multiplicity": "one"},
{"id": "internal_teeth", "type": "port.mesh.internal_cylindrical", "multiplicity": "many"},
{"id": "housing_mount", "type": "port.mount.stationary", "multiplicity": "one"}
],
"capability_refs": [
"capability.cad.simplecad_parts",
"capability.validation.internal_mesh"
],
"applicability": {
"current_project_tooth_forms": ["spur", "helical"]
},
"maturity": "production"
}
@@ -0,0 +1,18 @@
{
"schema_version": "1.0",
"id": "component.reference.housing",
"version": "1.0.0",
"kind": "housing",
"virtual": true,
"roles": ["housing", "ground_reference"],
"parameters": {},
"ports": [
{"id": "stationary_mount", "type": "port.mount.stationary", "multiplicity": "many"},
{"id": "main_axis", "type": "port.reference.axis", "multiplicity": "one"}
],
"capability_refs": ["capability.validation.fixed"],
"applicability": {
"note": "Topology reference; physical housing details are represented by industrial assembly knowledge."
},
"maturity": "production"
}
@@ -0,0 +1,17 @@
{
"schema_version": "1.0",
"id": "component.reference.main_axis",
"version": "1.0.0",
"kind": "axis",
"virtual": true,
"roles": ["main_axis"],
"parameters": {},
"ports": [
{"id": "axis", "type": "port.reference.axis", "multiplicity": "many"}
],
"capability_refs": ["capability.validation.coaxial"],
"applicability": {
"default_axis_xyz": [0.0, 0.0, 1.0]
},
"maturity": "production"
}
@@ -0,0 +1,30 @@
{
"schema_version": "1.0",
"id": "composition.coaxial_serial.two_simple_2kh",
"version": "1.0.0",
"composition_type": "serial_coaxial",
"source_port_types": ["port.rotation.coaxial"],
"target_port_types": ["port.rotation.coaxial"],
"connection_relation": "relation.joint.rigid",
"allowed_role_pairs": [
{"source": "configured_stage_output", "target": "configured_next_stage_input"}
],
"constraint_rules": [
"exactly_two_simple_2k_h_stages",
"axes_coaxial",
"no_conflicting_fixed_members",
"rigid_endpoint_speeds_equal",
"axial_stack_clearance"
],
"capability_refs": [
"capability.parameter_solver.simple_2kh_cascade",
"capability.layout.simple_2kh_cascade",
"capability.cad.simple_2kh_cascade"
],
"execution_support": {
"level": "full",
"legacy_family": "simple_2k_h_cascade",
"generic_n_stage": false
},
"maturity": "production"
}
@@ -0,0 +1,26 @@
{
"schema_version": "1.0",
"id": "evidence.legacy_baseline.2026_08_13",
"version": "1.0.0",
"evidence_type": "source_and_regression_baseline",
"subject_refs": [
"mechanism.planetary.simple_2k_h",
"mechanism.planetary.simple_2k_h_cascade",
"mechanism.planetary.ferguson_wolfrom"
],
"artifacts": [
{"path": "src/configurations/simple_2k_h/topology.template.json", "sha256": "a444fb2ea3ef16929f99a2f2a8cbd05442cedd91e7e3e67ec95cf344e3e282c3"},
{"path": "src/configurations/simple_2k_h_cascade/topology.template.json", "sha256": "308f60804aa1da2be791ce3e90e3d93c9a5f1f5cacdd0379818e908cbefb9613"},
{"path": "src/configurations/ferguson_wolfrom/topology.template.json", "sha256": "82a19e71d776628c79d9d16871d7e7a07f093ecaff84d1114d77f620d3ae074c"},
{"path": "tests/test_topology_and_kinematics.py", "purpose": "topology, parameter, and graph-kinematics regression"},
{"path": "tests/test_cad_integration.py", "purpose": "real CAD integration regression"},
{"path": "tests/test_validation.py", "purpose": "relation and physical validation regression"},
{"path": "tests/test_joint_module.py", "purpose": "joint-module and URDF regression"}
],
"claims": [
"The knowledge directory is detached from the existing src execution path.",
"The initial knowledge entries describe existing project behavior and do not add a new executable topology.",
"Legacy topology hashes must remain stable unless an intentional baseline update is reviewed."
],
"maturity": "production"
}
@@ -0,0 +1,67 @@
{
"schema_version": "1.0",
"id": "mechanism.planetary.ferguson_wolfrom",
"version": "1.0.0",
"legacy_family": "ferguson_wolfrom",
"description": "Ferguson-Wolfrom mechanical-paradox planetary reducer represented by two coupled sun/ring/planet sets.",
"members": [
{"id": "sun1", "component_ref": "component.gear.cylindrical.external", "role": "sun_fixed_side", "repeat": "single"},
{"id": "sun2", "component_ref": "component.gear.cylindrical.external", "role": "sun_output_side", "repeat": "single"},
{"id": "planet1", "component_ref": "component.gear.cylindrical.external", "role": "input_planet", "repeat": "planet_count"},
{"id": "planet2", "component_ref": "component.gear.cylindrical.external", "role": "output_planet", "repeat": "planet_count"},
{"id": "ring1", "component_ref": "component.gear.cylindrical.internal", "role": "ring_fixed_side", "repeat": "single"},
{"id": "ring2", "component_ref": "component.gear.cylindrical.internal", "role": "ring_output_side", "repeat": "single"},
{"id": "static_carrier", "component_ref": "component.carrier.planetary", "role": "static_carrier", "repeat": "single"},
{"id": "output_carrier", "component_ref": "component.carrier.planetary", "role": "output_carrier", "repeat": "single"},
{"id": "housing", "component_ref": "component.reference.housing", "role": "housing", "repeat": "single"},
{"id": "main_axis", "component_ref": "component.reference.main_axis", "role": "main_axis", "repeat": "single"}
],
"relations": [
{"id": "sun1_planet1", "source": "sun1", "target": "planet1", "relation_ref": "relation.mesh.external_cylindrical", "carrier_ref": "static_carrier"},
{"id": "ring1_planet1", "source": "ring1", "target": "planet1", "relation_ref": "relation.mesh.internal_cylindrical", "carrier_ref": "static_carrier"},
{"id": "sun2_planet2", "source": "sun2", "target": "planet2", "relation_ref": "relation.mesh.external_cylindrical", "carrier_ref": "output_carrier"},
{"id": "ring2_planet2", "source": "ring2", "target": "planet2", "relation_ref": "relation.mesh.internal_cylindrical", "carrier_ref": "output_carrier"},
{"id": "planet1_carrier", "source": "planet1", "target": "static_carrier", "relation_ref": "relation.joint.revolute"},
{"id": "planet2_carrier", "source": "planet2", "target": "output_carrier", "relation_ref": "relation.joint.revolute"},
{"id": "sun_pair", "source": "sun1", "target": "sun2", "relation_ref": "relation.joint.rigid"},
{"id": "ring_pair", "source": "ring1", "target": "ring2", "relation_ref": "relation.joint.rigid"},
{"id": "sun1_axis", "source": "sun1", "target": "main_axis", "relation_ref": "relation.alignment.coaxial"},
{"id": "sun2_axis", "source": "sun2", "target": "main_axis", "relation_ref": "relation.alignment.coaxial"},
{"id": "ring1_axis", "source": "ring1", "target": "main_axis", "relation_ref": "relation.alignment.coaxial"},
{"id": "ring2_axis", "source": "ring2", "target": "main_axis", "relation_ref": "relation.alignment.coaxial"},
{"id": "static_carrier_axis", "source": "static_carrier", "target": "main_axis", "relation_ref": "relation.alignment.coaxial"},
{"id": "output_carrier_axis", "source": "output_carrier", "target": "main_axis", "relation_ref": "relation.alignment.coaxial"},
{"id": "static_carrier_ground", "source": "static_carrier", "target": "housing", "relation_ref": "relation.joint.fixed"}
],
"external_ports": [
{"id": "input_rotation", "member": "planet1", "type": "port.joint.revolute"},
{"id": "output_rotation", "member": "output_carrier", "type": "port.rotation.coaxial"},
{"id": "housing_mount", "member": "housing", "type": "port.mount.stationary"}
],
"boundary_profiles": [
{"id": "planet1_in_static_carrier_fixed_output_carrier_out", "input": "planet1", "fixed": ["static_carrier"], "output": "output_carrier", "production_evidence": true}
],
"constraint_rules": [
"paired_suns_rigid",
"paired_rings_rigid",
"required_tooth_counts_explicit",
"minimum_teeth_exclusive_17",
"graph_kinematics_residual_within_tolerance"
],
"capability_refs": [
"capability.parameter_solver.ferguson_wolfrom",
"capability.kinematics.graph",
"capability.layout.ferguson_wolfrom",
"capability.cad.ferguson_wolfrom",
"capability.validation.reducer"
],
"source_topology": {
"path": "src/configurations/ferguson_wolfrom/topology.template.json",
"sha256": "82a19e71d776628c79d9d16871d7e7a07f093ecaff84d1114d77f620d3ae074c"
},
"limitations": [
"Current implementation supports kinematic_demo detail level only.",
"The eccentric planet-pin input is not compatible with the current coaxial joint-module adapter."
],
"maturity": "geometry_validated"
}
@@ -0,0 +1,60 @@
{
"schema_version": "1.0",
"id": "mechanism.planetary.simple_2k_h",
"version": "1.0.0",
"legacy_family": "simple_2k_h",
"description": "Single simple 2K-H planetary cell with interchangeable input, fixed, and output boundary members.",
"members": [
{"id": "sun", "component_ref": "component.gear.cylindrical.external", "role": "sun", "repeat": "single"},
{"id": "planet", "component_ref": "component.gear.cylindrical.external", "role": "planet", "repeat": "planet_count"},
{"id": "ring", "component_ref": "component.gear.cylindrical.internal", "role": "ring", "repeat": "single"},
{"id": "carrier", "component_ref": "component.carrier.planetary", "role": "carrier", "repeat": "single"},
{"id": "housing", "component_ref": "component.reference.housing", "role": "housing", "repeat": "single"},
{"id": "main_axis", "component_ref": "component.reference.main_axis", "role": "main_axis", "repeat": "single"}
],
"relations": [
{"id": "sun_planet", "source": "sun", "target": "planet", "relation_ref": "relation.mesh.external_cylindrical", "carrier_ref": "carrier"},
{"id": "ring_planet", "source": "ring", "target": "planet", "relation_ref": "relation.mesh.internal_cylindrical", "carrier_ref": "carrier"},
{"id": "planet_carrier", "source": "planet", "target": "carrier", "relation_ref": "relation.joint.revolute"},
{"id": "sun_axis", "source": "sun", "target": "main_axis", "relation_ref": "relation.alignment.coaxial"},
{"id": "ring_axis", "source": "ring", "target": "main_axis", "relation_ref": "relation.alignment.coaxial"},
{"id": "carrier_axis", "source": "carrier", "target": "main_axis", "relation_ref": "relation.alignment.coaxial"},
{"id": "fixed_member_housing", "source": "ring", "target": "housing", "relation_ref": "relation.joint.fixed", "boundary_substitutable": true}
],
"external_ports": [
{"id": "sun_rotation", "member": "sun", "type": "port.rotation.coaxial"},
{"id": "ring_rotation", "member": "ring", "type": "port.rotation.coaxial"},
{"id": "carrier_rotation", "member": "carrier", "type": "port.rotation.coaxial"},
{"id": "housing_mount", "member": "housing", "type": "port.mount.stationary"}
],
"boundary_profiles": [
{"id": "sun_in_ring_fixed_carrier_out", "input": "sun", "fixed": ["ring"], "output": "carrier", "production_evidence": true},
{"id": "ring_in_sun_fixed_carrier_out", "input": "ring", "fixed": ["sun"], "output": "carrier", "production_evidence": true},
{"id": "carrier_in_ring_fixed_sun_out", "input": "carrier", "fixed": ["ring"], "output": "sun", "production_evidence": true},
{"id": "sun_in_carrier_fixed_ring_out", "input": "sun", "fixed": ["carrier"], "output": "ring", "production_evidence": true}
],
"constraint_rules": [
"z_ring = z_sun + 2*z_planet",
"(z_sun + z_ring) % planet_count = 0",
"common_module",
"common_pressure_angle",
"planet_neighbor_clearance",
"maximum_outer_diameter"
],
"capability_refs": [
"capability.parameter_solver.simple_2kh",
"capability.kinematics.graph",
"capability.layout.simple_2kh",
"capability.cad.simple_2kh",
"capability.validation.reducer"
],
"source_topology": {
"path": "src/configurations/simple_2k_h/topology.template.json",
"sha256": "a444fb2ea3ef16929f99a2f2a8cbd05442cedd91e7e3e67ec95cf344e3e282c3"
},
"limitations": [
"Current CAD supports spur and helical tooth forms.",
"Industrial-core generation currently restricts boundary variants."
],
"maturity": "production"
}
@@ -0,0 +1,47 @@
{
"schema_version": "1.0",
"id": "mechanism.planetary.simple_2k_h_cascade",
"version": "1.0.0",
"legacy_family": "simple_2k_h_cascade",
"description": "Exactly two simple 2K-H stages connected by a rigid stage-output to next-stage-input coupling.",
"members": [
{"id": "stage_1", "mechanism_ref": "mechanism.planetary.simple_2k_h", "role": "first_stage", "repeat": "single"},
{"id": "stage_2", "mechanism_ref": "mechanism.planetary.simple_2k_h", "role": "second_stage", "repeat": "single"},
{"id": "housing", "component_ref": "component.reference.housing", "role": "housing", "repeat": "single"},
{"id": "main_axis", "component_ref": "component.reference.main_axis", "role": "main_axis", "repeat": "single"}
],
"relations": [
{"id": "stage_output_to_next_input", "source": "stage_1", "target": "stage_2", "relation_ref": "relation.joint.rigid", "composition_ref": "composition.coaxial_serial.two_simple_2kh"}
],
"external_ports": [
{"id": "input_rotation", "member": "stage_1", "type": "port.rotation.coaxial"},
{"id": "output_rotation", "member": "stage_2", "type": "port.rotation.coaxial"},
{"id": "housing_mount", "member": "housing", "type": "port.mount.stationary"}
],
"boundary_profiles": [
{"id": "per_stage_boundaries", "input": "stage_1.configured_input", "fixed": ["stage_1.configured_fixed", "stage_2.configured_fixed"], "output": "stage_2.configured_output", "production_evidence": true}
],
"constraint_rules": [
"exactly_two_stages",
"each_stage_is_simple_2k_h",
"stage_output_rigid_to_next_stage_input",
"common_main_axis",
"axial_stack_clearance"
],
"capability_refs": [
"capability.parameter_solver.simple_2kh_cascade",
"capability.kinematics.graph",
"capability.layout.simple_2kh_cascade",
"capability.cad.simple_2kh_cascade",
"capability.validation.reducer"
],
"source_topology": {
"path": "src/configurations/simple_2k_h_cascade/topology.template.json",
"sha256": "308f60804aa1da2be791ce3e90e3d93c9a5f1f5cacdd0379818e908cbefb9613"
},
"limitations": [
"This is a fixed two-stage implementation, not a generic N-stage composer.",
"Only the existing rigid output-to-input composition is production supported."
],
"maturity": "production"
}
@@ -0,0 +1,15 @@
{
"schema_version": "1.0",
"id": "ontology.maturity_levels",
"version": "1.0.0",
"ordered_levels": [
"declared",
"topology_validated",
"kinematically_validated",
"parameter_solved",
"cad_generated",
"geometry_validated",
"production"
],
"rule": "An entry may only claim the highest stage supported by recorded evidence and executable capability bindings."
}
@@ -0,0 +1,15 @@
{
"schema_version": "1.0",
"id": "ontology.port_types",
"version": "1.0.0",
"entries": [
{"id": "port.rotation.coaxial", "domain": "rotation", "axis_relation": "coaxial"},
{"id": "port.mesh.external_cylindrical", "domain": "gear_mesh", "axis_relation": "parallel"},
{"id": "port.mesh.internal_cylindrical", "domain": "gear_mesh", "axis_relation": "parallel"},
{"id": "port.joint.revolute", "domain": "joint", "axis_relation": "coaxial_local"},
{"id": "port.mount.stationary", "domain": "mount", "axis_relation": "not_applicable"},
{"id": "port.reference.axis", "domain": "reference", "axis_relation": "self"},
{"id": "port.support.radial", "domain": "support", "axis_relation": "coaxial"},
{"id": "port.support.axial", "domain": "support", "axis_relation": "coaxial"}
]
}
@@ -0,0 +1,13 @@
{
"schema_version": "1.0",
"id": "ontology.relation_types",
"version": "1.0.0",
"entries": [
{"type": "external_mesh", "knowledge_id": "relation.mesh.external_cylindrical"},
{"type": "internal_mesh", "knowledge_id": "relation.mesh.internal_cylindrical"},
{"type": "revolute_joint", "knowledge_id": "relation.joint.revolute"},
{"type": "coaxial", "knowledge_id": "relation.alignment.coaxial"},
{"type": "fixed", "knowledge_id": "relation.joint.fixed"},
{"type": "rigid", "knowledge_id": "relation.joint.rigid"}
]
}
+17
View File
@@ -0,0 +1,17 @@
{
"schema_version": "1.0",
"id": "ontology.units",
"version": "1.0.0",
"canonical": {
"length": "mm",
"angle": "deg",
"angular_speed": "rad_per_s",
"torque": "N_m",
"force": "N",
"mass": "kg"
},
"coordinate_convention": {
"handedness": "right_handed",
"default_rotation_axis": [0.0, 0.0, 1.0]
}
}
@@ -0,0 +1,15 @@
{
"schema_version": "1.0",
"id": "relation.alignment.coaxial",
"version": "1.0.0",
"relation_type": "coaxial",
"endpoint_rules": {
"source_port": "port.rotation.coaxial",
"target_port": "port.reference.axis",
"axis_relation": "coincident"
},
"constraint_rules": ["axes_parallel", "axis_distance_within_tolerance"],
"kinematic_effect": null,
"capability_refs": ["capability.validation.coaxial"],
"maturity": "production"
}
@@ -0,0 +1,18 @@
{
"schema_version": "1.0",
"id": "relation.joint.fixed",
"version": "1.0.0",
"relation_type": "fixed",
"endpoint_rules": {
"source_port": "port.rotation.coaxial",
"target_port": "port.mount.stationary",
"axis_relation": "mechanism_defined"
},
"constraint_rules": ["one_endpoint_is_grounded", "fixed_speed_is_zero"],
"kinematic_effect": {"equation": "omega_source = omega_target = 0"},
"capability_refs": [
"capability.kinematics.graph",
"capability.validation.fixed"
],
"maturity": "production"
}
@@ -0,0 +1,15 @@
{
"schema_version": "1.0",
"id": "relation.joint.revolute",
"version": "1.0.0",
"relation_type": "revolute_joint",
"endpoint_rules": {
"source_port": "port.joint.revolute",
"target_port": "port.joint.revolute",
"axis_relation": "coaxial_local"
},
"constraint_rules": ["joint_axis_defined", "carrier_pin_orbit_matches_layout"],
"kinematic_effect": null,
"capability_refs": ["capability.validation.revolute_joint"],
"maturity": "production"
}
@@ -0,0 +1,18 @@
{
"schema_version": "1.0",
"id": "relation.joint.rigid",
"version": "1.0.0",
"relation_type": "rigid",
"endpoint_rules": {
"source_port": "port.rotation.coaxial",
"target_port": "port.rotation.coaxial",
"axis_relation": "coaxial"
},
"constraint_rules": ["axes_coaxial", "angular_speeds_equal", "torque_capacity_compatible"],
"kinematic_effect": {"equation": "omega_source = omega_target"},
"capability_refs": [
"capability.kinematics.graph",
"capability.validation.rigid"
],
"maturity": "production"
}
@@ -0,0 +1,26 @@
{
"schema_version": "1.0",
"id": "relation.mesh.external_cylindrical",
"version": "1.0.0",
"relation_type": "external_mesh",
"endpoint_rules": {
"source_port": "port.mesh.external_cylindrical",
"target_port": "port.mesh.external_cylindrical",
"axis_relation": "parallel"
},
"constraint_rules": [
"equal_module",
"equal_pressure_angle",
"equal_tooth_form",
"spur_or_opposite_helical_hand",
"center_distance_equals_pitch_radius_sum"
],
"kinematic_effect": {
"equation": "(omega_source-omega_carrier)*z_source + (omega_target-omega_carrier)*z_target = 0"
},
"capability_refs": [
"capability.kinematics.graph",
"capability.validation.external_mesh"
],
"maturity": "production"
}
@@ -0,0 +1,26 @@
{
"schema_version": "1.0",
"id": "relation.mesh.internal_cylindrical",
"version": "1.0.0",
"relation_type": "internal_mesh",
"endpoint_rules": {
"source_port": "port.mesh.internal_cylindrical",
"target_port": "port.mesh.external_cylindrical",
"axis_relation": "parallel"
},
"constraint_rules": [
"equal_module",
"equal_pressure_angle",
"equal_tooth_form",
"spur_or_same_helical_hand",
"center_distance_equals_pitch_radius_difference"
],
"kinematic_effect": {
"equation": "(omega_source-omega_carrier)*z_source - (omega_target-omega_carrier)*z_target = 0"
},
"capability_refs": [
"capability.kinematics.graph",
"capability.validation.internal_mesh"
],
"maturity": "production"
}
@@ -0,0 +1,27 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "kb://schemas/capability.schema.json",
"title": "Executable capability binding",
"type": "object",
"required": ["schema_version", "id", "version", "kind", "implementation", "supports", "maturity"],
"properties": {
"schema_version": {"const": "1.0"},
"id": {"type": "string", "pattern": "^capability\\."},
"version": {"type": "string"},
"kind": {"type": "string"},
"implementation": {
"type": "object",
"required": ["mode", "source_path"],
"properties": {
"mode": {"enum": ["legacy_binding", "declarative"]},
"source_path": {"type": "string"},
"symbol": {"type": ["string", "null"]}
},
"additionalProperties": false
},
"supports": {"type": "array", "items": {"type": "string"}},
"limitations": {"type": "array", "items": {"type": "string"}},
"maturity": {"type": "string"}
},
"additionalProperties": false
}
@@ -0,0 +1,33 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "kb://schemas/component.schema.json",
"title": "Mechanical component knowledge entry",
"type": "object",
"required": ["schema_version", "id", "version", "kind", "parameters", "ports", "capability_refs", "maturity"],
"properties": {
"schema_version": {"const": "1.0"},
"id": {"type": "string", "pattern": "^component\\."},
"version": {"type": "string"},
"kind": {"type": "string"},
"virtual": {"type": "boolean", "default": false},
"roles": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
"parameters": {"type": "object"},
"ports": {
"type": "array",
"items": {
"type": "object",
"required": ["id", "type"],
"properties": {
"id": {"type": "string"},
"type": {"type": "string"},
"multiplicity": {"type": "string", "enum": ["one", "many"]}
},
"additionalProperties": false
}
},
"capability_refs": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
"applicability": {"type": "object"},
"maturity": {"type": "string"}
},
"additionalProperties": false
}
@@ -0,0 +1,22 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "kb://schemas/composition.schema.json",
"title": "Mechanism composition knowledge entry",
"type": "object",
"required": ["schema_version", "id", "version", "composition_type", "source_port_types", "target_port_types", "connection_relation", "constraint_rules", "capability_refs", "execution_support", "maturity"],
"properties": {
"schema_version": {"const": "1.0"},
"id": {"type": "string", "pattern": "^composition\\."},
"version": {"type": "string"},
"composition_type": {"type": "string"},
"source_port_types": {"type": "array", "items": {"type": "string"}},
"target_port_types": {"type": "array", "items": {"type": "string"}},
"connection_relation": {"type": "string"},
"allowed_role_pairs": {"type": "array", "items": {"type": "object"}},
"constraint_rules": {"type": "array", "items": {"type": "string"}},
"capability_refs": {"type": "array", "items": {"type": "string"}},
"execution_support": {"type": "object"},
"maturity": {"type": "string"}
},
"additionalProperties": false
}
@@ -0,0 +1,18 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "kb://schemas/evidence.schema.json",
"title": "Knowledge evidence entry",
"type": "object",
"required": ["schema_version", "id", "version", "evidence_type", "subject_refs", "artifacts", "maturity"],
"properties": {
"schema_version": {"const": "1.0"},
"id": {"type": "string", "pattern": "^evidence\\."},
"version": {"type": "string"},
"evidence_type": {"type": "string"},
"subject_refs": {"type": "array", "items": {"type": "string"}},
"artifacts": {"type": "array", "items": {"type": "object"}},
"claims": {"type": "array", "items": {"type": "string"}},
"maturity": {"type": "string"}
},
"additionalProperties": false
}
@@ -0,0 +1,24 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "kb://schemas/mechanism.schema.json",
"title": "Mechanism knowledge entry",
"type": "object",
"required": ["schema_version", "id", "version", "legacy_family", "members", "relations", "external_ports", "boundary_profiles", "constraint_rules", "capability_refs", "maturity"],
"properties": {
"schema_version": {"const": "1.0"},
"id": {"type": "string", "pattern": "^mechanism\\."},
"version": {"type": "string"},
"legacy_family": {"type": ["string", "null"]},
"description": {"type": "string"},
"members": {"type": "array", "items": {"type": "object"}},
"relations": {"type": "array", "items": {"type": "object"}},
"external_ports": {"type": "array", "items": {"type": "object"}},
"boundary_profiles": {"type": "array", "items": {"type": "object"}},
"constraint_rules": {"type": "array", "items": {"type": "string"}},
"capability_refs": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
"source_topology": {"type": "object"},
"limitations": {"type": "array", "items": {"type": "string"}},
"maturity": {"type": "string"}
},
"additionalProperties": false
}
@@ -0,0 +1,19 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "kb://schemas/relation.schema.json",
"title": "Mechanical relation knowledge entry",
"type": "object",
"required": ["schema_version", "id", "version", "relation_type", "endpoint_rules", "constraint_rules", "capability_refs", "maturity"],
"properties": {
"schema_version": {"const": "1.0"},
"id": {"type": "string", "pattern": "^relation\\."},
"version": {"type": "string"},
"relation_type": {"type": "string"},
"endpoint_rules": {"type": "object"},
"constraint_rules": {"type": "array", "items": {"type": "string"}},
"kinematic_effect": {"type": ["object", "null"]},
"capability_refs": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
"maturity": {"type": "string"}
},
"additionalProperties": false
}
@@ -0,0 +1,289 @@
#!/usr/bin/env python3
"""Read-only consistency validator for the detached knowledge base."""
from __future__ import annotations
import ast
import hashlib
import json
import sys
from pathlib import Path
from typing import Any
KNOWLEDGE_ROOT = Path(__file__).resolve().parents[1]
BACKEND_ROOT = KNOWLEDGE_ROOT.parent
ENTRY_DIRS = {
"component": KNOWLEDGE_ROOT / "components",
"relation": KNOWLEDGE_ROOT / "relations",
"mechanism": KNOWLEDGE_ROOT / "mechanisms",
"composition": KNOWLEDGE_ROOT / "compositions",
"evidence": KNOWLEDGE_ROOT / "evidence",
}
REQUIRED_FIELDS = {
"component": {"schema_version", "id", "version", "kind", "parameters", "ports", "capability_refs", "maturity"},
"relation": {"schema_version", "id", "version", "relation_type", "endpoint_rules", "constraint_rules", "capability_refs", "maturity"},
"mechanism": {"schema_version", "id", "version", "legacy_family", "members", "relations", "external_ports", "boundary_profiles", "constraint_rules", "capability_refs", "maturity"},
"composition": {"schema_version", "id", "version", "composition_type", "source_port_types", "target_port_types", "connection_relation", "constraint_rules", "capability_refs", "execution_support", "maturity"},
"capability": {"schema_version", "id", "version", "kind", "implementation", "supports", "maturity"},
"evidence": {"schema_version", "id", "version", "evidence_type", "subject_refs", "artifacts", "maturity"},
}
def load_json(path: Path, errors: list[str]) -> Any | None:
try:
return json.loads(path.read_text(encoding="utf-8"))
except Exception as exc: # noqa: BLE001
errors.append(f"invalid_json:{path.relative_to(BACKEND_ROOT)}:{exc}")
return None
def sha256_file(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as handle:
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
digest.update(chunk)
return digest.hexdigest()
def collect_entries(errors: list[str]) -> tuple[dict[str, dict[str, Any]], dict[str, str]]:
entries: dict[str, dict[str, Any]] = {}
kinds: dict[str, str] = {}
for kind, root in ENTRY_DIRS.items():
for path in sorted(root.rglob("*.json")):
data = load_json(path, errors)
if not isinstance(data, dict):
continue
entry_id = data.get("id")
if not isinstance(entry_id, str):
errors.append(f"missing_id:{path.relative_to(BACKEND_ROOT)}")
continue
if entry_id in entries:
errors.append(f"duplicate_id:{entry_id}")
continue
entries[entry_id] = data
kinds[entry_id] = kind
capability_registry_path = KNOWLEDGE_ROOT / "capabilities" / "registry.json"
registry = load_json(capability_registry_path, errors)
if isinstance(registry, dict):
for data in registry.get("entries", []):
if not isinstance(data, dict) or not isinstance(data.get("id"), str):
errors.append("invalid_capability_registry_entry")
continue
entry_id = data["id"]
if entry_id in entries:
errors.append(f"duplicate_id:{entry_id}")
continue
entries[entry_id] = data
kinds[entry_id] = "capability"
return entries, kinds
def validate_required_fields(
entries: dict[str, dict[str, Any]],
kinds: dict[str, str],
errors: list[str],
) -> None:
for entry_id, entry in entries.items():
kind = kinds[entry_id]
missing = sorted(REQUIRED_FIELDS[kind] - set(entry))
if missing:
errors.append(f"missing_fields:{entry_id}:{','.join(missing)}")
if entry.get("schema_version") != "1.0":
errors.append(f"unsupported_schema_version:{entry_id}:{entry.get('schema_version')}")
expected_prefix = f"{kind}."
if not entry_id.startswith(expected_prefix):
errors.append(f"bad_id_prefix:{entry_id}:expected={expected_prefix}")
def referenced_ids(entry: dict[str, Any], kind: str) -> list[str]:
refs: list[str] = []
refs.extend(entry.get("capability_refs", []))
if kind == "mechanism":
for member in entry.get("members", []):
refs.extend(
value
for key in ("component_ref", "mechanism_ref")
if isinstance((value := member.get(key)), str)
)
for relation in entry.get("relations", []):
refs.extend(
value
for key in ("relation_ref", "composition_ref")
if isinstance((value := relation.get(key)), str)
)
elif kind == "composition":
relation_ref = entry.get("connection_relation")
if isinstance(relation_ref, str):
refs.append(relation_ref)
elif kind == "evidence":
refs.extend(entry.get("subject_refs", []))
return refs
def validate_references(
entries: dict[str, dict[str, Any]],
kinds: dict[str, str],
port_ids: set[str],
errors: list[str],
) -> None:
for entry_id, entry in entries.items():
for reference in referenced_ids(entry, kinds[entry_id]):
if reference not in entries:
errors.append(f"unknown_reference:{entry_id}:{reference}")
for port in entry.get("ports", []):
port_type = port.get("type")
if port_type not in port_ids:
errors.append(f"unknown_port_type:{entry_id}:{port_type}")
for port in entry.get("external_ports", []):
port_type = port.get("type")
if port_type not in port_ids:
errors.append(f"unknown_port_type:{entry_id}:{port_type}")
for key in ("source_port_types", "target_port_types"):
for port_type in entry.get(key, []):
if port_type not in port_ids:
errors.append(f"unknown_port_type:{entry_id}:{port_type}")
def validate_mechanism_graphs(entries: dict[str, dict[str, Any]], errors: list[str]) -> None:
for entry_id, mechanism in entries.items():
if not entry_id.startswith("mechanism."):
continue
member_ids = {member.get("id") for member in mechanism.get("members", [])}
if None in member_ids or len(member_ids) != len(mechanism.get("members", [])):
errors.append(f"invalid_or_duplicate_member:{entry_id}")
relation_ids: set[str] = set()
for relation in mechanism.get("relations", []):
relation_id = relation.get("id")
if relation_id in relation_ids or not relation_id:
errors.append(f"invalid_or_duplicate_relation:{entry_id}:{relation_id}")
relation_ids.add(relation_id)
for endpoint in ("source", "target"):
if relation.get(endpoint) not in member_ids:
errors.append(
f"unknown_relation_endpoint:{entry_id}:{relation_id}:{endpoint}={relation.get(endpoint)}"
)
carrier = relation.get("carrier_ref")
if carrier is not None and carrier not in member_ids:
errors.append(f"unknown_carrier_ref:{entry_id}:{relation_id}:{carrier}")
for port in mechanism.get("external_ports", []):
if port.get("member") not in member_ids:
errors.append(f"unknown_external_port_member:{entry_id}:{port.get('id')}")
for profile in mechanism.get("boundary_profiles", []):
boundary_members = [profile.get("input"), profile.get("output"), *profile.get("fixed", [])]
for member in boundary_members:
root_member = member.split(".", 1)[0] if isinstance(member, str) else member
if root_member not in member_ids:
errors.append(f"unknown_boundary_member:{entry_id}:{profile.get('id')}:{member}")
def validate_capability_sources(entries: dict[str, dict[str, Any]], errors: list[str]) -> None:
for entry_id, entry in entries.items():
if not entry_id.startswith("capability."):
continue
implementation = entry.get("implementation", {})
source_path = implementation.get("source_path")
if not isinstance(source_path, str):
errors.append(f"missing_capability_source:{entry_id}")
continue
source = BACKEND_ROOT / source_path
if not source.is_file():
errors.append(f"missing_capability_source:{entry_id}:{source_path}")
continue
symbol = implementation.get("symbol")
if symbol:
try:
tree = ast.parse(source.read_text(encoding="utf-8"))
except SyntaxError as exc:
errors.append(f"invalid_capability_source_python:{entry_id}:{exc}")
continue
top_level_symbols = {
node.name
for node in tree.body
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef))
}
if symbol not in top_level_symbols:
errors.append(f"missing_capability_symbol:{entry_id}:{source_path}:{symbol}")
def validate_topology_hashes(entries: dict[str, dict[str, Any]], errors: list[str]) -> None:
for entry_id, entry in entries.items():
source_topology = entry.get("source_topology")
if not source_topology:
continue
source = BACKEND_ROOT / source_topology["path"]
if not source.is_file():
errors.append(f"missing_source_topology:{entry_id}:{source_topology['path']}")
continue
actual = sha256_file(source)
if actual != source_topology.get("sha256"):
errors.append(
f"source_topology_hash_mismatch:{entry_id}:expected={source_topology.get('sha256')}:actual={actual}"
)
def validate_detached_boundary(errors: list[str]) -> None:
for path in sorted((BACKEND_ROOT / "src").rglob("*.py")):
try:
tree = ast.parse(path.read_text(encoding="utf-8"))
except SyntaxError:
continue
for node in ast.walk(tree):
module: str | None = None
if isinstance(node, ast.ImportFrom):
module = node.module
elif isinstance(node, ast.Import):
for alias in node.names:
if alias.name == "knowledge" or alias.name.startswith("knowledge."):
errors.append(f"stable_kernel_imports_knowledge:{path.relative_to(BACKEND_ROOT)}:{alias.name}")
if module == "knowledge" or (module and module.startswith("knowledge.")):
errors.append(f"stable_kernel_imports_knowledge:{path.relative_to(BACKEND_ROOT)}:{module}")
def main() -> int:
errors: list[str] = []
for path in sorted(KNOWLEDGE_ROOT.rglob("*.json")):
load_json(path, errors)
catalog = load_json(KNOWLEDGE_ROOT / "catalog.json", errors)
if isinstance(catalog, dict):
if catalog.get("default_enabled") is not False:
errors.append("catalog_default_enabled_must_be_false")
if catalog.get("integration_state") != "not_connected":
errors.append("catalog_integration_state_must_be_not_connected")
entries, kinds = collect_entries(errors)
validate_required_fields(entries, kinds, errors)
ports = load_json(KNOWLEDGE_ROOT / "ontology" / "port_types.json", errors)
port_ids = {
entry["id"]
for entry in (ports or {}).get("entries", [])
if isinstance(entry, dict) and isinstance(entry.get("id"), str)
}
validate_references(entries, kinds, port_ids, errors)
validate_mechanism_graphs(entries, errors)
validate_capability_sources(entries, errors)
validate_topology_hashes(entries, errors)
validate_detached_boundary(errors)
if errors:
print(f"knowledge validation failed: {len(errors)} error(s)", file=sys.stderr)
for error in errors:
print(f"- {error}", file=sys.stderr)
return 1
counts: dict[str, int] = {}
for kind in kinds.values():
counts[kind] = counts.get(kind, 0) + 1
count_text = ", ".join(f"{kind}={counts[kind]}" for kind in sorted(counts))
print(f"knowledge validation passed: {len(entries)} entries ({count_text})")
print("integration_state=not_connected default_enabled=false stable_kernel_imports=0")
return 0
if __name__ == "__main__":
raise SystemExit(main())
+10
View File
@@ -103,6 +103,16 @@ def load_platform_assets(platform_profile: Path) -> tuple[JointPlatformProfile,
"motor_step": _resolve_path(profile.motor_step, requirement_path=platform_profile),
"housing_step": _resolve_path(profile.housing_step, requirement_path=platform_profile),
}
for field in (
"motor_reference_assembly_step",
"motor_stator_housing_step",
"motor_rotor_housing_step",
"motor_output_shaft_step",
"motor_windings_step",
):
value = getattr(profile, field)
if value is not None:
paths[field] = _resolve_path(value, requirement_path=platform_profile)
if profile.chip_step is not None:
paths["chip_step"] = _resolve_path(profile.chip_step, requirement_path=platform_profile)
if profile.reference_assembly_step is not None:
+422 -11
View File
@@ -38,7 +38,7 @@ from .models import (
StepShapeMetrics,
)
from .reducer_adapters import JointAdapterError, ReducerJointAdapter, get_reducer_joint_adapter
from .reference_assembly import ReferenceAssemblyError, infer_reference_placements
from .reference_assembly import ReferenceAssemblyError, compose_placements, infer_reference_placements
from .step_geometry import (
apply_placement_to_direction,
apply_placement_to_point,
@@ -115,6 +115,11 @@ def load_joint_requirement(path: Path) -> JointModuleRequirement:
profile = JointPlatformProfile.model_validate(read_json(profile_path))
asset_fields = (
"motor_step",
"motor_reference_assembly_step",
"motor_stator_housing_step",
"motor_rotor_housing_step",
"motor_output_shaft_step",
"motor_windings_step",
"housing_step",
"chip_step",
"reference_assembly_step",
@@ -309,6 +314,37 @@ def _motor_output_bore_face_axis_value(
return dot(rotated_bore_face_point, target_axis_direction)
def _motor_output_bore_axis_range(
*,
motor_metrics: StepShapeMetrics,
motor_placement: ComponentPlacement,
target_axis_direction: tuple[float, float, float],
) -> tuple[float, float] | None:
axis = motor_metrics.inferred_axis
if axis is None:
return None
values: list[float] = []
selected_axis_dimension = str(axis.details.get("selected_axis_dimension", "z"))
origin_axis_value = {
"x": axis.origin_mm[0],
"y": axis.origin_mm[1],
"z": axis.origin_mm[2],
}.get(selected_axis_dimension)
if origin_axis_value is None:
return None
for key in ("motor_output_bore_axis_min_mm", "motor_output_bore_axis_max_mm"):
raw_value = axis.details.get(key)
if not isinstance(raw_value, (int, float)):
return None
point = vector_add(
axis.origin_mm,
vector_scale(axis.direction_xyz, float(raw_value) - float(origin_axis_value)),
)
placed_point = apply_placement_to_point(point, motor_placement)
values.append(dot(placed_point, target_axis_direction))
return (min(values), max(values))
def _build_placements(
*,
requirement: JointModuleRequirement,
@@ -319,6 +355,7 @@ def _build_placements(
artifacts: ReducerRunArtifacts,
adapter: ReducerJointAdapter,
reference_placements: dict[str, ComponentPlacement] | None = None,
motor_shape=None,
) -> dict[str, ComponentPlacement]:
if motor_metrics.inferred_axis is None or housing_metrics.inferred_axis is None:
raise JointModuleError("datum_inference_failed: motor and housing axes are required")
@@ -409,7 +446,8 @@ def _build_placements(
)
motor_rotation = axis_angle_between(motor_metrics.inferred_axis.direction_xyz, motor_target_axis)
motor_rotation_only = ComponentPlacement(rotation_axis_angle_deg=motor_rotation)
rotated_motor = apply_transform(read_step_shape(Path(motor_metrics.step_path)), motor_rotation_only)
raw_motor_shape = motor_shape if motor_shape is not None else read_step_shape(Path(motor_metrics.step_path))
rotated_motor = apply_transform(raw_motor_shape, motor_rotation_only)
rotated_motor_bbox = metrics_for_shape(
component_id="motor",
step_path=Path(motor_metrics.step_path),
@@ -620,7 +658,12 @@ def _build_placements(
else:
motor_translation = _axis_align_translation(
axis_origin_target=input_axis_origin_target,
axis_direction_target=input_axis_direction_target,
# The face coordinates above are measured along the housing axis.
# The reducer input axis may point in the opposite direction; using
# it here mirrors the axial translation and ejects the motor from
# the housing. Keep the common-axis centering, but translate in the
# same coordinate direction used by the face values.
axis_direction_target=housing_axis_direction,
rotated_axis_origin=rotated_motor_axis_origin,
rotated_bbox_axis_value=motor_output_face_axis_value,
desired_bbox_axis_value=desired_motor_output_face_axis_value,
@@ -643,6 +686,42 @@ def _build_placements(
"motor_axis": motor_metrics.inferred_axis.model_dump(mode="json"),
},
)
actual_output_face = _motor_output_bore_face_axis_value(
motor_metrics=motor_metrics,
motor_output_side=motor_output_side,
motor_rotation=motor_placement,
target_axis_direction=housing_axis_direction,
)
actual_bore_range = _motor_output_bore_axis_range(
motor_metrics=motor_metrics,
motor_placement=motor_placement,
target_axis_direction=housing_axis_direction,
)
motor_placement = motor_placement.model_copy(
update={
"details": {
**motor_placement.details,
"motor_output_face_target_mm": desired_motor_output_face_axis_value,
"motor_output_face_axis_direction_xyz": housing_axis_direction,
**(
{
"motor_output_face_axis_value_mm": actual_output_face,
"motor_output_bore_face_axis_value_mm": actual_output_face,
}
if actual_output_face is not None
else {}
),
**(
{
"motor_output_bore_axis_min_placed_mm": actual_bore_range[0],
"motor_output_bore_axis_max_placed_mm": actual_bore_range[1],
}
if actual_bore_range is not None
else {}
),
}
}
)
placements = {
"housing": housing_placement,
"reducer": reducer_placement,
@@ -661,6 +740,30 @@ def run_joint_module(
prepare_joint_dir(out_dir)
requirement = load_joint_requirement(requirement_path)
motor_step = _resolve_path(requirement.motor_step, requirement_path=requirement_path)
split_motor_paths = {
component_id: _resolve_path(path_text, requirement_path=requirement_path)
for component_id, path_text in {
"motor_stator_housing": requirement.motor_stator_housing_step,
"motor_rotor_housing": requirement.motor_rotor_housing_step,
"motor_output_shaft": requirement.motor_output_shaft_step,
"motor_windings": requirement.motor_windings_step,
}.items()
if path_text is not None
}
motor_reference_assembly_step = (
_resolve_path(requirement.motor_reference_assembly_step, requirement_path=requirement_path)
if requirement.motor_reference_assembly_step
else None
)
if split_motor_paths and set(split_motor_paths) != {
"motor_stator_housing",
"motor_rotor_housing",
"motor_output_shaft",
"motor_windings",
}:
raise JointModuleError("split_motor_requires_stator_rotor_output_shaft_and_windings")
if split_motor_paths and motor_reference_assembly_step is None:
raise JointModuleError("split_motor_requires_motor_reference_assembly_step")
housing_step = _resolve_path(requirement.housing_step, requirement_path=requirement_path)
chip_step = (
_resolve_path(requirement.chip_step, requirement_path=requirement_path)
@@ -676,6 +779,13 @@ def run_joint_module(
if not motor_step.exists():
raise JointModuleError(f"missing_motor_step: {motor_step}")
for component_id, step_path in split_motor_paths.items():
if not step_path.exists():
raise JointModuleError(f"missing_split_motor_step: {component_id}: {step_path}")
if motor_reference_assembly_step is not None and not motor_reference_assembly_step.exists():
raise JointModuleError(
f"missing_motor_reference_assembly_step: {motor_reference_assembly_step}"
)
if not housing_step.exists():
raise JointModuleError(f"missing_housing_step: {housing_step}")
if chip_step is not None and not chip_step.exists():
@@ -690,7 +800,145 @@ def run_joint_module(
except JointAdapterError as exc:
raise JointModuleError(str(exc)) from exc
motor_metrics = infer_step_datum_axis(component_id="motor", step_path=motor_step, role="motor")
split_motor_relative_placements: dict[str, ComponentPlacement] = {}
split_motor_local_shapes: dict[str, object] = {}
split_motor_raw_metrics: dict[str, StepShapeMetrics] = {}
motor_shape = None
motor_metrics = infer_step_datum_axis(
component_id="motor", step_path=motor_step, role="motor"
)
if split_motor_paths:
try:
split_motor_relative_placements = infer_reference_placements(
reference_step=motor_reference_assembly_step,
component_steps=split_motor_paths,
anchor_component_id="motor_stator_housing",
)
except ReferenceAssemblyError as exc:
raise JointModuleError(str(exc)) from exc
raw_split_shapes: dict[str, object] = {}
for component_id, step_path in split_motor_paths.items():
raw_shape = read_step_shape(step_path)
raw_split_shapes[component_id] = raw_shape
split_motor_raw_metrics[component_id] = metrics_for_shape(
component_id=component_id,
step_path=step_path,
shape=raw_shape,
)
split_motor_local_shapes[component_id] = apply_transform(
raw_shape,
split_motor_relative_placements[component_id],
)
stator_axis_metrics = infer_step_datum_axis(
component_id="motor_stator_housing",
step_path=split_motor_paths["motor_stator_housing"],
role="motor",
)
if stator_axis_metrics.inferred_axis is None:
raise JointModuleError("split_motor_stator_axis_inference_failed")
shaft_local_bbox = metrics_for_shape(
component_id="motor_output_shaft",
step_path=split_motor_paths["motor_output_shaft"],
shape=split_motor_local_shapes["motor_output_shaft"],
).bbox
split_axis = stator_axis_metrics.inferred_axis
motor_axis = motor_metrics.inferred_axis
if motor_axis is None:
raise JointModuleError("motor_axis_inference_failed")
shaft_axis_min, shaft_axis_max = bbox_axis_range(
shaft_local_bbox, split_axis.direction_xyz
)
stator_axis_min, stator_axis_max = bbox_axis_range(
stator_axis_metrics.bbox,
split_axis.direction_xyz,
)
min_protrusion = stator_axis_min - shaft_axis_min
max_protrusion = shaft_axis_max - stator_axis_max
split_output_side = "axis_min" if min_protrusion >= max_protrusion else "axis_max"
motor_output_side = str(
motor_axis.details.get("motor_output_side", requirement.motor_output_side)
)
split_target_axis = (
motor_axis.direction_xyz
if split_output_side == motor_output_side
else vector_scale(motor_axis.direction_xyz, -1.0)
)
split_alignment_rotation = axis_angle_between(
split_axis.direction_xyz,
split_target_axis,
)
rotation_only = ComponentPlacement(
rotation_axis_angle_deg=split_alignment_rotation
)
rotated_shaft = apply_transform(
split_motor_local_shapes["motor_output_shaft"], rotation_only
)
rotated_shaft_bbox = metrics_for_shape(
component_id="motor_output_shaft",
step_path=split_motor_paths["motor_output_shaft"],
shape=rotated_shaft,
).bbox
rotated_shaft_min, rotated_shaft_max = bbox_axis_range(
rotated_shaft_bbox, motor_axis.direction_xyz
)
split_output_face = (
rotated_shaft_max
if motor_output_side == "axis_max"
else rotated_shaft_min
)
target_output_face = _motor_output_bore_face_axis_value(
motor_metrics=motor_metrics,
motor_output_side=motor_output_side,
motor_rotation=ComponentPlacement(),
target_axis_direction=motor_axis.direction_xyz,
)
if target_output_face is None:
target_output_face = (
bbox_axis_range(motor_metrics.bbox, motor_axis.direction_xyz)[1]
if motor_output_side == "axis_max"
else bbox_axis_range(motor_metrics.bbox, motor_axis.direction_xyz)[0]
)
rotated_split_axis_origin = apply_placement_to_point(
split_axis.origin_mm, rotation_only
)
split_alignment_translation = _axis_align_translation(
axis_origin_target=motor_axis.origin_mm,
axis_direction_target=motor_axis.direction_xyz,
rotated_axis_origin=rotated_split_axis_origin,
rotated_bbox_axis_value=split_output_face,
desired_bbox_axis_value=target_output_face,
)
split_alignment = ComponentPlacement(
translation_mm=split_alignment_translation,
rotation_axis_angle_deg=split_alignment_rotation,
source="aligned_to_legacy_motor_installation_frame",
details={
"policy": "preserve_existing_joint_motor_pose_and_replace_geometry_only",
"split_output_side": split_output_side,
"legacy_motor_output_side": motor_output_side,
"split_output_face_before_alignment_mm": split_output_face,
"legacy_output_face_target_mm": target_output_face,
},
)
aligned_relative_placements: dict[str, ComponentPlacement] = {}
aligned_local_shapes: dict[str, object] = {}
for component_id, occurrence_placement in split_motor_relative_placements.items():
aligned_placement = compose_placements(
outer=split_alignment,
inner=occurrence_placement,
source="split_motor_part_aligned_to_legacy_motor_frame",
details={
"motor_component_id": component_id,
"split_alignment": split_alignment.model_dump(mode="json"),
"reference_occurrence": occurrence_placement.model_dump(mode="json"),
},
)
aligned_relative_placements[component_id] = aligned_placement
aligned_local_shapes[component_id] = apply_transform(
raw_split_shapes[component_id], aligned_placement
)
split_motor_relative_placements = aligned_relative_placements
split_motor_local_shapes = aligned_local_shapes
housing_metrics = infer_step_datum_axis(component_id="housing", step_path=housing_step, role="housing")
chip_metrics = metrics_for_step("chip", chip_step) if chip_step is not None else None
reference_placements: dict[str, ComponentPlacement] | None = None
@@ -760,17 +1008,45 @@ def run_joint_module(
artifacts=artifacts,
adapter=adapter,
reference_placements=reference_placements,
motor_shape=motor_shape,
)
if split_motor_paths:
for component_id, relative_placement in split_motor_relative_placements.items():
placements[component_id] = compose_placements(
outer=placements["motor"],
inner=relative_placement,
source="split_motor_reference_pose_then_joint_placement",
details={
"motor_component_id": component_id,
"motor_reference_assembly_step": str(
motor_reference_assembly_step.resolve()
),
"relative_placement": relative_placement.model_dump(mode="json"),
"joint_motor_placement": placements["motor"].model_dump(mode="json"),
},
)
write_json(
out_dir / "joint_placement.json",
{component_id: placement.model_dump(mode="json") for component_id, placement in placements.items()},
)
placed_motor_shape = apply_transform(
motor_shape if motor_shape is not None else read_step_shape(motor_step),
placements["motor"],
)
placed_shapes = {
"motor": apply_transform(read_step_shape(motor_step), placements["motor"]),
"motor": placed_motor_shape,
"housing": apply_transform(read_step_shape(housing_step), placements["housing"]),
}
source_paths = {"motor": motor_step, "housing": housing_step}
joint_step_shapes = dict(placed_shapes)
if split_motor_paths:
joint_step_shapes.pop("motor")
for component_id, step_path in split_motor_paths.items():
joint_step_shapes[component_id] = apply_transform(
read_step_shape(step_path), placements[component_id]
)
source_paths[component_id] = step_path
if chip_step is not None and chip_metrics is not None:
chip_placement = placements.get("chip")
if chip_placement is None:
@@ -778,13 +1054,15 @@ def run_joint_module(
"chip_placement_requires_reference_assembly_step"
)
placed_shapes["chip"] = apply_transform(read_step_shape(chip_step), chip_placement)
joint_step_shapes["chip"] = placed_shapes["chip"]
source_paths["chip"] = chip_step
for component_id, shape in reducer_shapes.items():
placed_shapes[component_id] = apply_transform(shape, placements["reducer"])
joint_step_shapes[component_id] = placed_shapes[component_id]
source_paths[component_id] = reducer_step_paths[component_id]
joint_step_path = out_dir / "joint_module.step"
write_compound_step(placed_shapes, joint_step_path)
write_compound_step(joint_step_shapes, joint_step_path)
raw_external_by_id = {"motor": motor_metrics, "housing": housing_metrics}
if chip_metrics is not None:
@@ -795,7 +1073,11 @@ def run_joint_module(
placed_metrics = metrics_for_shape(
component_id=component_id,
step_path=source_paths[component_id],
shape=placed_shapes[component_id],
shape=(
placed_motor_shape
if component_id == "motor" and split_motor_paths
else placed_shapes[component_id]
),
)
axis = raw_metrics.inferred_axis
if axis is not None:
@@ -817,6 +1099,7 @@ def run_joint_module(
role="electronics_controller" if component_id == "chip" else component_id,
source_type="external_step",
source_step_path=str(source_paths[component_id].resolve()),
included_in_joint_step=not (component_id == "motor" and bool(split_motor_paths)),
placement=placements[component_id],
raw_metrics=raw_metrics,
placed_metrics=placed_metrics,
@@ -831,6 +1114,57 @@ def run_joint_module(
},
)
for component_id, step_path in split_motor_paths.items():
role = {
"motor_stator_housing": "motor_stator",
"motor_rotor_housing": "motor_rotor",
"motor_output_shaft": "motor_output",
"motor_windings": "motor_windings",
}[component_id]
placed_metrics = metrics_for_shape(
component_id=component_id,
step_path=step_path,
shape=joint_step_shapes[component_id],
)
aggregate_axis = motor_metrics.inferred_axis
if aggregate_axis is not None:
placed_metrics = placed_metrics.model_copy(
update={
"inferred_axis": _axis_with_placement(
origin=aggregate_axis.origin_mm,
direction=aggregate_axis.direction_xyz,
placement=placements["motor"],
confidence=aggregate_axis.confidence,
method="placed_split_motor_common_axis",
details={
"source": "split_motor_reference_assembly",
"motor_component_id": component_id,
},
)
}
)
joint_components[component_id] = JointComponentManifest(
component_id=component_id,
role=role,
source_type="external_step",
source_step_path=str(step_path.resolve()),
placement=placements[component_id],
raw_metrics=split_motor_raw_metrics[component_id],
placed_metrics=placed_metrics,
formula_ref=f"joint.split_motor.{component_id}",
details={
"placement_source": placements[component_id].source,
"motor_reference_assembly_step": str(
motor_reference_assembly_step.resolve()
),
"motion_group": (
"rotating"
if component_id in {"motor_rotor_housing", "motor_output_shaft"}
else "fixed"
),
},
)
for component_id, path in reducer_step_paths.items():
component = artifacts.manifest.components[component_id]
raw_shape = reducer_shapes[component_id]
@@ -867,21 +1201,48 @@ def run_joint_module(
},
)
interference_shapes = dict(placed_shapes)
if split_motor_paths:
interference_shapes.pop("motor", None)
interference_shapes.update(
{
component_id: joint_step_shapes[component_id]
for component_id in split_motor_paths
}
)
check_pairs = adapter.interference_check_pairs(
instance=artifacts.instance,
manifest=artifacts.manifest,
component_ids=set(placed_shapes),
)
if "chip" in placed_shapes:
if split_motor_paths:
check_pairs = [pair for pair in check_pairs if "motor" not in pair]
existing_pairs = {tuple(sorted(pair)) for pair in check_pairs}
for component_id in sorted(set(placed_shapes) - {"chip"}):
input_component_id = adapter.input_axis_component_id(
artifacts.instance, artifacts.manifest
)
external_targets = set(placed_shapes) - {"motor", "chip"}
for motor_component_id in sorted(split_motor_paths):
for target_id in sorted(external_targets):
if (
motor_component_id == "motor_output_shaft"
and target_id == input_component_id
):
continue
normalized = tuple(sorted((motor_component_id, target_id)))
if normalized not in existing_pairs:
check_pairs.append((motor_component_id, target_id))
existing_pairs.add(normalized)
if "chip" in interference_shapes:
existing_pairs = {tuple(sorted(pair)) for pair in check_pairs}
for component_id in sorted(set(interference_shapes) - {"chip"}):
pair = ("chip", component_id)
normalized = tuple(sorted(pair))
if normalized not in existing_pairs:
check_pairs.append(pair)
existing_pairs.add(normalized)
placed_metrics_raw, interference_results = detect_interferences_from_placed_shapes(
placed_shapes,
interference_shapes,
step_paths=source_paths,
check_pairs=check_pairs,
volume_tolerance_mm3=requirement.volume_tolerance_mm3,
@@ -907,6 +1268,45 @@ def run_joint_module(
interference_path = out_dir / "joint_interference_report.json"
requirement_hash = sha256_json(requirement.model_dump(mode="json"))
relations = adapter.joint_relations(artifacts.instance, artifacts.manifest)
if split_motor_paths:
relations.extend(
[
JointRelation(
relation_id="motor_stator_fixed_to_housing",
relation_type="fixed",
source_component_id="motor_stator_housing",
target_component_id="housing",
formula_ref="joint.split_motor.stator_fixed",
details={"semantic": "motor stator housing is fixed to the joint housing"},
),
JointRelation(
relation_id="motor_windings_fixed_to_stator",
relation_type="fixed",
source_component_id="motor_windings",
target_component_id="motor_stator_housing",
formula_ref="joint.split_motor.windings_fixed",
details={"semantic": "motor windings remain fixed with the stator"},
),
JointRelation(
relation_id="motor_rotor_rigid_to_output_shaft",
relation_type="rigid_rotation",
source_component_id="motor_rotor_housing",
target_component_id="motor_output_shaft",
formula_ref="joint.split_motor.rotor_output_rigid",
details={"semantic": "rotor housing and motor output shaft rotate together"},
),
JointRelation(
relation_id="motor_output_shaft_drives_reducer_input",
relation_type="input_coupling",
source_component_id="motor_output_shaft",
target_component_id=adapter.input_relation_target_component_id(
artifacts.instance, artifacts.manifest
),
formula_ref="joint.split_motor.output_to_reducer_input",
details={"semantic": "motor rotor speed equals reducer input speed"},
),
]
)
if "chip" in joint_components:
relations.append(
JointRelation(
@@ -945,7 +1345,18 @@ def run_joint_module(
"reducer_component_count": sum(
1 for component in joint_components.values() if component.source_type == "generated_reducer"
),
"external_component_count": len(raw_external_by_id),
"external_component_count": len(raw_external_by_id) + len(split_motor_paths),
"split_motor_enabled": bool(split_motor_paths),
"split_motor_fixed_components": [
component_id
for component_id in ("motor_stator_housing", "motor_windings")
if component_id in split_motor_paths
],
"split_motor_rotating_components": [
component_id
for component_id in ("motor_rotor_housing", "motor_output_shaft")
if component_id in split_motor_paths
],
"checked_interference_pair_count": len(check_pairs),
"positive_interference_count": sum(1 for item in interference_findings if item.interfering),
"excluded_reducer_shell_components": [
+10
View File
@@ -15,6 +15,11 @@ AxisAngle = tuple[float, float, float, float]
class JointPlatformProfile(StrictModel):
platform_id: str
motor_step: str
motor_reference_assembly_step: str | None = None
motor_stator_housing_step: str | None = None
motor_rotor_housing_step: str | None = None
motor_output_shaft_step: str | None = None
motor_windings_step: str | None = None
housing_step: str
chip_step: str | None = None
reference_assembly_step: str | None = None
@@ -25,6 +30,11 @@ class JointModuleRequirement(StrictModel):
platform_profile: str | None = None
platform_id: str | None = None
motor_step: str
motor_reference_assembly_step: str | None = None
motor_stator_housing_step: str | None = None
motor_rotor_housing_step: str | None = None
motor_output_shaft_step: str | None = None
motor_windings_step: str | None = None
housing_step: str
chip_step: str | None = None
reference_assembly_step: str | None = None
@@ -56,6 +56,47 @@ def _matrix_multiply(left: Matrix4, right: Matrix4) -> Matrix4:
) # type: ignore[return-value]
def _matrix_from_placement(placement: ComponentPlacement) -> Matrix4:
ax, ay, az, angle_deg = placement.rotation_axis_angle_deg
axis_length = math.sqrt(ax * ax + ay * ay + az * az)
if axis_length <= 1e-12 or abs(angle_deg) <= 1e-12:
rotation = (
(1.0, 0.0, 0.0),
(0.0, 1.0, 0.0),
(0.0, 0.0, 1.0),
)
else:
ux, uy, uz = ax / axis_length, ay / axis_length, az / axis_length
angle = math.radians(angle_deg)
cosine = math.cos(angle)
sine = math.sin(angle)
one_minus_cosine = 1.0 - cosine
rotation = (
(
cosine + ux * ux * one_minus_cosine,
ux * uy * one_minus_cosine - uz * sine,
ux * uz * one_minus_cosine + uy * sine,
),
(
uy * ux * one_minus_cosine + uz * sine,
cosine + uy * uy * one_minus_cosine,
uy * uz * one_minus_cosine - ux * sine,
),
(
uz * ux * one_minus_cosine - uy * sine,
uz * uy * one_minus_cosine + ux * sine,
cosine + uz * uz * one_minus_cosine,
),
)
tx, ty, tz = placement.translation_mm
return (
(*rotation[0], float(tx)),
(*rotation[1], float(ty)),
(*rotation[2], float(tz)),
(0.0, 0.0, 0.0, 1.0),
)
def _axis_angle_from_matrix(matrix: Matrix4) -> tuple[float, float, float, float]:
trace = matrix[0][0] + matrix[1][1] + matrix[2][2]
cosine = max(-1.0, min(1.0, (trace - 1.0) / 2.0))
@@ -105,6 +146,23 @@ def _placement_from_matrix(
)
def compose_placements(
*,
outer: ComponentPlacement,
inner: ComponentPlacement,
source: str = "composed_component_placement",
details: dict[str, object] | None = None,
) -> ComponentPlacement:
"""Compose two rigid placements so the inner transform is applied first."""
matrix = _matrix_multiply(_matrix_from_placement(outer), _matrix_from_placement(inner))
return ComponentPlacement(
translation_mm=(matrix[0][3], matrix[1][3], matrix[2][3]),
rotation_axis_angle_deg=_axis_angle_from_matrix(matrix),
source=source,
details=details or {},
)
def infer_reference_placements(
*,
reference_step: Path,
+13 -6
View File
@@ -220,12 +220,8 @@ def apply_transform(shape, placement: ComponentPlacement):
def write_compound_step(shapes: dict[str, object], path: Path) -> None:
if not shapes:
raise OcpInterferenceError("cannot_write_empty_joint_compound")
compound = compound_shape(shapes)
ocp = _import_ocp()
compound = ocp["TopoDS_Compound"]()
builder = ocp["BRep_Builder"]()
builder.MakeCompound(compound)
for shape in shapes.values():
builder.Add(compound, shape)
path.parent.mkdir(parents=True, exist_ok=True)
writer = ocp["STEPControl_Writer"]()
writer.Transfer(compound, ocp["STEPControl_AsIs"])
@@ -234,6 +230,18 @@ def write_compound_step(shapes: dict[str, object], path: Path) -> None:
raise OcpInterferenceError(f"step_write_failed: {path}")
def compound_shape(shapes: dict[str, object]):
if not shapes:
raise OcpInterferenceError("cannot_build_empty_joint_compound")
ocp = _import_ocp()
compound = ocp["TopoDS_Compound"]()
builder = ocp["BRep_Builder"]()
builder.MakeCompound(compound)
for shape in shapes.values():
builder.Add(compound, shape)
return compound
def detect_interferences_from_placed_shapes(
shapes: dict[str, object],
*,
@@ -288,4 +296,3 @@ def detect_interferences_from_placed_shapes(
)
)
return metrics, results
+313 -5
View File
@@ -126,6 +126,8 @@ def _export_stl(shape, path: Path, *, linear_deflection_mm: float) -> None:
writer = ocp["StlAPI_Writer"]()
if hasattr(writer, "SetASCIIMode"):
writer.SetASCIIMode(False)
elif hasattr(writer, "ASCIIMode"):
writer.ASCIIMode = False
ok = writer.Write(shape, str(path))
if ok is False:
raise UrdfExportError(f"stl_write_failed: {path}")
@@ -139,6 +141,10 @@ def _export_meshes(
) -> dict[str, str]:
mesh_paths: dict[str, str] = {}
mesh_dir.mkdir(parents=True, exist_ok=True)
# This directory is a generated artifact. Remove meshes from an earlier
# motor representation so the viewer cannot accidentally reuse them.
for stale_mesh in mesh_dir.glob("*.stl"):
stale_mesh.unlink()
for component_id, component in manifest.components.items():
if not component.included_in_joint_step:
continue
@@ -171,7 +177,7 @@ def _add_materials(robot: ET.Element) -> None:
def _material_for(component_id: str, role: str) -> str:
if component_id == "housing":
return "housing_gray"
if component_id == "motor":
if component_id == "motor" or component_id.startswith("motor_"):
return "motor_dark"
if component_id == "ring":
return "ring_blue"
@@ -246,6 +252,73 @@ def _mesh_uri(package_name: str, component_id: str) -> str:
return f"package://{package_name}/meshes/{component_id}.stl"
def _add_motor_links(
robot: ET.Element,
*,
manifest: JointModuleManifest,
package_name: str,
input_joint_name: str,
axis: tuple[float, float, float],
) -> bool:
split_ids = {
"motor_stator_housing",
"motor_rotor_housing",
"motor_output_shaft",
"motor_windings",
}
if not split_ids <= set(manifest.components):
return False
for component_id in ("motor_stator_housing", "motor_windings"):
component = manifest.components[component_id]
link_name = f"{component_id}_link"
_add_link(
robot,
link_name=link_name,
mesh_filename=_mesh_uri(package_name, component_id),
material=_material_for(component_id, component.role),
)
_add_joint(
robot,
name=f"base_to_{component_id}",
joint_type="fixed",
parent="base_link",
child=link_name,
)
rotor = manifest.components["motor_rotor_housing"]
_add_link(
robot,
link_name="motor_rotor_housing_link",
mesh_filename=_mesh_uri(package_name, "motor_rotor_housing"),
material=_material_for("motor_rotor_housing", rotor.role),
)
_add_joint(
robot,
name="motor_rotor_joint",
joint_type="continuous",
parent="base_link",
child="motor_rotor_housing_link",
axis=axis,
mimic=(input_joint_name, 1.0, 0.0),
)
output_shaft = manifest.components["motor_output_shaft"]
_add_link(
robot,
link_name="motor_output_shaft_link",
mesh_filename=_mesh_uri(package_name, "motor_output_shaft"),
material=_material_for("motor_output_shaft", output_shaft.role),
)
_add_joint(
robot,
name="motor_rotor_to_output_shaft",
joint_type="fixed",
parent="motor_rotor_housing_link",
child="motor_output_shaft_link",
)
return True
def _visual_origin_for_link(
manifest: JointModuleManifest,
component_id: str,
@@ -255,6 +328,52 @@ def _visual_origin_for_link(
return tuple(-_m(value) for value in link_frame_origin_mm)
def _adapt_reducer_urdf_to_joint_frame(
root: ET.Element,
*,
manifest: JointModuleManifest,
axis: tuple[float, float, float],
) -> None:
"""Retarget raw reducer motion frames to the placed joint-module frame."""
for joint in root.findall("joint"):
if joint.get("type") not in {"continuous", "revolute"}:
continue
axis_node = joint.find("axis")
if axis_node is not None:
axis_node.set("xyz", _fmt(axis))
for component_id, component in manifest.components.items():
if not (
component_id.startswith("s1_planet_")
or component_id.startswith("s2_planet_")
):
continue
parts = component_id.rsplit("_", 1)
if len(parts) != 2 or not parts[1].isdigit():
continue
stage_id = component_id.split("_", 1)[0]
index = int(parts[1])
center_mm = _axis_origin(component)
orbit_joint = root.find(
f"./joint[@name='{stage_id}_carrier_to_planet_{index}_orbit']"
)
if orbit_joint is not None:
origin = orbit_joint.find("origin")
if origin is not None:
origin.set("xyz", _fmt(tuple(_m(value) for value in center_mm)))
planet_link = root.find(f"./link[@name='{component_id}_link']")
if planet_link is not None:
visual_origin = _visual_origin_for_link(
manifest,
component_id,
link_frame_origin_mm=center_mm,
)
for origin in planet_link.findall("./visual/origin") + planet_link.findall(
"./collision/origin"
):
origin.set("xyz", _fmt(visual_origin))
def build_urdf_xml(
*,
manifest: JointModuleManifest,
@@ -274,8 +393,19 @@ def build_urdf_xml(
robot_name=robot_name,
)
root = ET.fromstring(reducer_xml)
input_joint_name = str(motion.get("input_joint") or "sun_input_joint")
axis_component = manifest.components.get("motor")
axis = _axis_direction(axis_component) if axis_component is not None else (0.0, 0.0, 1.0)
_adapt_reducer_urdf_to_joint_frame(root, manifest=manifest, axis=axis)
split_motor_added = _add_motor_links(
root,
manifest=manifest,
package_name=package_name,
input_joint_name=input_joint_name,
axis=axis,
)
motor = manifest.components.get("motor")
if motor is not None:
if motor is not None and not split_motor_added:
_add_link(
root,
link_name="motor_link",
@@ -302,7 +432,15 @@ def build_urdf_xml(
carrier_multiplier = float(motion["carrier_joint_multiplier"])
planet_spin_multiplier = float(motion["planet_spin_joint_multiplier"])
fixed_base_components = ["housing", "motor", "ring", "output_bearing_1", "output_bearing_2"]
split_motor_enabled = {
"motor_stator_housing",
"motor_rotor_housing",
"motor_output_shaft",
"motor_windings",
} <= set(manifest.components)
fixed_base_components = ["housing", "ring", "output_bearing_1", "output_bearing_2"]
if not split_motor_enabled:
fixed_base_components.insert(1, "motor")
for component_id in fixed_base_components:
component = manifest.components.get(component_id)
if component is None:
@@ -337,6 +475,13 @@ def build_urdf_xml(
child="sun_link",
axis=axis,
)
_add_motor_links(
robot,
manifest=manifest,
package_name=package_name,
input_joint_name="sun_input_joint",
axis=axis,
)
for component_id in ["sun_input_shaft", "sun_input_key"]:
component = manifest.components.get(component_id)
if component is None:
@@ -484,12 +629,148 @@ def _mesh_component_from_uri(uri: str) -> str | None:
return uri.removeprefix(prefix).removesuffix(".stl")
def _parse_xyz(value: str | None) -> tuple[float, float, float] | None:
if not value:
return None
try:
parts = tuple(float(item) for item in value.split())
except ValueError:
return None
return parts if len(parts) == 3 else None
def _joint_frame_validation_checks(
*,
root: ET.Element,
manifest: JointModuleManifest,
motion: dict[str, float | str | dict[str, float]],
) -> list[UrdfValidationCheck]:
checks: list[UrdfValidationCheck] = []
def add(code: str, passed: bool, message: str, actual=None, expected=None) -> None:
checks.append(
UrdfValidationCheck(
code=code,
passed=bool(passed),
message=message,
actual=actual,
expected=expected,
)
)
motor = manifest.components.get("motor")
expected_axis = _axis_direction(motor) if motor is not None else None
moving_axes: dict[str, tuple[float, float, float] | None] = {}
if expected_axis is not None:
for joint in _joint_elements(root):
if joint.get("type") not in {"continuous", "revolute"}:
continue
axis_node = joint.find("axis")
moving_axes[str(joint.get("name"))] = _parse_xyz(
None if axis_node is None else axis_node.get("xyz")
)
bad_axes = {
name: axis
for name, axis in moving_axes.items()
if axis is None
or abs(abs(sum(a * b for a, b in zip(axis, expected_axis))) - 1.0) > 1e-9
}
add(
"joint_moving_axes_match_placed_assembly_axis",
bool(moving_axes) and not bad_axes,
"all moving URDF joints use the placed joint-module axis",
actual=bad_axes,
expected={"axis_parallel_to": expected_axis},
)
split_ids = {
"motor_stator_housing",
"motor_rotor_housing",
"motor_output_shaft",
"motor_windings",
}
if split_ids <= set(manifest.components):
expected_links = {f"{component_id}_link" for component_id in split_ids}
found_links = {str(link.get("name")) for link in root.findall("link")}
rotor_joint = root.find("./joint[@name='motor_rotor_joint']")
mimic = None if rotor_joint is None else rotor_joint.find("mimic")
input_joint = str(motion.get("input_joint") or "")
multiplier = None
if mimic is not None:
try:
multiplier = float(mimic.get("multiplier", "nan"))
except ValueError:
multiplier = None
add(
"split_motor_links_and_drive_present",
expected_links <= found_links
and rotor_joint is not None
and rotor_joint.get("type") == "continuous"
and mimic is not None
and mimic.get("joint") == input_joint
and multiplier is not None
and abs(multiplier - 1.0) <= 1e-12,
"stator and windings are fixed while rotor housing and output shaft follow the reducer input 1:1",
actual={
"missing_links": sorted(expected_links - found_links),
"rotor_joint_type": None if rotor_joint is None else rotor_joint.get("type"),
"mimic_joint": None if mimic is None else mimic.get("joint"),
"mimic_multiplier": multiplier,
},
expected={"mimic_joint": input_joint, "mimic_multiplier": 1.0},
)
planet_frame_errors: list[dict[str, object]] = []
for component_id, component in manifest.components.items():
if not component_id.startswith(("s1_planet_", "s2_planet_")):
continue
suffix = component_id.rsplit("_", 1)[-1]
if not suffix.isdigit():
continue
stage_id = component_id.split("_", 1)[0]
orbit_joint = root.find(
f"./joint[@name='{stage_id}_carrier_to_planet_{int(suffix)}_orbit']"
)
planet_link = root.find(f"./link[@name='{component_id}_link']")
origin_node = None if orbit_joint is None else orbit_joint.find("origin")
visual_node = None if planet_link is None else planet_link.find("./visual/origin")
actual_origin = _parse_xyz(None if origin_node is None else origin_node.get("xyz"))
actual_visual = _parse_xyz(None if visual_node is None else visual_node.get("xyz"))
center_m = tuple(_m(value) for value in _axis_origin(component))
expected_visual = tuple(-value for value in center_m)
if (
actual_origin is None
or actual_visual is None
or any(abs(a - b) > 1e-9 for a, b in zip(actual_origin, center_m))
or any(abs(a - b) > 1e-9 for a, b in zip(actual_visual, expected_visual))
):
planet_frame_errors.append(
{
"component_id": component_id,
"orbit_origin": actual_origin,
"expected_orbit_origin": center_m,
"visual_origin": actual_visual,
"expected_visual_origin": expected_visual,
}
)
if any(component_id.startswith(("s1_planet_", "s2_planet_")) for component_id in manifest.components):
add(
"planet_orbit_frames_match_placed_gear_centers",
not planet_frame_errors,
"planet orbit joints and mesh-local origins use the placed assembly centers",
actual=planet_frame_errors,
expected=[],
)
return checks
def validate_urdf_export(
*,
urdf_path: Path,
mesh_files: dict[str, str],
motion: dict[str, float | str | dict[str, float]],
reducer_instance: ReducerInstance,
joint_manifest: JointModuleManifest | None = None,
out_path: Path | None = None,
) -> UrdfValidationReport:
if reducer_instance.topology_family != "simple_2k_h":
@@ -510,12 +791,30 @@ def validate_urdf_export(
)
for item in reducer_validation["checks"]
]
if joint_manifest is not None:
try:
root = ET.parse(urdf_path).getroot()
except Exception: # noqa: BLE001
root = None
if root is not None:
checks.extend(
_joint_frame_validation_checks(
root=root,
manifest=joint_manifest,
motion=motion,
)
)
summary = {
"total": len(checks),
"passed": sum(1 for item in checks if item.passed),
"failed": sum(1 for item in checks if not item.passed),
}
report = UrdfValidationReport(
run_id=reducer_instance.run_id,
overall_passed=bool(reducer_validation["overall_passed"]),
overall_passed=summary["failed"] == 0,
generated_at=utc_now_iso(),
checks=checks,
summary=dict(reducer_validation["summary"]),
summary=summary,
)
if out_path:
write_model_json(out_path, report)
@@ -540,6 +839,14 @@ def validate_urdf_export(
root = None
check("urdf_xml_parse", False, f"URDF XML parse failed: {exc}")
if root is not None:
if joint_manifest is not None:
checks.extend(
_joint_frame_validation_checks(
root=root,
manifest=joint_manifest,
motion=motion,
)
)
check("urdf_robot_root", root.tag == "robot" and bool(root.get("name")), "URDF root is a named robot")
link_names = [item.get("name") for item in root.findall("link")]
joint_names = [item.get("name") for item in _joint_elements(root)]
@@ -664,6 +971,7 @@ def export_urdf(
mesh_files=mesh_files,
motion=motion,
reducer_instance=reducer_instance,
joint_manifest=manifest,
out_path=validation_path,
)
if not validation.overall_passed:
+58 -7
View File
@@ -110,13 +110,14 @@ def relation_context_for_pair(component_a: str, component_b: str) -> str | None:
if "chip" in pair:
other = component_b if component_a == "chip" else component_a
return f"reference-mounted chip clearance around {other}"
if pair == {"motor", "housing"}:
motor_ids = {item for item in pair if item == "motor" or item.startswith("motor_")}
if motor_ids and "housing" in pair:
return "external motor installed relative to external housing"
if "housing" in pair:
other = component_b if component_a == "housing" else component_a
return f"external housing clearance around reducer component {other}"
if "motor" in pair:
other = component_b if component_a == "motor" else component_a
if motor_ids:
other = next(item for item in pair if item not in motor_ids)
return f"motor-to-reducer input-side clearance around {other}"
return None
@@ -125,15 +126,16 @@ def classify_joint_interference(component_a: str, component_b: str) -> str:
pair = {component_a, component_b}
if "chip" in pair:
return "electronics_mount_collision"
if pair == {"motor", "housing"}:
motor_ids = {item for item in pair if item == "motor" or item.startswith("motor_")}
if motor_ids and "housing" in pair:
return "motor_not_aligned"
if "housing" in pair:
other = component_b if component_a == "housing" else component_a
if other in {"ring", "carrier", "output_shaft"} or other.startswith(("planet_", "planet")):
return "housing_cavity_too_small"
return "reducer_outer_diameter_too_large"
if "motor" in pair:
other = component_b if component_a == "motor" else component_a
if motor_ids:
other = next(item for item in pair if item not in motor_ids)
if other in {"sun", "sun_input_shaft", "sun_input_key"}:
return "input_coupling_collision"
return "wrong_axial_offset"
@@ -561,8 +563,17 @@ def build_joint_module_validation_report(
motor_placement = manifest.placements.get("motor")
if adapter.input_insertion_required(reducer_instance, manifest) and input_shaft and motor and motor_placement:
insertion_axis = input_axis["direction_xyz"]
recorded_input_axis = motor_placement.details.get("input_axis_direction_xyz")
recorded_face_axis = motor_placement.details.get(
"motor_output_face_axis_direction_xyz"
)
if (
isinstance(recorded_face_axis, (list, tuple))
and len(recorded_face_axis) == 3
and all(isinstance(value, (int, float)) for value in recorded_face_axis)
):
insertion_axis = tuple(float(value) for value in recorded_face_axis)
recorded_input_axis = motor_placement.details.get("input_axis_direction_xyz")
if recorded_face_axis is None and (
isinstance(recorded_input_axis, (list, tuple))
and len(recorded_input_axis) == 3
and all(isinstance(value, (int, float)) for value in recorded_input_axis)
@@ -587,6 +598,20 @@ def build_joint_module_validation_report(
if shaft_points_along_motor_axis
else max(0.0, float(output_face) - shaft_min)
)
placed_bore_min = motor_placement.details.get(
"motor_output_bore_axis_min_placed_mm"
)
placed_bore_max = motor_placement.details.get(
"motor_output_bore_axis_max_placed_mm"
)
if isinstance(placed_bore_min, (int, float)) and isinstance(
placed_bore_max, (int, float)
):
insertion_depth = max(
0.0,
min(shaft_max, float(placed_bore_max))
- max(shaft_min, float(placed_bore_min)),
)
recorded_shaft_min = motor_placement.details.get("input_axis_component_axis_min_mm")
recorded_shaft_max = motor_placement.details.get("input_axis_component_axis_max_mm")
recorded_depth = motor_placement.details.get("input_shaft_insertion_depth_mm")
@@ -641,6 +666,32 @@ def build_joint_module_validation_report(
"shaft_radius_mm": "less than inferred motor output bore radius when available",
},
)
split_output = manifest.components.get("motor_output_shaft")
if split_output is not None:
split_min, split_max = _bbox_axis_range(
split_output.placed_metrics.bbox, insertion_axis
)
split_overlap = max(
0.0,
min(shaft_max, split_max) - max(shaft_min, split_min),
)
_check(
checks,
section="geometry",
code="split_motor_output_shaft_engages_reducer_input",
passed=split_overlap >= requirement.input_shaft_insertion_depth_mm,
message="split motor output shaft occupies the original motor coupling span and engages the reducer input",
actual={
"motor_output_shaft_axis_min_mm": split_min,
"motor_output_shaft_axis_max_mm": split_max,
"reducer_input_axis_min_mm": shaft_min,
"reducer_input_axis_max_mm": shaft_max,
"axial_overlap_mm": split_overlap,
},
expected={
"axial_overlap_mm": f">= {requirement.input_shaft_insertion_depth_mm}"
},
)
if reducer_instance.topology_family == "simple_2k_h":
_check(
checks,
+179
View File
@@ -0,0 +1,179 @@
from __future__ import annotations
import json
import math
from pathlib import Path
from .kinematics import solve_simple_2kh
from .models import BoundaryCondition, ReducerParameters
BACKEND_ROOT = Path(__file__).resolve().parents[1]
KNOWLEDGE_ROOT = BACKEND_ROOT / "knowledge"
def _read_knowledge(relative_path: str) -> dict[str, object]:
path = KNOWLEDGE_ROOT / relative_path
return json.loads(path.read_text(encoding="utf-8"))
def _geometry(
*,
z_sun: int,
z_planet: int,
z_ring: int,
planet_count: int,
module_mm: float,
) -> dict[str, float]:
center_distance = module_mm * (z_sun + z_planet) / 2.0
planet_outer_diameter = module_mm * (z_planet + 2.0)
neighbor_distance = 2.0 * center_distance * math.sin(math.pi / planet_count)
ring_outer_diameter = module_mm * z_ring + 2.0 * module_mm * 1.25 + 5.0
return {
"centerDistanceMm": center_distance,
"ringOuterDiameterMm": ring_outer_diameter,
"planetNeighborClearanceMm": neighbor_distance - planet_outer_diameter,
}
def _variant(
*,
variant_id: str,
active_gear: str,
active_from: int,
active_to: int,
z_sun: int,
z_planet: int,
planet_count: int,
module_mm: float,
) -> dict[str, object] | None:
z_ring = z_sun + 2 * z_planet
if min(z_sun, z_planet, z_ring) <= 17:
return None
if (z_sun + z_ring) % planet_count != 0:
return None
geometry = _geometry(
z_sun=z_sun,
z_planet=z_planet,
z_ring=z_ring,
planet_count=planet_count,
module_mm=module_mm,
)
if geometry["ringOuterDiameterMm"] > 100.0:
return None
if geometry["planetNeighborClearanceMm"] < 0.3:
return None
parameters = ReducerParameters(
z_sun=z_sun,
z_planet=z_planet,
z_ring=z_ring,
planet_count=planet_count,
module_mm=module_mm,
pressure_angle_deg=20.0,
face_width_mm=8.0,
backlash_mm=0.03,
addendum_coeff=1.0,
clearance_coeff=0.25,
ring_rim_thickness_mm=2.5,
tooth_form="spur",
)
solution = solve_simple_2kh(
run_id=f"knowledge_{variant_id}",
parameters=parameters,
boundary=BoundaryCondition(input="sun", fixed="ring", output="carrier"),
)
speeds = {key: round(value, 12) for key, value in solution.speeds.items()}
relative_planet_speed = speeds["planet"] - speeds["carrier"]
dependent_changes = []
baseline_ring = 130
if z_ring != baseline_ring:
dependent_changes.append(
{
"gear": "ring",
"fromTeeth": baseline_ring,
"toTeeth": z_ring,
"reason": "保持 z_ring = z_sun + 2×z_planet 的啮合闭合条件",
}
)
return {
"id": variant_id,
"origin": "knowledge_computed_candidate",
"knowledgeGenerated": True,
"topologyFamily": "simple_2k_h",
"title": f"更换{ '太阳轮' if active_gear == 'sun' else '行星轮' }{active_from}T → {active_to}T",
"activeChange": {
"gear": active_gear,
"fromTeeth": active_from,
"toTeeth": active_to,
},
"dependentChanges": dependent_changes,
"parameters": {
"zSun": z_sun,
"zPlanet": z_planet,
"zRing": z_ring,
"planetCount": planet_count,
"moduleMm": module_mm,
"pressureAngleDeg": 20.0,
},
"ratio": round(abs(solution.ratio), 9),
"direction": solution.direction,
"speeds": speeds,
"relativeSpeeds": {"planetAboutCarrier": round(relative_planet_speed, 12)},
"geometry": {key: round(value, 6) for key, value in geometry.items()},
"constraints": {
"toothClosure": z_ring == z_sun + 2 * z_planet,
"equalSpacing": (z_sun + z_ring) % planet_count == 0,
"outerDiameter": geometry["ringOuterDiameterMm"] <= 100.0,
"planetClearance": geometry["planetNeighborClearanceMm"] >= 0.3,
"kinematicResidual": solution.residual_max_abs,
},
"status": "参数与运动学计算通过",
"cadGenerated": False,
"validationLevel": "parameter_and_kinematics_only",
}
def generate_single_gear_variants() -> dict[str, object]:
mechanism = _read_knowledge("mechanisms/planetary/simple_2k_h.json")
composition = _read_knowledge("compositions/serial/two_simple_2kh.json")
capabilities = _read_knowledge("capabilities/registry.json")
capability_ids = {entry["id"] for entry in capabilities["entries"]}
required_capabilities = {
"capability.parameter_solver.simple_2kh",
"capability.kinematics.graph",
}
if not required_capabilities <= capability_ids:
missing = sorted(required_capabilities - capability_ids)
raise ValueError(f"knowledge_capability_missing: {missing}")
baseline = {"z_sun": 20, "z_planet": 55, "z_ring": 130, "planet_count": 3, "module_mm": 0.5}
proposals = []
for teeth in [23, 26, 29, 32, 35]:
proposals.append((f"sun_{teeth}", "sun", 20, teeth, teeth, 55))
for teeth in [43, 46, 49, 52, 58, 61, 64, 67]:
proposals.append((f"planet_{teeth}", "planet", 55, teeth, 20, teeth))
variants = []
for variant_id, active_gear, active_from, active_to, z_sun, z_planet in proposals:
item = _variant(
variant_id=variant_id,
active_gear=active_gear,
active_from=active_from,
active_to=active_to,
z_sun=z_sun,
z_planet=z_planet,
planet_count=baseline["planet_count"],
module_mm=baseline["module_mm"],
)
if item is not None:
variants.append(item)
return {
"enabled": True,
"mode": "optional_knowledge_adapter",
"baseline": baseline,
"candidateCount": len(variants),
"candidates": variants,
"knowledgeRefs": [mechanism["id"], composition["id"], *sorted(required_capabilities)],
"notice": "候选已通过参数和运动学计算;尚未生成 CAD,不等同于工程验证成品。",
}
+144 -1
View File
@@ -337,6 +337,129 @@ def _project_summary(run_name: str) -> dict[str, object]:
}
def _design_catalog() -> list[dict[str, object]]:
title_by_topology = {
"simple_2k_h": "单级 2K-H 关节模组",
"simple_2k_h_cascade": "两级 2K-H 串联关节",
"ferguson_wolfrom": "高传动比复合行星减速器",
}
designs: list[dict[str, object]] = []
for run_dir in sorted(RUN_ROOT.iterdir() if RUN_ROOT.exists() else []):
if not run_dir.is_dir():
continue
urdf_dir = run_dir / "exports" / "urdf"
urdf_path = urdf_dir / "joint_module.urdf"
kind = "joint_module"
if not urdf_path.exists():
urdf_path = urdf_dir / "reducer.urdf"
kind = "reducer"
if not urdf_path.exists():
continue
formula_candidates = [
run_dir / "joint" / "reducer" / "formula" / "formula_instance.json",
run_dir / "reducer" / "formula" / "formula_instance.json",
run_dir / "formula" / "formula_instance.json",
]
formula_path = next((path for path in formula_candidates if path.exists()), None)
formula = read_json(formula_path) if formula_path else {}
topology = str(formula.get("topology_family") or "planetary_reducer")
derived = formula.get("derived") if isinstance(formula.get("derived"), dict) else {}
parameters = formula.get("parameters") if isinstance(formula.get("parameters"), dict) else {}
motion_path = urdf_dir / ("joint_motion_demo.json" if kind == "joint_module" else "reducer_motion_demo.json")
motion = read_json(motion_path) if motion_path.exists() else {}
ratio = motion.get("ratio") or derived.get("ratio")
task_manifest = read_task_manifest(run_dir.name)
request = str((task_manifest or {}).get("request") or "")
joint_validation_path = run_dir / "joint_module_validation_report.json"
reducer_validation_candidates = [
run_dir / "reducer" / "validation" / "validation_report.json",
run_dir / "joint" / "reducer" / "validation" / "validation_report.json",
run_dir / "validation" / "validation_report.json",
]
reducer_validation_path = next(
(path for path in reducer_validation_candidates if path.exists()),
None,
)
validation_path = (
joint_validation_path
if kind == "joint_module" and joint_validation_path.exists()
else reducer_validation_path
)
validation = read_json(validation_path) if validation_path else {}
validation_summary = (
validation.get("summary")
if isinstance(validation.get("summary"), dict)
else {}
)
failed_checks = [
{
"code": str(check.get("code") or "validation_failed"),
"message": str(check.get("message") or "验证未通过"),
}
for check in validation.get("checks", [])
if isinstance(check, dict) and check.get("passed") is False
]
physically_verified = bool(validation) and validation.get("overall_passed") is True
source = "api_task" if task_manifest else "project_preset_run"
is_test_artifact = bool(task_manifest) and bool(
re.search(r"(?:集成验证|测试|\btest\b)", request, flags=re.IGNORECASE)
)
root = ET.fromstring(urdf_path.read_text(encoding="utf-8"))
link_names = [str(node.get("name") or "") for node in root.findall("link")]
continuous_joints = [
str(node.get("name") or "")
for node in root.findall("joint")
if node.get("type") in {"continuous", "revolute"}
]
title = title_by_topology.get(topology, "行星传动方案")
if task_manifest and request:
title = request
designs.append({
"id": run_dir.name,
"runId": run_dir.name,
"kind": kind,
"topology": topology,
"title": title,
"request": request,
"ratio": ratio,
"parameters": parameters,
"derived": derived,
"urdfUrl": f"/api/design-assets/{run_dir.name}/{urdf_path.name}",
"assetBase": f"/api/design-assets/{run_dir.name}",
"motion": motion,
"linkCount": len(link_names),
"movingJointCount": len(continuous_joints),
"motorIncluded": any(
link_name == "motor_link" or link_name.startswith("motor_")
for link_name in link_names
),
# A successful generation task only means that files were written.
# Catalog eligibility is based on the relevant physical validation
# report, never on the presence of a URDF or on task status alone.
"status": "verified" if physically_verified else "invalid",
"eligible": physically_verified,
"source": source,
"sourceLabel": "CAD API 测试任务" if is_test_artifact else (
"CAD API 生成" if task_manifest else "项目预设生成"
),
"catalogClass": "test_artifact" if is_test_artifact else "design",
"validationScope": "关节模组" if kind == "joint_module" else "减速器",
"validationSummary": validation_summary,
"failedChecks": failed_checks,
"validationReport": str(validation_path.relative_to(run_dir)) if validation_path else "",
})
preferred = {
"frontend_split_motor_7nm": 0,
"frontend_split_motor_7nm_cascade": 1,
"ferguson_wolfrom_ratio_531p25_spur": 2,
}
return sorted(designs, key=lambda item: (preferred.get(str(item["id"]), 10), str(item["id"])))
def _artifact_record(task_id: str, name: str, path: Path, role: str, kind: str) -> dict[str, object]:
return {
"name": name,
@@ -811,6 +934,21 @@ class ToolchainHandler(BaseHTTPRequestHandler):
if parsed.path == "/api/tasks":
_send_json(self, 200, {"tasks": list_tasks()})
return
if parsed.path == "/api/designs":
_send_json(self, 200, {"designs": _design_catalog()})
return
if parsed.path.startswith("/api/design-assets/"):
parts = parsed.path.split("/")
if len(parts) < 5:
_send_json(self, 400, {"error": "invalid_design_asset_path"})
return
run_name = safe_task_id(parts[3])
relative_path = Path(unquote("/".join(parts[4:])))
if relative_path.is_absolute() or ".." in relative_path.parts:
_send_json(self, 400, {"error": "invalid_design_asset_path"})
return
_send_file(self, RUN_ROOT / run_name / "exports" / "urdf" / relative_path)
return
if parsed.path.startswith("/api/tasks/"):
parts = parsed.path.split("/")
if len(parts) >= 4:
@@ -854,7 +992,12 @@ class ToolchainHandler(BaseHTTPRequestHandler):
kind = str(payload.get("kind") or "joint_module")
task = ensure_task(str(payload.get("taskId") or "") or None, request=str(payload.get("request") or "CAD generation"))
task_id = task["taskId"]
update_task(task_id, {"status": "running", "kind": kind, "request": payload.get("request") or task["manifest"].get("request")})
update_task(task_id, {
"status": "running",
"kind": kind,
"request": payload.get("request") or task["manifest"].get("request"),
"planner": payload.get("planner") if isinstance(payload.get("planner"), dict) else {},
})
try:
result = generate_reducer_task(task_id, payload) if kind == "reducer" else generate_joint_task(task_id, payload)
_send_json(self, 200, result)
+8 -2
View File
@@ -55,6 +55,7 @@ ACTUATOR_HOUSING_STEP = ROOT / "input" / "assets" / "housings" / "actuator_v2_kn
CHIP_STEP = ROOT / "input" / "assets" / "chip" / "chip.step"
REFERENCE_ASSEMBLY_STEP = ROOT / "input" / "assets" / "references" / "actuator_v2_knee_abd_reference.step"
FIXED_PLATFORM_PROFILE = ROOT / "input" / "assets" / "platforms" / "actuator_v2_fixed_platform.json"
SPLIT_MOTOR_DIR = ROOT / "input" / "assets" / "motors" / "3500_split"
FERGUSON_REDUCER_REQ = ROOT / "input" / "requirements" / "reducers" / "ferguson_wolfrom" / "ferguson_wolfrom_ratio_531p25_spur.json"
CASCADE_TEMPLATE = ROOT / "src" / "configurations" / "simple_2k_h_cascade" / "topology.template.json"
FERGUSON_TEMPLATE = ROOT / "src" / "configurations" / "ferguson_wolfrom" / "topology.template.json"
@@ -224,7 +225,12 @@ def test_joint_requirement_loads() -> None:
assert Path(requirement.housing_step) == ACTUATOR_HOUSING_STEP
assert requirement.chip_step is None
assert Path(requirement.reference_assembly_step or "") == REFERENCE_ASSEMBLY_STEP
assert requirement.motor_placement_policy == "external_flush_input_end"
assert Path(requirement.motor_reference_assembly_step or "") == SPLIT_MOTOR_DIR / "3500_motor_reference.step"
assert Path(requirement.motor_stator_housing_step or "") == SPLIT_MOTOR_DIR / "3500-定子外壳.STEP"
assert Path(requirement.motor_rotor_housing_step or "") == SPLIT_MOTOR_DIR / "3500-转子外壳.STEP"
assert Path(requirement.motor_output_shaft_step or "") == SPLIT_MOTOR_DIR / "输出轴.STEP"
assert Path(requirement.motor_windings_step or "") == SPLIT_MOTOR_DIR / "线圈.STEP"
assert requirement.motor_placement_policy == "reference_pose"
assert requirement.motor_to_reducer_relation == "motor_output_to_sun_keyed_input"
assert requirement.housing_relation == "ring_fixed_to_housing"
@@ -250,7 +256,7 @@ def test_cascade_joint_requirement_loads_and_ports_resolve() -> None:
assert requirement.platform_id == "actuator_v2_fixed_platform"
assert requirement.chip_step is None
assert Path(requirement.reference_assembly_step or "") == REFERENCE_ASSEMBLY_STEP
assert requirement.motor_placement_policy == "external_flush_input_end"
assert requirement.motor_placement_policy == "reference_pose"
assert instance.boundary.input == "s1_sun"
assert instance.boundary.output == "s2_carrier"
assert instance.boundary.fixed_members == ["s1_ring", "s2_ring"]
+25
View File
@@ -0,0 +1,25 @@
from __future__ import annotations
from src.knowledge_adapter import generate_single_gear_variants
def test_single_gear_variants_are_solved_and_keep_baseline_immutable() -> None:
result = generate_single_gear_variants()
assert result["enabled"] is True
assert result["candidateCount"] == 13
assert result["baseline"] == {
"z_sun": 20,
"z_planet": 55,
"z_ring": 130,
"planet_count": 3,
"module_mm": 0.5,
}
assert len({candidate["ratio"] for candidate in result["candidates"]}) == 13
for candidate in result["candidates"]:
parameters = candidate["parameters"]
assert parameters["zRing"] == parameters["zSun"] + 2 * parameters["zPlanet"]
assert candidate["constraints"]["equalSpacing"] is True
assert candidate["constraints"]["planetClearance"] is True
assert candidate["constraints"]["kinematicResidual"] < 1e-10
assert candidate["speeds"]["ring"] == 0.0
assert candidate["cadGenerated"] is False
+21
View File
@@ -0,0 +1,21 @@
from __future__ import annotations
import subprocess
import sys
from pathlib import Path
BACKEND_ROOT = Path(__file__).resolve().parents[1]
def test_detached_knowledge_base_is_consistent() -> None:
completed = subprocess.run(
[sys.executable, "knowledge/tools/validate_knowledge.py"],
cwd=BACKEND_ROOT,
check=False,
capture_output=True,
text=True,
)
assert completed.returncode == 0, completed.stdout + completed.stderr
assert "integration_state=not_connected" in completed.stdout
assert "stable_kernel_imports=0" in completed.stdout
+1 -1
View File
@@ -4,7 +4,7 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="theme-color" content="#090b0e" />
<title>Orbit Joint Studio</title>
<title>灵心造物</title>
</head>
<body>
<div id="root"></div>
+698 -414
View File
File diff suppressed because it is too large Load Diff
+15 -668
View File
File diff suppressed because one or more lines are too long
+2
View File
@@ -16,6 +16,8 @@ export default defineConfig({
'/api/chat': `http://${agentHost}:${agentPort}`,
'/api/config': `http://${agentHost}:${agentPort}`,
'/api/project-summary': `http://${backendHost}:${backendPort}`,
'/api/designs': `http://${backendHost}:${backendPort}`,
'/api/design-assets': `http://${backendHost}:${backendPort}`,
'/api/generate': `http://${backendHost}:${backendPort}`,
'/api/tasks': `http://${backendHost}:${backendPort}`,
'/api/upload': `http://${backendHost}:${backendPort}`,