minducer / tests /test_space.py
aibergman's picture
Refine exchange plot labels and induced terminology
6e1bb63
Raw
History Blame Contribute Delete
9.95 kB
from __future__ import annotations
import pytest
import numpy as np
from induced_exchange import SublatticeClassification
from induced_exchange.io_uppasd import load_uppasd
from induced_exchange.space import (
UploadMappingError,
_figure_dressed,
_figure_dressed_realspace,
_figure_dressed_realspace_delta,
_figure_dressed_realspace_relative,
_figure_exchange,
_figure_magnons,
_figure_path,
_figure_realspace,
_figure_response,
analyse_model,
classification_from_induced_sites,
classification_rows,
default_stiffness_q_max,
default_induced_sites,
dressed_shell_rows,
load_uploaded_set,
magnon_markdown,
response_markdown,
)
def test_individual_uploads_require_inpsd_cell_instead_of_assuming_identity(tmp_path):
posfile = tmp_path / "posfile"
momfile = tmp_path / "momfile"
jfile = tmp_path / "jfile"
posfile.write_text("1 1 0 0 0\n", encoding="utf-8")
momfile.write_text("1 1 1 0 0 1\n", encoding="utf-8")
jfile.write_text("1 1 0 0 0 1\n", encoding="utf-8")
with pytest.raises(UploadMappingError, match="inpsd.dat.*cell vectors"):
load_uploaded_set(
None,
[posfile, momfile, jfile],
manual_mapping={"posfile": "posfile", "momfile": "momfile", "exchange": "jfile"},
)
def test_magnon_summary_displays_stiffness_and_polesya_equivalence():
loaded = load_uppasd("examples/induced_toy/inpsd.dat", energy_unit="meV")
session = analyse_model(loaded, SublatticeClassification.from_inputs([1], [2]), mesh_size=2, x=0.5)
summary = magnon_markdown(session)
assert {"dressed", "polesya"}.issubset(session.magnons)
assert "Spin stiffness" in summary
assert "Mryasov / Polesya equivalence" in summary
def test_role_toggle_defaults_small_moments_to_induced_and_completes_classification():
loaded = load_uppasd("examples/uppasd_style/inpsd.dat")
assert default_induced_sites(loaded.model) == (1,)
assert [row[3] for row in classification_rows(loaded.model)] == ["induced", "robust"]
classification = classification_from_induced_sites([1], loaded.model)
assert classification.robust_sites == (2,)
assert classification.induced_sites == (1,)
all_robust = classification_from_induced_sites([], loaded.model)
assert all_robust.robust_sites == (1, 2)
assert all_robust.induced_sites == ()
def test_stiffness_interval_scales_with_cell_units():
loaded = load_uppasd("examples/uppasd_style/inpsd.dat")
assert default_stiffness_q_max(loaded.model) > 1e9
def test_seekpath_and_realspace_shells_handle_alat_scaled_cells():
loaded = load_uppasd("examples/uppasd_style/inpsd.dat", expand_symmetry=True)
session = analyse_model(loaded, SublatticeClassification.from_inputs([1], [2]), mesh_size=2, x=0.5)
assert session.path is not None
assert session.path.path.source == "seekpath"
rows = dressed_shell_rows(session)
assert rows
assert all(np.isfinite(row[1]) and np.isfinite(row[5]) for row in rows)
assert all(np.isfinite(row[7]) for row in rows)
def test_space_plot_helpers_close_figures_after_building_them():
matplotlib = pytest.importorskip("matplotlib")
matplotlib.use("Agg")
plt = pytest.importorskip("matplotlib.pyplot")
loaded = load_uppasd("examples/induced_toy/inpsd.dat", energy_unit="meV")
session = analyse_model(loaded, SublatticeClassification.from_inputs([1], [2]), mesh_size=2, x=0.5)
plt.close("all")
figures = [
_figure_exchange(session),
_figure_realspace(session),
_figure_path(session),
_figure_response(session),
_figure_dressed(session),
_figure_dressed_realspace(session),
_figure_dressed_realspace_delta(session),
_figure_dressed_realspace_relative(session),
_figure_magnons(session),
]
assert plt.get_fignums() == []
assert all(hasattr(figure, "savefig") for figure in figures)
def test_space_q_dependent_analysis_uses_one_seekpath_coordinate():
loaded = load_uppasd("examples/induced_toy/inpsd.dat", energy_unit="meV")
session = analyse_model(loaded, SublatticeClassification.from_inputs([1], [2]), mesh_size=3, x=0.5)
assert session.path is not None
assert session.path.path.source == "seekpath"
assert np.allclose(session.q_fractional, session.path.path.q_fractional)
assert np.allclose(session.response_scan.q_fractional, session.q_fractional)
assert np.allclose(session.magnons["raw"].q_fractional, session.q_fractional)
assert np.allclose(session.magnons["dressed"].q_fractional, session.q_fractional)
assert session.dressed_real_space is not None
assert session.dressed_real_space.q_count == 27
def test_space_plot_labels_explain_path_and_response_normalization():
pytest.importorskip("matplotlib.pyplot")
loaded = load_uppasd("examples/induced_toy/inpsd.dat", energy_unit="meV")
session = analyse_model(loaded, SublatticeClassification.from_inputs([1], [2]), mesh_size=2, x=0.5)
exchange_figure = _figure_exchange(session)
response_figure = _figure_response(session)
assert "high-symmetry path" in exchange_figure.axes[0].get_title()
assert exchange_figure.axes[0].get_xlabel() == "q-path"
assert "p_ind(Γ)" in response_figure.axes[0].get_title()
assert "normalized induced polarization" in response_figure.axes[0].get_ylabel()
assert "coherent unit-amplitude" in response_markdown(session)
def test_raw_exchange_plot_separates_robust_cross_and_induced_markers():
pytest.importorskip("matplotlib.pyplot")
loaded = load_uppasd("examples/induced_toy/inpsd.dat", energy_unit="meV")
session = analyse_model(loaded, SublatticeClassification.from_inputs([1], [2]), mesh_size=2, x=0.5)
figure = _figure_realspace(session)
labels = {r"$J_{MM}$", r"$J_{Mm}$", r"$J_{mm}$"}
labelled = [collection for collection in figure.axes[0].collections if collection.get_label() in labels]
assert {collection.get_label() for collection in labelled} == labels
marker_shapes = {tuple(np.round(collection.get_paths()[0].vertices.ravel(), 8)) for collection in labelled}
assert len(marker_shapes) == 3
def test_dressed_realspace_plots_separate_induced_correction_and_label_channels():
pytest.importorskip("matplotlib.pyplot")
loaded = load_uppasd("examples/induced_toy/inpsd.dat", energy_unit="meV")
session = analyse_model(loaded, SublatticeClassification.from_inputs([1], [2]), mesh_size=2, x=0.5)
channels = _figure_dressed_realspace(session)
induced = _figure_dressed_realspace_delta(session)
relative = _figure_dressed_realspace_relative(session)
channel_labels = {line.get_label() for line in channels.axes[0].lines if not line.get_label().startswith("_")}
induced_labels = {line.get_label() for line in induced.axes[0].lines if not line.get_label().startswith("_")}
relative_labels = {line.get_label() for line in relative.axes[0].lines if not line.get_label().startswith("_")}
assert {"J_MM(r)", "J_Mm(r) = K_Mm(r)", "J_Mryasov(r)", "J_Polesya(r) = J_Mryasov(r)"}.issubset(channel_labels)
assert "ΔJ_induced(r)" not in channel_labels
assert induced_labels == {"ΔJ_induced(r)"}
assert relative_labels == {"ΔJ_induced(r) / J_MM(r)"}
assert relative.axes[0].get_ylabel() == "ΔJ_induced / J_MM"
def test_realspace_plot_uses_implicit_alat_one_coordinates():
pytest.importorskip("matplotlib.pyplot")
loaded = load_uppasd("examples/uppasd_style/inpsd.dat", expand_symmetry=True)
session = analyse_model(loaded, SublatticeClassification.from_inputs([1], [2]), mesh_size=2, x=0.5)
figure = _figure_dressed_realspace(session)
distances = np.linalg.norm(session.dressed_real_space.displacements, axis=1) / loaded.config.alat
scale = max(float(np.max(distances, initial=0.0)), 1.0e-300)
normalized = distances / scale
expected = np.asarray([
float(np.mean(distances[np.isclose(normalized, key, atol=1e-8, rtol=0.0)]))
for key in sorted({round(float(value), 8) for value in normalized})
])
assert figure.axes[0].get_xlabel() == "Interatomic distance (alat)"
assert np.allclose(figure.axes[0].lines[0].get_xdata(), expected)
assert np.max(figure.axes[0].lines[0].get_xdata()) < 10.0
def test_upload_staging_preserves_nested_uppasd_alias_paths(tmp_path):
inpsd = tmp_path / "inpsd.dat"
posfile = tmp_path / "posfile"
momfile = tmp_path / "momfile"
jfile = tmp_path / "jfile"
inpsd.write_text(
"simid demo\n"
"cell 1 0 0\n"
" 0 1 0\n"
" 0 0 1\n"
"positions data/posfile\n"
"moments data/momfile\n"
"exchange data/jfile\n",
encoding="utf-8",
)
posfile.write_text("1 1 0 0 0\n", encoding="utf-8")
momfile.write_text("1 1 1 0 0 1\n", encoding="utf-8")
jfile.write_text("1 1 0 0 0 1\n", encoding="utf-8")
loaded, mapping = load_uploaded_set(inpsd, [posfile, momfile, jfile])
assert mapping.ok
assert loaded.model.sites[0].moment == 1.0
assert loaded.model.exchange_bonds[0].jij == 1.0
def test_upload_staging_rejects_parent_directory_traversal(tmp_path):
input_dir = tmp_path / "input"
input_dir.mkdir()
inpsd = input_dir / "inpsd.dat"
posfile = tmp_path / "posfile"
momfile = input_dir / "momfile"
jfile = input_dir / "jfile"
inpsd.write_text(
"simid demo\n"
"cell 1 0 0\n"
" 0 1 0\n"
" 0 0 1\n"
"posfile ../posfile\n"
"momfile momfile\n"
"exchange jfile\n",
encoding="utf-8",
)
posfile.write_text("1 1 0 0 0\n", encoding="utf-8")
momfile.write_text("1 1 1 0 0 1\n", encoding="utf-8")
jfile.write_text("1 1 0 0 0 1\n", encoding="utf-8")
with pytest.raises(UploadMappingError, match="escapes the upload staging directory"):
load_uploaded_set(inpsd, [posfile, momfile, jfile])