rapmat 0.2.2__tar.gz → 0.2.3__tar.gz
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.
- {rapmat-0.2.2 → rapmat-0.2.3}/PKG-INFO +1 -1
- {rapmat-0.2.2 → rapmat-0.2.3}/rapmat/calculators/vasp.py +2 -1
- {rapmat-0.2.2 → rapmat-0.2.3}/rapmat/core/csp.py +1 -14
- {rapmat-0.2.2 → rapmat-0.2.3}/rapmat/storage/base.py +2 -1
- {rapmat-0.2.2 → rapmat-0.2.3}/rapmat/storage/surrealdb_store.py +46 -32
- {rapmat-0.2.2 → rapmat-0.2.3}/rapmat/tui/screens/base_results.py +71 -4
- {rapmat-0.2.2 → rapmat-0.2.3}/rapmat/tui/screens/hull.py +16 -117
- {rapmat-0.2.2 → rapmat-0.2.3}/rapmat/tui/screens/results.py +4 -2
- {rapmat-0.2.2 → rapmat-0.2.3}/tests/conftest.py +28 -19
- {rapmat-0.2.2 → rapmat-0.2.3}/tests/test_db_config_and_locking.py +0 -23
- {rapmat-0.2.2 → rapmat-0.2.3}/tests/test_dedup_analysis.py +7 -12
- {rapmat-0.2.2 → rapmat-0.2.3}/tests/test_hull.py +34 -26
- {rapmat-0.2.2 → rapmat-0.2.3}/tests/test_processing_loop.py +4 -9
- {rapmat-0.2.2 → rapmat-0.2.3}/tests/test_render.py +3 -3
- {rapmat-0.2.2 → rapmat-0.2.3}/tests/test_storage.py +95 -29
- {rapmat-0.2.2 → rapmat-0.2.3}/tests/test_tui.py +39 -1
- {rapmat-0.2.2 → rapmat-0.2.3}/tests/test_tui_layout.py +3 -3
- {rapmat-0.2.2 → rapmat-0.2.3}/.github/workflows/python-publish.yml +0 -0
- {rapmat-0.2.2 → rapmat-0.2.3}/.gitignore +0 -0
- {rapmat-0.2.2 → rapmat-0.2.3}/LICENSE +0 -0
- {rapmat-0.2.2 → rapmat-0.2.3}/README.md +0 -0
- {rapmat-0.2.2 → rapmat-0.2.3}/pyproject.toml +0 -0
- {rapmat-0.2.2 → rapmat-0.2.3}/rapmat/__init__.py +0 -0
- {rapmat-0.2.2 → rapmat-0.2.3}/rapmat/__main__.py +0 -0
- {rapmat-0.2.2 → rapmat-0.2.3}/rapmat/calculators/__init__.py +0 -0
- {rapmat-0.2.2 → rapmat-0.2.3}/rapmat/calculators/factory.py +0 -0
- {rapmat-0.2.2 → rapmat-0.2.3}/rapmat/calculators/mattersim.py +0 -0
- {rapmat-0.2.2 → rapmat-0.2.3}/rapmat/calculators/nequip.py +0 -0
- {rapmat-0.2.2 → rapmat-0.2.3}/rapmat/calculators/upet.py +0 -0
- {rapmat-0.2.2 → rapmat-0.2.3}/rapmat/config.py +0 -0
- {rapmat-0.2.2 → rapmat-0.2.3}/rapmat/core/__init__.py +0 -0
- {rapmat-0.2.2 → rapmat-0.2.3}/rapmat/core/dedup.py +0 -0
- {rapmat-0.2.2 → rapmat-0.2.3}/rapmat/core/dedup_analysis.py +0 -0
- {rapmat-0.2.2 → rapmat-0.2.3}/rapmat/core/evaluation.py +0 -0
- {rapmat-0.2.2 → rapmat-0.2.3}/rapmat/core/generation_worker.py +0 -0
- {rapmat-0.2.2 → rapmat-0.2.3}/rapmat/core/hull.py +0 -0
- {rapmat-0.2.2 → rapmat-0.2.3}/rapmat/core/phonon.py +0 -0
- {rapmat-0.2.2 → rapmat-0.2.3}/rapmat/core/phonon_stability.py +0 -0
- {rapmat-0.2.2 → rapmat-0.2.3}/rapmat/core/relaxation.py +0 -0
- {rapmat-0.2.2 → rapmat-0.2.3}/rapmat/core/sanity.py +0 -0
- {rapmat-0.2.2 → rapmat-0.2.3}/rapmat/db_config.py +0 -0
- {rapmat-0.2.2 → rapmat-0.2.3}/rapmat/main.py +0 -0
- {rapmat-0.2.2 → rapmat-0.2.3}/rapmat/storage/__init__.py +0 -0
- {rapmat-0.2.2 → rapmat-0.2.3}/rapmat/storage/descriptors.py +0 -0
- {rapmat-0.2.2 → rapmat-0.2.3}/rapmat/tui/__init__.py +0 -0
- {rapmat-0.2.2 → rapmat-0.2.3}/rapmat/tui/app.py +0 -0
- {rapmat-0.2.2 → rapmat-0.2.3}/rapmat/tui/router.py +0 -0
- {rapmat-0.2.2 → rapmat-0.2.3}/rapmat/tui/screens/__init__.py +0 -0
- {rapmat-0.2.2 → rapmat-0.2.3}/rapmat/tui/screens/csp_resume.py +0 -0
- {rapmat-0.2.2 → rapmat-0.2.3}/rapmat/tui/screens/csp_search.py +0 -0
- {rapmat-0.2.2 → rapmat-0.2.3}/rapmat/tui/screens/db_settings.py +0 -0
- {rapmat-0.2.2 → rapmat-0.2.3}/rapmat/tui/screens/dedup.py +0 -0
- {rapmat-0.2.2 → rapmat-0.2.3}/rapmat/tui/screens/eval.py +0 -0
- {rapmat-0.2.2 → rapmat-0.2.3}/rapmat/tui/screens/home.py +0 -0
- {rapmat-0.2.2 → rapmat-0.2.3}/rapmat/tui/screens/phonon.py +0 -0
- {rapmat-0.2.2 → rapmat-0.2.3}/rapmat/tui/screens/status.py +0 -0
- {rapmat-0.2.2 → rapmat-0.2.3}/rapmat/tui/screens/study_create.py +0 -0
- {rapmat-0.2.2 → rapmat-0.2.3}/rapmat/tui/screens/study_detail.py +0 -0
- {rapmat-0.2.2 → rapmat-0.2.3}/rapmat/tui/screens/study_list.py +0 -0
- {rapmat-0.2.2 → rapmat-0.2.3}/rapmat/tui/state.py +0 -0
- {rapmat-0.2.2 → rapmat-0.2.3}/rapmat/tui/tasks.py +0 -0
- {rapmat-0.2.2 → rapmat-0.2.3}/rapmat/tui/widgets/__init__.py +0 -0
- {rapmat-0.2.2 → rapmat-0.2.3}/rapmat/tui/widgets/calc_fields.py +0 -0
- {rapmat-0.2.2 → rapmat-0.2.3}/rapmat/tui/widgets/config_grid.py +0 -0
- {rapmat-0.2.2 → rapmat-0.2.3}/rapmat/tui/widgets/dialog.py +0 -0
- {rapmat-0.2.2 → rapmat-0.2.3}/rapmat/tui/widgets/dropdown.py +0 -0
- {rapmat-0.2.2 → rapmat-0.2.3}/rapmat/tui/widgets/form.py +0 -0
- {rapmat-0.2.2 → rapmat-0.2.3}/rapmat/tui/widgets/progress.py +0 -0
- {rapmat-0.2.2 → rapmat-0.2.3}/rapmat/tui/widgets/status_bar.py +0 -0
- {rapmat-0.2.2 → rapmat-0.2.3}/rapmat/tui/widgets/table.py +0 -0
- {rapmat-0.2.2 → rapmat-0.2.3}/rapmat/utils/__init__.py +0 -0
- {rapmat-0.2.2 → rapmat-0.2.3}/rapmat/utils/common.py +0 -0
- {rapmat-0.2.2 → rapmat-0.2.3}/rapmat/utils/console.py +0 -0
- {rapmat-0.2.2 → rapmat-0.2.3}/rapmat/utils/structure.py +0 -0
- {rapmat-0.2.2 → rapmat-0.2.3}/tests/test_checkbox.py +0 -0
- {rapmat-0.2.2 → rapmat-0.2.3}/tests/test_evaluation.py +0 -0
- {rapmat-0.2.2 → rapmat-0.2.3}/tests/test_relaxation.py +0 -0
- {rapmat-0.2.2 → rapmat-0.2.3}/tests/test_sanity.py +0 -0
- {rapmat-0.2.2 → rapmat-0.2.3}/tests/test_ui_toggle.py +0 -0
- {rapmat-0.2.2 → rapmat-0.2.3}/tests/test_utils.py +0 -0
- {rapmat-0.2.2 → rapmat-0.2.3}/tests/test_vasp.py +0 -0
|
@@ -5,7 +5,8 @@ from ase.calculators.vasp import Vasp
|
|
|
5
5
|
|
|
6
6
|
def build_calculator_vasp(config: dict, directory: Path | None = None) -> Vasp:
|
|
7
7
|
kwargs = dict(config)
|
|
8
|
-
|
|
8
|
+
|
|
9
|
+
if directory is not None and "directory" not in kwargs:
|
|
9
10
|
kwargs["directory"] = str(directory)
|
|
10
11
|
|
|
11
12
|
if "txt" not in kwargs:
|
|
@@ -31,8 +31,7 @@ def run_processing_loop(
|
|
|
31
31
|
from rapmat.core.sanity import check_sanity
|
|
32
32
|
from rapmat.utils.console import get_logger
|
|
33
33
|
logger = get_logger("rapmat.csp")
|
|
34
|
-
from rapmat.utils.structure import
|
|
35
|
-
standardize_atoms)
|
|
34
|
+
from rapmat.utils.structure import format_spg, standardize_atoms
|
|
36
35
|
|
|
37
36
|
class _ProgressCalcCallback:
|
|
38
37
|
|
|
@@ -161,17 +160,11 @@ def run_processing_loop(
|
|
|
161
160
|
|
|
162
161
|
_report("Metadata preparation...")
|
|
163
162
|
energy = relaxed_structure.info["energy"]
|
|
164
|
-
volume = relaxed_structure.get_volume()
|
|
165
|
-
enthalpy = energy + pressure_evA3 * volume
|
|
166
163
|
|
|
167
164
|
meta = {
|
|
168
165
|
"energy_per_atom": energy / len(relaxed_structure),
|
|
169
|
-
"energy_total": energy,
|
|
170
|
-
"enthalpy_per_atom": enthalpy / len(relaxed_structure),
|
|
171
|
-
"volume": volume,
|
|
172
166
|
"fmax": relaxed_structure.info["fmax"],
|
|
173
167
|
"converged": relaxed_structure.info["converged"],
|
|
174
|
-
"thickness": 0.0,
|
|
175
168
|
}
|
|
176
169
|
|
|
177
170
|
_report("Checking sanity...")
|
|
@@ -191,12 +184,6 @@ def run_processing_loop(
|
|
|
191
184
|
)
|
|
192
185
|
break
|
|
193
186
|
|
|
194
|
-
if search_dim == 2:
|
|
195
|
-
_report("Calculating thickness...")
|
|
196
|
-
current_thickness = calculate_thickness(relaxed_structure)
|
|
197
|
-
relaxed_structure.info["thickness"] = current_thickness
|
|
198
|
-
meta["thickness"] = current_thickness
|
|
199
|
-
|
|
200
187
|
_report("Saving to database...")
|
|
201
188
|
store.update_structure(
|
|
202
189
|
struct_id,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from pymatgen.util import string
|
|
2
2
|
from abc import ABC, abstractmethod
|
|
3
3
|
from pathlib import Path
|
|
4
|
-
from typing import List, Optional, Tuple
|
|
4
|
+
from typing import Callable, List, Optional, Tuple
|
|
5
5
|
|
|
6
6
|
import numpy as np
|
|
7
7
|
from ase import Atoms
|
|
@@ -108,6 +108,7 @@ class StructureStore(ABC):
|
|
|
108
108
|
status: Optional[str] = None,
|
|
109
109
|
statuses: Optional[tuple[str, ...]] = None,
|
|
110
110
|
symprec: float = 1e-3,
|
|
111
|
+
progress_callback: Optional[Callable[..., None]] = None,
|
|
111
112
|
) -> List[dict]: ...
|
|
112
113
|
|
|
113
114
|
@abstractmethod
|
|
@@ -2,17 +2,18 @@ import json
|
|
|
2
2
|
import threading
|
|
3
3
|
from datetime import datetime
|
|
4
4
|
from pathlib import Path
|
|
5
|
-
from typing import List, Optional, Tuple
|
|
5
|
+
from typing import Callable, List, Optional, Tuple
|
|
6
6
|
|
|
7
7
|
import numpy as np
|
|
8
8
|
from ase import Atoms
|
|
9
9
|
from ase.io.jsonio import decode as ase_decode
|
|
10
10
|
from ase.io.jsonio import encode as ase_encode
|
|
11
|
+
from ase.units import GPa
|
|
11
12
|
from filelock import FileLock, Timeout
|
|
12
13
|
from surrealdb import RecordID, Surreal
|
|
13
14
|
|
|
14
15
|
from rapmat.storage.base import StructureStore, StructureDescriptor
|
|
15
|
-
from rapmat.utils.structure import format_spg
|
|
16
|
+
from rapmat.utils.structure import calculate_thickness, format_spg
|
|
16
17
|
|
|
17
18
|
# ------------------------------------------------------------------ #
|
|
18
19
|
# Schema
|
|
@@ -46,19 +47,19 @@ DEFINE FIELD IF NOT EXISTS status ON structure TYPE string
|
|
|
46
47
|
ASSERT $value IN ["generating", "generated", "relaxed", "discarded", "error"];
|
|
47
48
|
DEFINE FIELD IF NOT EXISTS gen_spg ON structure TYPE option<int>;
|
|
48
49
|
DEFINE FIELD IF NOT EXISTS gen_fu ON structure TYPE option<int>;
|
|
49
|
-
DEFINE FIELD IF NOT EXISTS formula ON structure TYPE string;
|
|
50
50
|
DEFINE FIELD IF NOT EXISTS energy_per_atom ON structure TYPE float;
|
|
51
|
-
DEFINE FIELD IF NOT EXISTS energy_total ON structure TYPE float;
|
|
52
51
|
DEFINE FIELD IF NOT EXISTS fmax ON structure TYPE float;
|
|
53
52
|
DEFINE FIELD IF NOT EXISTS converged ON structure TYPE bool;
|
|
54
|
-
DEFINE FIELD IF NOT EXISTS thickness ON structure TYPE option<float>;
|
|
55
|
-
DEFINE FIELD IF NOT EXISTS enthalpy_per_atom ON structure TYPE option<float>;
|
|
56
|
-
DEFINE FIELD IF NOT EXISTS volume ON structure TYPE option<float>;
|
|
57
53
|
DEFINE FIELD IF NOT EXISTS min_phonon_freq ON structure TYPE option<float>;
|
|
58
54
|
DEFINE FIELD IF NOT EXISTS duplicate ON structure TYPE option<bool>;
|
|
59
55
|
DEFINE FIELD IF NOT EXISTS initial_atoms_json ON structure TYPE string;
|
|
60
56
|
DEFINE FIELD IF NOT EXISTS final_atoms_json ON structure TYPE string;
|
|
61
57
|
DEFINE FIELD IF NOT EXISTS timestamp ON structure TYPE string;
|
|
58
|
+
REMOVE FIELD IF EXISTS formula ON TABLE structure;
|
|
59
|
+
REMOVE FIELD IF EXISTS energy_total ON TABLE structure;
|
|
60
|
+
REMOVE FIELD IF EXISTS volume ON TABLE structure;
|
|
61
|
+
REMOVE FIELD IF EXISTS enthalpy_per_atom ON TABLE structure;
|
|
62
|
+
REMOVE FIELD IF EXISTS thickness ON TABLE structure;
|
|
62
63
|
DEFINE INDEX IF NOT EXISTS idx_struct_run ON structure FIELDS run;
|
|
63
64
|
DEFINE INDEX IF NOT EXISTS idx_struct_status ON structure FIELDS status;
|
|
64
65
|
DEFINE INDEX IF NOT EXISTS idx_struct_run_status ON structure FIELDS run, status;
|
|
@@ -500,14 +501,9 @@ class SurrealDBStore(StructureStore):
|
|
|
500
501
|
"status": "generating",
|
|
501
502
|
"gen_spg": spg,
|
|
502
503
|
"gen_fu": fu,
|
|
503
|
-
"formula": "",
|
|
504
504
|
"energy_per_atom": 0.0,
|
|
505
|
-
"energy_total": 0.0,
|
|
506
505
|
"fmax": 0.0,
|
|
507
506
|
"converged": False,
|
|
508
|
-
"thickness": None,
|
|
509
|
-
"enthalpy_per_atom": None,
|
|
510
|
-
"volume": None,
|
|
511
507
|
"min_phonon_freq": None,
|
|
512
508
|
"initial_atoms_json": "",
|
|
513
509
|
"final_atoms_json": "",
|
|
@@ -543,7 +539,6 @@ class SurrealDBStore(StructureStore):
|
|
|
543
539
|
) -> None:
|
|
544
540
|
updates: dict = {
|
|
545
541
|
"status": "generated",
|
|
546
|
-
"formula": atoms.get_chemical_formula(),
|
|
547
542
|
"initial_atoms_json": ase_encode(atoms),
|
|
548
543
|
"timestamp": datetime.now().isoformat(),
|
|
549
544
|
}
|
|
@@ -583,24 +578,13 @@ class SurrealDBStore(StructureStore):
|
|
|
583
578
|
|
|
584
579
|
if atoms is not None:
|
|
585
580
|
updates["final_atoms_json"] = ase_encode(atoms)
|
|
586
|
-
updates["formula"] = atoms.get_chemical_formula()
|
|
587
581
|
if vector is not None:
|
|
588
582
|
col = self._vec_col()
|
|
589
583
|
updates[col] = vector.astype(np.float32).tolist()
|
|
590
584
|
|
|
591
585
|
updates["energy_per_atom"] = float(meta.get("energy_per_atom", 0.0))
|
|
592
|
-
updates["energy_total"] = float(meta.get("energy_total", 0.0))
|
|
593
586
|
updates["fmax"] = float(meta.get("fmax", 0.0))
|
|
594
587
|
updates["converged"] = bool(meta.get("converged", False))
|
|
595
|
-
updates["thickness"] = (
|
|
596
|
-
float(meta.get("thickness", 0.0))
|
|
597
|
-
if meta.get("thickness") is not None
|
|
598
|
-
else None
|
|
599
|
-
)
|
|
600
|
-
if "enthalpy_per_atom" in meta:
|
|
601
|
-
updates["enthalpy_per_atom"] = float(meta["enthalpy_per_atom"])
|
|
602
|
-
if "volume" in meta:
|
|
603
|
-
updates["volume"] = float(meta["volume"])
|
|
604
588
|
|
|
605
589
|
self._db.query(
|
|
606
590
|
f"UPDATE {_record_id('structure', struct_id)} MERGE $data",
|
|
@@ -697,6 +681,7 @@ class SurrealDBStore(StructureStore):
|
|
|
697
681
|
status: Optional[str] = None,
|
|
698
682
|
statuses: Optional[tuple[str, ...]] = None,
|
|
699
683
|
symprec: float = 1e-3,
|
|
684
|
+
progress_callback: Optional[Callable[..., None]] = None,
|
|
700
685
|
) -> List[dict]:
|
|
701
686
|
effective = statuses or ((status,) if status else None)
|
|
702
687
|
|
|
@@ -714,28 +699,57 @@ class SurrealDBStore(StructureStore):
|
|
|
714
699
|
self._db.query(f"SELECT * FROM structure WHERE {where}", params)
|
|
715
700
|
)
|
|
716
701
|
|
|
702
|
+
try:
|
|
703
|
+
meta = self.get_run_metadata(run_name)
|
|
704
|
+
cfg = (meta or {}).get("config", {})
|
|
705
|
+
except Exception:
|
|
706
|
+
cfg = {}
|
|
707
|
+
pressure_gpa = float(cfg.get("pressure_gpa", 0.0))
|
|
708
|
+
domain = cfg.get("domain", "bulk")
|
|
709
|
+
pressure_evA3 = pressure_gpa * GPa
|
|
710
|
+
|
|
717
711
|
results: List[dict] = []
|
|
718
|
-
|
|
712
|
+
total = len(rows)
|
|
713
|
+
for i, row in enumerate(rows):
|
|
714
|
+
if progress_callback is not None:
|
|
715
|
+
progress_callback(i + 1, total, f"Processing structure {i + 1}/{total}…")
|
|
719
716
|
initial_raw = row.get("initial_atoms_json", "")
|
|
720
717
|
final_raw = row.get("final_atoms_json", "")
|
|
721
718
|
initial_atoms = ase_decode(initial_raw) if initial_raw else None
|
|
722
719
|
final_atoms = ase_decode(final_raw) if final_raw else None
|
|
723
720
|
|
|
721
|
+
atoms = final_atoms if final_atoms is not None else initial_atoms
|
|
722
|
+
energy_per_atom = row["energy_per_atom"]
|
|
723
|
+
|
|
724
|
+
n = len(atoms) if atoms is not None else 0
|
|
725
|
+
formula = atoms.get_chemical_formula() if atoms is not None else ""
|
|
726
|
+
volume = atoms.get_volume() if atoms is not None else None
|
|
727
|
+
energy_total = energy_per_atom * n if n else 0.0
|
|
728
|
+
if pressure_gpa > 0 and volume is not None and n:
|
|
729
|
+
enthalpy_per_atom = energy_per_atom + pressure_evA3 * volume / n
|
|
730
|
+
else:
|
|
731
|
+
enthalpy_per_atom = None
|
|
732
|
+
thickness = (
|
|
733
|
+
calculate_thickness(atoms)
|
|
734
|
+
if domain != "bulk" and atoms is not None
|
|
735
|
+
else None
|
|
736
|
+
)
|
|
737
|
+
|
|
724
738
|
d: dict = {
|
|
725
739
|
"id": _extract_id(row["id"]),
|
|
726
|
-
"formula":
|
|
727
|
-
"energy_per_atom":
|
|
728
|
-
"energy_total":
|
|
729
|
-
"enthalpy_per_atom":
|
|
730
|
-
"volume":
|
|
740
|
+
"formula": formula,
|
|
741
|
+
"energy_per_atom": energy_per_atom,
|
|
742
|
+
"energy_total": energy_total,
|
|
743
|
+
"enthalpy_per_atom": enthalpy_per_atom,
|
|
744
|
+
"volume": volume,
|
|
731
745
|
"fmax": row["fmax"],
|
|
732
746
|
"converged": row["converged"],
|
|
733
|
-
"thickness":
|
|
747
|
+
"thickness": thickness,
|
|
734
748
|
"min_phonon_freq": row.get("min_phonon_freq"),
|
|
735
749
|
"duplicate": row.get("duplicate"),
|
|
736
750
|
"initial_atoms": initial_atoms,
|
|
737
751
|
"final_atoms": final_atoms,
|
|
738
|
-
"atoms":
|
|
752
|
+
"atoms": atoms,
|
|
739
753
|
"status": row["status"],
|
|
740
754
|
"timestamp": row.get("timestamp", ""),
|
|
741
755
|
"gen_spg": row.get("gen_spg"),
|
|
@@ -12,6 +12,7 @@ from rapmat.tui.router import ScreenRouter
|
|
|
12
12
|
from rapmat.tui.state import AppState
|
|
13
13
|
from rapmat.tui.tasks import BackgroundTask
|
|
14
14
|
from rapmat.tui.widgets.dropdown import DropdownSelect
|
|
15
|
+
from rapmat.tui.widgets.progress import ProgressPanel
|
|
15
16
|
from rapmat.tui.widgets.table import SortableTable
|
|
16
17
|
|
|
17
18
|
|
|
@@ -230,10 +231,74 @@ class BaseResultsScreen:
|
|
|
230
231
|
self._phonon_task: "BackgroundTask | None" = None
|
|
231
232
|
self._phonon_modal_body: urwid.Widget | None = None
|
|
232
233
|
|
|
234
|
+
self._outer_placeholder: urwid.WidgetPlaceholder | None = None
|
|
235
|
+
self._loading_task: "BackgroundTask | None" = None
|
|
236
|
+
|
|
233
237
|
def build(self) -> urwid.Widget:
|
|
234
|
-
self.
|
|
235
|
-
self.
|
|
236
|
-
|
|
238
|
+
self._outer_placeholder = urwid.WidgetPlaceholder(urwid.SolidFill(" "))
|
|
239
|
+
self.update_footer_keys()
|
|
240
|
+
self._start_async_fetch()
|
|
241
|
+
return self._outer_placeholder
|
|
242
|
+
|
|
243
|
+
def _loading_title(self) -> str:
|
|
244
|
+
return f" Loading {self.title} "
|
|
245
|
+
|
|
246
|
+
def _start_async_fetch(self) -> None:
|
|
247
|
+
panel = ProgressPanel(title=self._loading_title())
|
|
248
|
+
panel.set_progress(0, 1, "Loading structures from database…")
|
|
249
|
+
|
|
250
|
+
loading_widget = urwid.Filler(urwid.BoxAdapter(panel, 10), valign="middle")
|
|
251
|
+
if self._outer_placeholder is not None:
|
|
252
|
+
self._outer_placeholder.original_widget = loading_widget
|
|
253
|
+
|
|
254
|
+
loop = self._state.loop
|
|
255
|
+
|
|
256
|
+
if loop is None:
|
|
257
|
+
self._fetch_data()
|
|
258
|
+
self._main_frame = self._build_frame()
|
|
259
|
+
if self._outer_placeholder is not None:
|
|
260
|
+
self._outer_placeholder.original_widget = self._main_frame
|
|
261
|
+
return
|
|
262
|
+
|
|
263
|
+
def _worker(progress) -> None:
|
|
264
|
+
def _cb(
|
|
265
|
+
current: int, total: int, message: str = "", is_log: bool = False
|
|
266
|
+
) -> None:
|
|
267
|
+
if progress.cancelled:
|
|
268
|
+
raise KeyboardInterrupt("Cancelled by user")
|
|
269
|
+
progress.update(current=current, total=total, message=message)
|
|
270
|
+
if is_log and message:
|
|
271
|
+
progress.log(message)
|
|
272
|
+
|
|
273
|
+
self._fetch_data(progress_callback=_cb)
|
|
274
|
+
|
|
275
|
+
def _on_progress(current: int, total: int, message: str) -> None:
|
|
276
|
+
panel.set_progress(current, total, message)
|
|
277
|
+
|
|
278
|
+
def _on_log(line: str) -> None:
|
|
279
|
+
panel.add_log(line)
|
|
280
|
+
|
|
281
|
+
def _on_complete() -> None:
|
|
282
|
+
self._main_frame = self._build_frame()
|
|
283
|
+
if self._outer_placeholder is not None:
|
|
284
|
+
self._outer_placeholder.original_widget = self._main_frame
|
|
285
|
+
|
|
286
|
+
current = self._router.current
|
|
287
|
+
if current is not self and current is not None:
|
|
288
|
+
current.on_resume()
|
|
289
|
+
|
|
290
|
+
def _on_error(error: str) -> None:
|
|
291
|
+
panel.set_finished(False, f"Error: {error}")
|
|
292
|
+
|
|
293
|
+
self._loading_task = BackgroundTask(
|
|
294
|
+
fn=_worker,
|
|
295
|
+
loop=loop,
|
|
296
|
+
on_progress=_on_progress,
|
|
297
|
+
on_log=_on_log,
|
|
298
|
+
on_complete=_on_complete,
|
|
299
|
+
on_error=_on_error,
|
|
300
|
+
)
|
|
301
|
+
self._loading_task.start()
|
|
237
302
|
|
|
238
303
|
def on_resume(self) -> None:
|
|
239
304
|
self.update_footer_keys()
|
|
@@ -244,7 +309,7 @@ class BaseResultsScreen:
|
|
|
244
309
|
def update_footer_keys(self, message: str = "") -> None:
|
|
245
310
|
pass
|
|
246
311
|
|
|
247
|
-
def _fetch_data(self) -> None:
|
|
312
|
+
def _fetch_data(self, progress_callback=None) -> None:
|
|
248
313
|
pass
|
|
249
314
|
|
|
250
315
|
def _columns_def(self) -> list[tuple[str, int]]:
|
|
@@ -798,6 +863,8 @@ class BaseResultsScreen:
|
|
|
798
863
|
return None
|
|
799
864
|
if self._phonon_task is not None:
|
|
800
865
|
self._phonon_task.cancel()
|
|
866
|
+
if self._loading_task is not None:
|
|
867
|
+
self._loading_task.cancel()
|
|
801
868
|
self._router.pop()
|
|
802
869
|
return None
|
|
803
870
|
return key
|
|
@@ -1,13 +1,9 @@
|
|
|
1
1
|
from pathlib import Path
|
|
2
2
|
|
|
3
|
-
import urwid
|
|
4
|
-
|
|
5
3
|
from rapmat.tui.router import ScreenRouter
|
|
6
4
|
from rapmat.tui.screens.base_results import BaseResultsScreen, _dyn_stability
|
|
7
5
|
from rapmat.tui.state import AppState
|
|
8
|
-
from rapmat.tui.tasks import BackgroundTask
|
|
9
6
|
from rapmat.tui.widgets.dialog import ModalDialog
|
|
10
|
-
from rapmat.tui.widgets.progress import ProgressPanel
|
|
11
7
|
|
|
12
8
|
|
|
13
9
|
class PhaseAnalysisScreen(BaseResultsScreen):
|
|
@@ -21,8 +17,6 @@ class PhaseAnalysisScreen(BaseResultsScreen):
|
|
|
21
17
|
self._use_enthalpy: bool = False
|
|
22
18
|
self._show_all: bool = True
|
|
23
19
|
self._hull_cutoff: float = 0.0
|
|
24
|
-
self._loading_task: BackgroundTask | None = None
|
|
25
|
-
self._outer_placeholder: urwid.WidgetPlaceholder | None = None
|
|
26
20
|
|
|
27
21
|
def update_footer_keys(self, message: str = "") -> None:
|
|
28
22
|
if not self._state.status_bar:
|
|
@@ -47,118 +41,9 @@ class PhaseAnalysisScreen(BaseResultsScreen):
|
|
|
47
41
|
self._state.status_bar.clear_message()
|
|
48
42
|
|
|
49
43
|
# ------------------------------------------------------------------ #
|
|
50
|
-
#
|
|
44
|
+
# Data fetch
|
|
51
45
|
# ------------------------------------------------------------------ #
|
|
52
46
|
|
|
53
|
-
def build(self) -> urwid.Widget:
|
|
54
|
-
self._outer_placeholder = urwid.WidgetPlaceholder(urwid.SolidFill(" "))
|
|
55
|
-
self.update_footer_keys()
|
|
56
|
-
self._start_async_fetch()
|
|
57
|
-
return self._outer_placeholder
|
|
58
|
-
|
|
59
|
-
def _start_async_fetch(self) -> None:
|
|
60
|
-
panel = ProgressPanel(title=" Phase Analysis ")
|
|
61
|
-
panel.set_progress(0, 1, "Computing phase analysis...")
|
|
62
|
-
panel.add_log("Loading structures from database...")
|
|
63
|
-
|
|
64
|
-
loading_widget = urwid.Filler(urwid.BoxAdapter(panel, 10), valign="middle")
|
|
65
|
-
if self._outer_placeholder is not None:
|
|
66
|
-
self._outer_placeholder.original_widget = loading_widget
|
|
67
|
-
|
|
68
|
-
loop = self._state.loop
|
|
69
|
-
if loop is None:
|
|
70
|
-
self._fetch_data()
|
|
71
|
-
self._main_frame = self._build_frame()
|
|
72
|
-
if self._outer_placeholder is not None:
|
|
73
|
-
self._outer_placeholder.original_widget = self._main_frame
|
|
74
|
-
return
|
|
75
|
-
|
|
76
|
-
_store = self._state.store
|
|
77
|
-
_active_study = self._state.active_study
|
|
78
|
-
_show_all = self._show_all
|
|
79
|
-
_hull_cutoff = self._hull_cutoff
|
|
80
|
-
|
|
81
|
-
_result_box: dict = {}
|
|
82
|
-
|
|
83
|
-
def _worker(progress) -> None:
|
|
84
|
-
progress.log("Resolving study...")
|
|
85
|
-
study_id = _active_study
|
|
86
|
-
if not study_id:
|
|
87
|
-
progress.fail("No active study selected.")
|
|
88
|
-
return
|
|
89
|
-
if isinstance(study_id, str) and ":" in study_id:
|
|
90
|
-
study_id = study_id.split(":")[-1]
|
|
91
|
-
|
|
92
|
-
study = _store.get_study(str(study_id))
|
|
93
|
-
if not study:
|
|
94
|
-
progress.fail(f"Study '{study_id}' not found.")
|
|
95
|
-
return
|
|
96
|
-
|
|
97
|
-
symprec = study.get("config", {}).get("symprec", 1e-3)
|
|
98
|
-
system = study.get("system", "")
|
|
99
|
-
from rapmat.utils.common import parse_system
|
|
100
|
-
|
|
101
|
-
elements = parse_system(system)
|
|
102
|
-
system_size = len(elements)
|
|
103
|
-
|
|
104
|
-
progress.log(
|
|
105
|
-
f"System: {system} ({system_size} element{'s' if system_size != 1 else ''})"
|
|
106
|
-
)
|
|
107
|
-
|
|
108
|
-
if system_size < 2:
|
|
109
|
-
from rapmat.core.hull import build_energy_ranking
|
|
110
|
-
|
|
111
|
-
progress.log("Building energy ranking...")
|
|
112
|
-
sd, use_enthalpy = build_energy_ranking(
|
|
113
|
-
_store, str(study_id), show_all=_show_all, hull_cutoff=_hull_cutoff
|
|
114
|
-
)
|
|
115
|
-
else:
|
|
116
|
-
from rapmat.core.hull import build_phase_diagram
|
|
117
|
-
|
|
118
|
-
progress.log("Building phase diagram...")
|
|
119
|
-
_, sd, use_enthalpy = build_phase_diagram(
|
|
120
|
-
_store,
|
|
121
|
-
str(study_id),
|
|
122
|
-
symprec=symprec,
|
|
123
|
-
show_all=_show_all,
|
|
124
|
-
hull_cutoff=_hull_cutoff,
|
|
125
|
-
)
|
|
126
|
-
|
|
127
|
-
progress.update(1, 1, f"Done - {len(sd)} structures")
|
|
128
|
-
progress.log(f"Computed {len(sd)} structures.")
|
|
129
|
-
|
|
130
|
-
_result_box["study_id"] = str(study_id)
|
|
131
|
-
_result_box["study"] = study
|
|
132
|
-
_result_box["system"] = system
|
|
133
|
-
_result_box["system_size"] = system_size
|
|
134
|
-
_result_box["sd"] = sd
|
|
135
|
-
_result_box["use_enthalpy"] = use_enthalpy
|
|
136
|
-
|
|
137
|
-
def _on_progress(current: int, total: int, message: str) -> None:
|
|
138
|
-
panel.set_progress(current, total, message)
|
|
139
|
-
|
|
140
|
-
def _on_log(line: str) -> None:
|
|
141
|
-
panel.add_log(line)
|
|
142
|
-
|
|
143
|
-
def _on_complete() -> None:
|
|
144
|
-
self._apply_fetch_result(_result_box)
|
|
145
|
-
self._main_frame = self._build_frame()
|
|
146
|
-
if self._outer_placeholder is not None:
|
|
147
|
-
self._outer_placeholder.original_widget = self._main_frame
|
|
148
|
-
|
|
149
|
-
def _on_error(error: str) -> None:
|
|
150
|
-
panel.set_finished(False, f"Error: {error}")
|
|
151
|
-
|
|
152
|
-
self._loading_task = BackgroundTask(
|
|
153
|
-
fn=_worker,
|
|
154
|
-
loop=loop,
|
|
155
|
-
on_progress=_on_progress,
|
|
156
|
-
on_log=_on_log,
|
|
157
|
-
on_complete=_on_complete,
|
|
158
|
-
on_error=_on_error,
|
|
159
|
-
)
|
|
160
|
-
self._loading_task.start()
|
|
161
|
-
|
|
162
47
|
def _apply_fetch_result(self, box: dict) -> None:
|
|
163
48
|
self._study_id = box.get("study_id", "")
|
|
164
49
|
self._study_system = box.get("system", "")
|
|
@@ -202,7 +87,12 @@ class PhaseAnalysisScreen(BaseResultsScreen):
|
|
|
202
87
|
r.get("duplicate") is not None for r in self._results
|
|
203
88
|
)
|
|
204
89
|
|
|
205
|
-
def _fetch_data(self) -> None:
|
|
90
|
+
def _fetch_data(self, progress_callback=None) -> None:
|
|
91
|
+
def _log(msg: str) -> None:
|
|
92
|
+
if progress_callback is not None:
|
|
93
|
+
progress_callback(0, 1, msg, True)
|
|
94
|
+
|
|
95
|
+
_log("Resolving study…")
|
|
206
96
|
study_id = self._state.active_study
|
|
207
97
|
if not study_id:
|
|
208
98
|
return
|
|
@@ -221,10 +111,15 @@ class PhaseAnalysisScreen(BaseResultsScreen):
|
|
|
221
111
|
|
|
222
112
|
elements = parse_system(system)
|
|
223
113
|
system_size = len(elements)
|
|
114
|
+
_log(
|
|
115
|
+
f"System: {system} "
|
|
116
|
+
f"({system_size} element{'s' if system_size != 1 else ''})"
|
|
117
|
+
)
|
|
224
118
|
|
|
225
119
|
if system_size < 2:
|
|
226
120
|
from rapmat.core.hull import build_energy_ranking
|
|
227
121
|
|
|
122
|
+
_log("Building energy ranking…")
|
|
228
123
|
sd, use_enthalpy = build_energy_ranking(
|
|
229
124
|
store,
|
|
230
125
|
str(study_id),
|
|
@@ -234,6 +129,7 @@ class PhaseAnalysisScreen(BaseResultsScreen):
|
|
|
234
129
|
else:
|
|
235
130
|
from rapmat.core.hull import build_phase_diagram
|
|
236
131
|
|
|
132
|
+
_log("Building phase diagram…")
|
|
237
133
|
_, sd, use_enthalpy = build_phase_diagram(
|
|
238
134
|
store,
|
|
239
135
|
str(study_id),
|
|
@@ -242,6 +138,9 @@ class PhaseAnalysisScreen(BaseResultsScreen):
|
|
|
242
138
|
hull_cutoff=self._hull_cutoff,
|
|
243
139
|
)
|
|
244
140
|
|
|
141
|
+
if progress_callback is not None:
|
|
142
|
+
progress_callback(1, 1, f"Done - {len(sd)} structures")
|
|
143
|
+
|
|
245
144
|
box.update(
|
|
246
145
|
{
|
|
247
146
|
"study_id": str(study_id),
|
|
@@ -30,7 +30,7 @@ class ResultsScreen(BaseResultsScreen):
|
|
|
30
30
|
else:
|
|
31
31
|
self._state.status_bar.clear_message()
|
|
32
32
|
|
|
33
|
-
def _fetch_data(self) -> None:
|
|
33
|
+
def _fetch_data(self, progress_callback=None) -> None:
|
|
34
34
|
run_name = self._state.active_run or ""
|
|
35
35
|
self._run_name = run_name
|
|
36
36
|
|
|
@@ -41,7 +41,9 @@ class ResultsScreen(BaseResultsScreen):
|
|
|
41
41
|
self._pressure_gpa = float(config.get("pressure_gpa", 0.0))
|
|
42
42
|
self._phonon_cutoff = config.get("phonon_cutoff")
|
|
43
43
|
|
|
44
|
-
records = store.get_structures(
|
|
44
|
+
records = store.get_structures(
|
|
45
|
+
run_name, status="relaxed", progress_callback=progress_callback
|
|
46
|
+
)
|
|
45
47
|
|
|
46
48
|
if self._pressure_gpa > 0:
|
|
47
49
|
records.sort(
|
|
@@ -52,6 +52,21 @@ def _ensure_descriptor(store: StructureStore) -> None:
|
|
|
52
52
|
store.register_descriptor(_TEST_DESCRIPTOR)
|
|
53
53
|
|
|
54
54
|
|
|
55
|
+
def add_generated_candidate(
|
|
56
|
+
store: StructureStore,
|
|
57
|
+
run_name: str,
|
|
58
|
+
struct_id: str,
|
|
59
|
+
atoms: Atoms,
|
|
60
|
+
vector: np.ndarray | None = None,
|
|
61
|
+
spg: int = 1,
|
|
62
|
+
fu: int = 1,
|
|
63
|
+
) -> str:
|
|
64
|
+
"""Insert a single 'generated' candidate via the placeholder pipeline."""
|
|
65
|
+
store.add_generation_placeholders(run_name, [(struct_id, spg, fu)])
|
|
66
|
+
store.update_generated_structure(struct_id, atoms, vector=vector)
|
|
67
|
+
return struct_id
|
|
68
|
+
|
|
69
|
+
|
|
55
70
|
def add_relaxed_structure(
|
|
56
71
|
store: StructureStore,
|
|
57
72
|
run_name: str,
|
|
@@ -59,35 +74,29 @@ def add_relaxed_structure(
|
|
|
59
74
|
energy_per_atom: float,
|
|
60
75
|
struct_id: str,
|
|
61
76
|
vector: np.ndarray | None = None,
|
|
62
|
-
enthalpy_per_atom: float | None = None,
|
|
63
77
|
) -> None:
|
|
64
|
-
"""Add a structure and immediately mark it relaxed with the given energy.
|
|
65
|
-
|
|
66
|
-
|
|
78
|
+
"""Add a structure and immediately mark it relaxed with the given energy.
|
|
79
|
+
|
|
80
|
+
Derived quantities (formula, volume, energy_total, enthalpy, thickness)
|
|
81
|
+
are computed at read time by get_structures, so only the canonical
|
|
82
|
+
metadata is written here.
|
|
83
|
+
"""
|
|
67
84
|
if vector is None:
|
|
68
85
|
vector = np.zeros(VECTOR_DIM, dtype=np.float32)
|
|
69
86
|
_ensure_descriptor(store)
|
|
70
87
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
{"id": struct_id, "atoms": atoms, "vector": vector, "status": "generated"},
|
|
74
|
-
])
|
|
75
|
-
|
|
76
|
-
# Immediately update to relaxed
|
|
77
|
-
metadata = {
|
|
78
|
-
"energy_per_atom": energy_per_atom,
|
|
79
|
-
"energy_total": energy_total,
|
|
80
|
-
"fmax": 0.01,
|
|
81
|
-
"converged": True,
|
|
82
|
-
}
|
|
83
|
-
if enthalpy_per_atom is not None:
|
|
84
|
-
metadata["enthalpy_per_atom"] = enthalpy_per_atom
|
|
88
|
+
add_generated_candidate(store, run_name, struct_id, atoms, vector=vector)
|
|
89
|
+
|
|
85
90
|
store.update_structure(
|
|
86
91
|
struct_id,
|
|
87
92
|
"relaxed",
|
|
88
93
|
atoms=atoms,
|
|
89
94
|
vector=vector,
|
|
90
|
-
metadata=
|
|
95
|
+
metadata={
|
|
96
|
+
"energy_per_atom": energy_per_atom,
|
|
97
|
+
"fmax": 0.01,
|
|
98
|
+
"converged": True,
|
|
99
|
+
},
|
|
91
100
|
)
|
|
92
101
|
|
|
93
102
|
|
|
@@ -16,35 +16,12 @@ from rapmat.storage import SurrealDBStore
|
|
|
16
16
|
class TestDbConfig:
|
|
17
17
|
def test_load_returns_none_when_no_file(self, tmp_path, monkeypatch):
|
|
18
18
|
monkeypatch.setattr("rapmat.db_config._DB_CONFIG_FILE", tmp_path / "nope.toml")
|
|
19
|
-
monkeypatch.delenv("RAPMAT_DB_URL", raising=False)
|
|
20
|
-
monkeypatch.delenv("RAPMAT_DB_NS", raising=False)
|
|
21
|
-
monkeypatch.delenv("RAPMAT_DB_NAME", raising=False)
|
|
22
|
-
monkeypatch.delenv("RAPMAT_DB_USER", raising=False)
|
|
23
|
-
monkeypatch.delenv("RAPMAT_DB_PASSWORD", raising=False)
|
|
24
19
|
assert load_db_config() is None
|
|
25
20
|
|
|
26
|
-
def test_env_vars_override(self, tmp_path, monkeypatch):
|
|
27
|
-
cfg_file = tmp_path / "db.toml"
|
|
28
|
-
cfg_file.write_text(
|
|
29
|
-
'[server]\nurl = "ws://old/rpc"\nnamespace = "ns"\n'
|
|
30
|
-
'database = "db"\nusername = "u"\npassword = "p"\n'
|
|
31
|
-
)
|
|
32
|
-
monkeypatch.setattr("rapmat.db_config._DB_CONFIG_FILE", cfg_file)
|
|
33
|
-
monkeypatch.setenv("RAPMAT_DB_URL", "ws://new/rpc")
|
|
34
|
-
full = load_db_config()
|
|
35
|
-
assert full is not None
|
|
36
|
-
assert full["server"]["url"] == "ws://new/rpc"
|
|
37
|
-
assert full["server"]["namespace"] == "ns"
|
|
38
|
-
|
|
39
21
|
def test_save_and_load_roundtrip(self, tmp_path, monkeypatch):
|
|
40
22
|
cfg_file = tmp_path / "db.toml"
|
|
41
23
|
monkeypatch.setattr("rapmat.db_config._DB_CONFIG_FILE", cfg_file)
|
|
42
24
|
monkeypatch.setattr("rapmat.db_config.APP_CONFIG_DIR", tmp_path)
|
|
43
|
-
monkeypatch.delenv("RAPMAT_DB_URL", raising=False)
|
|
44
|
-
monkeypatch.delenv("RAPMAT_DB_NS", raising=False)
|
|
45
|
-
monkeypatch.delenv("RAPMAT_DB_NAME", raising=False)
|
|
46
|
-
monkeypatch.delenv("RAPMAT_DB_USER", raising=False)
|
|
47
|
-
monkeypatch.delenv("RAPMAT_DB_PASSWORD", raising=False)
|
|
48
25
|
|
|
49
26
|
save_db_config(
|
|
50
27
|
general={"mode": "remote"},
|