Files
cadSet/SimpleCADAPI/design-docs/step-brep-recursive-geometric-explanation.md
T

52 KiB
Raw Blame History

STEP B-Rep 基础几何解释与残差递归算法规范

文档状态

  • 状态:Proposed
  • 规范版本:0.2
  • 目标项目:SimpleCADAPI 2.x
  • 输入:STEP 导入后得到的 B-Rep snapshot
  • 输出:可重放的几何程序、有限解释证据、评分分量和最终验证指标
  • v0.2 实体基础单元:Box、Cylinder、Cone、Sphere、Torus、RawBRep
  • v0.2 组合操作:Union、Subtract
  • v0.2 NURBS 能力:解析曲面约化、NURBS 面证据匹配、RawBRep 兜底;不从开放 NURBS 面猜测实体
  • 非目标:证明原始 CAD feature history、识别 NACA 等领域参数族、保证全局最优、从 mesh 恢复解析 B-Rep

1. 摘要

STEP B-Rep 提供有限数量的 Face、Edge、Vertex、支撑几何及 topology,但通常不提供原始 primitive、Boolean 和 feature tree。本规范把逆向定义为以下有限搜索:

  1. 将输入 B-Rep 转换为有限的面、边和邻接证据原子。
  2. 只从输入中出现的有限几何事件枚举 Box、Cylinder、Cone、Sphere 和 Torus 候选。
  3. 用显式谓词计算每个候选解释的证据原子 bitset。
  4. 用版本化数值代价表达基础单元常见性和参数复杂度。
  5. 对候选计算 R+ = T - HR- = H - T,再递归解释每个 residual solid。
  6. 用有硬预算的 Beam Search 枚举有限个完整程序。
  7. 从头重放完整程序,并用 symmetric-difference、双向边界距离和法向误差判定几何等价。
  8. 在验证通过且所有目标 SurfaceAtom 都有归属的程序中选择总代价最低者。

本文的规范性术语均映射到有限集合、公式、确定排序或返回 OK | FAILED | TIMEOUT | BUDGET_EXHAUSTED 的 kernel 调用。说明性文字不创建额外算法分支。

2. 输入、程序和总函数

2.1 输入

输入实体记为:

[ T=(V,E,F,A) ]

其中 VEF 和 topology relation 集合 A 均为有限集合。

2.2 输出程序

Program := Box(parameters)
         | Cylinder(parameters)
         | Cone(parameters)
         | Sphere(parameters)
         | Torus(parameters)
         | RawBRep(snapshot, byte_length, raw_stats)
         | Union(Program, Program)
         | Subtract(Program, Program)

内部搜索额外使用占位节点:

Template := Program | Slot(slot_id)

完成程序不允许包含 Slot

RawBRep 的参数不是 process-local kernel object。它是以下总函数的结果:

SnapshotOf(shape, context) -> KernelResult[BRepSnapshot]

BRepSnapshot
  profile = "simplecad-brep-snapshot-0.2"
  bytes
  sha256 = SHA256(bytes)

该 profile 的 encoder 必须保存重放 shape 所需的 Vertex、Edge curve、Face surface、trim、orientation、tolerance 和 topology relation,并对相同 evaluated B-Rep 和 profile 产生相同 bytes。若 backend 不提供该 profileSnapshotOf 返回 FAILED(UNSUPPORTED_SNAPSHOT_PROFILE)RawBRepKey = snapshot.sha256byte_length = len(snapshot.bytes);反序列化时必须重新计算 SHA-256 和 byte length 并相等。

raw_stats 是从 snapshot 解码结果重新枚举得到的 closed record

RawStats
  analytic_face_count
  nurbs_face_records     # 每项含 NurbsCost 所需全部整数
  edge_count
  vertex_count

创建节点时从 ResidualAnalysis 复制;反序列化时从 snapshot 重算并要求相等。

2.3 Kernel 调用总类型

所有可能失败的 kernel 操作统一返回:

KernelResult[T] :=
  OK(value: T)
  | FAILED(code: FailureCode)
  | TIMEOUT
  | BUDGET_EXHAUSTED

每次调用前执行:

KernelContext.try_reserve(kind) :=
  if monotonic_clock() >= deadline: TIMEOUT
  elif total_calls + 1 > max_total_calls: BUDGET_EXHAUSTED
  elif kind == BOOLEAN and boolean_calls + 1 > max_boolean_calls: BUDGET_EXHAUSTED
  else increment counters and OK

所有 kernel API 都接收 deadline。分析阶段调用失败时,整个输入降级为 RawBRep;候选阶段调用失败时丢弃该候选;最终验证阶段调用失败时,该程序验证失败。

本规范的调用计数单位是本节列出的逻辑 kernel API 调用,不是 CAD kernel 内部函数调用次数。调用包装器必须在进入 backend 前执行一次 try_reserve。纯 CPU 循环每处理 1024 个元素检查一次 deadline;到期后返回 TIMEOUT

特殊失败动作只有以下四类,后文不得隐式覆盖:

initial/residual analysis failure -> 当前 task 只允许 RawExpansion
candidate construction/measurement failure -> 丢弃当前 candidate
residual generation failure -> 丢弃当前 candidate
validation failure -> 当前 program 的 ValidProgram = false

初始输入 analysis 失败时,只有 SnapshotOf(target) 成功才能返回 RawBRep;否则返回 ANALYSIS_FAILEDprogram = null

2.4 空 shape

Boolean 结果类型为:

BooleanShape := EMPTY | NONEMPTY(shape)

定义:

volume(EMPTY) = 0
surface_area(EMPTY) = 0
components(EMPTY) = []
ValidSolid(EMPTY) = false

EMPTY 没有 bbox、Face、Edge 或 Vertex,不能成为 Program 叶节点。它只允许出现在 residual 和程序简化过程中。

2.5 几何程序执行

Render(Box/Cylinder/Cone/Sphere/Torus) := kernel_construct_primitive(parameters)
Render(RawBRep(snapshot,byte_length,raw_stats)) := kernel_copy(snapshot)
Render(Union(A,B)) := kernel_regularized_union(Render(A), Render(B))
Render(Subtract(A,B)) := kernel_regularized_cut(Render(A), Render(B))

任一子调用不是 OK 时,父调用返回相同失败状态。

内部验证允许程序简化常量 EMPTY

Union(EMPTY,X) = X
Union(X,EMPTY) = X
Subtract(EMPTY,X) = EMPTY
Subtract(X,EMPTY) = X

完成输出程序执行常量折叠后不得含 EMPTY

3. 完整配置

3.1 配置结构

ReverseConfig
  tolerance:
    eps_linear
    eps_angular
    eps_radius
    eps_surface
    eps_normal
    eps_volume
    eps_area
    eps_length
  discretization:
    face_deflection
    face_angular_deflection
    edge_step
    nurbs_u_samples
    nurbs_v_samples
    max_mesh_area_relative_error
  analysis:
    max_analysis_entities
    max_analysis_total_kernel_calls
    max_event_records
    analysis_timeout_seconds
  candidates:
    max_direction_clusters
    max_extent_pairs_per_axis
    max_box_frames
    max_box_candidates_per_frame
    max_candidates_per_type
    boolean_prefilter_top_m
    min_trigger_entities
    min_support_coverage
    max_candidate_attempts_per_task
    max_candidate_kernel_calls_per_task
  score:
    reward_support
    reward_boundary
    reward_edge
    reward_adjacency
    penalty_leaf_cost
    unmatched_support_penalty
    unmatched_boundary_penalty
    unmatched_edge_penalty
    expansion_margin
  cost:
    type_cost_box
    type_cost_cylinder
    type_cost_cone
    type_cost_sphere
    type_cost_torus
    type_cost_nurbs_face
    type_cost_raw_brep
    operation_cost_union
    operation_cost_subtract
    scalar_parameter_cost
  search:
    beam_width
    max_task_depth
    max_expanded_states
    max_search_total_kernel_calls
    max_search_boolean_calls
    search_timeout_seconds
    max_completed_programs_to_validate
    max_pending_tasks_per_state
  validation:
    max_validation_total_kernel_calls
    max_validation_boolean_calls
    validation_timeout_seconds

