or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

analysis-tools.mdauxiliary-data.mdconverters.mdcoordinate-transformations.mdcore-functionality.mdindex.mdio-formats.mdselection-language.mdtopology-handling.mdunits-utilities.md

selection-language.mddocs/

0

# Selection Language

1

2

MDAnalysis provides a powerful and flexible selection language for identifying atoms, residues, and molecular components. The selection syntax is similar to that used in VMD and PyMOL, making it familiar to molecular visualization users while extending functionality for analysis workflows.

3

4

## Overview

5

6

The selection language allows you to:

7

- Select atoms based on properties (names, types, residues, segments)

8

- Use geometric criteria (distances, shapes, regions)

9

- Combine selections with Boolean logic

10

- Create dynamic selections that update with trajectory changes

11

- Perform complex queries with multiple criteria

12

13

## Basic Selection Syntax

14

15

### Core Selection Function

16

17

```python { .api }

18

def select_atoms(universe_or_atomgroup, *args, **kwargs):

19

"""

20

Select atoms using the MDAnalysis selection language.

21

22

Parameters

23

----------

24

universe_or_atomgroup : Universe or AtomGroup

25

Source for atom selection.

26

*args : str

27

Selection string(s) in MDAnalysis selection language.

28

updating : bool, optional

29

Whether selection should update when topology changes (default False).

30

periodic : bool, optional

31

Whether to account for periodic boundary conditions in

32

geometric selections (default True).

33

rtol : float, optional

34

Relative tolerance for floating point comparisons (default 1e-5).

35

atol : float, optional

36

Absolute tolerance for floating point comparisons (default 1e-8).

37

38

Returns

39

-------

40

AtomGroup

41

Selected atoms matching the criteria.

42

43

Examples

44

--------

45

>>> # Basic property selections

46

>>> ca_atoms = u.select_atoms("name CA")

47

>>> protein = u.select_atoms("protein")

48

>>> waters = u.select_atoms("resname SOL TIP3 WAT")

49

50

>>> # Geometric selections

51

>>> near_protein = u.select_atoms("around 5.0 protein")

52

>>> binding_site = u.select_atoms("resid 23-45 and around 8.0 resname LIG")

53

54

>>> # Complex Boolean logic

55

>>> flexible = u.select_atoms("protein and not backbone and not resname PRO")

56

"""

57

```

58

59

## Property-Based Selections

60

61

### Atom Properties

62

63

```python { .api }

64

# Atom name selections

65

u.select_atoms("name CA") # Alpha carbons

66

u.select_atoms("name CA CB CG") # Multiple atom names

67

u.select_atoms("name C*") # Wildcard: all carbon atoms

68

u.select_atoms("name *A") # Names ending with A

69

70

# Atom type selections

71

u.select_atoms("type CT NH1 OH") # Specific atom types

72

u.select_atoms("type CT*") # Types starting with CT

73

74

# Mass selections

75

u.select_atoms("mass 12.01") # Specific mass (carbon)

76

u.select_atoms("mass > 14.0") # Heavier than nitrogen

77

u.select_atoms("mass 12.0:16.0") # Mass range

78

79

# Charge selections

80

u.select_atoms("charge 0.0") # Neutral atoms

81

u.select_atoms("charge < -0.5") # Highly negative

82

u.select_atoms("charge -0.1:0.1") # Nearly neutral range

83

84

# Element selections (if available)

85

u.select_atoms("element C") # Carbon atoms

86

u.select_atoms("element N O") # Nitrogen or oxygen

87

```

88

89

### Residue Properties

90

91

```python { .api }

92

# Residue name selections

93

u.select_atoms("resname ALA") # Alanine residues

94

u.select_atoms("resname ALA GLY PRO") # Multiple residue types

95

u.select_atoms("resname A*") # Names starting with A

96

97

# Residue ID selections

98

u.select_atoms("resid 1") # Residue 1

99

u.select_atoms("resid 1 5 10") # Specific residues

100

u.select_atoms("resid 1-10") # Range of residues

101

u.select_atoms("resid 1:10:2") # Range with step (1,3,5,7,9)

102

103

# Residue number selections (different from resid)

104

u.select_atoms("resnum 1-100") # Sequential numbering

105

106

# Insertion codes (for PDB files)

107

u.select_atoms("icode A") # Specific insertion code

108

u.select_atoms("icode ''") # Empty insertion code

109

```

