-
-
Notifications
You must be signed in to change notification settings - Fork 4
Report why tie connections fail to couple, refuse silent drops #427
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
Closed
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -705,6 +705,7 @@ export interface CoupledModel { | |
| solidPool: Map<number, number>; // original vertex index → pool index (solid) | ||
| shellPool: number[]; // local shell index → pool index | ||
| refPool: Map<number, number>; // reference-point vertex index → pool index | ||
| tieReports: TieCouplingReport[]; // what each tie connection contributed | ||
| } | ||
|
|
||
| // Boundary of a tet mesh: the faces used by exactly one element, as a flat | ||
|
|
@@ -939,6 +940,78 @@ export interface TieSurfaces { | |
| maxSeparation: number; | ||
| } | ||
|
|
||
| // Why a tie connection ended up coupling nothing. The all-solid weld path already | ||
| // refuses a connection that joins nothing (solver.worker: "connected no nodes"), | ||
| // because an assembly that stays split solves to a plausible-looking but | ||
| // structurally wrong shape — the couplings are the load path. The coupled path | ||
| // used to drop the same connection silently (KOF-203); it now says which of the | ||
| // four ways it failed, since each has a different fix. | ||
| export type TieCouplingDrop = | ||
| // One or both picked surfaces contributed no node to the solved pool. | ||
| | { kind: "no-pool-nodes"; side: "A" | "B" | "both" } | ||
| // The two surfaces never saw each other, even after widening the search. | ||
| | { kind: "out-of-reach"; searched: number } | ||
| // They do meet, but no closer than the connection's own search distance. | ||
| | { kind: "beyond-search-distance"; gap: number; reach: number } | ||
| // In range, but no reference found the three partners an RBE3 needs. | ||
| | { kind: "too-few-partners"; refs: number; radius: number }; | ||
|
|
||
| // What one tie connection actually contributed to the coupled model. | ||
| export interface TieCouplingReport { | ||
| name: string; | ||
| nCoupled: number; // reference nodes that got a distributing coupling | ||
| nPartners: number; // partner slots those references distribute onto | ||
| nShared: number; // nodes the two surfaces already have in common | ||
| gap: number; // measured closest approach, 0 when never measured | ||
| drop?: TieCouplingDrop; | ||
| } | ||
|
|
||
| // The sentence for a connection that coupled nothing, or undefined when it did | ||
| // couple. Nodes the two surfaces SHARE are already rigidly joined through the | ||
| // common pool DOFs, so a connection that only shares is connected, not dropped. | ||
| export function tieCouplingProblem( | ||
| report: TieCouplingReport, | ||
| ): string | undefined { | ||
| if (report.nCoupled > 0 || report.nShared > 0 || !report.drop) | ||
| return undefined; | ||
| const head = `Tie "${report.name}" coupled no nodes`; | ||
| const drop = report.drop; | ||
| switch (drop.kind) { | ||
| case "no-pool-nodes": | ||
| return ( | ||
| `${head} — ${drop.side === "both" ? "neither picked surface has" : `picked surface ${drop.side} has`} ` + | ||
| "a node in the solved model. Re-pick its surfaces after remeshing, or " + | ||
| "mark the body Solid if the tie lands on a wall that was idealised as shell." | ||
| ); | ||
| case "out-of-reach": | ||
| return ( | ||
| `${head} — its two surfaces are more than ${drop.searched.toFixed(4)} mm apart. ` + | ||
| "They are not the surfaces that touch; re-pick them." | ||
| ); | ||
| case "beyond-search-distance": | ||
| return ( | ||
| `${head} — its surfaces come no closer than ${drop.gap.toFixed(4)} mm, ` + | ||
| `beyond its ${drop.reach.toFixed(4)} mm search distance. Increase the ` + | ||
| "distance, or couple the full surface." | ||
| ); | ||
| case "too-few-partners": | ||
| return ( | ||
| `${head} — none of its ${drop.refs} in-range reference node(s) found the ` + | ||
| `three partners a distributing coupling needs within ${drop.radius.toFixed(4)} mm. ` + | ||
| "Refine the mesh on the other surface, or pick more of it." | ||
| ); | ||
| // TieCouplingDrop is a closed union, so this is unreachable. The `never` | ||
| // binding turns adding a drop kind without a message into a compile error, | ||
| // rather than a connection that coupled nothing and reports no reason. | ||
| default: { | ||
| const unhandled: never = drop; | ||
| throw new Error( | ||
| `Cannot say why tie "${report.name}" coupled nothing: unhandled drop ${JSON.stringify(unhandled)}`, | ||
| ); | ||
| } | ||
| } | ||
|
Comment on lines
+979
to
+1012
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| } | ||
|
|
||
| // Distance from each ref node to its nearest master node, for the refs that have | ||
| // one within `reach`. Grid cells are `reach` wide, so the 27-cell scan sees every | ||
| // candidate in range. | ||
|
|
@@ -998,58 +1071,112 @@ function nearestMasterDistances( | |
| // A "within distance" connection additionally keeps only the references within | ||
| // its search distance of the other surface, which is what limits the tie to the | ||
| // part of the surface that actually touches. | ||
| // | ||
| // Every connection gets a report, whether or not it coupled: a connection the | ||
| // user declared and that produced nothing is a missing load path, and the caller | ||
| // refuses it rather than solving a split assembly (KOF-203). | ||
| function tieCouplings( | ||
| ppt: (i: number) => [number, number, number], | ||
| ties: { name: string; masters: number[]; refs: number[]; reach: number }[], | ||
| ties: PoolTie[], | ||
| medEdge: number, | ||
| maxCoupledNodes: number, | ||
| ): CouplingSet { | ||
| ): { coupling: CouplingSet; reports: TieCouplingReport[] } { | ||
| let all: CouplingSet = { ref: [], offsets: [0], solid: [] }; | ||
| const reports: TieCouplingReport[] = []; | ||
| for (const tie of ties) { | ||
| // Nodes the two surfaces share are already rigidly joined through the common | ||
| // pool DOFs, and a coupling reference must not also be a partner. | ||
| const shared = new Set(tie.masters.filter((pi) => tie.refs.includes(pi))); | ||
| const refSet = new Set(tie.refs); | ||
| const shared = new Set(tie.masters.filter((pi) => refSet.has(pi))); | ||
| const masters = tie.masters.filter((pi) => !shared.has(pi)); | ||
| const refs = tie.refs.filter((pi) => !shared.has(pi)); | ||
| if (masters.length === 0 || refs.length === 0) continue; | ||
| const base: TieCouplingReport = { | ||
| name: tie.name, | ||
| nCoupled: 0, | ||
| nPartners: 0, | ||
| nShared: shared.size, | ||
| gap: 0, | ||
| }; | ||
| const dropped = (drop: TieCouplingDrop, gap = 0): void => { | ||
| reports.push({ ...base, gap, drop }); | ||
| }; | ||
| if (masters.length === 0 || refs.length === 0) { | ||
| dropped({ | ||
| kind: "no-pool-nodes", | ||
| side: masters.length === 0 ? (refs.length === 0 ? "both" : "A") : "B", | ||
| }); | ||
| continue; | ||
| } | ||
|
|
||
| let distances = new Map<number, number>(); | ||
| let searched = 0; | ||
| for ( | ||
| let reach = Math.max(2 * medEdge, 1e-9), doublings = 0; | ||
| doublings <= 6 && distances.size === 0; | ||
| reach *= 2, doublings++ | ||
| ) | ||
| ) { | ||
| searched = reach; | ||
| distances = nearestMasterDistances(ppt, masters, refs, reach); | ||
| if (distances.size === 0) continue; // the surfaces never reach each other | ||
| } | ||
| if (distances.size === 0) { | ||
| dropped({ kind: "out-of-reach", searched }); | ||
| continue; | ||
| } | ||
|
|
||
| const gap = Math.min(...distances.values()); | ||
| const kept = [...distances] | ||
| .filter(([, distance]) => distance <= tie.reach) | ||
| .map(([pi]) => pi); | ||
| if (kept.length === 0) continue; | ||
|
|
||
| all = concatCouplings( | ||
| all, | ||
| autoDetectCouplings( | ||
| ppt, | ||
| masters, | ||
| kept, | ||
| partnerSearchRadius(medEdge, gap), | ||
| maxCoupledNodes, | ||
| ), | ||
| if (kept.length === 0) { | ||
| dropped({ kind: "beyond-search-distance", gap, reach: tie.reach }, gap); | ||
| continue; | ||
| } | ||
|
|
||
| const radius = partnerSearchRadius(medEdge, gap); | ||
| const one = autoDetectCouplings( | ||
| ppt, | ||
| masters, | ||
| kept, | ||
| radius, | ||
| maxCoupledNodes, | ||
| ); | ||
| if (one.ref.length === 0) { | ||
| dropped({ kind: "too-few-partners", refs: kept.length, radius }, gap); | ||
| continue; | ||
| } | ||
| all = concatCouplings(all, one); | ||
| reports.push({ | ||
| ...base, | ||
| gap, | ||
| nCoupled: one.ref.length, | ||
| nPartners: one.solid.length, | ||
| }); | ||
| } | ||
| return { ref: all.ref, offsets: all.offsets, solid: all.solid }; | ||
| return { | ||
| coupling: { ref: all.ref, offsets: all.offsets, solid: all.solid }, | ||
| reports, | ||
| }; | ||
| } | ||
|
|
||
| // One tie connection mapped onto pool nodes: surface A becomes the coupling | ||
| // partners, surface B the references. | ||
| interface PoolTie { | ||
| name: string; | ||
| masters: number[]; | ||
| refs: number[]; | ||
| reach: number; | ||
| } | ||
|
|
||
| // Map a connection's two picked surfaces from store vertex indices onto pool | ||
| // nodes. A vertex with no pool node is skipped: on the auto-shell path the thin | ||
| // walls a picked surface covered were replaced by mid-surface shell nodes, which | ||
| // the seam coupling already ties. | ||
| // the seam coupling already ties. A surface left with NO pool node is reported as | ||
| // a dropped tie rather than skipped silently — the seam ties that shell back to | ||
| // its own retained solid, not to the body on the other side of this connection. | ||
| function tiesToPool( | ||
| ties: TieSurfaces[], | ||
| poolOfVertex: (vi: number) => number | undefined, | ||
| ): { name: string; masters: number[]; refs: number[]; reach: number }[] { | ||
| ): PoolTie[] { | ||
| const toPool = (vertices: number[]): number[] => { | ||
| const out: number[] = []; | ||
| for (const vi of vertices) { | ||
|
|
@@ -1192,7 +1319,7 @@ export function buildCoupledModel( | |
| // The tie connections first — their reference nodes become coupling-dependent, | ||
| // so the shell↔solid seam detection must not also target them (a target DOF | ||
| // must be independent). | ||
| const solidCoupling = tieCouplings( | ||
| const { coupling: solidCoupling, reports: tieReports } = tieCouplings( | ||
| ppt, | ||
| tiesToPool(ties, (vi) => solidPool.get(vi)), | ||
| medEdge, | ||
|
|
@@ -1225,6 +1352,7 @@ export function buildCoupledModel( | |
| solidPool, | ||
| shellPool, | ||
| refPool, | ||
| tieReports, | ||
| // The shell↔solid seam is continuous material (a thin wall idealised as shell, | ||
| // tied back to its retained solid) — not a tie connection between parts, and | ||
| // never something the user declares — so it uses the relaxed MPC coupling | ||
|
|
@@ -1255,6 +1383,7 @@ export interface ExplicitCoupledModel { | |
| coupling: CouplingSet; | ||
| poolOfVertex: Map<number, number>; // store vertex index → pool index | ||
| shellPoolIndex: Set<number>; // pool indices carrying shell stiffness | ||
| tieReports: TieCouplingReport[]; // what each tie connection contributed | ||
| } | ||
|
|
||
| export function buildExplicitCoupledModel( | ||
|
|
@@ -1321,7 +1450,7 @@ export function buildExplicitCoupledModel( | |
| const medEdge = medianTetEdge(ppt, tets); | ||
| // Gapped solid↔solid interfaces (a pin in a hole) tied across the clearance by | ||
| // the model's tie connections — the same couplings the auto-shell path builds. | ||
| const solidCoupling = tieCouplings( | ||
| const { coupling: solidCoupling, reports: tieReports } = tieCouplings( | ||
| ppt, | ||
| tiesToPool(ties, (vi) => poolOfVertex.get(vi)), | ||
| medEdge, | ||
|
|
@@ -1370,6 +1499,7 @@ export function buildExplicitCoupledModel( | |
| ), | ||
| poolOfVertex, | ||
| shellPoolIndex, | ||
| tieReports, | ||
| }; | ||
| } | ||
|
|
||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Any code paths that do not have explicit returns will return
undefined. It is recommended to replace any implicit dead-ends that returnundefinedwith areturn nullstatement.