3.2 默认容差

令输入 bbox 对角线为 DSTEP entity tolerance 最大值为 t_step

[ \epsilon_{linear}=\max(t_{step},10^{-9}D) ]

[ \epsilon_{surface}=2\epsilon_{linear} ]

[ \epsilon_{radius}=2\epsilon_{linear} ]

[ \epsilon_{angular}=10^{-7} ]

[ \epsilon_{normal}=10^{-6} ]

[ \epsilon_{volume}=\max(10^{-12}D^3,10\epsilon_{linear}^3) ]

[ \epsilon_{area}=\max(10^{-12}D^2,10\epsilon_{linear}^2) ]

[ \epsilon_{length}=\epsilon_{linear} ]

角度单位为 radian,长度单位采用导入后统一的模型单位。

3.3 默认离散化与预算

face_deflection = max(eps_surface / 2, 1e-6 * D)
face_angular_deflection = eps_normal / 2
edge_step = max(eps_length, 1e-4 * D)
nurbs_u_samples = 9
nurbs_v_samples = 9
max_mesh_area_relative_error = 1e-3

max_analysis_entities = 1000000
max_analysis_total_kernel_calls = 1000000
max_event_records = 1000000
analysis_timeout_seconds = 120

max_direction_clusters = 12
max_extent_pairs_per_axis = 8
max_box_frames = 64
max_box_candidates_per_frame = 16
max_candidates_per_type = 24
boolean_prefilter_top_m = 16
min_trigger_entities = 1
min_support_coverage = 0.01
max_candidate_attempts_per_task = 10000
max_candidate_kernel_calls_per_task = 100000

reward_support = 10.0
reward_boundary = 5.0
reward_edge = 2.0
reward_adjacency = 1.0
penalty_leaf_cost = 1.0
unmatched_support_penalty = 5.0
unmatched_boundary_penalty = 2.0
unmatched_edge_penalty = 1.0
expansion_margin = 2.0

type_cost_box = 1.0
type_cost_cylinder = 1.0
type_cost_cone = 1.5
type_cost_sphere = 1.5
type_cost_torus = 2.0
type_cost_nurbs_face = 4.0
type_cost_raw_brep = 8.0
operation_cost_union = 0.5
operation_cost_subtract = 0.5
scalar_parameter_cost = 0.05

beam_width = 8
max_task_depth = 10
max_expanded_states = 1000
max_search_total_kernel_calls = 1000000
max_search_boolean_calls = 500
search_timeout_seconds = 120
max_completed_programs_to_validate = 32
max_pending_tasks_per_state = 256

max_validation_total_kernel_calls = 1000000
max_validation_boolean_calls = 500
validation_timeout_seconds = 120

3.4 配置合法性

ConfigValid(config) 当且仅当:

  • 所有浮点配置均为 finite。
  • 所有 eps_*face_deflectionface_angular_deflectionedge_step 和 timeout 严格大于 0
  • 0 < eps_angular < pi/20 < eps_normal < pi/2
  • nurbs_u_samples >= 3nurbs_v_samples >= 3
  • 0 <= max_mesh_area_relative_error < 1
  • 所有 max_*beam_widthboolean_prefilter_top_m 是有限正整数。
  • min_trigger_entities 是有限非负整数。
  • 0 <= min_support_coverage <= 1
  • 所有 reward、penalty 和 cost 是 finite 且大于等于 0

输入数值合法性:

FiniteBRep(T) :=
  every bbox coordinate, Vertex coordinate, curve parameter,
  surface parameter, knot, weight and tolerance used by this algorithm is finite
  and every radius used by an analytic record is > 0
  and every direction consumed by normalize has norm > eps_linear

t_step 为所有 finite entity tolerance 的最大值;没有 entity tolerance 时取 0FiniteBRep == false 时返回 INVALID_INPUT

ConfigValid == false 时返回 INVALID_CONFIG,不调用 kernel。

4. 量化与确定顺序

4.1 量化

定义半数向正无穷舍入:

[ Q(x,\epsilon)=\lfloor x/\epsilon+1/2\rfloor ]

[ Q_3(p,\epsilon)=(Q(p_x,\epsilon),Q(p_y,\epsilon),Q(p_z,\epsilon)) ]

单位方向 d 的规范符号:找到绝对值最大的分量;多个分量同大时按 x,y,z 选择第一个;若该分量小于 0,令 d := -d

[ Q_d(d)=Q_3(d,\sin(\epsilon_{angular})) ]

4.2 基本方向谓词

定义有向夹角:

[ angle(a,b)=acos(clamp(dot(a/|a|,b/|b|),-1,1)) ]

只有 norm(a) > eps_linearnorm(b) > eps_linear 时定义;否则调用该谓词的 match 结果为 false

[ parallel(a,b):=angleAbs(a,b)\leq\epsilon_{angular} ]

其中:

[ angleAbs(a,b)=acos(clamp(|dot(a,b)|,-1,1)) ]

[ perpendicular(a,b):=|dot(a,b)|\leq\sin(\epsilon_{angular}) ]

两轴线 (q1,d1)(q2,d2) 共轴:

coaxial := parallel(d1,d2)
           and norm((q2-q1) - d1*dot(d1,q2-q1)) <= eps_linear

4.3 Bbox 相交

闭区间 bbox AB 相交当且仅当每个轴 k 满足:

[ A_{min,k}\leq B_{max,k}\land B_{min,k}\leq A_{max,k} ]

expand(B, e) 将每个最小坐标减 e,每个最大坐标加 e

4.4 Canonical serialization

所有 canonical record 使用 UTF-8 JSONobject key 按 UTF-8 byte order 排序,禁止 NaN 和 Infinity,浮点参数先替换为本文定义的量化整数。

CandidateKey = canonical_json({type, quantized_parameters})
RawBRepKey = snapshot.sha256
ProgramKey = canonical_json(program tree with CandidateKey or RawBRepKey leaves)

程序树不做交换律重排;FoldUnion 的输入排序在第 15 节定义。

5. 输入分析

5.1 初始化顺序

  1. 从导入 shape 计算 bbox;失败返回 ANALYSIS_FAILEDRawBRep
  2. 计算 D;若 D <= 0 返回 INVALID_INPUT
  3. 若 bbox 坐标或 D 非 finite,返回 INVALID_INPUT
  4. 计算默认配置或读取覆盖配置。
  5. ConfigValid == false 返回 INVALID_CONFIG
  6. 创建独立 analysis context,其 deadline 和调用上限来自 analysis.*
  7. 若实体总数超过 max_analysis_entities,返回 ANALYSIS_FAILED
  8. 检查 FiniteBRep
  9. 执行本节其余步骤。