110

111

### Segment Properties

112

113

```python { .api }

114

# Segment ID selections

115

u.select_atoms("segid PROA") # Specific segment

116

u.select_atoms("segid PROA PROB") # Multiple segments

117

u.select_atoms("segid PRO*") # Wildcard matching

118

119

# Combining segment and residue selections

120

u.select_atoms("segid PROA and resid 1-100")

121

u.select_atoms("segid PROB and resname ALA")

122

```

123

124

## Predefined Selections

125

126

### Common Macros

127

128

```python { .api }

129

# Protein components

130

u.select_atoms("protein") # All protein atoms

131

u.select_atoms("backbone") # Protein backbone (N, CA, C, O)

132

u.select_atoms("sidechain") # Protein sidechains

133

u.select_atoms("nucleic") # Nucleic acid atoms

134

u.select_atoms("nucleicacid") # Alias for nucleic

135

136

# Water and ions

137

u.select_atoms("water") # Water molecules

138

u.select_atoms("ion") # Ion atoms

139

140

# Secondary structure (if available)

141

u.select_atoms("alpha") # Alpha helix

142

u.select_atoms("beta") # Beta sheet

143

u.select_atoms("coil") # Random coil

144

145

# Atom categories

146

u.select_atoms("hydrogen") # Hydrogen atoms

147

u.select_atoms("heavy") # Heavy (non-hydrogen) atoms

148

u.select_atoms("donor") # Hydrogen bond donors

149

u.select_atoms("acceptor") # Hydrogen bond acceptors

150

```

151

152

### Predefined Residue Groups

153

154

```python { .api }

155

# Amino acid categories

156

u.select_atoms("acidic") # Acidic residues (ASP, GLU)

157

u.select_atoms("basic") # Basic residues (LYS, ARG, HIS)

158

u.select_atoms("polar") # Polar residues

159

u.select_atoms("nonpolar") # Nonpolar residues

160

u.select_atoms("charged") # Charged residues (acidic + basic)

161

u.select_atoms("aromatic") # Aromatic residues (PHE, TYR, TRP)

162

u.select_atoms("hydrophobic") # Hydrophobic residues

163

u.select_atoms("small") # Small residues (ALA, GLY, SER)

164

u.select_atoms("medium") # Medium-sized residues

165

u.select_atoms("large") # Large residues

166

167

# Specific amino acid types

168

u.select_atoms("aliphatic") # Aliphatic residues

169

u.select_atoms("sulfur") # Sulfur-containing (CYS, MET)

170

u.select_atoms("alcoholic") # Alcohol groups (SER, THR, TYR)

171

u.select_atoms("amide") # Amide groups (ASN, GLN)

172

```

173

174

## Geometric Selections

175

176

### Distance-Based Selections

177

178

```python { .api }

179

# Around selections - atoms within distance

180

u.select_atoms("around 5.0 protein") # Within 5Å of protein

181

u.select_atoms("around 3.5 resname LIG") # Near ligand

182

u.select_atoms("around 8.0 (resid 23 and name CA)") # Near specific atom

183

184

# Point selections - around coordinates

185

u.select_atoms("point 10.0 20.0 30.0 5.0") # Within 5Å of point

186

u.select_atoms("point 0 0 0 10.0") # Near origin

187

188

# Spherical layer selections

189

u.select_atoms("sphlayer 2.0 5.0 protein") # Shell around protein

190

u.select_atoms("spherical_layer 1.5 4.0 resname LIG") # Full name

191

192

# Cylindrical selections

193

u.select_atoms("cylayer 2.0 5.0 10.0 -5.0 protein") # Cylindrical shell

194

u.select_atoms("cylinder 5.0 10.0 -5.0 protein") # Inside cylinder

195

196

# Isolation selections - isolated atoms/residues

197

u.select_atoms("isolated") # Atoms with no neighbors

198

u.select_atoms("isolated 5.0") # No neighbors within 5Å

199

```

200

201

### Shape-Based Selections

202

203

