feat: add knowledge-driven joint design studio
This commit is contained in:
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
|
||||
Reference in New Issue
Block a user