aegiss 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. aegiss/__init__.py +37 -0
  2. aegiss/_version.py +24 -0
  3. aegiss/active_space/__init__.py +21 -0
  4. aegiss/active_space/aegiss.py +446 -0
  5. aegiss/active_space/ao_projection.py +622 -0
  6. aegiss/active_space/entropy_screening.py +279 -0
  7. aegiss/cli.py +154 -0
  8. aegiss/config_dataclasses/__init__.py +22 -0
  9. aegiss/config_dataclasses/dmrg_config.py +159 -0
  10. aegiss/config_dataclasses/entropy_config.py +69 -0
  11. aegiss/config_dataclasses/output_config.py +58 -0
  12. aegiss/config_dataclasses/projection_config.py +170 -0
  13. aegiss/initial_guess/__init__.py +19 -0
  14. aegiss/initial_guess/scf.py +173 -0
  15. aegiss/localization/__init__.py +35 -0
  16. aegiss/localization/fnos.py +66 -0
  17. aegiss/localization/nos.py +77 -0
  18. aegiss/localization/orbital_localization.py +380 -0
  19. aegiss/py.typed +0 -0
  20. aegiss/utils/__init__.py +59 -0
  21. aegiss/utils/banner.py +48 -0
  22. aegiss/utils/dmrg_utils.py +274 -0
  23. aegiss/utils/dump.py +119 -0
  24. aegiss/utils/env.py +110 -0
  25. aegiss/utils/logging.py +91 -0
  26. aegiss/utils/mo_composition.py +353 -0
  27. aegiss/utils/mol_tagger/README.md +34 -0
  28. aegiss/utils/mol_tagger/__init__.py +93 -0
  29. aegiss/utils/mol_tagger/_ao_suggest.py +439 -0
  30. aegiss/utils/mol_tagger/_atoms.py +248 -0
  31. aegiss/utils/mol_tagger/_constants.py +136 -0
  32. aegiss/utils/mol_tagger/_renderer.py +172 -0
  33. aegiss/utils/mol_tagger/_window.py +647 -0
  34. aegiss/utils/orb_display.py +210 -0
  35. aegiss/utils/orbital_indices.py +244 -0
  36. aegiss/utils/plotting.py +981 -0
  37. aegiss/wavefunction/__init__.py +20 -0
  38. aegiss/wavefunction/dmrg_subprocess.py +289 -0
  39. aegiss/wavefunction/methods.py +102 -0
  40. aegiss-0.1.0.dist-info/METADATA +223 -0
  41. aegiss-0.1.0.dist-info/RECORD +45 -0
  42. aegiss-0.1.0.dist-info/WHEEL +4 -0
  43. aegiss-0.1.0.dist-info/entry_points.txt +2 -0
  44. aegiss-0.1.0.dist-info/licenses/LICENSE +201 -0
  45. aegiss-0.1.0.dist-info/licenses/NOTICE +6 -0