5.2 Solid 有效性

ValidSolid(T) :=
  kernel_is_valid(T) == OK(true)
  and solid_count(T) == 1
  and open_shell_count(T) == 0
  and closed_shell_count(T) >= 1
  and non_manifold_edge_count(T) == 0
  and abs(volume(T)) > eps_volume

任一测量调用失败,ValidSolidfalse。若为 false,返回 RawBRep(T)INVALID_SOLID。v0.2 不隐式 repair。

6. 有限证据原子

6.1 SurfaceAtom

对每张 Face 调用固定参数三角化。调用失败时分析失败。对每个面积严格大于 0 的三角形 (a,b,c) 生成:

SurfaceAtom
  atom_id
  source_face_id
  point = (a+b+c)/3
  oriented_normal
  area = norm(cross(b-a,c-a))/2

oriented_normal 按 Face orientation 修正。atom_id(source_face_key, Q3(point), Q(area, eps_area), local_triangle_key) 排序后编号。local_triangle_key 是三角形三个量化顶点排序后的 tuple。

记集合为 SA(T)

[ W_S(T)=\sum_{a\in SA(T)}a.area ]

调用 kernel 计算真实表面积 A_kernel。只有满足:

[ W_S(T)>0 ]

且:

[ |W_S(T)-A_{kernel}|\leq\max(\epsilon_{area}, maxMeshAreaRelativeError\cdot A_{kernel}) ]

分析才继续,否则返回 MESH_COVERAGE_FAILEDRawBRep

6.2 EdgeAtom

跳过 kernel 标记为 degenerated 或 seam 的 Edge。对其余 Edge 计算弧长 L。失败时跳过该 Edge 并记录 diagnostic。若 L <= eps_length,跳过。

[ n=\max(1,\lceil L/edgeStep\rceil) ]

按等弧长区间取中点和单位切向,生成:

EdgeAtom
  atom_id
  source_edge_id
  point
  unit_tangent
  length_weight = L/n

任一中点求值失败时跳过整条 Edge。记集合为 EA(T)

[ W_E(T)=\sum_{e\in EA(T)}e.lengthWeight ]

6.3 AdjacencyAtom

对每条非 seam、非 degenerated Edge,取得 incident oriented Face uses。若恰有两个不同 Face,生成:

AdjacencyAtom
  atom_id
  source_edge_id
  face_a_id = min(face keys)
  face_b_id = max(face keys)
  weight = edge_length

Edge 长度失败或小于等于 eps_length 时不生成。集合记为 AA(T)

[ W_A(T)=\sum_{r\in AA(T)}r.weight ]

7. 支撑几何 key

7.1 Plane

Plane 用单位法向 nh = dot(n,origin) 表示。按第 4.1 节规范 n 符号;若翻转 n,同时翻转 h

PlaneKey = (Q_d(n), Q(h, eps_linear))

7.2 Axis

轴线 (p,d) 转换为距原点最近点:

[ q=p-d\cdot dot(d,p) ]

规范 d 符号后:

AxisKey = (Q_d(d), Q3(q, eps_linear))

7.3 解析 support

CylinderKey = (AxisKey, Q(radius, eps_radius))
ConeKey = (AxisKey, Q3(apex, eps_linear), Q(semi_angle, eps_angular))
SphereKey = (Q3(center, eps_linear), Q(radius, eps_radius))
TorusKey = (AxisKey, Q(major_radius, eps_radius), Q(minor_radius, eps_radius))
LineKey = (AxisKey)
CircleKey = (AxisKey, Q3(center, eps_linear), Q(radius, eps_radius))

7.4 NURBS exact key

v0.2 的 exact key 不试图消除参数反转或重参数化。使用 STEP/kernel 提供的参数化顺序,并将控制点转换到 world coordinates

NurbsExactKey = SHA256(
  degree_u,
  degree_v,
  periodic_u,
  periodic_v,
  Q(knots_u, 1e-12),
  Q(knots_v, 1e-12),
  multiplicities_u,
  multiplicities_v,
  Q(weights, 1e-12),
  Q3(world_control_points, eps_linear)
)

7.5 NURBS 解析约化

RecognizeAnalytic(surface, eps_surface) :=
  OK(CertifiedAnalytic(PlaneParameters, max_error))
  | OK(CertifiedAnalytic(CylinderParameters, max_error))
  | OK(CertifiedAnalytic(ConeParameters, max_error))
  | OK(CertifiedAnalytic(SphereParameters, max_error))
  | OK(CertifiedAnalytic(TorusParameters, max_error))
  | OK(NONE)
  | FAILED
  | TIMEOUT
  | BUDGET_EXHAUSTED

只有 backend 能认证整个输入 support surface 在其完整参数域内与返回解析曲面的最大距离不超过 eps_surface,并返回 max_error <= eps_surface 时才允许 CertifiedAnalytic。参数 record 必须包含第 7.1 到 7.3 节 key 所需的全部数值。固定采样只能产生第 12.5 节的近似 evidence,不能把 NONE 升级为解析类型。没有认证接口的 backend 必须返回 OK(NONE)

8. EvidenceGroup 与有限事件

8.1 Group 代表值

每个原始几何 record 为:

EvidenceRecord
  canonical_bucket_key
  exact_parameters
  weight
  source_entity_key
  provenance

provenance 是有限的 source entity key 集合。按 canonical_bucket_key 分组。对同一 source entity 只保留排序键最小的 record;组权重是剩余 record weight 之和;组 provenance 是 record provenance 的并集。代表 record 按以下键取最小:

(-weight, source_entity_key, canonical_json(exact_parameters))

候选实际参数只取代表 record 的 exact_parametersbucket 中其他 record 只增加 trigger provenance 和 group weight。

Face record 的 source_entity_key 和单元素 provenance 为 Face keyweight 为 Face areaEdge 对应 Edge key 和 Edge lengthVertex 对应 Vertex key 和 eps_areabbox corner 对应 bbox.corner.<0..7>0world axis 对应 world.x/y/z0。事件的 exact_parameters 包含实际 value 或 direction。事件 group 输出显式字段:

EventGroup
  bucket_key
  representative_value_or_direction
  group_weight
  provenance

8.2 DirectionEvent

来源:

  • Plane Face normal,权重为 Face area。
  • Cylinder/Cone/Torus axis,权重为 Face area。
  • LINE Edge direction,权重为 Edge length。
  • world axes,权重为 0source key 固定为 world.x/y/z

Q_d 分组。若组总正权重 W > 0

[ d=normalize(\sum_i w_i d_i) ]

所有 d_i 先规范符号。若 W = 0,代表方向取 source key 最小 record 的 exact direction。按 (-group_weight, Q_d(d)) 排序,保留前 max_direction_clusters 个。

8.3 PositionEvent

给定单位方向 d,产生 record

  • 每个 Vertexvalue = dot(d,p)
  • 每个 normal 与 d 满足 parallel 的 Planevalue = dot(d,origin)
  • bbox 八个 cornervalue = dot(d,corner)

每个来源按第 8.1 节生成完整 EvidenceRecordcanonical_bucket_key = Q(value,eps_linear)。按第 8.1 节分组并选择 exact representative value。

8.4 AxialEvent

给定轴 (q,d),产生 record

  • 每个 Vertexvalue = dot(d,p-q)
  • 每个与该轴满足 coaxial 的 CIRCLE Edgevalue = dot(d,center-q)weight 为 Edge length。
  • 每个 normal 与 d 满足 parallel 的 Planevalue = dot(d,origin-q)
  • bbox cornervalue = dot(d,corner-q)