```python { .api }

204

# Slab selections - between parallel planes

205

u.select_atoms("slab 10.0 20.0 z") # Z-coordinate slab

206

u.select_atoms("slab -5.0 5.0 x") # X-coordinate slab

207

208

# Box selections - rectangular regions

209

u.select_atoms("prop x > 10.0") # X coordinate > 10

210

u.select_atoms("prop y < -5.0") # Y coordinate < -5

211

u.select_atoms("prop z 0.0:10.0") # Z range

212

213

# Complex geometric combinations

214

u.select_atoms("around 8.0 protein and prop z > 0") # Upper half near protein

215

u.select_atoms("sphlayer 3.0 6.0 protein and not water") # Protein shell, no water

216

```

217

218

## Boolean Logic and Combinations

219

220

### Logical Operators

221

222

```python { .api }

223

# AND operations

224

u.select_atoms("protein and name CA") # Protein alpha carbons

225

u.select_atoms("resname ALA and name CB") # Alanine beta carbons

226

u.select_atoms("resid 1-10 and backbone") # N-terminal backbone

227

228

# OR operations

229

u.select_atoms("resname ALA or resname GLY") # Alanine or glycine

230

u.select_atoms("name CA or name CB") # Alpha or beta carbons

231

u.select_atoms("segid PROA or segid PROB") # Multiple segments

232

233

# NOT operations

234

u.select_atoms("protein and not backbone") # Protein sidechains

235

u.select_atoms("not water and not ion") # Non-solvent atoms

236

u.select_atoms("around 5.0 protein and not protein") # Solvent around protein

237

238

# Complex combinations with parentheses

239

u.select_atoms("(resname ALA or resname GLY) and name CA")

240

u.select_atoms("protein and not (backbone or resname PRO)")

241

u.select_atoms("(around 5.0 resname LIG) and (protein or nucleic)")

242

```

243

244

### Selection Algebra

245

246

```python { .api }

247

# Using AtomGroup operations for complex logic

248

protein = u.select_atoms("protein")

249

ligand = u.select_atoms("resname LIG")

250

waters = u.select_atoms("resname SOL")

251

252

# Intersection

253

binding_site_protein = protein & u.select_atoms("around 5.0 resname LIG")

254

255

# Union

256

protein_and_ligand = protein | ligand

257

258

# Difference

259

sidechain = protein - u.select_atoms("backbone")

260

261

# Symmetric difference

262

mobile_region = (protein | ligand) ^ u.select_atoms("around 3.0 (protein or resname LIG)")

263

```

264

265

## Dynamic and Updating Selections

266

267

### Updating Selections

268

269

```python { .api }

270

# Static selections (default) - fixed atom set

271

static_near = u.select_atoms("around 5.0 resname LIG")

272

273

# Dynamic selections - update each frame

274

dynamic_near = u.select_atoms("around 5.0 resname LIG", updating=True)

275

276

# Example of difference

277

ligand = u.select_atoms("resname LIG")

278

for ts in u.trajectory:

279

# Static selection - same atoms throughout

280

static_atoms = static_near.positions

281

282

# Dynamic selection - atoms change as ligand moves

283

dynamic_atoms = dynamic_near.positions

284

285

print(f"Frame {ts.frame}: Static={len(static_near)}, Dynamic={len(dynamic_near)}")

286

```

287

288

### Periodic Boundary Considerations

289

290

```python { .api }

291

# Selections accounting for periodic boundaries

292

u.select_atoms("around 5.0 protein", periodic=True) # Default behavior

293

u.select_atoms("around 5.0 protein", periodic=False) # Ignore PBC

294

295

# Important for systems where molecules cross box boundaries

296

wrapped_selection = u.select_atoms("sphlayer 2.0 5.0 protein", periodic=True)

297

```

298

299

## Advanced Selection Patterns

300

301

### Property Ranges and Comparisons

302

303

```python { .api }

304

# Numerical comparisons

305

u.select_atoms("mass > 15.0") # Heavy atoms

306

u.select_atoms("charge < -0.5") # Negative atoms

307

u.select_atoms("charge >= -0.1 and charge <= 0.1") # Nearly neutral

308

309

# Range specifications

310

u.select_atoms("resid 1:100") # Residue range

311

u.select_atoms("mass 12.0:16.0") # Mass range

312

u.select_atoms("prop x -10.0:10.0") # Coordinate range

313

314

# Multiple ranges

315

u.select_atoms("resid 1-50 100-150 200-250") # Multiple residue ranges

316

u.select_atoms("name CA CB CG CD CE") # Multiple names

317

```

318

319

### Pattern Matching

320

321

