feat: add knowledge-driven joint design studio
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
*.STEP binary
|
||||
*.step binary
|
||||
+289
-11
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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"}
|
||||
]
|
||||
}
|
||||
@@ -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())
|
||||
@@ -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:
|
||||
|
||||
@@ -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": [
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,不等同于工程验证成品。",
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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
|
||||
@@ -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
@@ -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
File diff suppressed because it is too large
Load Diff
+15
-668
File diff suppressed because one or more lines are too long
@@ -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}`,
|
||||
|
||||
Reference in New Issue
Block a user