aegiss/__init__.py ADDED
@@ -0,0 +1,37 @@
1
+ # Copyright 2026 Algorithmiq
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """AEGISS: Atomic Orbital and Entropy based Guided Inference for Space Selection."""
16
+
17
+ from aegiss._version import __version__
18
+ from aegiss.config_dataclasses import (
19
+ DMRGConfig,
20
+ EntropyConfig,
21
+ OutputConfig,
22
+ ProjectionConfig,
23
+ )
24
+
25
+ __all__ = [
26
+ "DMRGConfig",
27
+ "EntropyConfig",
28
+ "OutputConfig",
29
+ "ProjectionConfig",
30
+ "__version__",
31
+ "active_space",
32
+ "config_dataclasses",
33
+ "initial_guess",
34
+ "localization",
35
+ "utils",
36
+ "wavefunction",
37
+ ]
aegiss/_version.py ADDED
@@ -0,0 +1,24 @@
1
+ # file generated by vcs-versioning
2
+ # don't change, don't track in version control
3
+ from __future__ import annotations
4
+
5
+ __all__ = [
6
+ "__version__",
7
+ "__version_tuple__",
8
+ "version",
9
+ "version_tuple",
10
+ "__commit_id__",
11
+ "commit_id",
12
+ ]
13
+
14
+ version: str
15
+ __version__: str
16
+ __version_tuple__: tuple[int | str, ...]
17
+ version_tuple: tuple[int | str, ...]
18
+ commit_id: str | None
19
+ __commit_id__: str | None
20
+
21
+ __version__ = version = '0.1.0'
22
+ __version_tuple__ = version_tuple = (0, 1, 0)
23
+
24
+ __commit_id__ = commit_id = None
@@ -0,0 +1,21 @@
1
+ # Copyright 2026 Algorithmiq
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """Active space selection methods: entropy screening and AO projection."""
16
+
17
+ from aegiss.active_space.aegiss import select_aegiss
18
+ from aegiss.active_space.ao_projection import select_by_ao_projection
19
+ from aegiss.active_space.entropy_screening import select_by_entropy
20
+
21
+ __all__ = ["select_aegiss", "select_by_ao_projection", "select_by_entropy"]
@@ -0,0 +1,446 @@
1
+ # Copyright 2026 Algorithmiq
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """Full AEGISS active space selection procedure."""
16
+
17
+ from __future__ import annotations
18
+
19
+ import logging
20
+ from pathlib import Path
21
+ from typing import TYPE_CHECKING, Any
22
+
23
+ import numpy as np
24
+
25
+ from aegiss.active_space.ao_projection import select_by_ao_projection
26
+ from aegiss.active_space.entropy_screening import (
27
+ select_by_entropy,
28
+ select_by_spin_entropy,
29
+ )
30
+ from aegiss.utils.dump import save_orbitals, save_orbitals_from_entropies
31
+ from aegiss.utils.orbital_indices import get_subspaces_from_cas
32
+ from aegiss.utils.plotting import (
33
+ _make_soe_plots,
34
+ plot_pie_contribution,
35
+ )
36
+ from aegiss.wavefunction.dmrg_subprocess import run_dmrg_subprocess
37
+
38
+ if TYPE_CHECKING:
39
+ from aegiss.config_dataclasses import (
40
+ DMRGConfig,
41
+ EntropyConfig,
42
+ OutputConfig,
43
+ ProjectionConfig,
44
+ )
45
+
46
+ logger = logging.getLogger(__name__)
47
+
48
+ # A per-spin entropy window: (occupied, virtual) index/entropy pairs.
49
+ _OccVirt = tuple[list[tuple[int, float]], list[tuple[int, float]]]
50
+ # Entropy screening output: one window (spin-summed) or one window per spin
51
+ # channel (spin-resolved).
52
+ _Screened = _OccVirt | dict[str, _OccVirt]
53
+ # A restricted_space handed to select_by_ao_projection: a single occupied/virtual
54
+ # list (spin-summed subset), a whole (occ, virt) window, or a per-spin window.
55
+ _RestrictedSpace = list[tuple[int, float]] | _OccVirt | dict[str, _OccVirt]
56
+ # Maps a spin-channel name to its row in an unrestricted ``mo_coeff``.
57
+ _CHANNEL_SPIN = {"alpha": 0, "beta": 1}
58
+
59
+
60
+ def _screen_by_entropy(
61
+ entropy_config: EntropyConfig,
62
+ active_orbs: np.ndarray,
63
+ virt_orbs: np.ndarray,
64
+ soe1: np.ndarray | None,
65
+ *,
66
+ spin_resolved: bool,
67
+ ) -> _Screened:
68
+ """Screen the DMRG active window by single-orbital entropy.
69
+
70
+ Dispatches to the spin-summed or spin-resolved screener according to
71
+ *spin_resolved* (from :meth:`DMRGConfig.spin_resolved`).
72
+
73
+ Args:
74
+ entropy_config (EntropyConfig): Screening fraction / ranking settings.
75
+ active_orbs (np.ndarray): Occupied indices of the DMRG active window.
76
+ virt_orbs (np.ndarray): Virtual indices of the DMRG active window.
77
+ soe1 (np.ndarray or None): Single-orbital entropies -- 1-D (spin-summed)
78
+ or ``(2, ncas)`` (spin-resolved).
79
+ spin_resolved (bool): Whether to screen the alpha/beta channels apart.
80
+
81
+ Returns:
82
+ screened (_Screened): A spin-summed ``(occ, virt)`` window, or a
83
+ spin-resolved ``{"alpha": (occ, virt), "beta": (occ, virt)}`` mapping.
84
+ """
85
+ if spin_resolved:
86
+ return select_by_spin_entropy(
87
+ occ_orbs=active_orbs,
88
+ virt_orbs=virt_orbs,
89
+ soe1=soe1,
90
+ fraction=entropy_config.fraction,
91
+ fraction_type=entropy_config.fraction_type,
92
+ ranking=entropy_config.ranking,
93
+ )
94
+ return select_by_entropy(
95
+ occ_orbs=active_orbs,
96
+ virt_orbs=virt_orbs,
97
+ soe1=soe1,
98
+ ranking=entropy_config.ranking,
99
+ fraction=entropy_config.fraction,
100
+ fraction_type=entropy_config.fraction_type,
101
+ )
102
+
103
+
104
+ def _screening_empty(screened: _Screened) -> bool:
105
+ """Return ``True`` if no orbital survived entropy screening (any channel)."""
106
+ if isinstance(screened, dict):
107
+ return all(not occ and not virt for occ, virt in screened.values())
108
+ occ, virt = screened
109
+ return not occ and not virt
110
+
111
+
112
+ def _ao_projection_passes(
113
+ screened: _Screened,
114
+ ranking: str,
115
+ ) -> list[tuple[str, _RestrictedSpace]]:
116
+ """Build the ``(save_suffix, restricted_space)`` AO-projection passes.
117
+
118
+ For ``'global'`` ranking the occupied and virtual windows are projected in a
119
+ single pass; for ``'subset'`` they are projected separately. Spin-resolved
120
+ input keeps its ``{"alpha", "beta"}`` structure so each pass projects both
121
+ channels on their own window (each half wrapped as an ``(occ, virt)`` window
122
+ with the other half empty, to keep a uniform per-channel value type).
123
+
124
+ Args:
125
+ screened (_Screened): Entropy-screening output.
126
+ ranking (str): ``'global'`` or ``'subset'``.
127
+
128
+ Returns:
129
+ passes (list of tuple[str, _RestrictedSpace]): Save-path suffix and the
130
+ *restricted_space* to hand to :func:`select_by_ao_projection`.
131
+ """
132
+ if ranking != "subset":
133
+ return [("", screened)]
134
+
135
+ passes: list[tuple[str, _RestrictedSpace]] = []
136
+ for pos, suffix in ((0, "_occ"), (1, "_virt")):
137
+ space: _RestrictedSpace
138
+ if isinstance(screened, dict):
139
+ space = {ch: _half_window(win, pos) for ch, win in screened.items()}
140
+ nonempty = any(win[pos] for win in screened.values())
141
+ else:
142
+ space = screened[pos]
143
+ nonempty = bool(space)
144
+ if nonempty:
145
+ passes.append((suffix, space))
146
+ return passes
147
+
148
+
149
+ def _half_window(window: _OccVirt, pos: int) -> _OccVirt:
150
+ """Keep only the occupied (``pos=0``) or virtual (``pos=1``) half of a window."""
151
+ return (window[0], []) if pos == 0 else ([], window[1])
152
+
153
+
154
+ def _collect_active_indices(
155
+ mf: Any,
156
+ projection_config: ProjectionConfig,
157
+ passes: list[tuple[str, _RestrictedSpace]],
158
+ base_path: str | None,
159
+ ) -> np.ndarray | dict[str, np.ndarray]:
160
+ """Run the AO-projection passes and merge their selected indices.
161
+
162
+ Indices from different passes (e.g. the occupied and virtual subsets) are
163
+ unioned *within* each spin channel. For an unrestricted mean-field the alpha
164
+ and beta selections are returned as a split ``{"alpha", "beta"}`` mapping and
165
+ are deliberately **not** unioned across channels: index *k* refers to a
166
+ different MO in each spin channel, so a single spatial active space is not
167
+ defined here (the caller consumes the split directly).
168
+
169
+ Args:
170
+ mf (pyscf.scf.hf.SCF): Mean-field object.
171
+ projection_config (ProjectionConfig): AO-projection settings.
172
+ passes (list of tuple[str, _RestrictedSpace]): Output of
173
+ :func:`_ao_projection_passes`.
174
+ base_path (str or None): Plot path stem, or ``None`` for no plotting.
175
+
176
+ Returns:
177
+ active_indices (np.ndarray, or dict[str, np.ndarray]): Sorted spatial
178
+ indices (restricted), or a split alpha/beta mapping (unrestricted).
179
+ """
180
+ channels: dict[str, set[int]] = {"alpha": set(), "beta": set()}
181
+ flat: set[int] = set()
182
+ is_split = False
183
+ for suffix, restricted_space in passes:
184
+ save_path = base_path + suffix if base_path else None
185
+ result = select_by_ao_projection(
186
+ mf,
187
+ ao_labels=projection_config.ao_labels,
188
+ contribution=projection_config.contribution,
189
+ projected_basis=projection_config.basis,
190
+ contribution_type=projection_config.contribution_type,
191
+ top_n=projection_config.top_n,
192
+ restricted_space=restricted_space,
193
+ save_path=save_path,
194
+ )
195
+ if isinstance(result, dict):
196
+ is_split = True
197
+ for channel, indices in result.items():
198
+ channels[channel].update(int(i) for i in indices)
199
+ else:
200
+ flat.update(int(i) for i in result)
201
+
202
+ if is_split:
203
+ return {
204
+ channel: np.asarray(sorted(indices))
205
+ for channel, indices in channels.items()
206
+ }
207
+ return np.asarray(sorted(flat))
208
+
209
+
210
+ def _maybe_plot_soe(
211
+ output_config: OutputConfig,
212
+ active_orbs: np.ndarray,
213
+ virt_orbs: np.ndarray,
214
+ soe1: np.ndarray | None,
215
+ ) -> None:
216
+ """Write the single-orbital-entropy plot when enabled and spin-summed.
217
+
218
+ Spin-resolved ``(2, ncas)`` entropies have no plotter yet, so that case is
219
+ skipped with a log note rather than raising.
220
+
221
+ Args:
222
+ output_config (OutputConfig): Output settings (gates on ``save_plots``).
223
+ active_orbs (np.ndarray): Occupied indices of the DMRG active window.
224
+ virt_orbs (np.ndarray): Virtual indices of the DMRG active window.
225
+ soe1 (np.ndarray or None): Single-orbital entropies.
226
+ """
227
+ if not output_config.save_plots or soe1 is None:
228
+ return
229
+ if np.ndim(soe1) != 1:
230
+ logger.info(
231
+ "Spin-resolved single-orbital entropies are not plotted; skipping "
232
+ "the SOE plot."
233
+ )
234
+ return
235
+ _make_soe_plots(
236
+ active_orbs,
237
+ virt_orbs,
238
+ soe1,
239
+ output_config.saving_folder,
240
+ output_config.saving_prefix,
241
+ )
242
+
243
+
244
+ def _save_entropy_orbitals(
245
+ mol: Any,
246
+ mf: Any,
247
+ screened: _Screened,
248
+ output_config: OutputConfig,
249
+ ) -> None:
250
+ """Dump cube files for the entropy-screened orbitals of every window.
251
+
252
+ For an unrestricted reference each channel is saved from its own spin block
253
+ of ``mf.mo_coeff`` with a channel-tagged prefix.
254
+
255
+ Args:
256
+ mol (pyscf.gto.Mole): Molecule.
257
+ mf (pyscf.scf.hf.SCF): Mean-field object.
258
+ screened (_Screened): Entropy-screening output.
259
+ output_config (OutputConfig): Output settings.
260
+ """
261
+ mo_coeff = np.asarray(mf.mo_coeff)
262
+ if isinstance(screened, dict):
263
+ for channel, window in screened.items():
264
+ coeff = mo_coeff[_CHANNEL_SPIN[channel]]
265
+ for subspace in window:
266
+ save_orbitals_from_entropies(
267
+ mol,
268
+ coeff,
269
+ subspace,
270
+ filename_prefix=f"{output_config.saving_prefix}_entropy_{channel}",
271
+ folder=output_config.saving_folder,
272
+ )
273
+ return
274
+ for subspace in screened:
275
+ save_orbitals_from_entropies(
276
+ mol,
277
+ mo_coeff,
278
+ subspace,
279
+ filename_prefix=output_config.saving_prefix + "_entropy",
280
+ folder=output_config.saving_folder,
281
+ )
282
+
283
+
284
+ def _save_active_orbitals(
285
+ mol: Any,
286
+ mf: Any,
287
+ active_indices: np.ndarray | dict[str, np.ndarray],
288
+ output_config: OutputConfig,
289
+ ) -> None:
290
+ """Dump cube files for the final AEGISS active orbitals.
291
+
292
+ Args:
293
+ mol (pyscf.gto.Mole): Molecule.
294
+ mf (pyscf.scf.hf.SCF): Mean-field object.
295
+ active_indices (np.ndarray, or dict[str, np.ndarray]): Selected indices;
296
+ a split alpha/beta mapping for an unrestricted reference.
297
+ output_config (OutputConfig): Output settings.
298
+ """
299
+ mo_coeff = np.asarray(mf.mo_coeff)
300
+ if isinstance(active_indices, dict):
301
+ for channel, indices in active_indices.items():
302
+ save_orbitals(
303
+ mol,
304
+ mo_coeff[_CHANNEL_SPIN[channel]],
305
+ indices,
306
+ filename_prefix=f"{output_config.saving_prefix}_AEGISS_{channel}",
307
+ folder=output_config.saving_folder,
308
+ )
309
+ return
310
+ save_orbitals(
311
+ mol,
312
+ mo_coeff,
313
+ active_indices,
314
+ filename_prefix=output_config.saving_prefix + "_AEGISS",
315
+ folder=output_config.saving_folder,
316
+ )
317
+
318
+
319
+ def _plot_active(
320
+ mol: Any,
321
+ mf: Any,
322
+ active_indices: np.ndarray | dict[str, np.ndarray],
323
+ projection_config: ProjectionConfig,
324
+ output_config: OutputConfig,
325
+ ) -> None:
326
+ """Plot the AO pie contribution for the final active orbitals.
327
+
328
+ The pie plotter takes a single flat orbital list, so the split alpha/beta
329
+ selection of an unrestricted reference is skipped with a log note.
330
+
331
+ Args:
332
+ mol (pyscf.gto.Mole): Molecule.
333
+ mf (pyscf.scf.hf.SCF): Mean-field object.
334
+ active_indices (np.ndarray, or dict[str, np.ndarray]): Selected indices.
335
+ projection_config (ProjectionConfig): AO-projection settings.
336
+ output_config (OutputConfig): Output settings.
337
+ """
338
+ if isinstance(active_indices, dict):
339
+ logger.info(
340
+ "AO pie-contribution plotting is not available for the split "
341
+ "alpha/beta selection; skipping."
342
+ )
343
+ return
344
+ plot_pie_contribution(
345
+ mol,
346
+ mf,
347
+ ao_labels=projection_config.ao_labels,
348
+ labels_names=projection_config.ao_labels,
349
+ orbs=active_indices,
350
+ folder=output_config.saving_folder,
351
+ filename=output_config.saving_prefix + "_composition",
352
+ )
353
+
354
+
355
+ def select_aegiss(
356
+ mol: Any,
357
+ mf: Any,
358
+ dmrg_config: DMRGConfig,
359
+ entropy_config: EntropyConfig,
360
+ projection_config: ProjectionConfig,
361
+ output_config: OutputConfig,
362
+ ) -> np.ndarray | dict[str, np.ndarray]:
363
+ """Run the full AEGISS active space selection pipeline.
364
+
365
+ Runs DMRG on the specified CAS, then filters the resulting orbitals
366
+ through entropy screening and AO projection. An orbital is included
367
+ in the final active space when it has been selected by *both* criteria.
368
+ Therefore, only highly entangled orbitals are kept. These orbitals
369
+ must also have significant contribution on the specified AOs.
370
+
371
+ For an unrestricted (UHF) mean-field the whole pipeline runs per spin
372
+ channel: when ``dmrg_config.spin_soe`` resolves to spin-resolved the alpha
373
+ and beta channels are screened on their own entropy windows, otherwise they
374
+ share one spin-summed window; either way the AO projection is done per spin.
375
+ The alpha and beta selections are returned split and are **not** reconciled
376
+ into a single spatial active space -- index *k* denotes a different MO in
377
+ each channel, so no common spatial index set is defined here.
378
+
379
+ Args:
380
+ mol (pyscf.gto.Mole): Molecular geometry and basis set.
381
+ mf (pyscf.scf.hf.SCF): Converged mean-field object.
382
+ dmrg_config (DMRGConfig): Configuration for the DMRG calculation (CAS size, bond dimension, symmetry).
383
+ entropy_config (EntropyConfig): Configuration for the entropy-based screening step.
384
+ projection_config (ProjectionConfig): Configuration for the AO projection-based screening step.
385
+ output_config (OutputConfig): Configuration for output options (file saving, plots).
386
+
387
+ Returns:
388
+ active_indices (np.ndarray of int, or dict[str, np.ndarray]): Sorted
389
+ orbital indices of the selected active space. A single array for a
390
+ restricted mean-field; a split ``{"alpha": ..., "beta": ...}``
391
+ mapping (kept separate, not reconciled) for an unrestricted one.
392
+ When no orbital passes the entropy screening, an empty flat
393
+ ``np.ndarray`` is returned, also for an unrestricted reference.
394
+ """
395
+ active_orbs, virt_orbs = get_subspaces_from_cas(
396
+ mf, ncas=dmrg_config.ncas, nelecas=dmrg_config.nelecas
397
+ )
398
+ spin_resolved = dmrg_config.spin_resolved()
399
+ _energy, soe1 = run_dmrg_subprocess(
400
+ mf,
401
+ ncas=dmrg_config.ncas,
402
+ nelecas=dmrg_config.nelecas,
403
+ symm_type=dmrg_config.symm_type,
404
+ max_bond_dim=dmrg_config.max_bond_dim,
405
+ verbosity=dmrg_config.verbosity,
406
+ unrestricted=dmrg_config.unrestricted,
407
+ reorder=dmrg_config.reorder,
408
+ spin_resolved=spin_resolved,
409
+ )
410
+ _maybe_plot_soe(output_config, active_orbs, virt_orbs, soe1)
411
+
412
+ screened = _screen_by_entropy(
413
+ entropy_config,
414
+ active_orbs,
415
+ virt_orbs,
416
+ soe1,
417
+ spin_resolved=spin_resolved,
418
+ )
419
+ if _screening_empty(screened):
420
+ logger.info(
421
+ "No orbitals passed the entropy screening step. Consider adjusting the "
422
+ "EntropyConfig.fraction or using 'global' ranking for a more inclusive selection."
423
+ )
424
+ return np.array([], dtype=int)
425
+
426
+ if output_config.save_orb:
427
+ _save_entropy_orbitals(mol, mf, screened, output_config)
428
+
429
+ base_path = (
430
+ str(Path(output_config.saving_folder) / output_config.saving_prefix)
431
+ if output_config.save_plots
432
+ else None
433
+ )
434
+ logger.info(
435
+ "Ranking orbitals for AO projection based on %s contribution.",
436
+ projection_config.ranking,
437
+ )
438
+ passes = _ao_projection_passes(screened, projection_config.ranking)
439
+ active_indices = _collect_active_indices(mf, projection_config, passes, base_path)
440
+
441
+ if output_config.save_orb:
442
+ _save_active_orbitals(mol, mf, active_indices, output_config)
443
+ if output_config.save_plots:
444
+ _plot_active(mol, mf, active_indices, projection_config, output_config)
445
+
446
+ return active_indices