Skip to content

Commit d4cad11

Browse files
authored
Merge pull request #50 from zirenjin/master
fix(dpa-adapt): resolve pre-commit errors, desc_cache import cycle, descriptor hook accumulator
2 parents 6d15369 + 89d353e commit d4cad11

7 files changed

Lines changed: 211 additions & 153 deletions

File tree

doc/conf.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@
77

88
# -- Path setup --------------------------------------------------------------
99

10+
from __future__ import (
11+
annotations,
12+
)
13+
1014
import datetime
1115

1216
# If extensions (or modules to document with autodoc) are in another directory,
@@ -224,3 +228,54 @@
224228
bibtex_bibfiles = ["../CITATIONS.bib"]
225229

226230
remove_from_toctrees = ["autoapi/**/*", "API_CC/*", "api_c/*", "api_core/*"]
231+
232+
233+
# Auto-generated CLI reference pages (sphinx-argparse) nest a section per
234+
# subcommand and per argument group. Under the global ``:numbered:`` toctree
235+
# this explodes into unhelpful deep numbers (e.g. ``9.3.3.6.3.1.1.``). Cap the
236+
# section numbering at the given depth (number of dotted components); headings
237+
# deeper than that are left unnumbered. Only the listed pages are affected.
238+
from typing import (
239+
TYPE_CHECKING,
240+
)
241+
242+
from docutils import (
243+
nodes,
244+
)
245+
246+
if TYPE_CHECKING:
247+
from sphinx.application import (
248+
Sphinx,
249+
)
250+
251+
cli_secnumber_max_depth = {
252+
"dpa_adapt/cli": 5,
253+
}
254+
255+
256+
def _cap_cli_secnumbers(app: Sphinx, doctree: nodes.document, docname: str) -> None:
257+
"""Drop section numbers below ``cli_secnumber_max_depth`` for CLI pages."""
258+
max_depth = cli_secnumber_max_depth.get(docname)
259+
if max_depth is None:
260+
return
261+
secnumbers = app.env.toc_secnumbers.get(docname)
262+
if not secnumbers:
263+
return
264+
# The empty anchor "" holds the page chapter number (e.g. ``(9, 3)``).
265+
# It must be dropped from the map, otherwise the writer falls back to it for
266+
# the now-unnumbered deep sections; re-attach it to the page title instead.
267+
page_number = secnumbers.get("")
268+
app.env.toc_secnumbers[docname] = {
269+
anchor: number
270+
for anchor, number in secnumbers.items()
271+
if anchor != "" and len(number) <= max_depth
272+
}
273+
if page_number:
274+
for title in doctree.findall(nodes.title):
275+
title["secnumber"] = page_number
276+
break
277+
278+
279+
def setup(app: Sphinx) -> dict[str, bool]:
280+
app.connect("doctree-resolved", _cap_cli_secnumbers)
281+
return {"parallel_read_safe": True, "parallel_write_safe": True}

doc/dpa_adapt/input_formats.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ auto-detect the input type and route it to the correct pipeline:
1212
**formula table** → random doping from a POSCAR template,
1313
**structure files** → dpdata (auto-detect or explicit `--fmt`).
1414

15-
## 1. SMILES Tables (CSV)
15+
## SMILES Tables (CSV)
1616

1717
**Trigger:** file extension `.csv` **and** a SMILES column.
1818
By default, the converter reads `SMILES`/`smiles`; use `--smiles-col` for
@@ -46,7 +46,7 @@ dpaad data convert --input data.csv --output ./npy --fmt smiles \
4646
--split-seed 42 --conformer-seed 43
4747
```
4848

49-
## 2. Formula Tables (CSV/TXT + POSCAR Template)
49+
## Formula Tables (CSV/TXT + POSCAR Template)
5050

5151
**Trigger:** `--fmt formula`. Reads a table of elemental composition formulas
5252
(e.g. `Ni0.65Gd0.15O2H1`) and a template POSCAR, then generates doped
@@ -91,7 +91,7 @@ dpa-adapt data convert --input compositions.txt --output ./npy --fmt formula \
9191
--poscar template.POSCAR --formula-col 0 --property-col 1
9292
```
9393

94-
## 3. Structure Files via dpdata
94+
## Structure Files via dpdata
9595

9696
**Trigger:** inputs not routed to the SMILES or formula pipelines. This means
9797
`--fmt` is neither `smiles` nor `formula`; when `--fmt` is omitted, CSV inputs