```python { .api }

322

# Wildcard patterns

323

u.select_atoms("name C*") # Names starting with C

324

u.select_atoms("name *A") # Names ending with A

325

u.select_atoms("name H*") # All hydrogens

326

u.select_atoms("resname A*") # Residues starting with A

327

u.select_atoms("segid *A") # Segments ending with A

328

329

# Regular expressions (when supported)

330

u.select_atoms("name ~/^C[0-9]+$/") # C followed by numbers

331

u.select_atoms("resname ~/^(ALA|GLY|PRO)$/") # Specific residues

332

```

333

334

### Index-Based Selections

335

336

```python { .api }

337

# Atom index selections

338

u.select_atoms("index 0") # First atom

339

u.select_atoms("index 0 10 20 30") # Specific indices

340

u.select_atoms("index 0-99") # Index range

341

u.select_atoms("index 0:100:10") # Every 10th atom

342

343

# Combining indices with other criteria

344

u.select_atoms("index 0-999 and protein") # First 1000 protein atoms

345

u.select_atoms("protein and index 0:1000:2") # Every other protein atom

346

```

347

348

## Selection Language Extensions

349

350

### Custom Selection Keywords

351

352

```python { .api }

353

# MDAnalysis allows custom selection keyword definitions

354

from MDAnalysis.core.selection import SelectionParser

355

356

def hydrophobic_residues(group):

357

"""Custom selection for hydrophobic residues."""

358

hydrophobic = ['ALA', 'VAL', 'LEU', 'ILE', 'PHE', 'PRO', 'MET', 'TRP']

359

return group.residues[np.isin(group.residues.resnames, hydrophobic)].atoms

360

361

# Register custom selection

362

SelectionParser.parse_tokens.append(('hydrophobic_residues', hydrophobic_residues))

363

364

# Now can use in selections

365

u.select_atoms("hydrophobic_residues and name CA")

366

```

367

368

### Precomputed Selections

369

370

```python { .api }

371

def create_selection_library(universe):

372

"""

373

Create library of commonly used selections for a system.

374

"""

375

selections = {}

376

377

# Basic components

378

selections['protein'] = universe.select_atoms("protein")

379

selections['backbone'] = universe.select_atoms("backbone")

380

selections['sidechain'] = universe.select_atoms("protein and not backbone")

381

selections['water'] = universe.select_atoms("resname SOL WAT TIP*")

382

selections['ions'] = universe.select_atoms("ion")

383

384

# Secondary structure regions (if available)

385

try:

386

selections['helix'] = universe.select_atoms("alpha")

387

selections['sheet'] = universe.select_atoms("beta")

388

selections['loop'] = universe.select_atoms("coil")

389

except:

390

pass

391

392

# Functional regions

393

selections['ca_atoms'] = universe.select_atoms("name CA")

394

selections['heavy_atoms'] = universe.select_atoms("not name H*")

395

396

# Binding site (example - customize for specific system)

397

if 'LIG' in universe.residues.resnames:

398

selections['ligand'] = universe.select_atoms("resname LIG")

399

selections['binding_site'] = universe.select_atoms("around 5.0 resname LIG and protein")

400

selections['first_shell'] = universe.select_atoms("sphlayer 0.0 5.0 resname LIG")

401

selections['second_shell'] = universe.select_atoms("sphlayer 5.0 10.0 resname LIG")

402

403

return selections

404

405

# Create and use selection library

406

sel_lib = create_selection_library(u)

407

protein_ca = sel_lib['ca_atoms']

408

binding_site = sel_lib.get('binding_site', u.select_atoms("name CA")) # Fallback

409

```

410

411

## Performance Optimization

412

413

### Selection Caching

414

415

