From 6f44a7e4dbc580af826dd0264d4f8e56bf28eff2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 21 Oct 2026 02:54:22 +0000 Subject: [PATCH 1/2] Initial plan From 6303399634b815de2b4d6e65c520eb854ce695ac Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 21 Oct 2026 03:23:25 +0000 Subject: [PATCH 2/2] Apply remaining changes Co-authored-by: chemplexity <5705840+chemplexity@users.noreply.github.com> --- src/layout/engine/cleanup/stage-runner.js | 16 +++++++++++++++- src/layout/engine/cleanup/symmetry-tidy.js | 10 ++++++++++ src/layout/engine/pipeline.js | 6 +++++- 3 files changed, 30 insertions(+), 2 deletions(-) diff --git a/src/layout/engine/cleanup/stage-runner.js b/src/layout/engine/cleanup/stage-runner.js index e74753ac..85baa0ef 100644 --- a/src/layout/engine/cleanup/stage-runner.js +++ b/src/layout/engine/cleanup/stage-runner.js @@ -135,6 +135,20 @@ function resolveParentStageResult(stageResults, baselineStage, parentStage, best return null; } +function movesFixedAtoms(layoutGraph, parentCoords, candidateCoords) { + if (!layoutGraph || layoutGraph.options?.preserveFixed === false || !(layoutGraph.fixedCoords?.size > 0)) { + return false; + } + for (const atomId of layoutGraph.fixedCoords.keys()) { + const before = parentCoords.get(atomId); + const after = candidateCoords.get(atomId); + if (before && after && (before.x !== after.x || before.y !== after.y)) { + return true; + } + } + return false; +} + /** * Executes a cleanup-stage DAG while tracking the current incumbent and stage sidecars. * @param {object[]} stages - Ordered stage descriptors. @@ -209,7 +223,7 @@ export function runStageGraph(stages, baselineStage, context, seedState = null) } const prevBestStage = bestStage; - const accepted = !stage.comparatorFn || stage.comparatorFn(stageResult, bestStage, context, stageResults); + const accepted = !movesFixedAtoms(context.layoutGraph, parentStageResult.coords, stageResult.coords) && (!stage.comparatorFn || stage.comparatorFn(stageResult, bestStage, context, stageResults)); stageExecution.accepted = accepted; if (accepted) { bestStage = stageResult; diff --git a/src/layout/engine/cleanup/symmetry-tidy.js b/src/layout/engine/cleanup/symmetry-tidy.js index d3895b25..8a702a51 100644 --- a/src/layout/engine/cleanup/symmetry-tidy.js +++ b/src/layout/engine/cleanup/symmetry-tidy.js @@ -7,6 +7,10 @@ import { angleOf, angularDifference, centroidForAtomIds, rotate, sub } from '../ const IDEMPOTENT_AXIS_EPSILON = 1e-12; const LOCAL_AXIS_SNAP_ANGLE_EPSILON = Math.PI / 720; +function isFixedAtom(layoutGraph, atomId) { + return layoutGraph?.options?.preserveFixed !== false && layoutGraph?.fixedCoords?.has(atomId) === true; +} + /** * Returns the qualifying fused ring-junction pairs that should be snapped to an axis. * @param {object} layoutGraph - Layout graph shell. @@ -43,6 +47,9 @@ function collectJunctionTargets(layoutGraph) { if (!component) { continue; } + if (component.atomIds.some(atomId => isFixedAtom(layoutGraph, atomId))) { + continue; + } if (!targets.has(component.id)) { targets.set(component.id, { atomIds: [...component.atomIds], @@ -330,6 +337,9 @@ export function tidySymmetry(inputCoords, options = {}) { for (const [atomId, position] of junctionSnap.coords) { const nextPosition = { ...position }; + if (isFixedAtom(options.layoutGraph, atomId)) { + continue; + } if (Math.abs(nextPosition.x) <= epsilon) { const snappedPosition = { ...nextPosition, x: 0 }; if (!wouldAxisSnapDistortLocalGeometry(options.layoutGraph, coords, atomId, snappedPosition)) { diff --git a/src/layout/engine/pipeline.js b/src/layout/engine/pipeline.js index 211b6836..fcf139d9 100644 --- a/src/layout/engine/pipeline.js +++ b/src/layout/engine/pipeline.js @@ -16600,6 +16600,7 @@ export function runPipeline(molecule, options = {}) { const postRoutingLargeDirtyResidualRetouch = timeFinalRetouch('postRoutingLargeDirtyResidualRetouch', () => runLargeMoleculeResidualRetouch(layoutGraph, finalCoords, { bondLength: normalizedOptions.bondLength, + frozenAtomIds: placement.frozenAtomIds, residualOnly: true }) ); @@ -16827,7 +16828,8 @@ export function runPipeline(molecule, options = {}) { if (placement.placedFamilies.includes('large-molecule') || familySummary.primaryFamily === 'large-molecule') { const largeMoleculeResidualRetouch = timeFinalRetouch('largeMoleculeResidualRetouch', () => runLargeMoleculeResidualRetouch(layoutGraph, finalCoords, { - bondLength: normalizedOptions.bondLength + bondLength: normalizedOptions.bondLength, + frozenAtomIds: placement.frozenAtomIds }) ); if (largeMoleculeResidualRetouch.changed) { @@ -18623,6 +18625,7 @@ export function runPipeline(molecule, options = {}) { const lateLargeMoleculeResidualRetouch = timeFinalRetouch('lateLargeMoleculeResidualRetouch', () => runLargeMoleculeResidualRetouch(layoutGraph, finalCoords, { bondLength: normalizedOptions.bondLength, + frozenAtomIds: placement.frozenAtomIds, residualOnly: true }) ); @@ -18711,6 +18714,7 @@ export function runPipeline(molecule, options = {}) { const finalLargeMoleculeResidualRetouch = timeFinalRetouch('finalLargeMoleculeResidualRetouch', () => runLargeMoleculeResidualRetouch(layoutGraph, finalCoords, { bondLength: normalizedOptions.bondLength, + frozenAtomIds: placement.frozenAtomIds, residualOnly: true }) );