每个来源按第 8.1 节生成完整 EvidenceRecordcanonical_bucket_key = Q(value,eps_linear)。按第 8.1 节分组。

DirectionEvent、PositionEvent、AxialEvent 生成 record 时,每增加一个 record 都递增当前 analysis/task 的 event_record_count。达到 max_event_records 时停止生成更多 record,并返回 BUDGET_EXHAUSTED;初始 analysis 因此降级 RawBRepresidual task 因此只允许 RawExpansion。

8.5 ExtentPair

对排序后的不同 event i < j 生成:

ExtentPair
  low = min(event_i.value, event_j.value)
  high = max(event_i.value, event_j.value)
  weight = event_i.group_weight + event_j.group_weight
  provenance = union(event_i.provenance, event_j.provenance)

只保留 high-low > eps_linear。按:

(-weight, -(high-low), Q(low,eps_linear), Q(high,eps_linear))

排序,保留前 max_extent_pairs_per_axis 个。

9. 有限候选枚举

9.0 Primitive 参数 record

所有方向均为 world-coordinate unit vector,所有 frame 均为右手正交 frame,所有 extent 满足 high-low > eps_linear

BoxParameters
  frame_origin
  x_axis
  y_axis
  z_axis = cross(x_axis,y_axis)
  x_low, x_high
  y_low, y_high
  z_low, z_high

CylinderParameters
  axis_point                    # 距 world origin 最近点 q
  axis_direction
  radius > eps_radius
  axial_low, axial_high

ConeParameters
  axis_point                    # 距 world origin 最近点 q
  axis_direction
  apex_axial_coordinate
  semi_angle in (eps_angular, pi/2-eps_angular)
  axial_low, axial_high

SphereParameters
  center
  radius > eps_radius

TorusParameters
  axis_point
  axis_direction
  major_radius
  minor_radius

Box 的 world point 为:

[ frameOrigin+x\cdot xAxis+y\cdot yAxis+z\cdot zAxis ]

Cylinder/Cone 的 world axial point 为 axis_point + z*axis_direction。Cone 在轴坐标 z 的半径为:

[ r(z)=|z-apexAxialCoordinate|\tan(semiAngle) ]

候选只允许 r(axial_low)r(axial_high) 至少一个大于 eps_radius。这些 record 的字段顺序就是 CandidateKey 的字段顺序。kernel_construct_primitive 只接受这些 record。

9.1 CandidateValid

CandidateValid(H,T) :=
  construction returned OK
  and kernel_is_valid(H) == OK(true)
  and solid_count(H) == 1
  and volume(H) > eps_volume
  and BBox(H) intersects expand(BBox(T), eps_linear)
  and TriggerEntityCount(H) >= min_trigger_entities

其中每个 kernel 测量按第 2.3 节执行;任一不是 OK 时结果为 false

TriggerEntityCount 是候选使用的 extent、direction、axis、radius 等 event provenance 中不同 Face/Edge/Vertex key 的数量;world.* 和 bbox provenance 不计数。

9.2 Box

  1. 从 DirectionEvent 枚举无序三元组,三元组内部按 Q_d 排序。
  2. 只保留三对方向均满足 perpendicular 的三元组。
  3. 令第一个方向为 x0,从第二个方向减去在 x0 上的投影后规范化为 y0,令 z0 = normalize(cross(x0,y0))
  4. angleAbs(z0, third_direction) > eps_angular,丢弃。
  5. frame 固定为右手系 (x0,y0,z0)
  6. frame 按三个 direction group weight 总和降序、frame key 升序排序,只保留前 max_box_frames 个。
  7. 对每个 frame axis 生成 PositionEvent 和 ExtentPair。
  8. frame_origin = (0,0,0),三个 extent 均为 world origin 在对应 frame axis 上的投影坐标;枚举三个 ExtentPair 集合的笛卡尔积并构造 oriented Box。
  9. 每个 frame 内按 (-trigger_weight,-volume,CandidateKey) 排序,保留前 max_box_candidates_per_frame 个。

trigger_weight 是所有 trigger provenance 对应 record weight 的去重和。

9.3 Cylinder

候选轴和半径来源:

  • 每个 CylinderKey Face group 的代表参数。
  • 每个 CircleKey group;同一 AxisKey 和 radius bucket 下至少存在两个不同 Circle center axial bucket。

对每个 (axis,radius) 生成 AxialEvent 和 ExtentPair。Cylinder Face group 只提供 axis/radius,有限 axial extent 一律来自 ExtentPair。每个 pair 构造 CylinderParameters 并按 CandidateValid 过滤。

9.4 Cone

来源一:每个 ConeKey Face group 的代表参数。

来源二:同一 AxisKey 的两个 Circle group (z1,r1)(z2,r2),要求:

[ |z_1-z_2|>\epsilon_{linear} ]

且:

[ |r_1-r_2|>\epsilon_{radius} ]

计算:

[ k=(r_2-r_1)/(z_2-z_1) ]

[ z_{apex}=z_1-r_1/k ]

[ semiAngle=atan(|k|) ]

Cone Face group 只提供 axis/apex/semi-angle,有限 extent 一律来自 AxialEvent 的 ExtentPair。来源二的 apex_axial_coordinate = z_apex。截面半径小于 eps_radius 时置为 0。构造 ConeParameters 后按 CandidateValid 过滤。

9.5 Sphere

每个 SphereKey Face group 的代表参数生成一个完整 Sphere。v0.2 不从任意圆环拟合 Sphere。

9.6 Torus

每个 TorusKey Face group 的代表参数生成一个完整 Torus,且要求:

major_radius > minor_radius > eps_radius

v0.2 不生成 horn torus 或 spindle torus,不从曲率样本拟合 Torus。

9.7 NURBS 证据记录,不是实体候选

NURBS 不进入本节的实体候选列表,不参与 PrimitiveCost、signed residual 或 Beam Search。分析阶段只生成:

NurbsEvidenceRecord
  face_ids sharing the same NurbsExactKey
  NurbsCost

v0.2 只按 NurbsExactKey 分组,不计算一般 NURBS 的 pairwise 等价关系。

9.8 去重和预算

候选只按 CandidateKey 去重。相同 key 合并 trigger provenance。v0.2 不调用 Boolean 做候选几何去重。

每种类型按第 13.3 节排序后保留前 max_candidates_per_type 个。

每个 task 在尝试构造候选前递增 candidate_attempt_count;在候选专属 kernel 调用前递增 candidate_kernel_call_count。达到 max_candidate_attempts_per_taskmax_candidate_kernel_calls_per_task 后,停止该 task 的候选枚举,并继续保留其无条件 RawExpansion。该规则同时限制 Circle pair、Cone pair 和 Box extent 笛卡尔积的实际尝试数。

10. 先验与描述代价

10.1 PrimitiveCost

ParameterCodeUnits(Box) = 9
ParameterCodeUnits(Cylinder) = 8
ParameterCodeUnits(Cone) = 9
ParameterCodeUnits(Sphere) = 4
ParameterCodeUnits(Torus) = 9
PrimitiveCost(H) =
  configured_type_cost(H.type)
  + scalar_parameter_cost * ParameterCodeUnits(H.type)

ParameterCodeUnits 是 v0.2 先验表中的版本化整数,不声称等于参数 record 字段数或最小几何自由度。