```python { .api }

416

class SelectionCache:

417

"""

418

Cache frequently used selections for performance.

419

"""

420

421

def __init__(self, universe):

422

self.universe = universe

423

self._cache = {}

424

425

def get_selection(self, selection_string, **kwargs):

426

"""

427

Get selection with caching.

428

429

Parameters

430

----------

431

selection_string : str

432

Selection string.

433

**kwargs

434

Additional selection arguments.

435

436

Returns

437

-------

438

AtomGroup

439

Cached or newly created selection.

440

"""

441

# Create cache key from selection string and kwargs

442

cache_key = (selection_string, tuple(sorted(kwargs.items())))

443

444

if cache_key not in self._cache:

445

self._cache[cache_key] = self.universe.select_atoms(

446

selection_string, **kwargs

447

)

448

449

return self._cache[cache_key]

450

451

def clear_cache(self):

452

"""Clear selection cache."""

453

self._cache.clear()

454

455

# Usage

456

cache = SelectionCache(u)

457

458

# These will be cached after first call

459

protein = cache.get_selection("protein")

460

ca_atoms = cache.get_selection("name CA")

461

backbone = cache.get_selection("backbone")

462

463

# Fast subsequent access

464

for ts in u.trajectory:

465

# Fast access to cached selections

466

protein_com = protein.center_of_mass()

467

ca_positions = ca_atoms.positions

468

```

469

470

### Efficient Selection Strategies

471

472

```python { .api }

473

def optimize_selection_workflow():

474

"""

475

Demonstrate efficient selection strategies.

476

"""

477

# Strategy 1: Use broad selections first, then narrow

478

# Less efficient:

479

# all_ca = u.select_atoms("name CA")

480

# protein_ca = all_ca.select_atoms("protein") # Re-selection overhead

481

482

# More efficient:

483

protein_ca = u.select_atoms("protein and name CA") # Single selection

484

485

# Strategy 2: Reuse selections when possible

486

protein = u.select_atoms("protein")

487

backbone = protein.select_atoms("backbone") # Subset of existing selection

488

sidechain = protein.select_atoms("not backbone") # Complement

489

490

# Strategy 3: Use static selections for unchanging criteria

491

static_protein = u.select_atoms("protein") # Won't change

492

493

# Strategy 4: Use updating selections only when needed

494

dynamic_contacts = u.select_atoms("around 5.0 resname LIG", updating=True)

495

496

# Strategy 5: Batch similar selections

497

selections = {

498

'ca': u.select_atoms("name CA"),

499

'cb': u.select_atoms("name CB"),

500

'backbone': u.select_atoms("backbone"),

501

'sidechain': u.select_atoms("protein and not backbone")

502

}

503

504

return selections

505

506

# Apply optimization strategies

507

optimized_sels = optimize_selection_workflow()

508

```

509

510

## Usage Examples

511

512

### Complex Selection Workflows

513

514

```python { .api }

515

def analyze_binding_pocket(universe, ligand_resname="LIG"):

516

"""

517

Comprehensive binding pocket analysis using selections.

518

"""

519

# Define key selections

520

ligand = universe.select_atoms(f"resname {ligand_resname}")

521

if len(ligand) == 0:

522

raise ValueError(f"No ligand found with resname {ligand_resname}")

523

524

# Binding pocket definition with multiple criteria

525

pocket_atoms = universe.select_atoms(

526

f"(around 5.0 resname {ligand_resname}) and protein and (not backbone)"

527

)

528

529

# Different shells around ligand

530

first_shell = universe.select_atoms(f"sphlayer 0.0 4.0 resname {ligand_resname}")

531

second_shell = universe.select_atoms(f"sphlayer 4.0 8.0 resname {ligand_resname}")

532

533

# Specific interaction partners

534

hbond_partners = universe.select_atoms(

535

f"(around 3.5 resname {ligand_resname}) and (donor or acceptor)"

536

)

537

538

# Hydrophobic contacts

539

hydrophobic_contacts = universe.select_atoms(

540

f"(around 4.5 resname {ligand_resname}) and "

541

f"(resname ALA VAL LEU ILE PHE PRO MET TRP) and "

542

f"(not backbone)"

543

)

544

545

# Salt bridge partners

546

salt_bridge_partners = universe.select_atoms(

547

f"(around 6.0 resname {ligand_resname}) and "

548

f"(resname LYS ARG ASP GLU) and "

549

f"(name NZ NH1 NH2 OD1 OD2 OE1 OE2)"

550

)

551

552

# Analysis results

553

results = {

554

'ligand_atoms': len(ligand),

555

'pocket_atoms': len(pocket_atoms),

556

'first_shell_atoms': len(first_shell),

557

'second_shell_atoms': len(second_shell),

558

'hbond_partners': len(hbond_partners),

559

'hydrophobic_contacts': len(hydrophobic_contacts),

560

'salt_bridge_partners': len(salt_bridge_partners)

561

}

562

563

# Residue-level analysis

564

pocket_residues = pocket_atoms.residues

565

results['pocket_residues'] = len(pocket_residues)

566

results['pocket_resnames'] = list(set(pocket_residues.resnames))

567

568

return results, {

569

'ligand': ligand,

570

'pocket': pocket_atoms,

571

'hbond_partners': hbond_partners,

572

'hydrophobic': hydrophobic_contacts,

573

'salt_bridges': salt_bridge_partners

574

}

575

576

# Run binding pocket analysis

577

results, selections = analyze_binding_pocket(u, "LIG")

578

print("Binding Pocket Analysis:")

579

for key, value in results.items():

580

print(f" {key}: {value}")

581

```

