Skip to content

Commit 73c7100

Browse files
Add a preflight network check to error out with disconnected subunits
1 parent a027be6 commit 73c7100

File tree

3 files changed

+385
-1
lines changed

3 files changed

+385
-1
lines changed

ionerdss/model/pdb/main.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
from .template_builder import TemplateBuilder
2323
from .system_builder import SystemBuilder
2424
from .file_manager import WorkspaceManager
25-
from .structure_validation import StructureValidationArtifacts
25+
from .structure_validation import StructureValidationArtifacts, get_disconnected_design_message
2626

2727

2828
class PDBModelBuilder:
@@ -239,6 +239,13 @@ def notice(self, message, *args, **kwargs):
239239
system = system_builder.get_system()
240240
self.system = system
241241

242+
disconnected_design_message = get_disconnected_design_message(
243+
system,
244+
prefix="Preflight error",
245+
)
246+
if disconnected_design_message is not None:
247+
raise ValueError(disconnected_design_message)
248+
242249
# Calculate default molecule counts if not provided (using stoichiometry)
243250
if molecule_counts is None:
244251
molecule_counts = {}

ionerdss/model/pdb/structure_validation.py

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ class StructureValidationArtifacts:
5252
designed_coordinates: Dict[str, Tuple[float, float, float]]
5353
target_file: Path
5454
nerdss_files: Dict[str, Path]
55+
preflight_warning_message: Optional[str] = None
5556

5657

5758
@dataclass(frozen=True)
@@ -292,6 +293,82 @@ def get_structure_validation_counts(system: System) -> Dict[str, int]:
292293
return dict(sorted(counts.items()))
293294

294295

296+
def _get_designed_connected_components(system: System) -> list[tuple[str, ...]]:
297+
"""Return connected components of the designed assembly using molecule instance names."""
298+
adjacency: Dict[str, set[str]] = defaultdict(set)
299+
for molecule_instance in system.molecule_instances:
300+
adjacency.setdefault(molecule_instance.name, set())
301+
for _interface_instance, partner_instance in molecule_instance.interfaces_neighbors_map.items():
302+
if partner_instance is None:
303+
continue
304+
adjacency[molecule_instance.name].add(partner_instance.name)
305+
adjacency[partner_instance.name].add(molecule_instance.name)
306+
307+
components: list[tuple[str, ...]] = []
308+
visited: set[str] = set()
309+
for instance_name in sorted(adjacency):
310+
if instance_name in visited:
311+
continue
312+
313+
stack = [instance_name]
314+
visited.add(instance_name)
315+
component: list[str] = []
316+
while stack:
317+
current = stack.pop()
318+
component.append(current)
319+
for neighbor in sorted(adjacency[current]):
320+
if neighbor not in visited:
321+
visited.add(neighbor)
322+
stack.append(neighbor)
323+
324+
components.append(tuple(sorted(component)))
325+
326+
return components
327+
328+
329+
def _format_disconnected_design_warning(components: Sequence[Sequence[str]]) -> Optional[str]:
330+
"""Describe disconnected designed subunit groups for early validation feedback."""
331+
if len(components) <= 1:
332+
return None
333+
formatted_components = [
334+
", ".join(component)
335+
for component in sorted((tuple(component) for component in components), key=lambda comp: comp)
336+
]
337+
if len(formatted_components) == 2:
338+
return (
339+
"Validation preflight warning: the designed assembly graph is disconnected, so it cannot form a "
340+
f"single N-mer. Subunits {formatted_components[0]} are disconnected from subunits "
341+
f"{formatted_components[1]}."
342+
)
343+
344+
return (
345+
"Validation preflight warning: the designed assembly graph is disconnected, so it cannot form a "
346+
f"single N-mer. Connected subunit groups: {'; '.join(formatted_components)}."
347+
)
348+
349+
350+
def get_disconnected_design_message(system: System, *, prefix: str) -> Optional[str]:
351+
"""Return a formatted disconnected-assembly message for the given system."""
352+
components = _get_designed_connected_components(system)
353+
if len(components) <= 1:
354+
return None
355+
356+
formatted_components = [
357+
", ".join(component)
358+
for component in sorted((tuple(component) for component in components), key=lambda comp: comp)
359+
]
360+
if len(formatted_components) == 2:
361+
return (
362+
f"{prefix}: the designed assembly graph is disconnected, so it cannot form a single N-mer. "
363+
f"Subunits {formatted_components[0]} are disconnected from subunits {formatted_components[1]}."
364+
)
365+
366+
return (
367+
f"{prefix}: the designed assembly graph is disconnected, so it cannot form a single N-mer. "
368+
f"Connected subunit groups: {'; '.join(formatted_components)}."
369+
)
370+
371+
295372
def build_validation_molecule_counts(system: System, initial_molecule_count: int = 1) -> Dict[str, int]:
296373
"""Return validation counts with a configurable initial copy number per molecule type."""
297374
target_counts = get_structure_validation_counts(system)
@@ -403,6 +480,9 @@ def prepare_structure_validation(
403480
initial_molecule_count=config.initial_molecule_count,
404481
)
405482
target_counts = get_structure_validation_counts(system)
483+
preflight_warning_message = _format_disconnected_design_warning(
484+
_get_designed_connected_components(system)
485+
)
406486

407487
final_designed_coordinates = {
408488
key: tuple(float(value) for value in coords)
@@ -442,12 +522,16 @@ def prepare_structure_validation(
442522
shutil.copyfile(parms_path, titration_parms_path)
443523
nerdss_files["parms_titrate"] = titration_parms_path
444524

525+
if preflight_warning_message:
526+
warnings.warn(preflight_warning_message, RuntimeWarning)
527+
445528
return StructureValidationArtifacts(
446529
molecule_counts=molecule_counts,
447530
target_counts=target_counts,
448531
designed_coordinates=final_designed_coordinates,
449532
target_file=target_file,
450533
nerdss_files=nerdss_files,
534+
preflight_warning_message=preflight_warning_message,
451535
)
452536

453537

0 commit comments

Comments
 (0)