10.2 NurbsCost

NurbsCost(face) =
  type_cost_nurbs_face
  + 0.02 * control_point_count
  + 0.01 * (knot_u_count + knot_v_count)
  + 0.01 * non_unit_weight_count
  + 0.02 * degree_u * degree_v
  + 0.05 * trim_edge_count

同一 NurbsExactKey group 的代价为一张 support 的 NurbsCost0.1 * (face_count-1)

10.3 RawBRepCost

RawBRepCost(analysis_or_raw_stats) =
  type_cost_raw_brep
  + sum(NurbsCost(record) for nurbs_face_records)
  + 0.20 * analytic_face_count
  + 0.05 * edge_count
  + 0.01 * vertex_count

10.4 LeafCost

LeafCost(primitive) = PrimitiveCost(primitive)
LeafCost(RawBRep(...,raw_stats)) = RawBRepCost(raw_stats)

这些代价是“常见性”的唯一规范表示。数值越低,先验偏好越高。

11. 点、法向和 Edge 投影谓词

11.1 ProjectFace

ProjectFace(point, face) := kernel_nearest_point_on_trimmed_face(point, face)

成功值包含:

distance
projected_point
all_outward_normals_at_projection

投影位于 smooth interior 时 normal 集合大小为 1;位于 Edge/Vertex 时包含所有 incident Face outward normals;无法取得 normal 时调用失败。

11.2 BoundaryExplainsOnFace

BoundaryExplainsOnFace(atom, face) :=
  ProjectFace(atom.point, face) == OK(p)
  and p.distance <= eps_surface
  and exists n in p.all_outward_normals_at_projection:
        angle(atom.oriented_normal, n) <= eps_normal

11.3 ProjectEdge

EdgeExplainsOnEdge(atom, edge) :=
  kernel_nearest_point_on_edge(atom.point, edge) == OK(p)
  and p.distance <= eps_surface
  and angleAbs(atom.unit_tangent, p.unit_tangent) <= eps_normal

12. 支撑面匹配

12.1 无向 support 匹配

SupportExplains 故意忽略材料方向,因为同一个 Cylinder support 可以是凸台外壁,也可以在 Subtract 后成为孔壁。方向只在候选实际边界和最终 leaf ownership 中检查。

Plane

[ |dot(n,a.point)-h|\leq\epsilon_{surface} ]

angleAbs(n,a.oriented_normal) <= eps_normal

Cylinder:令:

[ v=a.point-q-d\cdot dot(d,a.point-q) ]

要求:

[ ||v|-r|\leq\epsilon_{surface} ]

norm(v) > eps_linear,并满足 angleAbs(v/norm(v),a.oriented_normal) <= eps_normal

Cone、Sphere、Torus:调用无限支撑面的 kernel projection,要求 distance 不超过 eps_surface,且 projected normal 与 atom normal 的 angleAbs 不超过 eps_normal

SupportExplains(H,a) :=
  exists support s of primitive H: support_match(s,a)

12.2 实际边界匹配

BoundaryExplains(H,a) :=
  exists boundary Face f of H: BoundaryExplainsOnFace(a,f)

12.3 Edge 匹配

NaturalEdges(H) 是 primitive 构造结果中排除 seam 和 degenerated Edge 后的有限 Edge 集合。

EdgeExplains(H,e) :=
  exists g in NaturalEdges(H): EdgeExplainsOnEdge(e,g)

12.4 FaceMap

对目标 Face f 和候选边界 Face b

[ M(f,b)=\sum_{a\in SA(T),a.sourceFace=f} a.area\cdot I[BoundaryExplainsOnFace(a,b)] ]

FaceMap(f,H)M(f,b) 最大的候选 Face;最大值小于 eps_area 时为 NONE。同分时按:

BoundaryFaceKey = (support_key,
                   Q3(face_centroid, eps_linear),
                   Q(face_area, eps_area),
                   construction_local_face_id)

取 key 最小者。construction_local_face_id 由 primitive builder 按固定面角色分配,不使用 kernel 枚举序号。

13. 候选解释与局部分数

13.1 Bitset

CandidateExplanation
  support_surface_atom_bits[a] = SupportExplains(H,a)
  boundary_surface_atom_bits[a] = BoundaryExplains(H,a)
  edge_atom_bits[e] = EdgeExplains(H,e)
  adjacency_atom_bits[r] = AdjacencyExplains(H,r)

AdjacencyExplains(H,r) 当且仅当:

  • FaceMap(r.face_a,H)FaceMap(r.face_b,H) 均不是 NONE
  • 两个映射结果不同。
  • 两个候选 Face 在候选 B-Rep 中共享至少一条非 degenerated Edge。

13.2 覆盖率

[ C_S=\frac{\sum_a a.area\cdot I[supportBits[a]]}{W_S(T)} ]

[ C_B=\frac{\sum_a a.area\cdot I[boundaryBits[a]]}{W_S(T)} ]

W_E(T)>0

[ C_E=\frac{\sum_e e.lengthWeight\cdot I[edgeBits[e]]}{W_E(T)} ]

否则 C_E=0 且局部分数中的 Edge reward 置 0

W_A(T)>0

[ C_A=\frac{\sum_r r.weight\cdot I[adjacencyBits[r]]}{W_A(T)} ]

否则 C_A=0 且局部分数中的 adjacency reward 置 0

13.3 LocalScore

[ LocalScore(H,T)= rewardSupport\cdot C_S+ rewardBoundary\cdot C_B+ rewardEdge'\cdot C_E+ rewardAdjacency'\cdot C_A- penaltyLeafCost\cdot PrimitiveCost(H) ]

候选只在 C_S >= min_support_coverage 时保留。排序键:

(-LocalScore, -C_S, -C_B, PrimitiveCost, CandidateKey)

每种类型保留前 max_candidates_per_type 个,合并后保留前 boolean_prefilter_top_m 个。

14. Signed residual

对当前任务 solid S 和候选 H,连续执行两个 kernel Boolean

RplusResult: BooleanShape = regularized_cut(S,H)
RminusResult: BooleanShape = regularized_cut(H,S)

调用前必须为两个 Boolean 分别成功 reserve budget。任一结果失败时淘汰候选。

EMPTY 使用空 component list;对 NONEMPTY(shape) 调用 components(shape)。调用失败时淘汰候选。每个非空 component 必须满足 ValidSolid。体积小于等于 eps_volume 的 component 不进入列表,删除体积累加为 discardedVolume。若:

[ discardedVolume>\epsilon_{volume} ]

则淘汰候选。

剩余 component 分别记为:

P = sorted components of Rplus
M = sorted components of Rminus

每个剩余 component 立即执行第 15.1 节 AnalyzeResidual。任一分析失败时淘汰候选。排序键为 analysis 中的 snapshot.sha256

15. 程序模板状态转移

15.1 ResidualAnalysis

ResidualAnalysis
  shape                         # 只在当前进程计算期间使用
  snapshot: BRepSnapshot        # 可序列化、可重放
  surface_atoms
  edge_atoms
  adjacency_atoms
  support_groups
  direction_events
  face_count
  edge_count
  vertex_count
  analytic_face_count
  nurbs_face_count
  raw_stats
  volume
  surface_area
AnalyzeResidual(S,config,context) :=
  ValidSolid(S)
  + SnapshotOf(S)
  + 第 6 至 8 节全部分析

