| """Hugging Face / Gradio entry point for the Induced-Moment Exchange Explorer.""" |
|
|
| from __future__ import annotations |
|
|
| import hashlib |
| import inspect |
| import os |
| import sys |
| import tempfile |
| from pathlib import Path |
|
|
| import numpy as np |
|
|
| |
| |
| try: |
| import spaces |
| except ImportError: |
| class _LocalSpaces: |
| @staticmethod |
| def GPU(*args, **kwargs): |
| def decorate(function): |
| return function |
|
|
| return decorate |
|
|
| spaces = _LocalSpaces() |
|
|
|
|
| |
| |
| |
| @spaces.GPU(duration=1) |
| def _zerogpu_probe(): |
| return True |
|
|
|
|
| |
| SRC = Path(__file__).resolve().parent / "src" |
| if str(SRC) not in sys.path: |
| sys.path.insert(0, str(SRC)) |
|
|
| |
| |
| MPL_CACHE = Path(tempfile.gettempdir()) / "induced_exchange_mpl" |
| MPL_CACHE.mkdir(parents=True, exist_ok=True) |
| os.environ.setdefault("MPLCONFIGDIR", str(MPL_CACHE)) |
|
|
| APP_CSS = """ |
| :root { --imx-ink: #183b56; --imx-teal: #18a999; --imx-coral: #e07a5f; --imx-paper: #f7fafc; --imx-page: #102a43; --imx-panel: #183b56; --imx-text-muted: #c7d6df; color-scheme: dark; } |
| html, body, .gradio-container { background: var(--imx-page) !important; color: var(--imx-paper) !important; } |
| .gradio-container { |
| --body-background-fill: var(--imx-page) !important; |
| --background-fill-primary: var(--imx-panel) !important; |
| --background-fill-secondary: var(--imx-page) !important; |
| --body-text-color: var(--imx-paper) !important; |
| --body-text-color-subdued: var(--imx-text-muted) !important; |
| --block-background-fill: var(--imx-panel) !important; |
| --block-info-text-color: var(--imx-text-muted) !important; |
| --block-label-background-fill: var(--imx-teal) !important; |
| --block-label-text-color: var(--imx-page) !important; |
| --block-title-text-color: var(--imx-page) !important; |
| --checkbox-label-text-color: var(--imx-paper) !important; |
| --table-text-color: var(--imx-paper) !important; |
| --table-even-background-fill: #214b63 !important; |
| --table-odd-background-fill: var(--imx-panel) !important; |
| --button-secondary-background-fill: #214b63 !important; |
| --button-secondary-text-color: var(--imx-paper) !important; |
| --input-background-fill: var(--imx-paper) !important; |
| --input-background-fill-hover: #ffffff !important; |
| --input-background-fill-focus: #ffffff !important; |
| --input-text-color: #000000 !important; |
| --input-placeholder-color: #597080 !important; |
| } |
| .gradio-container .gradio-dropdown input, |
| .gradio-container .gradio-dropdown .wrap, |
| .gradio-container .gradio-dropdown .options, |
| .gradio-container .gradio-dropdown .options *, |
| .gradio-container .gradio-dropdown [role="listbox"], |
| .gradio-container .gradio-dropdown [role="listbox"] * { |
| color: #000000 !important; |
| } |
| /* DropdownOptions is rendered as a global fixed-position menu in Gradio. */ |
| ul.options, |
| ul.options *, |
| [role="option"], |
| [role="option"] * { |
| color: #000000 !important; |
| } |
| input[role="listbox"] { |
| color: #000000 !important; |
| -webkit-text-fill-color: #000000 !important; |
| } |
| .imx-light-field input[type="number"], |
| .imx-light-field .gradio-dropdown input, |
| .imx-light-field input[role="listbox"] { |
| color: #000000 !important; |
| -webkit-text-fill-color: #000000 !important; |
| } |
| ul.options, |
| ul.options .item, |
| ul.options [role="option"] { |
| background-color: #ffffff !important; |
| } |
| ul.options .item:hover, |
| ul.options .item.active, |
| ul.options [role="option"]:hover, |
| ul.options [role="option"][aria-selected="true"] { |
| background-color: #f0f0f0 !important; |
| } |
| .gradio-container .prose, |
| .gradio-container .prose p, |
| .gradio-container .prose li, |
| .gradio-container .prose h1, |
| .gradio-container .prose h2, |
| .gradio-container .prose h3, |
| .gradio-container .prose h4, |
| .gradio-container .prose blockquote, |
| .gradio-container .label-wrap label, |
| .gradio-container .block-info, |
| .gradio-container .info, |
| .gradio-container .description, |
| .gradio-container .tab-nav button { color: var(--imx-paper) !important; } |
| .gradio-container .imx-plot-heading { color: var(--imx-teal) !important; } |
| .gradio-container .imx-file-upload .wrap { color: var(--imx-paper) !important; } |
| .gradio-container .imx-file-upload .wrap .or { color: var(--imx-text-muted) !important; } |
| .gradio-container .imx-file-upload .wrap .icon-wrap { color: var(--imx-text-muted) !important; } |
| .imx-shell { max-width: 1380px; margin: 0 auto; } |
| .imx-hero { background: linear-gradient(125deg, #183b56 0%, #236477 58%, #18a999 100%); color: white; border-radius: 20px; padding: 30px 34px; margin-bottom: 18px; box-shadow: 0 14px 32px rgba(24,59,86,.16); } |
| .imx-hero h1 { margin: 0 0 8px; font-size: 2.2rem; letter-spacing: -.03em; } |
| .imx-hero p { margin: 0; max-width: 860px; color: #e7f4f2; font-size: 1.05rem; } |
| .imx-kicker { text-transform: uppercase; letter-spacing: .12em; font-size: .75rem; font-weight: 700; color: #a9ebe0; } |
| .imx-note { border-left: 4px solid var(--imx-coral); background: #fff7f3; padding: 12px 15px; border-radius: 8px; color: #5b3b35; } |
| .imx-card { border: 1px solid #dfe8ed; background: rgba(255,255,255,.72); border-radius: 14px; padding: 6px; } |
| .imx-muted { color: #597080; } |
| .imx-plot-heading { color: var(--imx-teal); font-size: .95rem; font-weight: 700; margin: 4px 0 6px; padding: 0 6px; } |
| .imx-plot-heading p { margin: 0; } |
| footer { display: none !important; } |
| """ |
|
|
|
|
| def _configure_launch_options(demo, gr): |
| """Apply theme/CSS across Gradio versions around the 5-to-6 transition.""" |
|
|
| theme = gr.themes.Soft(primary_hue="teal", secondary_hue="slate") |
| parameters = inspect.signature(demo.launch).parameters |
| options = {} |
| if "theme" in parameters: |
| options["theme"] = theme |
| else: |
| |
| |
| |
| |
| demo.theme = theme |
| demo.theme_css = theme._get_theme_css() |
| demo.stylesheets = theme._stylesheets |
| demo.theme_hash = hashlib.sha256(demo.theme_css.encode("utf-8")).hexdigest() |
| if "css" in parameters: |
| options["css"] = APP_CSS |
| else: |
| demo.css = APP_CSS |
| return options |
|
|
|
|
| def build_demo(): |
| """Build the UI lazily so the scientific package remains Gradio-free.""" |
|
|
| try: |
| import gradio as gr |
| except ImportError as exc: |
| raise RuntimeError("The Space UI needs Gradio. Install the dependencies from requirements.txt.") from exc |
|
|
| from induced_exchange.space import ( |
| UploadMappingError, |
| AnalysisSession, |
| _figure_dressed, |
| _figure_dressed_realspace, |
| _figure_dressed_realspace_delta, |
| _figure_dressed_realspace_relative, |
| _figure_exchange, |
| _figure_magnons, |
| _figure_realspace, |
| _figure_response, |
| analyse_model, |
| classification_from_induced_sites, |
| classification_rows, |
| cleanup_analysis_artifacts, |
| default_induced_sites, |
| default_stiffness_q_max, |
| dressed_shell_rows, |
| export_files, |
| inference_rows, |
| inspect_upload_mapping, |
| load_uploaded_set, |
| magnon_markdown, |
| model_summary, |
| ordering_markdown, |
| response_markdown, |
| shell_rows, |
| ) |
| from induced_exchange.io_uppasd import InputFormatError, LoadedUppASD |
|
|
| examples_root = Path(__file__).resolve().parent / "examples" |
| example_choices = ["Upload my files", "Simple analytic FM", "Induced-moment toy", "FePt-style UppASD", "UppASD symmetry-reduced Jij"] |
| example_paths = { |
| "Simple analytic FM": examples_root / "simple_fm" / "inpsd.dat", |
| "Induced-moment toy": examples_root / "induced_toy" / "inpsd.dat", |
| "FePt-style UppASD": examples_root / "fept_style" / "inpsd.dat", |
| "UppASD symmetry-reduced Jij": examples_root / "uppasd_style" / "inpsd.dat", |
| } |
|
|
| def plot_heading(text): |
| """Render plot context outside the Matplotlib canvas.""" |
|
|
| return gr.Markdown(f"<div class='imx-plot-heading'>{text}</div>") |
|
|
| with gr.Blocks(title="Induced-Moment Exchange Explorer") as demo: |
| session_state = gr.State(None) |
| gr.HTML("<div class='imx-hero'><div class='imx-kicker'>Scientific exchange analysis · CPU-friendly</div><h1>Induced-Moment Exchange Explorer</h1><p>From UppASD-style Jij input to reciprocal exchange, induced response, downfolded interactions, and FM-compatible magnon diagnostics.</p></div>", padding=False) |
|
|
| with gr.Tabs(): |
| with gr.Tab("1 · Input"): |
| gr.Markdown("### Bring an UppASD input set into the explorer\nUpload `inpsd.dat` and its referenced files. The `inpsd.dat` file is required because it supplies the explicit cell vectors used for reciprocal-space analysis; individual `posfile`, `momfile`, and `jfile` uploads alone are not loaded and no identity cell is assumed. If `inpsd.dat` contains `alat` (in metres), the loader applies it automatically to the cell, positions, and exchange displacements. References are matched by basename because browser uploads do not preserve arbitrary relative paths; manual assignment is available below.") |
| with gr.Row(): |
| example = gr.Dropdown(example_choices, value="Upload my files", label="Starting point") |
| energy_unit = gr.Dropdown(["mRy", "meV", "eV", "unspecified"], value="mRy", label="Energy unit (default: mRy; confirm input)") |
| symmetry_mode = gr.Radio( |
| [("Complete jfile (use as supplied)", "complete"), ("Symmetry-reduced jfile (expand with spglib)", "reduced")], |
| value="complete", |
| label="Jij symmetry", |
| info="Complete accepts a full UppASD neighbour setup. Reduced generates symmetry-related neighbours; conflicting Jij values are reported, never averaged.", |
| ) |
| with gr.Row(): |
| inpsd_file = gr.File(label="inpsd.dat", file_count="single", type="filepath", elem_classes=["imx-file-upload"]) |
| supporting_files = gr.File(label="posfile / momfile / jfile (max 32 files / 100 MiB)", file_count="multiple", type="filepath", elem_classes=["imx-file-upload"]) |
| with gr.Row(): |
| refresh_mapping = gr.Button("Inspect file mapping", variant="secondary") |
| load_button = gr.Button("Load input set", variant="primary") |
| mapping_table = gr.Dataframe(headers=["reference", "uploaded basename", "status"], datatype=["str", "str", "str"], row_count=1, label="Referenced-file mapping", interactive=False) |
| with gr.Row(): |
| manual_pos = gr.Dropdown([], label="Manual posfile assignment", allow_custom_value=False, elem_classes=["imx-light-field"]) |
| manual_mom = gr.Dropdown([], label="Manual momfile assignment", allow_custom_value=False, elem_classes=["imx-light-field"]) |
| manual_exchange = gr.Dropdown([], label="Manual exchange assignment", allow_custom_value=False, elem_classes=["imx-light-field"]) |
| load_status = gr.Markdown() |
| parsed_summary = gr.Markdown() |
| classification = gr.Dataframe(headers=["site", "atom_type", "moment (mu_B)", "role"], datatype=["number", "str", "number", "str"], type="array", row_count=1, label="Role preview (default: m < 0.5 μB is induced)", interactive=False) |
| induced_site_toggle = gr.CheckboxGroup(choices=[], value=[], label="Induced sites (toggle)", info="Select induced sites explicitly. By default, sites with reference moment m < 0.5 μB are selected; all other sites are robust.") |
| with gr.Row(): |
| mesh_size = gr.Slider(4, 16, value=8, step=1, label="Seekpath points per segment", info="The exchange, response, dressed-exchange, and magnon plots use this high-symmetry path. The same value controls the auxiliary 3-D mesh used only for J_eff(r).", elem_classes=["imx-light-field"]) |
| analyze_button = gr.Button("Run analysis", variant="primary") |
|
|
| with gr.Tab("2 · Raw exchange"): |
| gr.Markdown("### Raw rigid-site exchange\nAll supplied sites and literal UppASD jfile Jij rows are retained. The ordering diagnostic is the largest eigenvalue of J(q) under the UppASD ordered-pair convention `H = −Σᵢ≠ⱼ Jij eᵢ·eⱼ`. The scan is restricted to the seekpath high-symmetry path (or the explicitly labelled fallback path if seekpath cannot classify the structure); it is not a global 3-D ordering search. The distance plot separates `J_MM` (robust–robust, circles), `J_Mm` (robust–induced, squares), and `J_mm` (induced–induced, triangles).") |
| raw_ordering = gr.Markdown("Load an input set and run analysis.") |
| with gr.Row(): |
| with gr.Column(): |
| plot_heading("Raw Jij by subspace · distance") |
| distance_plot = gr.Plot(label="Raw Jij by subspace · distance", show_label=False) |
| with gr.Column(): |
| plot_heading("J(q) eigenvalue scan · high-symmetry path") |
| raw_plot = gr.Plot(label="J(q) eigenvalue scan · high-symmetry path", show_label=False) |
| shell_table = gr.Dataframe(headers=["shell", "radius", "count", "mean Jij", "min Jij", "max Jij"], row_count=1, label="Radial exchange shells", interactive=False) |
|
|
| with gr.Tab("3 · Induced response"): |
| gr.Markdown("### Induced response\nClassify sites explicitly in the Input tab. The induced response is algebraic and instantaneous; induced sites are not independent LLG/LSWT degrees of freedom. The response panel uses a coherent unit-amplitude robust spin spiral along the same seekpath high-symmetry path.") |
| with gr.Row(): |
| response_mode = gr.Radio([("J-weighted approximation", "j_weighted"), ("Historical unweighted", "historical")], value="j_weighted", label="Response model") |
| x_override = gr.Textbox(label="X override", placeholder="blank = infer · e.g. 0.12 or 2:0.12, 3:0.08", info="J-weighted mode: X has inverse-energy units and p_ind = X K e, where p_ind = m_ind/|m_ind⁰|.") |
| include_induced = gr.Checkbox(value=True, label="Include induced–induced propagation") |
| response_recompute = gr.Button("Recompute response and dressing", variant="primary") |
| response_info = gr.Markdown() |
| with gr.Row(): |
| with gr.Column(): |
| plot_heading("p_ind(q) / p_ind(Γ) · coherent spiral") |
| response_plot = gr.Plot(label="p_ind(q) / p_ind(Γ) · coherent spiral", show_label=False) |
| inference_table = gr.Dataframe(headers=["site", "m_ref (mu_B)", "source field (energy)", "X (1/energy)", "warnings"], row_count=1, label="X inference and source-field normalization", interactive=False) |
|
|
| with gr.Tab("4 · Dressed exchange"): |
| gr.Markdown("### Robust-space exchange after induced-moment elimination\nThe induced contribution is a model-dependent correction using the selected response approximation. `J_eff(q)` is the Fourier-space matrix in the robust basis; the q-plot shows its leading eigenvalue. For one robust site (such as Fe with Pt treated as induced), this matrix is a scalar. The first real-space plot uses shell means to compare the direct `J_RR(r)`, cross-block `K_RI(r)`, Mryasov/downfolded `J_Mryasov(r)`, and Polesya-like induced `J_Polesya(r)` channels. With the same static response model, the last two are numerically equivalent and are shown with separate labels. The induced correction `ΔJ_induced(r)` and its relative value `ΔJ_induced(r) / J_RR(r)` are plotted separately below. Real-space x-axes use the implicit UppASD lattice parameter convention (`alat = 1`), while all curves are reconstructed from a separate complete q mesh; moment normalization remains separate in the magnon calculation. The exported `dressed_jfile` is directly compatible with UppASD's ordered-pair convention.") |
| dressed_info = gr.Markdown() |
| with gr.Row(): |
| with gr.Column(): |
| plot_heading("Robust-space J_eff(q) · high-symmetry path") |
| dressed_plot = gr.Plot(label="Robust-space J_eff(q) · high-symmetry path", show_label=False) |
| with gr.Column(): |
| plot_heading("Real-space exchange channels · shell means") |
| dressed_realspace_plot = gr.Plot(label="Real-space exchange channels · shell means", show_label=False) |
| with gr.Row(): |
| with gr.Column(): |
| plot_heading("Induced correction ΔJ_induced(r) · shell means") |
| dressed_delta_realspace_plot = gr.Plot(label="Induced correction ΔJ_induced(r) · shell means", show_label=False) |
| with gr.Column(): |
| plot_heading("Relative induced correction ΔJ_induced/J_MM · shell means") |
| dressed_relative_realspace_plot = gr.Plot(label="Relative induced correction ΔJ_induced/J_MM · shell means", show_label=False) |
| dressed_table = gr.Dataframe(headers=["shell", "radius", "count", "mean J_MM(r)", "mean ΔJ_induced(r)", "mean J_eff(r)", "max |Im J_eff|", "mean J_eff(r)/m_R"], row_count=1, label="Auxiliary-mesh reconstructed robust-space exchange shells", interactive=False) |
|
|
| with gr.Tab("5 · Magnons"): |
| gr.Markdown("### FM-compatible spectra\nIf Gamma is not locally stable, the plot is explicitly signed harmonic data, not a stable magnon spectrum.") |
| magnon_info = gr.Markdown() |
| plot_heading("Magnon bands") |
| magnon_plot = gr.Plot(label="Magnon bands", show_label=False) |
|
|
| with gr.Tab("6 · Compare datasets"): |
| gr.Markdown("### Compare two compatible Jij datasets\nUseful for LKAG vs frozen-magnon comparisons. Basis cell/positions must match; exchange values are allowed to differ.") |
| with gr.Row(): |
| b_inpsd = gr.File(label="Dataset B · inpsd.dat", file_count="single", type="filepath", elem_classes=["imx-file-upload"]) |
| b_files = gr.File(label="Dataset B · referenced files", file_count="multiple", type="filepath", elem_classes=["imx-file-upload"]) |
| external_response_file = gr.File(label="Optional external induced response (qx qy qz m_ind or path_coordinate m_ind)", file_count="single", type="filepath", elem_classes=["imx-file-upload"]) |
| compare_observable = gr.Dropdown(["raw", "dressed", "magnons"], value="raw", label="Compare observable") |
| compare_button = gr.Button("Compare A vs B", variant="primary") |
| compare_status = gr.Markdown() |
| compare_table = gr.Dataframe(headers=["check / diagnostic", "value"], row_count=1, label="Compatibility and diagnostics", interactive=False) |
| plot_heading("Raw exchange comparison") |
| compare_plot = gr.Plot(label="Raw exchange comparison", show_label=False) |
| plot_heading("Optional induced-response comparison") |
| compare_response_plot = gr.Plot(label="Optional induced-response comparison", show_label=False) |
|
|
| with gr.Tab("7 · Methods / limitations"): |
| gr.Markdown(""" |
| ### Methods in one page |
| |
| **Raw rigid-site model.** Every supplied magnetic site is treated as an ordinary rigid moment. This is diagnostic when some sites are physically induced. |
| |
| **Polesya-like induced moments.** Induced moments are eliminated as algebraic variables. The historical mode uses an unweighted local neighbour sum; the selected J-weighted mode evaluates the labelled **J-weighted induced-response approximation**. |
| |
| **Mryasov-like downfolding.** The induced subspace is eliminated variationally from one quadratic energy functional, yielding a robust-space `J_eff(q)`. No induced magnon branches are added. |
| |
| **Reciprocal sampling.** The exchange, response, dressed-exchange, and magnon plots share a seekpath high-symmetry path. The dressed real-space channel, absolute-correction, and relative-correction plots are reconstructed from a separate complete auxiliary mesh and are not inferred from that line. |
| |
| **Important limitation.** Conventional input Jij are not, by themselves, an exact first-principles susceptibility or induction kernel. Results depend on the explicit classification, X values, response mode, and conditioning of `I − X K_II`. |
| |
| The conceptual foundations are discussed in Mryasov *et al.* on induced moments and two-ion anisotropy in FePt, and Polesya *et al.* on induced moments in FePd/CoPt. See the repository README for the scope and citations. Unsupported cases are flagged rather than silently converted: noncollinear order, AF/ferrimagnetic LSWT, DMI, exchange tensors, SOC, anisotropy, and frequency-dependent susceptibility. |
| """) |
|
|
| gr.Markdown("### Downloads\nEvery successful analysis writes canonical model JSON, validation diagnostics, raw/dressed J(q), response data, dressed Jij where resolvable, and magnon bands.") |
| downloads = gr.File(label="Analysis files", file_count="multiple", interactive=False) |
|
|
| def do_mapping(inpsd, files): |
| try: |
| mapping = inspect_upload_mapping(inpsd, files) |
| choices = list(mapping.choices) |
| updates = [gr.Dropdown(choices=choices, value=mapping.references.get(key)) for key in ("posfile", "momfile", "exchange")] |
| return mapping.rows(), *updates |
| except Exception as exc: |
| return [["error", str(exc), "unavailable"]], gr.Dropdown(choices=[]), gr.Dropdown(choices=[]), gr.Dropdown(choices=[]) |
|
|
| def _induced_site_update(model): |
| default_sites = default_induced_sites(model) |
| choices = [ |
| (f"site {site.index} · type {site.atom_type} · m={site.moment:.6g} μB", site.index) |
| for site in model.sites |
| if site.moment is not None |
| ] |
| return gr.CheckboxGroup(choices=choices, value=list(default_sites)) |
|
|
| def _role_preview(induced_selection, current): |
| if current is None: |
| return [] |
| loaded = current.loaded if isinstance(current, AnalysisSession) else current |
| if not isinstance(loaded, LoadedUppASD): |
| return [] |
| classification_obj = classification_from_induced_sites(induced_selection, loaded.model) |
| return classification_rows(loaded.model, classification_obj.site_role) |
|
|
| def do_load(current, inpsd, files, selected_example, energy, symmetry_mode_flag, manual_pos_name, manual_mom_name, manual_exchange_name): |
| old_session = current if isinstance(current, AnalysisSession) else None |
| try: |
| expand_symmetry = symmetry_mode_flag == "reduced" |
| if selected_example in example_paths: |
| example_dir = example_paths[selected_example].parent |
| example_files = [example_dir / name for name in ("posfile", "momfile", "jfile")] |
| loaded, mapping = load_uploaded_set(example_paths[selected_example], example_files, energy_unit=energy, expand_symmetry=expand_symmetry) |
| else: |
| loaded, mapping = load_uploaded_set(inpsd, files, manual_mapping={"posfile": manual_pos_name or "", "momfile": manual_mom_name or "", "exchange": manual_exchange_name or ""}, energy_unit=energy, expand_symmetry=expand_symmetry) |
| cleanup_analysis_artifacts(old_session) |
| return loaded, f"✅ Loaded `{mapping.inpsd}` with {len(loaded.model.sites)} basis sites and {len(loaded.model.exchange_bonds)} exchange rows.", model_summary(loaded), classification_rows(loaded.model), _induced_site_update(loaded.model), mapping.rows() |
| except UploadMappingError as exc: |
| return None, f"⚠️ **Could not load input:** {exc}", "", [], gr.CheckboxGroup(choices=[], value=[]), exc.mapping.rows() |
| except (FileNotFoundError, InputFormatError, ValueError) as exc: |
| return None, f"⚠️ **Could not load input:** {exc}", "", [], gr.CheckboxGroup(choices=[], value=[]), [] |
|
|
| def do_analyse(current, induced_selection, mesh, mode, x_value, include): |
| if current is None: |
| return (None, "⚠️ Load an input set first.", "", [], None, None, "", [], None, "", None, None, None, None, [], "", None, []) |
| old_session = current if isinstance(current, AnalysisSession) else None |
| try: |
| |
| |
| |
| |
| |
| |
| loaded = current.loaded if isinstance(current, AnalysisSession) else current |
| if not isinstance(loaded, LoadedUppASD): |
| raise ValueError("analysis state is neither a loaded UppASD input nor an analysis session") |
| classification_obj = classification_from_induced_sites(induced_selection, loaded.model) |
| analysis = analyse_model(loaded, classification_obj, mesh_size=int(mesh), mode=mode, x=x_value, include_induced_induced=include) |
| cleanup_analysis_artifacts(old_session) |
| dressed_rows = [] |
| if analysis.downfolding is not None: |
| dressed_rows = dressed_shell_rows(analysis) |
| delta_max = 0.0 if analysis.downfolding is None else float(np.max(np.abs(analysis.downfolding.delta_induced), initial=0.0)) |
| path_source = "unknown" if analysis.path is None else analysis.path.path.source |
| realspace_note = "controlled auxiliary complete-mesh J_eff(r) is available" if analysis.dressed_real_space is not None else "controlled auxiliary J_eff(r) reconstruction is unavailable" |
| rescale_note = "For one positive-moment robust site, the real-space panel also shows J_eff(r)/m_R, the q-dependent exchange scale entering the moment-normalized FM dynamical matrix." if len(analysis.robust_sites) == 1 else "The robust-space result remains matrix-valued for multiple robust sites; no scalar rescaling is implied." |
| dressed_text = ordering_markdown("Robust-only", analysis.robust_ordering) + " \n" + ordering_markdown("Dressed", analysis.dressed_ordering) + f" \n**Induced correction:** max |ΔJ(q)| = `{delta_max:.6g}` \n**q sampling:** seekpath high-symmetry path (`{path_source}`) \n**Real-space output:** {realspace_note}; it is not obtained by inverse-transforming the 1-D path. \n{rescale_note}" |
| return (analysis, "✅ Analysis complete.", ordering_markdown("Raw", analysis.raw_ordering), shell_rows(analysis.loaded.model), _figure_exchange(analysis), _figure_realspace(analysis), response_markdown(analysis), inference_rows(analysis.inference), _figure_response(analysis), dressed_text, _figure_dressed(analysis), _figure_dressed_realspace(analysis), _figure_dressed_realspace_delta(analysis), _figure_dressed_realspace_relative(analysis), dressed_rows, magnon_markdown(analysis), _figure_magnons(analysis), export_files(analysis)) |
| except Exception as exc: |
| return (current, f"⚠️ Analysis failed: {exc}", "", [], None, None, f"⚠️ {exc}", [], None, "", None, None, None, None, [], f"⚠️ {exc}", None, []) |
|
|
| def do_compare(current, b_input, b_supporting, external_response, observable, energy, symmetry_mode_flag, induced_selection, mesh, mode, x_value, include): |
| if current is None: |
| return "⚠️ Load and analyze Dataset A first.", [], None, None |
| b_analysis = None |
| try: |
| b_loaded, _ = load_uploaded_set(b_input, b_supporting, energy_unit=energy, expand_symmetry=symmetry_mode_flag == "reduced") |
| classification_obj = classification_from_induced_sites(induced_selection, current.loaded.model) |
| b_classification = classification_from_induced_sites(induced_selection, b_loaded.model) |
| b_analysis = analyse_model(b_loaded, b_classification, mesh_size=int(mesh), mode=mode, x=x_value, include_induced_induced=include) |
| from induced_exchange import ExchangeDataset, compare_exchange_datasets |
| a_dataset = ExchangeDataset(current.loaded.model, label="Dataset A", robust_sites=classification_obj.robust_sites, induced_sites=classification_obj.induced_sites, x=_parse_x_for_compare(x_value), mode=mode) |
| b_dataset = ExchangeDataset(b_analysis.loaded.model, label="Dataset B", robust_sites=b_analysis.robust_sites, induced_sites=b_analysis.induced_sites, x=_parse_x_for_compare(x_value), mode=mode) |
| comparison = compare_exchange_datasets(a_dataset, b_dataset, current.q_fractional, include_magnons=True, stiffness_q_max=default_stiffness_q_max(current.loaded.model), external_response=external_response) |
| rows_out = [["compatible", comparison.compatibility.compatible], *[["warning", item] for item in comparison.compatibility.warnings], *[["diagnostic", item] for item in comparison.diagnostics]] |
| for analysis in (comparison.dataset_a, comparison.dataset_b): |
| for name, stiffness in (('raw', analysis.raw_stiffness), ('robust-only', analysis.robust_stiffness), ('dressed', analysis.dressed_stiffness)): |
| if stiffness is not None: |
| rows_out.append([f"{analysis.dataset.label} {name} stiffness", stiffness.coefficient if stiffness.coefficient is not None else "unavailable"]) |
| response_plot = None |
| if comparison.induced_response is not None: |
| induced = comparison.induced_response |
| if induced.external is not None: |
| rows_out.append(["external induced response source", induced.external.source or "uploaded data"]) |
| for label, metrics in (("Dataset A", induced.metrics_a), ("Dataset B", induced.metrics_b)): |
| if metrics is not None: |
| rows_out.append([f"{label} external response relative RMSE", metrics.relative_rmse if metrics.relative_rmse is not None else "unavailable"]) |
| from induced_exchange import plot_induced_response_comparison |
| response_plot = plot_induced_response_comparison(comparison) |
| import matplotlib.pyplot as plt |
| fig, ax = plt.subplots(figsize=(8.4, 4.1)) |
| from induced_exchange import plot_comparison |
| plot_comparison(comparison, observable=observable, ax=ax) |
| ax.set_title(f"Dataset A vs B · {observable}") |
| fig.tight_layout() |
| if response_plot is not None: |
| response_plot = response_plot.figure |
| plt.close(response_plot) |
| plt.close(fig) |
| cleanup_analysis_artifacts(b_analysis) |
| return f"✅ Compared `{comparison.dataset_a.dataset.label}` and `{comparison.dataset_b.dataset.label}`.", rows_out, fig, response_plot |
| except Exception as exc: |
| cleanup_analysis_artifacts(b_analysis) |
| return f"⚠️ Comparison unavailable: {exc}", [], None, None |
|
|
| def _parse_x_for_compare(value): |
| if value is None or not str(value).strip(): |
| return None |
| text = str(value).strip() |
| if ":" in text or "=" in text: |
| return {int(k.strip()): float(v) for k, v in (token.replace("=", ":", 1).split(":", 1) for token in text.replace(";", ",").split(",") if token.strip())} |
| vals = [float(token.strip()) for token in text.replace(";", ",").split(",") if token.strip()] |
| return vals[0] if len(vals) == 1 else vals |
|
|
| refresh_mapping.click(do_mapping, [inpsd_file, supporting_files], [mapping_table, manual_pos, manual_mom, manual_exchange]) |
| load_button.click(do_load, [session_state, inpsd_file, supporting_files, example, energy_unit, symmetry_mode, manual_pos, manual_mom, manual_exchange], [session_state, load_status, parsed_summary, classification, induced_site_toggle, mapping_table]) |
| induced_site_toggle.change(_role_preview, [induced_site_toggle, session_state], classification) |
| outputs = [session_state, load_status, raw_ordering, shell_table, raw_plot, distance_plot, response_info, inference_table, response_plot, dressed_info, dressed_plot, dressed_realspace_plot, dressed_delta_realspace_plot, dressed_relative_realspace_plot, dressed_table, magnon_info, magnon_plot, downloads] |
| for button in (analyze_button, response_recompute): |
| button.click(do_analyse, [session_state, induced_site_toggle, mesh_size, response_mode, x_override, include_induced], outputs) |
| compare_button.click(do_compare, [session_state, b_inpsd, b_files, external_response_file, compare_observable, energy_unit, symmetry_mode, induced_site_toggle, mesh_size, response_mode, x_override, include_induced], [compare_status, compare_table, compare_plot, compare_response_plot]) |
|
|
| return demo |
|
|
|
|
| if __name__ == "__main__": |
| import gradio as gr |
|
|
| demo = build_demo() |
| demo.launch(**_configure_launch_options(demo, gr)) |
|
|