Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion backend/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -525,7 +525,7 @@ def result_literature_compare(
raise HTTPException(status_code=404, detail=f"Unknown result_id: {result_id}")

provider_ids = [str(item).strip() for item in (request.provider_ids or []) if str(item).strip()]
if not provider_ids and str(record.get("analysis_type") or "").upper() in {"XRD", "DSC", "DTA", "TGA"}:
if not provider_ids and str(record.get("analysis_type") or "").upper() in {"XRD", "DSC", "DTA", "TGA", "FTIR"}:
provider_ids = ["openalex_like_provider"]
try:
providers, provider_scope = resolve_literature_providers(
Expand Down
3 changes: 3 additions & 0 deletions core/literature_compare.py
Original file line number Diff line number Diff line change
Expand Up @@ -749,6 +749,7 @@ def _compare_generic_result_to_literature(
normalized_user_documents = _normalize_user_document_sources(user_documents)

query_count = 0
executed_queries: list[str] = []
comparisons: list[dict[str, Any]] = []
citations_by_identity: dict[str, dict[str, Any]] = {}
provider_request_ids: list[str] = []
Expand All @@ -774,6 +775,7 @@ def _compare_generic_result_to_literature(
}
claim_filters = _search_filters_for_claim(claim, filters)
for query in build_claim_queries(claim):
executed_queries.append(query)
for candidate in provider.search(query, filters=claim_filters):
source_key = _search_result_identity(candidate)
if not source_key:
Expand Down Expand Up @@ -885,6 +887,7 @@ def _compare_generic_result_to_literature(
if len(provider_scope) > 1
else (sorted(provider_result_sources)[0] if len(provider_result_sources) == 1 else _provider_result_source(provider, provider_scope=provider_scope))
),
query_text=executed_queries[0] if executed_queries else "",
query_count=query_count,
source_count=len(all_sources_seen),
citation_count=len(citations_by_identity),
Expand Down
15 changes: 14 additions & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@
that tests are deterministic and require no external files.
"""

import sys
import os
import sys
import uuid
from pathlib import Path

# Ensure the thermoanalyzer package root is importable regardless of where
# pytest is invoked from.
Expand All @@ -18,6 +20,9 @@
if _THERMOANALYZER_ROOT not in sys.path:
sys.path.insert(0, _THERMOANALYZER_ROOT)

_LOCAL_TMP_ROOT = Path(_THERMOANALYZER_ROOT) / "pytest_temp"
_LOCAL_TMP_ROOT.mkdir(parents=True, exist_ok=True)

import numpy as np
import pandas as pd
import pytest
Expand Down Expand Up @@ -100,6 +105,14 @@ def isolated_thermoanalyzer_home(monkeypatch, tmp_path):
yield


@pytest.fixture
def tmp_path():
"""Use a workspace-local temp directory to avoid sandboxed system-temp permission issues."""
path = _LOCAL_TMP_ROOT / f"case_{uuid.uuid4().hex[:8]}"
path.mkdir(parents=True, exist_ok=False)
return path


@pytest.fixture(scope="session")
def temperature_range():
"""Uniform temperature grid from 30 to 300 degrees C with 500 points."""
Expand Down
57 changes: 57 additions & 0 deletions tests/test_backend_details.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,46 @@ def _seed_thermal_result_store(analysis_type: str) -> tuple[ProjectStore, str, s
return store, project_id, record["id"]


def _seed_spectral_result_store(analysis_type: str) -> tuple[ProjectStore, str, str]:
normalized = analysis_type.upper()
record = make_result_record(
result_id=f"{normalized.lower()}_demo",
analysis_type=normalized,
status="stable",
dataset_key=f"{normalized.lower()}_demo",
metadata={"sample_name": f"{normalized} Sample"},
summary={
"sample_name": f"{normalized} Sample",
"match_status": "no_match",
"candidate_count": 1,
"top_match_name": "Cellulose-like pattern",
"top_match_score": 0.41,
"confidence_band": "low",
},
rows=[
{
"rank": 1,
"candidate_name": "Cellulose-like pattern",
"normalized_score": 0.41,
"confidence_band": "low",
}
],
scientific_context={
"scientific_claims": [
{
"id": "C1",
"claim": f"The {normalized} result suggests a qualitative band-pattern interpretation that remains non-confirmatory.",
}
],
"uncertainty_assessment": {"overall_confidence": "low", "items": ["Interpretation remains qualitative."]},
},
validation={"status": "pass", "warnings": [], "issues": []},
)
store = ProjectStore()
project_id = store.put(normalize_workspace_state({"results": {record["id"]: record}}))
return store, project_id, record["id"]


def _headers() -> dict[str, str]:
return {"X-TA-Token": "details-token"}

Expand Down Expand Up @@ -425,6 +465,23 @@ def test_result_literature_compare_endpoint_defaults_live_provider_for_thermal_r
assert payload["literature_context"]["query_text"]


def test_result_literature_compare_endpoint_defaults_live_provider_for_ftir_results():
store, project_id, result_id = _seed_spectral_result_store("FTIR")
client = TestClient(create_app(api_token="details-token", store=store))

response = client.post(
f"/workspace/{project_id}/results/{result_id}/literature/compare",
headers=_headers(),
json={"persist": True},
)

assert response.status_code == 200
payload = response.json()
assert payload["literature_context"]["provider_scope"] == ["openalex_like_provider"]
assert payload["literature_context"]["analysis_type"] == "FTIR"
assert payload["literature_context"]["query_text"]


def test_result_literature_compare_endpoint_validates_typed_user_documents():
store, project_id, result_id = _seed_xrd_result_store()
client = TestClient(create_app(api_token="details-token", store=store))
Expand Down
27 changes: 23 additions & 4 deletions tests/test_literature_compare_panel.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
from core.chemical_formula_formatting import format_chemical_formula_text
from ui.components import literature_compare_panel

_REPO_ROOT = Path(__file__).resolve().parents[1]


class _FakeResponse:
def __init__(self, status_code: int, payload: dict):
Expand Down Expand Up @@ -268,6 +270,15 @@ def test_default_compare_request_uses_real_provider_for_thermal_modalities():
assert payload["filters"]["allow_fixture_fallback"] is False


def test_default_compare_request_uses_real_provider_for_ftir():
payload = literature_compare_panel._default_compare_request(current_record={"analysis_type": "FTIR"})

assert payload["provider_ids"] == ["openalex_like_provider"]
assert payload["max_claims"] == 2
assert payload["filters"]["analysis_type"] == "FTIR"
assert payload["filters"]["allow_fixture_fallback"] is False


def test_render_literature_sections_renders_xrd_candidate_summary_before_paper_cards(monkeypatch):
captions: list[str] = []
markdowns: list[str] = []
Expand Down Expand Up @@ -1147,16 +1158,24 @@ def test_render_literature_compare_panel_updates_session_state_on_success(monkey


def test_xrd_page_results_summary_uses_literature_compare_panel():
source = Path("C:/MaterialScope/ui/xrd_page.py").read_text(encoding="utf-8")
source = (_REPO_ROOT / "ui" / "xrd_page.py").read_text(encoding="utf-8")

assert "render_literature_compare_panel(" in source


def test_thermal_pages_results_summary_use_literature_compare_panel():
for path in (
"C:/MaterialScope/ui/dsc_page.py",
"C:/MaterialScope/ui/dta_page.py",
"C:/MaterialScope/ui/tga_page.py",
_REPO_ROOT / "ui" / "dsc_page.py",
_REPO_ROOT / "ui" / "dta_page.py",
_REPO_ROOT / "ui" / "tga_page.py",
):
source = Path(path).read_text(encoding="utf-8")
assert "render_literature_compare_panel(" in source


def test_ftir_results_summary_uses_literature_compare_panel():
source = (_REPO_ROOT / "ui" / "spectral_page.py").read_text(encoding="utf-8")

assert "_render_literature_compare_if_supported(" in source
assert "_LITERATURE_COMPARE_ENABLED_TYPES = {\"FTIR\"}" in source
assert "render_literature_compare_panel(" in source
63 changes: 63 additions & 0 deletions tests/test_report_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -643,6 +643,69 @@ def test_generate_docx_report_renders_ftir_no_match_caution_fields():
assert "cloud_search" in xml


def test_generate_docx_report_renders_ftir_literature_sections():
ftir_record, ftir_dataset = _make_ftir_no_match_record()
ftir_record["literature_context"] = {
"analysis_type": "FTIR",
"provider_scope": ["openalex_like_provider"],
"provider_request_ids": ["openalex_req_ftir_001"],
"query_text": "\"cellulose\" FTIR band assignment qualitative interpretation",
"query_rationale": "The FTIR literature search is centered on the leading band-pattern interpretation.",
"real_literature_available": True,
"restricted_content_used": False,
}
ftir_record["literature_claims"] = [
{
"claim_id": "C1",
"claim_text": "The FTIR result suggests a cellulose-like band pattern that remains qualitative.",
}
]
ftir_record["literature_comparisons"] = [
{
"claim_id": "C1",
"claim_text": "The FTIR result suggests a cellulose-like band pattern that remains qualitative.",
"support_label": "related_but_inconclusive",
"confidence": "low",
"rationale": "Accessible literature discusses a similar FTIR band pattern, but the assignment remains non-confirmatory.",
"citation_ids": ["ref_ftir_001"],
}
]
ftir_record["citations"] = [
{
"citation_id": "ref_ftir_001",
"title": "Cellulose FTIR band assignments",
"authors": ["A. Author"],
"journal": "Journal of Spectroscopy",
"year": 2024,
"doi": "10.1000/ftir-cellulose",
"url": "https://doi.org/10.1000/ftir-cellulose",
"access_class": "open_access_full_text",
"available_fields": ["metadata", "abstract", "oa_full_text"],
"source_license_note": "open_access",
"provenance": {
"provider_id": "openalex_like_provider",
"request_id": "openalex_req_ftir_001",
"result_source": "openalex_api",
"provider_scope": ["openalex_like_provider"],
"provider_request_ids": ["openalex_req_ftir_001"],
},
}
]

docx_bytes = generate_docx_report(
results={ftir_record["id"]: ftir_record},
datasets={"synthetic_ftir": ftir_dataset},
)

with zipfile.ZipFile(io.BytesIO(docx_bytes), "r") as archive:
xml = archive.read("word/document.xml").decode("utf-8")

assert "Literature Comparison" in xml
assert "Supporting References" in xml
assert "Cellulose FTIR band assignments" in xml
assert "Recommended Follow-Up Literature Checks" in xml


def test_generate_docx_report_renders_xrd_no_match_caution_fields():
xrd_record, xrd_dataset = _make_xrd_no_match_record()
docx_bytes = generate_docx_report(
Expand Down
43 changes: 43 additions & 0 deletions tests/test_spectral_page.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
from __future__ import annotations

from types import SimpleNamespace

from ui import spectral_page


def test_render_literature_compare_if_supported_runs_for_ftir(monkeypatch):
captions: list[str] = []
divider_calls: list[bool] = []
log_calls: list[tuple[tuple, dict]] = []

monkeypatch.setattr(
spectral_page,
"st",
SimpleNamespace(
divider=lambda: divider_calls.append(True),
caption=lambda text: captions.append(str(text)),
session_state={"lang": "en"},
),
)
monkeypatch.setattr(spectral_page, "t", lambda key: key)
monkeypatch.setattr(spectral_page, "_log_event", lambda *args, **kwargs: log_calls.append((args, kwargs)))
monkeypatch.setattr(
spectral_page,
"render_literature_compare_panel",
lambda **kwargs: (
{**dict(kwargs["record"] or {}), "literature_context": {"comparison_run_id": "litcmp_ftir_001"}},
{"status": "success"},
),
)

updated = spectral_page._render_literature_compare_if_supported(
analysis_type="FTIR",
selected_key="demo",
record={"id": "ftir_demo"},
title_key="ftir.title",
)

assert divider_calls == [True]
assert captions
assert updated["literature_context"]["comparison_run_id"] == "litcmp_ftir_001"
assert log_calls
2 changes: 1 addition & 1 deletion ui/components/literature_compare_panel.py
Original file line number Diff line number Diff line change
Expand Up @@ -871,7 +871,7 @@ def _default_compare_request(
) -> dict[str, Any]:
payload = copy.deepcopy(DEFAULT_LITERATURE_COMPARE_REQUEST)
analysis_type = _clean_text((current_record or {}).get("analysis_type")).upper()
if analysis_type in {"XRD", "DSC", "DTA", "TGA"}:
if analysis_type in {"XRD", "DSC", "DTA", "TGA", "FTIR"}:
payload["filters"]["analysis_type"] = analysis_type or "XRD"
else:
payload = {
Expand Down
46 changes: 46 additions & 0 deletions ui/spectral_page.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from core.validation import validate_thermal_dataset
from ui.components.chrome import render_page_header
from ui.components.history_tracker import _log_event
from ui.components.literature_compare_panel import render_literature_compare_panel
from ui.components.preset_manager import render_processing_preset_panel, seed_pending_workflow_template
from ui.components.plot_builder import (
apply_plot_display_settings,
Expand Down Expand Up @@ -66,6 +67,8 @@
},
}

_LITERATURE_COMPARE_ENABLED_TYPES = {"FTIR"}


def _get_spectral_datasets(analysis_type: str):
datasets = st.session_state.get("datasets", {})
Expand Down Expand Up @@ -372,6 +375,43 @@ def _seed_spectral_processing_defaults(token: str, processing, workflow_template
return seeded


def _render_literature_compare_if_supported(
*,
analysis_type: str,
selected_key: str,
record: dict | None,
title_key: str,
) -> dict | None:
token = str(analysis_type or "").upper()
if token not in _LITERATURE_COMPARE_ENABLED_TYPES:
return record

result_id = f"{token.lower()}_{selected_key}"
saved_record = record
st.divider()
if saved_record:
st.caption(tx("Kaydedilmiş sonuç kimliği: {result_id}", "Saved result ID: {result_id}", result_id=result_id))
saved_record, literature_action = render_literature_compare_panel(
record=saved_record,
result_id=result_id if saved_record else None,
lang=st.session_state.get("lang", "tr"),
key_prefix=f"{token.lower()}_literature_compare_{selected_key}",
)
if literature_action and literature_action.get("status") == "success":
_log_event(
tx("Literatür Karşılaştırması", "Literature Compare"),
tx(
"{result_id} için literatür karşılaştırması güncellendi.",
"Literature comparison was refreshed for {result_id}.",
result_id=result_id,
),
t(title_key),
dataset_key=selected_key,
result_id=result_id,
)
return saved_record


def render_spectral_page(
analysis_type: str,
*,
Expand Down Expand Up @@ -782,4 +822,10 @@ def render_spectral_page(
)
summary_rows = [{"key": key, "value": value} for key, value in summary.items()]
st.dataframe(pd.DataFrame(summary_rows), width="stretch", hide_index=True)
_render_literature_compare_if_supported(
analysis_type=token,
selected_key=selected_key,
record=record,
title_key=title_key,
)