结果是 KernelResult[ResidualAnalysis]。所有测量只在这里执行并缓存;候选排序、cost、proxy、task 选择和 StateKey 不允许再次隐式调用 kernel。初始 target 也产生相同结构的 ResidualAnalysis

15.2 FoldUnion

统一排序函数:

UnionOrderKey(primitive leaf) = (0, CandidateKey)
UnionOrderKey(RawBRep leaf) = (1, RawBRepKey)
UnionOrderKey(Slot) = (2, slot_id)
UnionOrderKey(composite Program) = (3, ProgramKey)

输入按 UnionOrderKey 排序。

FoldUnion([]) = EMPTY
FoldUnion([x]) = x
FoldUnion([x1,...,xn]) = Union(...Union(Union(x1,x2),x3)...,xn)

EMPTY 只用于构造规则,不是 Program 节点。

15.3 CandidateExpansion

Pp 个 componentsMm 个 components。为每个 component 创建唯一 SlotID 为:

SHA256(parent_slot_id, sign, component_analysis.snapshot.sha256,
       same_fingerprint_ordinal)

其中 sign 只用于 ID,取 PLUSMINUSordinal 是排序后同 fingerprint 的从零计数。

base = FoldUnion([H] + plus_slots)
replacement = base                         if m == 0
replacement = Subtract(base,
                       FoldUnion(minus_slots)) if m > 0

将当前 Slot 替换为 replacement。每个新 Slot 对应:

ResidualTask
  slot_id
  analysis: ResidualAnalysis
  depth = parent.depth + 1

若新 task 数使状态的 pending_task_count > max_pending_tasks_per_state,丢弃该 CandidateExpansion,只保留父 task 的 RawExpansion。

新插入 Boolean 节点数:

union_count = p + max(m-1, 0)
subtract_count = 1 if m > 0 else 0

立即增加的 committed cost

PrimitiveCost(H)
+ union_count * operation_cost_union
+ subtract_count * operation_cost_subtract

15.4 RawExpansion

task.analysis 已含 SnapshotOf(task.analysis.shape) 的结果。将当前 Slot 替换为 RawBRep(task.analysis.snapshot, len(snapshot.bytes), task.analysis.raw_stats),删除该 task,立即增加:

RawBRepCost(task.analysis)

15.5 成本不变量

对任一搜索状态:

committed_cost =
  sum(cost of every committed leaf)
  + sum(cost of every committed Boolean node)

当没有 Slot 时:

committed_cost == ProgramCost(completed_program)

16. Residual 代理代价和展开门槛

16.1 ResidualProxyCost

ResidualProxyCost(analysis) =
  1.00 * solid_count
  + 0.20 * analytic_face_count
  + 0.50 * nurbs_face_count
  + 0.02 * edge_count
  + 0.01 * vertex_count

component list 的 proxy 是每个 component proxy 之和。

16.2 ExpansionEstimate