582

583

### Dynamic Selection Analysis

584

585

```python { .api }

586

def track_selection_changes(universe, selection_string, updating=True):

587

"""

588

Track how selection membership changes over trajectory.

589

"""

590

selection = universe.select_atoms(selection_string, updating=updating)

591

592

results = {

593

'frame': [],

594

'n_atoms': [],

595

'atom_indices': [],

596

'unique_atoms': set()

597

}

598

599

for ts in universe.trajectory:

600

frame = ts.frame

601

n_atoms = len(selection)

602

atom_indices = selection.indices.copy()

603

604

results['frame'].append(frame)

605

results['n_atoms'].append(n_atoms)

606

results['atom_indices'].append(atom_indices)

607

results['unique_atoms'].update(atom_indices)

608

609

# Summary statistics

610

results['mean_atoms'] = np.mean(results['n_atoms'])

611

results['std_atoms'] = np.std(results['n_atoms'])

612

results['min_atoms'] = np.min(results['n_atoms'])

613

results['max_atoms'] = np.max(results['n_atoms'])

614

results['total_unique'] = len(results['unique_atoms'])

615

616

return results

617

618

# Example: Track water molecules near protein

619

water_dynamics = track_selection_changes(

620

u, "resname SOL and around 5.0 protein", updating=True

621

)

622

623

print(f"Water dynamics around protein:")

624

print(f" Average water molecules: {water_dynamics['mean_atoms']:.1f}")

625

print(f" Range: {water_dynamics['min_atoms']}-{water_dynamics['max_atoms']}")

626

print(f" Total unique waters: {water_dynamics['total_unique']}")

627

```

628

629

### Selection Validation

630

631

```python { .api }

632

def validate_selections(universe, selection_dict):

633

"""

634

Validate multiple selections and check for issues.

635

"""

636

validation_results = {}

637

638

for name, selection_string in selection_dict.items():

639

try:

640

# Test selection

641

atoms = universe.select_atoms(selection_string)

642

643

# Basic validation

644

results = {

645

'valid': True,

646

'n_atoms': len(atoms),

647

'n_residues': len(atoms.residues) if len(atoms) > 0 else 0,

648

'n_segments': len(atoms.segments) if len(atoms) > 0 else 0,

649

'warnings': []

650

}

651

652

# Check for empty selection

653

if len(atoms) == 0:

654

results['warnings'].append("Selection returned no atoms")

655

656

# Check for very large selections (might be inefficient)

657

if len(atoms) > len(universe.atoms) * 0.8:

658

results['warnings'].append("Selection includes >80% of system")

659

660

# Check for geometric selections without proper context

661

if any(keyword in selection_string.lower()

662

for keyword in ['around', 'within', 'sphlayer']):

663

if 'updating' not in selection_string:

664

results['warnings'].append(

665

"Geometric selection might benefit from updating=True"

666

)

667

668

validation_results[name] = results

669

670

except Exception as e:

671

validation_results[name] = {

672

'valid': False,

673

'error': str(e),

674

'n_atoms': 0

675

}

676

677

return validation_results

678

679

# Example selection validation

680

test_selections = {

681

'protein': "protein",

682

'ca_atoms': "name CA",

683

'binding_site': "around 5.0 resname LIG and protein",

684

'invalid': "name INVALID_ATOM",

685

'water_shell': "sphlayer 5.0 10.0 protein and resname SOL"

686

}

687

688

validation = validate_selections(u, test_selections)

689

for name, result in validation.items():

690

if result['valid']:

691

print(f"{name}: {result['n_atoms']} atoms")

692

for warning in result.get('warnings', []):

693

print(f" Warning: {warning}")

694

else:

695

print(f"{name}: ERROR - {result['error']}")

696

```