File size: 8,149 Bytes
bc2957c | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 | """Command-line interface for the FourJ package."""
from __future__ import annotations
import argparse
import sys
from pathlib import Path
from .workflow import FrozenMagnonWorkflow, WorkflowConfig
def structure_metadata(workflow: FrozenMagnonWorkflow) -> list[tuple[str, str]]:
"""Return best-effort structure labels for CLI output."""
rows: list[tuple[str, str]] = []
if workflow.structure is None:
return rows
try:
from .visualization import SeekPath
_points, _segments, bravais = SeekPath(workflow.structure, workflow.config.symprec).get()
rows.append(("Bravais", bravais))
except Exception:
pass
try:
import spglib # type: ignore
dataset = spglib.get_symmetry_dataset(workflow.structure.spglib_cell, symprec=workflow.config.symprec)
if dataset is not None:
number = getattr(dataset, "number", None) if not isinstance(dataset, dict) else dataset.get("number")
international = getattr(dataset, "international", None) if not isinstance(dataset, dict) else dataset.get("international")
hall = getattr(dataset, "hall", None) if not isinstance(dataset, dict) else dataset.get("hall")
if international and number:
rows.append(("Space group", f"{international} ({number})"))
elif international:
rows.append(("Space group", str(international)))
if hall:
rows.append(("Hall", str(hall)))
except Exception:
pass
return rows
def parse_args(argv: list[str]) -> argparse.Namespace:
parser = argparse.ArgumentParser(description="FourJ E(q) -> J(q) -> J_ij transform for Elk spin spirals.")
parser.add_argument("--energy", default="energy_vs_q.dat", type=Path, help="Input E(q) table")
parser.add_argument("--elk", default="elk.tmp", type=Path, help="Elk input/tmp file with scale, avec, atoms")
parser.add_argument("--vectors", type=Path, default=None, help="Optional jfile-like R-vector list; columns 3-5 are integer direct-lattice R. Defaults to ./jfile if present; otherwise infer from q mesh unless --rmax is set.")
parser.add_argument("--rmax", type=float, default=None, help="Generate all integer direct-lattice R vectors up to this distance in Angstrom when no vector file is given")
parser.add_argument("--theta", type=float, default=90.0, help="Cone angle in degrees")
parser.add_argument("--symmetry", choices=("none", "time-reversal", "lattice", "spglib"), default="time-reversal", help="q-space symmetry averaging/completion mode")
parser.add_argument("--e0", choices=("q0", "min"), default="q0", help="Reference energy for E(q)-E0")
parser.add_argument("--output-prefix", default="fourj", type=Path)
parser.add_argument("--tol", type=float, default=1e-8)
parser.add_argument("--round-decimals", type=int, default=10)
parser.add_argument("--symprec", type=float, default=1e-5, help="spglib symmetry tolerance")
parser.add_argument("--no-realspace-pair-symmetry", action="store_true", help="Do not enforce J(R)=conj[J(-R)]")
parser.add_argument("--gui", action="store_true", help="Launch the Plotly dashboard preloaded with these CLI input paths and settings.")
parser.add_argument("--gui-host", default="127.0.0.1", help="Host for --gui dashboard server")
parser.add_argument("--gui-port", default=8050, type=int, help="Port for --gui dashboard server")
parser.add_argument("--gui-debug", action="store_true", help="Run --gui dashboard in Dash debug mode")
parser.add_argument("--plot-path", action="store_true", help="Plot an E(q)-derived spectrum along Seekpath symmetry segments.")
parser.add_argument("--plot-kind", choices=("magnon", "energy"), default="magnon")
parser.add_argument("--plot-tol", type=float, default=1e-7)
parser.add_argument("--plot-min-points", type=int, default=2)
parser.add_argument("--plot-lswt", action="store_true", help="Overlay J(0)-J(q) reconstructed from extracted J_ij.")
parser.add_argument("--lswt-moment", type=float, default=None, help="If set, plot 4[J(0)-J(q)]/M in meV.")
parser.add_argument("--lswt-dense-path", action="store_true", help="Evaluate LSWT/exchange overlay on a dense Seekpath line.")
parser.add_argument("--lswt-path-points", type=int, default=202)
parser.add_argument("--fit-lsq", action="store_true", help="Fit J(0)-J(q) to symmetry-grouped real-space exchange shells.")
parser.add_argument("--fit-num-shells", type=int, default=None, help="Use only this many shortest shells, e.g. 2 for NN+NNN.")
parser.add_argument("--fit-rmax", type=float, default=None)
parser.add_argument("--fit-distance-tol", type=float, default=1e-6)
return parser.parse_args(argv)
def main(argv: list[str] | None = None) -> int:
args = parse_args(sys.argv[1:] if argv is None else argv)
if args.gui:
from .dashboard import PathAnalysisConfig, create_app
app = create_app(
PathAnalysisConfig(
elk_path=args.elk,
energy_path=args.energy,
vectors_path=args.vectors,
theta=args.theta,
symmetry=args.symmetry,
e0=args.e0,
rmax=args.rmax,
fit_lsq=args.fit_lsq,
fit_shells=args.fit_num_shells,
moment=args.lswt_moment,
dense_points=args.lswt_path_points,
)
)
app.run(host=args.gui_host, port=args.gui_port, debug=args.gui_debug)
return 0
workflow = FrozenMagnonWorkflow(
WorkflowConfig(
energy_path=args.energy,
elk_path=args.elk,
vectors_path=args.vectors,
rmax=args.rmax,
theta=args.theta,
symmetry=args.symmetry,
e0=args.e0,
output_prefix=args.output_prefix,
tol=args.tol,
round_decimals=args.round_decimals,
symprec=args.symprec,
realspace_pair_symmetry=not args.no_realspace_pair_symmetry,
)
)
result = workflow.run_transform()
jij_path, dist_path, jq_path = workflow.write_transform_outputs()
print(f"Read {len(workflow.energy_table.q)} raw q points from {args.energy}")
print(f"Using {len(result.q)} symmetry-averaged q samples; symmetry mode: {workflow.symmetry_used}")
print(f"Read/generated {len(result.vectors)} R vectors")
print(f"R vectors are integer direct-lattice coordinates; source: {workflow.vector_source}")
for label, value in structure_metadata(workflow):
print(f"{label}: {value}")
print(f"Wrote {jij_path}")
print(f"Wrote {dist_path}")
print(f"Wrote {jq_path}")
print(f"Max |Im J_ij| after selected symmetrization: {workflow.max_imag_jij():.6e} mRy")
if args.fit_lsq:
lsq_result = workflow.fit_lsq(args.fit_num_shells, args.fit_rmax, args.fit_distance_tol)
shell_path, fit_path = workflow.write_lsq_outputs()
print(f"LSQ fit used {len(lsq_result.shells)} symmetry shells; rotation mode: {lsq_result.rotation_source}")
print(f"LSQ RMSE: {lsq_result.rmse_mry:.6f} mRy; max |error|: {lsq_result.max_abs_error_mry:.6f} mRy")
print(f"Wrote {shell_path}")
print(f"Wrote {fit_path}")
if args.plot_path:
plot_path, data_path, skipped_segments = workflow.plot_seekpath(
plot_kind=args.plot_kind,
plot_lswt=args.plot_lswt,
lswt_moment=args.lswt_moment,
dense_path=args.lswt_dense_path,
path_points=args.lswt_path_points,
tol=args.plot_tol,
min_points=args.plot_min_points,
)
print(f"Wrote {plot_path}")
print(f"Wrote {data_path}")
if args.plot_lswt and args.lswt_dense_path:
print(f"Wrote {args.output_prefix.with_suffix(f'.seekpath_{args.plot_kind}_lswt_dense.dat')}")
if skipped_segments:
print("Skipped Seekpath segments without enough existing q points: " + ", ".join(skipped_segments))
return 0
if __name__ == "__main__":
raise SystemExit(main())
|