dpa_adapt/cv.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -481,7 +481,7 @@ def cross_validate(
481481
# This reuses existing desc_mean.npy when present, extracts only missing
482482
# systems one-by-one. Peak memory is one system's descriptors at a time.
483483
if is_cheap:
484-
from dpa_adapt.data.desc_cache import (
484+
from dpa_adapt.finetuner import (
485485
ensure_per_system_cache,
486486
)
487487

dpa_adapt/data/desc_cache.py

Lines changed: 5 additions & 140 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@
88
#
99
# Systems are ``dpdata.System`` objects; cache keys are computed from
1010
# data fingerprints and resolved checkpoint metadata.
11+
#
12+
# Note: ``load_or_extract()`` and ``ensure_per_system_cache()`` live in
13+
# ``dpa_adapt.finetuner`` to avoid an import cycle (those functions need
14+
# ``DPAFineTuner``, while ``finetuner`` imports cache helpers from here).
1115

1216
from __future__ import (
1317
annotations,
@@ -125,78 +129,7 @@ def _cache_key(
125129

126130

127131
# ---------------------------------------------------------------------------
128-
# bulk cache
129-
# ---------------------------------------------------------------------------
130-
131-
132-
def load_or_extract(
133-
systems: list,
134-
pretrained: str,
135-
model_branch: str | None = None,
136-
pooling: str = "mean",
137-
cache: bool = True,
138-
type_map: list[str] | tuple[str, ...] | None = None,
139-
) -> np.ndarray:
140-
"""Return descriptors for *systems*, using the cache when possible.
141-
142-
Parameters
143-
----------
144-
systems : list[dpdata.System]
145-
Systems to extract descriptors from.
146-
pretrained : str
147-
Path to the DPA checkpoint.
148-
model_branch : str, optional
149-
Branch name.
150-
pooling : str
151-
Pooling strategy.
152-
cache : bool
153-
If False the cache is bypassed entirely.
154-
type_map : list[str] or tuple[str, ...], optional
155-
Element symbols used to build the descriptor model and cache key.
156-
157-
Returns
158-
-------
159-
np.ndarray, shape ``(n_frames_total, feat_dim)``
160-
"""
161-
if cache:
162-
key = _cache_key(
163-
systems,
164-
pretrained,
165-
model_branch,
166-
pooling,
167-
type_map=type_map,
168-
)
169-
cache_path = _cache_dir() / f"{key}.npy"
170-
if cache_path.is_file():
171-
_LOG.info("Descriptor cache hit: %s", cache_path.name)
172-
return np.load(cache_path)
173-
_LOG.info("Descriptor cache miss; extracting...")
174-
else:
175-
_LOG.info("Descriptor cache bypassed (cache=False).")
176-
177-
from dpa_adapt.finetuner import (
178-
DPAFineTuner,
179-
)
180-
181-
extractor = DPAFineTuner(
182-
pretrained=pretrained,
183-
model_branch=model_branch,
184-
predictor="linear",
185-
pooling=pooling,
186-
type_map=list(type_map) if type_map else None,
187-
)
188-
descriptors = extractor._extract_features(systems)
189-
190-
if cache:
191-
cache_path.parent.mkdir(parents=True, exist_ok=True)
192-
np.save(cache_path, descriptors)
193-
_LOG.info("Cached descriptors to %s", cache_path)
194-
195-
return descriptors
196-
197-
198-
# ---------------------------------------------------------------------------
199-
# per-system cache — used by cross_validate to avoid OOM
132+
# per-system cache path helpers
200133
# ---------------------------------------------------------------------------
201134

202135

@@ -216,74 +149,6 @@ def _per_system_cache_path(
216149
return _cache_dir() / "per_system" / f"{fp}.npy"
217150

218151

219-
def ensure_per_system_cache(
220-
systems: list,
221-
pretrained: str,
222-
model_branch: str | None = None,
223-
pooling: str = "mean",
224-
type_map: list[str] | tuple[str, ...] | None = None,
225-
) -> None:
226-
"""Ensure every system has its descriptors cached to disk.
227-
228-
Existing cache files are reused as-is. Missing ones are extracted one
229-
system at a time for low peak memory.
230-
"""
231-
missing: list = []
232-
for system in systems:
233-
if not _per_system_cache_path(
234-
system,
235-
pretrained,
236-
model_branch,
237-
pooling,
238-
type_map,
239-
).is_file():
240-
missing.append(system)
241-
242-
if not missing:
243-
_LOG.info(
244-
"All %d systems have per-system cache; nothing to extract.", len(systems)
245-
)
246-
return
247-
248-
import torch
249-
250-
from dpa_adapt.finetuner import (
251-
DPAFineTuner,
252-
)
253-
254-
_LOG.info(
255-
"%d/%d systems missing per-system cache; extracting one by one...",
256-
len(missing),
257-
len(systems),
258-
)
259-
260-
extractor = DPAFineTuner(
261-
pretrained=pretrained,
262-
model_branch=model_branch,
263-
predictor="linear",
264-
pooling=pooling,
265-
type_map=list(type_map) if type_map else None,
266-
)
267-
268-
for i, system in enumerate(missing):
269-
cache_path = _per_system_cache_path(
270-
system,
271-
pretrained,
272-
model_branch,
273-
pooling,
274-
type_map,
275-
)
276-
cache_path.parent.mkdir(parents=True, exist_ok=True)
277-
desc = extractor._extract_features([system])
278-
np.save(cache_path, desc)
279-
if extractor._device is not None and extractor._device.type == "cuda":
280-
torch.cuda.empty_cache()
281-
if i > 0 and i % 50 == 0:
282-
_LOG.info(" per-system cache: %d/%d done", i, len(missing))
283-
284-
_LOG.info("Per-system cache ready (%d systems).", len(systems))
285-
286-
287152
def get_per_system_descriptor(
288153
system: dpdata.System,
289154
pretrained: str,

0 commit comments

Comments
 (0)