[ ExpansionEstimate= PrimitiveCost(H)+ unionCount\cdot operationCostUnion+ subtractCount\cdot operationCostSubtract+ Proxy(P)+Proxy(M)+ unmatchedSupportPenalty\cdot(1-C_S)+ unmatchedBoundaryPenalty\cdot(1-C_B)+ unmatchedEdgePenalty'\cdot(1-C_E) ]

W_E=0,最后一项为 0

只有满足:

[ ExpansionEstimate\leq RawBRepCost(currentTask.analysis)+expansionMargin ]

的候选才执行第 15.3 节状态转移。

17.1 SearchState

SearchState
  template
  tasks_by_slot_id
  committed_cost
  estimated_remaining_cost
  state_key
estimated_remaining_cost =
  sum(MinLeafCost for each pending task)
MinLeafCost = min(
  type_cost_box + 9*scalar_parameter_cost,
  type_cost_cylinder + 8*scalar_parameter_cost,
  type_cost_cone + 9*scalar_parameter_cost,
  type_cost_sphere + 4*scalar_parameter_cost,
  type_cost_torus + 9*scalar_parameter_cost,
  type_cost_raw_brep
)

该值只用于排序;不声称是包含 Boolean 操作的严格数学下界。

17.2 StateKey

state_key = SHA256(
  canonical serialization of template,
  sorted tuples(slot_id, task.depth, task.analysis.snapshot.sha256)
)

同一 state_key 只保留 committed_cost 最小者;同分保留 template serialization 字典序最小者。

17.3 任务选择

从 pending task 中按以下键取最小:

(-RawBRepCost(task.analysis),
 -task.analysis.volume,
 task.analysis.snapshot.sha256,
 slot_id)

17.4 单状态展开

if task.depth >= max_task_depth:
    children = [RawExpansion(state, task)]
else:
    children = every accepted CandidateExpansion
               plus one unconditional RawExpansion

展开一个状态前检查 expanded_states + 1 <= max_expanded_states。每个 kernel 调用由 KernelContext.try_reserve 单独检查,因此 Boolean 和总调用硬上限不会超出。

17.5 Frontier

算法每次从 frontier 取排序键最小的一个状态展开,再将 children 放回 frontier。排序键:

(
  committed_cost + estimated_remaining_cost,
  sum(RawBRepCost(task.analysis) for pending tasks),
  pending_task_count,
  state_key
)

插入 children、去重、排序后只保留前 beam_width 个状态。

17.6 搜索终止

搜索上下文保存唯一 termination_reason

FRONTIER_EMPTY
STATE_LIMIT
BOOLEAN_LIMIT
KERNEL_CALL_LIMIT
SEARCH_TIMEOUT
ENOUGH_COMPLETED_PROGRAMS

循环开始及每次 kernel reserve 前更新该值。达到任何预算后不再生成候选。若一个状态已从 frontier 弹出但尚未完成展开,将该未修改状态加入 fallbackStates;已经完整生成的 children 保留在 frontier。把 frontier union fallbackStates 中每个状态的全部 pending Slot 按 slot ID 升序执行 RawExpansion,得到完整程序。若 frontier 正常变空且 termination reason 尚未设置,则设置为 FRONTIER_EMPTY

完成程序按 (ProgramCost,ProgramKey) 去重,只保留前 max_completed_programs_to_validate 个。

17.7 显式调用上界

若实际展开状态数为 S <= max_expanded_states,每状态经过预筛选的候选数为 P <= boolean_prefilter_top_msigned residual 最多调用:

[ 2SP ]

次 Boolean。实际数量同时受 max_search_boolean_calls 限制。候选构造、投影和测量计入 max_search_total_kernel_calls。最终程序重放和验证使用独立 validation context,不消费搜索预算。

18. 完整程序代价

ProgramCost(primitive) = PrimitiveCost(primitive)
ProgramCost(RawBRep(...,raw_stats)) = RawBRepCost(raw_stats)
ProgramCost(Union(A,B)) = ProgramCost(A)+ProgramCost(B)+operation_cost_union
ProgramCost(Subtract(A,B)) = ProgramCost(A)+ProgramCost(B)+operation_cost_subtract

19. 完整程序验证

19.1 重放

整个 validation 阶段创建一个共享 validationContext;所有完成程序和最终 RawBRep fallback 共用它的 deadline 和调用计数。依次执行按 (ProgramCost,ProgramKey) 排序的程序;context 到期或预算耗尽后不再验证后续程序。

对一个程序只执行一次 Render(P) 并保存 ReplayResult = R。失败时:

ValidProgram(P,T) = false

19.2 Symmetric difference

成功重放结果为 R

D1: BooleanShape = regularized_cut(T,R)
D2: BooleanShape = regularized_cut(R,T)
symdiff_volume = volume(D1)+volume(D2)  # volume(EMPTY)=0

任一调用失败则验证失败。

VolumeEquivalent := symdiff_volume <= eps_volume

19.3 双向边界距离和法向

用第 6.1 节相同配置生成 SA(R),并对 mesh area 执行相同覆盖检查。对 SA(T) 中每个 atom 投影到 boundary(R),对 SA(R) 中每个 atom 投影到 boundary(T)

每次 projection 必须成功。距离集合 Dists 和最小有向法向误差集合 Angles

distance = nearest boundary distance
angle = min(angle(atom.oriented_normal,n)
            for n in all outward normals at projection)

两个 atom 集合都非空,因此:

[ maxBoundaryDistance=\max(Dists) ]

[ rmsBoundaryDistance=\sqrt{ \frac{\sum_i area_i\cdot distance_i^2}{\sum_i area_i}} ]

[ maxNormalError=\max(Angles) ]

BoundaryEquivalent := maxBoundaryDistance <= eps_surface
NormalEquivalent := maxNormalError <= eps_normal

rmsBoundaryDistance 只作为输出指标,不参与 v0.2 acceptance。

19.4 可计算 topology signature

v0.2 不用原始 V-E+F 计算 Euler characteristic。定义:

TopologySignature(S) = (
  solid_count(S),
  closed_shell_count(S),
  open_shell_count(S),
  non_manifold_edge_count(S)
)

任一计数调用失败则验证失败。

TopologySignatureMatches := TopologySignature(T) == TopologySignature(R)

19.5 ValidProgram

ValidProgram(P,T) :=
  ReplayResult R exists
  and kernel_is_valid(R) == OK(true)
  and VolumeEquivalent
  and BoundaryEquivalent
  and NormalEquivalent
  and TopologySignatureMatches

ValidProgram 是 acceptance profile 名称,含义仅为本节 finite tests 在给定配置下通过,不表示符号等价或完整 Hausdorff/topology 证明。

20. SurfaceAtom 归属与有效解释

20.1 Leaf polarity

从程序根向叶递归:

polarity(root) = +1
polarity(Union.left/right) = polarity(parent)
polarity(Subtract.left) = polarity(parent)
polarity(Subtract.right) = -polarity(parent)

20.2 LeafSupportEvidence

对 primitive 或 RawBRep 叶节点 L,将其支撑面投影法向乘 polarity(L)

LeafSupportEvidence(L,a) :=
  exists support face f of L:
    projection of a.point to support(f) succeeds
    and distance <= eps_surface
    and exists normal n at projection:
        angle(a.oriented_normal, polarity(L)*n) <= eps_normal

支撑面使用未裁剪 support,因此 Boolean 改变 trim 后仍可产生解释证据。这个 predicate 不声称该叶节点是原始历史来源,也不证明该 support 在 Boolean 中对最终边界有唯一贡献。

20.3 唯一 owner

对每个目标 SurfaceAtom 枚举有限叶节点集合。满足 LeafSupportEvidence 的叶节点按:

(
  0 if primitive else 1,
  LeafCost,
  -leaf_depth,
  leaf_program_key
)

取最小者。输出字段名为 support_evidence_owner;没有匹配者时为 NONE

20.4 ValidExplanation

ValidExplanation(P,T) :=
  ValidProgram(P,T)
  and every a in SA(T) has support_evidence_owner != NONE

本文中的“100% 解释”严格指:第 6.1 节有限 SurfaceAtom 集合全部有 support evidence,并且第 19 节 acceptance profile 通过;不表示连续曲面上的数学全称命题。第 6.1 节同时限制离散 surface area 与 kernel surface area 的差值。

21. Edge 和 adjacency 节点归属

Edge 和 adjacency 可能由 Boolean 生成,因此不强制归属到叶节点。

21.1 节点重放缓存

完整程序验证时缓存每个语法树节点的 Render(node)。节点深度从根为 0 向下递增。

21.2 EdgeOwner

NodeExplainsEdge(node,e) :=
  exists non-seam, non-degenerated Edge g in Render(node):
    EdgeExplainsOnEdge(e,g)

候选 node 按 (-node_depth,node_program_key) 取最小。最终 root 若仍不能解释,则 owner 为 UNATTRIBUTED

21.3 AdjacencyOwner

对每个 node 的 rendered shape,用第 12.4 节方式把目标两个 Face 映射到 node result Face

NodeExplainsAdjacency(node,r) :=
  both FaceMap values exist and differ
  and the mapped result Faces share a non-degenerated Edge

(-node_depth,node_program_key) 选择 owner。失败时为 UNATTRIBUTED

Edge 或 adjacency 的 UNATTRIBUTED 数量进入输出,但 v0.2 不把它们加入 ValidExplanation;几何正确性由第 19 节约束。

22. 最终选择

先对最多 max_completed_programs_to_validate 个完成程序执行第 19、20 节。令:

VP = finite set of programs with ValidExplanation == true

VP 非空,排序键为:

(
  ProgramCost,
  RawBRep_leaf_count,
  total_NURBS_face_count_inside_RawBRep,
  Boolean_node_count,
  ProgramKey
)

返回最小者,status 为 VALID_EXPLANATION

VP 为空,验证 RawBRep(T)。若其 ValidExplanation 为 true,返回它,status 为 RAW_BREP_ONLY;否则返回 FINAL_VALIDATION_FAILED

23. 主算法伪代码

def reverse_step(target, overrides=None):
    bbox = kernel_bbox(target)
    if not bbox.ok:
        return snapshot_or_failure(target, "ANALYSIS_FAILED")
    if not bbox.finite or bbox.diagonal <= 0:
        return snapshot_or_failure(target, "INVALID_INPUT")

    config = resolve_config(target, bbox, overrides)
    if not config_valid(config):
        return failure("INVALID_CONFIG")

    analysis_ctx = KernelContext.for_analysis(config)
    analysis = analyze_residual(target, config, analysis_ctx)
    if not analysis.ok:
        return snapshot_or_failure(target, analysis.status)

    search_ctx = KernelContext.for_search(config)
    initial_slot = Slot(id=sha256("root"))
    initial_task = ResidualTask(
        slot_id=initial_slot.id,
        analysis=analysis.value,
        depth=0,
    )
    frontier = [SearchState(
        template=initial_slot,
        tasks={initial_slot.id: initial_task},
        committed_cost=0,
    )]
    completed = []
    fallback_states = []
    expanded_states = 0

    while frontier and not search_ctx.stopped:
        state = pop_min(frontier, key=frontier_sort_key)

        if not state.tasks:
            completed.append(state.template)
            if len(completed) >= config.search.max_completed_programs_to_validate:
                search_ctx.stop("ENOUGH_COMPLETED_PROGRAMS")
            continue

        if expanded_states >= config.search.max_expanded_states:
            search_ctx.stop("STATE_LIMIT")
            fallback_states.append(state)
            break

        task = select_task(state.tasks)
        expanded_states += 1
        children = []

        if task.depth < config.search.max_task_depth:
            candidates = enumerate_candidates(task.analysis, config, search_ctx)
            explanations = compute_explanations(candidates, task.analysis, config, search_ctx)
            ranked = rank_and_limit(explanations, config)

            for candidate, explanation in ranked:
                residuals = signed_residuals(task.analysis, candidate, config, search_ctx)
                if not residuals.ok:
                    if search_ctx.stopped:
                        break
                    continue
                if expansion_estimate(candidate, explanation, residuals, config) \
                        > raw_brep_cost(task.analysis, config) + config.score.expansion_margin:
                    continue
                children.append(candidate_expansion(
                    state, task, candidate, residuals, config
                ))

        if search_ctx.stopped:
            fallback_states.append(state)
            break

        children.append(raw_expansion(state, task, config))
        frontier = deduplicate(frontier + children)
        frontier.sort(key=frontier_sort_key)
        frontier = frontier[:config.search.beam_width]

    if not search_ctx.termination_reason:
        search_ctx.stop("FRONTIER_EMPTY")

    completed.extend(materialize_all_slots_as_raw(
        deduplicate(frontier + fallback_states), config
    ))
    completed = deduplicate_and_sort_programs(completed, config)
    completed = completed[:config.search.max_completed_programs_to_validate]

    valid = []
    validation_ctx = KernelContext.for_validation(config)
    for program in completed:
        if validation_ctx.stopped:
            break
        metrics = validate_program_and_assign_atoms(
            program, target, analysis, config, validation_ctx
        )
        if metrics.valid_explanation:
            valid.append((final_program_sort_key(program, metrics), program, metrics))

    if valid:
        valid.sort(key=lambda x: x[0])
        return result(valid[0], search_ctx.termination_reason)

    return validate_and_return_raw_brep(
        target, analysis, config, validation_ctx
    )

snapshot_or_failure 创建一个独立、只允许一次 SnapshotOf 的 emergency context;成功时返回 RawBRep,失败时 program = null。这条 emergency 调用及其 deadline 单独写入结果,不能复用已经耗尽的阶段 context。

24. 状态与诊断枚举

INVALID_CONFIG
INVALID_INPUT
ANALYSIS_FAILED
INVALID_SOLID
MESH_COVERAGE_FAILED
CANDIDATE_CONSTRUCTION_FAILED
BOOLEAN_FAILED
BOOLEAN_TIMEOUT
NUMERICALLY_UNSTABLE
STATE_LIMIT
BOOLEAN_LIMIT
KERNEL_CALL_LIMIT
SEARCH_TIMEOUT
FRONTIER_EMPTY
ENOUGH_COMPLETED_PROGRAMS
RAW_BREP_ONLY
FINAL_VALIDATION_FAILED
VALID_EXPLANATION

自由文本 diagnostic 不参与控制流。

25. 输出结构

ExplanationResult
  status
  search_termination_reason
  input_fingerprint
  config
  program
  program_key
  program_cost
  candidate_records
  surface_atom_owners
  edge_atom_owners
  adjacency_atom_owners
  validation_metrics
  kernel_call_counts
  diagnostics

validation_metrics 至少包含:

symdiff_volume
max_boundary_distance
rms_boundary_distance
max_normal_error
topology_signature_input
topology_signature_result
surface_atom_count
owned_surface_atom_count
unattributed_edge_atom_count
unattributed_adjacency_atom_count
valid_program
valid_explanation

26. MVP 模块

实现模块和唯一输入输出如下:

模块 输入 输出
BRepAnalyzer B-Rep、Config、KernelContext descriptors、SA、EA、AA、support groups
CandidateEnumerator analysis、Config finite primitive candidates
CandidateScorer candidate、current residual analysis bitsets、coverage、LocalScore
ResidualEngine residual、candidate、KernelContext sorted Rplus/Rminus components
ProgramSearch root residual、Config finite completed programs
ProgramValidator program、target atoms、Config validation metrics、atom owners

27. 测试要求

27.1 确定性

对相同 STEP bytes、backend version 和 config 重复运行两次,以下 bytes 必须相同:

  • atom records canonical JSON。
  • event records canonical JSON。
  • CandidateKey 序列。
  • ProgramKey。
  • 非 timeout 情况下的最终 ExplanationResult canonical JSON;运行时间字段除外。

27.2 单 primitive

对 Box、Cylinder、Cone、Sphere 和 ring Torus fixture

ValidExplanation(expected_primitive_program, fixture) == true
ProgramCost(expected_primitive_program) < RawBRepCost(fixture)

27.3 Boolean

对 Box 减 Cylinder、Box 加 Cylinder、同轴 Cylinder 相减 fixture

  • 返回 VALID_EXPLANATION
  • symdiff_volume <= eps_volume
  • max_boundary_distance <= eps_surface
  • max_normal_error <= eps_normal
  • owned_surface_atom_count == surface_atom_count

27.4 Face split

对几何相同但 Face split 不同的两个 STEP

  • 最优程序的 ProgramKey 相同。
  • 最优程序的 ProgramCost 相同。
  • 两者各自 ValidExplanation == true

不比较 atom 数量,因为三角化可能跟随输入 Face split 变化。

27.5 NURBS

  • 可约化 NURBS CylinderRecognizeAnalytic 返回完整 Cylinder 参数且通过采样验证。
  • exact-key 相同的 NURBS Face:产生同一个 exact group。
  • pairwise sampled match 不取传递闭包。
  • 全自由曲面 closed solid:在预算内返回 RawBRep 或包含 RawBRep residual 的有效程序。

27.6 硬预算

每次测试断言:

search_boolean_calls <= max_search_boolean_calls
search_total_kernel_calls <= max_search_total_kernel_calls
expanded_states <= max_expanded_states
each_task_depth <= max_task_depth
validation_boolean_calls <= max_validation_boolean_calls
validation_total_kernel_calls <= max_validation_total_kernel_calls

28. 算法总流程

STEP B-Rep
  |
  v
解析并验证 Config
  |
  v
ValidSolid + 固定参数三角化/边离散
  |
  v
构造有限 SA / EA / AA 和 support groups
  |
  v
从有限 Direction/Position/Axial/Extent 事件枚举 primitive candidates
  |
  v
计算 explanation bitsets、coverage 和 LocalScore
  |
  v
Top-M 候选计算 R+ 与 R-
  |
  v
ExpansionEstimate 门槛
  |
  v
固定模板替换规则 + 有界 Beam Search
  |                              \
  |                               +--> 每个 Slot 无条件可替换为 RawBRep
  v
有限个完成 Program
  |
  v
Render + symmetric difference + 双向边界/法向 + topology signature
  |
  v
SurfaceAtom polarity owner
  |
  v
在 ValidExplanation 集合中最小化 ProgramCost

29. 结论

v0.2 将原始设想翻译为以下有限优化问题。给定输入 T、固定配置 C 和搜索实际枚举到的有限程序集合 Programs(T,C)

[ P^*=argmin_{P\in Programs(T,C)} ProgramCost(P) ]

约束:

[ ValidExplanation(P,T)=true ]

基础单元常见性由 PrimitiveCostNurbsCostRawBRepCost 表示;解释多少边和面由 CandidateExplanation bitset 及加权覆盖率表示;“基础几何元素 + 剩余部分”由两个 regularized cut 和第 15.3 节固定程序模板表示;递归由有限事件、候选上限、Beam Width、任务深度、状态数、kernel 调用数、Boolean 调用数和 deadline 限制。

因此该规范不需要枚举任意空间中的全部几何体,也不需要对一般 NURBS 求不可控的全局拟合;在所有搜索路径失败或预算耗尽时,RawBRep 分支给出有限、可重放的兜底程序。