diff --git a/MANIFEST.in b/MANIFEST.in index b324840..8011e27 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -15,3 +15,5 @@ recursive-include src/diffractscout/benchmark_data *.cif *.json recursive-include docs/joss *.md *.json include scripts/check_joss_artifacts.py + +recursive-include paper *.pdf *.json diff --git a/README.md b/README.md index d04ce70..9f77ac6 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@

- DiffractScout: provenance-first phase scouting and indexed powder diffraction references. + DiffractScout: Traceable powder-diffraction references. AI-generated conceptual illustration.

# DiffractScout @@ -27,6 +27,8 @@ for retained formats, engine differences and validation limits. ## Why this software exists +

Workflow from structure inputs to a verified diffraction-reference bundle.

+ Candidate-phase assessment commonly involves several disconnected operations: interpret an alloy grade, enumerate chemical subsystems, query a computed-materials database, download structures, inspect CIF metadata, calculate theoretical reflections, locate elastic constants, and prepare tables for experimental planning. Ad hoc scripts often lose the relationship between the provider record, exact CIF setting, tensor basis, diffraction settings, and final spreadsheet. DiffractScout represents that chain as one research object. It supports two entry points: diff --git a/README.zh-CN.md b/README.zh-CN.md index 74eb29d..fe3c2de 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -1,5 +1,5 @@

- DiffractScout:可追溯的候选相筛选与理论粉末衍射参考工作流。 + DiffractScout: Traceable powder-diffraction references. AI-generated conceptual illustration.

# DiffractScout 中文说明 diff --git a/docs/assets/hero-generation.json b/docs/assets/hero-generation.json new file mode 100644 index 0000000..3645c42 --- /dev/null +++ b/docs/assets/hero-generation.json @@ -0,0 +1,12 @@ +{ + "name": "DiffractScout", + "subtitle": "Traceable powder-diffraction references", + "palette": "Warm ivory, ink navy, cobalt blue and muted copper", + "motif": "A crystalline unit-cell wireframe transitioning through three beautifully arranged translucent document sheets into a crisp idealized powder-diffraction stick pattern. The sheets imply a CIF and provenance record. No scientific numerical values, fake interface, or illegible paragraph text.", + "tool": "image_gen built-in; model identifier not exposed", + "revision": null, + "prompt": "Create one exceptionally polished scientific software README cover banner, wide landscape approximately 2.4:1 aspect ratio, premium editorial art direction, high resolution. Project: DiffractScout. Palette: Warm ivory, ink navy, cobalt blue and muted copper. Concept: A crystalline unit-cell wireframe transitioning through three beautifully arranged translucent document sheets into a crisp idealized powder-diffraction stick pattern. The sheets imply a CIF and provenance record. No scientific numerical values, fake interface, or illegible paragraph text. Compose generous negative space and sharply legible refined sans-serif typography integrated with the scientific motif. Exact text, no other copy: \"DiffractScout\" large, \"Traceable powder-diffraction references\" smaller, and a discreet \"Conceptual illustration\". Make name/subtitle read immediately at GitHub width. Restrained three-dimensional material, carefully controlled highlights, precise linework, sophisticated hierarchy and balanced composition. No generic AI neon clouds, no random particles, no badges, no claims, no photoreal experimental data, no fake application screenshot, no watermarks or publisher branding. This is an editorial cover, not a quantitative paper figure.", + "created_on": "2026-09-27", + "purpose": "README conceptual cover only, not experimental data or software output", + "sha256": "af2419e8a167f26909203ab2d04153a78fef7a3f5604a943c713896bdef63f2c" +} diff --git a/docs/assets/hero.png b/docs/assets/hero.png new file mode 100644 index 0000000..a6c91c4 Binary files /dev/null and b/docs/assets/hero.png differ diff --git a/docs/joss/submission-record.json b/docs/joss/submission-record.json index 8514bd3..22c10db 100644 --- a/docs/joss/submission-record.json +++ b/docs/joss/submission-record.json @@ -45,7 +45,8 @@ }, "ai_assistance": { "current_round": "OpenAI Codex, GPT-6, 2026-09-27: repository audit, packaging, tests, documentation, manuscript and bibliography preparation, validation and Git integration.", - "historical_disclosure": "Retain project-specific disclosures; AbsSAXS AI history is not copied to these projects." + "historical_disclosure": "Retain project-specific disclosures; AbsSAXS AI history is not copied to these projects.", + "visual_revision": "2026-09-27: Codex revised reproducible manuscript figure scripts, captured real application widgets and prepared conceptual README covers using the built-in image-generation tool (model identifier not exposed). Prompt and image provenance are retained in the repository. Final author review remains pending." }, "submission_date": null, "related_publications": [ diff --git a/docs/joss/visual-provenance.md b/docs/joss/visual-provenance.md new file mode 100644 index 0000000..b99043a --- /dev/null +++ b/docs/joss/visual-provenance.md @@ -0,0 +1,16 @@ +# Visual sources and reproduction + +The September 2026 visual revision separates editorial illustrations from research-software evidence. + +- `docs/assets/hero.png` is an AI-generated **conceptual illustration** for the README. It is not detector data, a calculated result, or an application screenshot. The prompt, tool description and file hash are in `hero-generation.json` beside it. The built-in image tool did not expose a model identifier; no specific model-version claim is made. +- The editable section headers use repository-owned SVG. The previous SVG cover remains as an alternative source asset. +- Manuscript diagrams and numerical plots are generated by the repository scripts below, with SVG and PDF vector exports and 450 dpi PNGs. Scientific arrays, reflection positions, and solver observations are not modified by an image-generation model. +- Numerical demonstrations use only the bundled synthetic fixtures and actual software calculations. AnisoScope's interface figure, where applicable, is captured from real Qt widgets; the capture JSON records its inputs and hash. + +From the repository root, with the project's dependencies and Matplotlib installed: + +```bash +python paper/make_figures.py +``` + +Inspect the generated images and recompile `paper/paper.md` using the official draft-PDF workflow after any figure change. The README cover is not included as a scientific manuscript figure. Synthetic verification is not independent reproduction of the Acta Materialia experimental study. diff --git a/paper/README.md b/paper/README.md index fb2d499..59e7c17 100644 --- a/paper/README.md +++ b/paper/README.md @@ -1,38 +1,9 @@ -# JOSS paper sources +# JOSS manuscript and figure sources -This directory contains the JOSS manuscript and its reproducible figures: +The current manuscript is `paper.md`; bibliography is `paper.bib`. The two figures are generated by `python paper/make_figures.py` from the software workflow and the bundled synthetic FCC fixture. PNG (450 dpi), SVG and PDF exports are retained. The script verifies the generated result bundle before plotting. -- **paper.md**: Markdown manuscript with the required JOSS sections and YAML metadata. -- **paper.bib**: cited crystallography, software, data-service, and source-project records. -- **fig_workflow.png** and **fig_validation.png**: manuscript figures; their editable SVGs are retained beside them. -- **make_figures.py**: deterministic source for the two figures. -- **paper.pdf**: a generated preview only. It is stale after any manuscript, bibliography, or figure change. The Open Journals draft build is authoritative. +`paper.pdf` is a generated preview, not publication evidence. The exact-source artifact from the Open Journals draft-PDF workflow is authoritative after every manuscript or figure change. Official draft watermarks and publication-field placeholders remain in this preview. -The current four-page A4 preview was generated by the [Open Journals build on 26 September 2026](https://github.com/D-sudoasd/DiffractScout/actions/runs/36242597087) from [commit a09ee1b](https://github.com/D-sudoasd/DiffractScout/commit/a09ee1b0e48de4bf07898bceda4bec46b70dab43). Its source files and PDF bytes were checked against the workflow artifact, and all four rendered pages and the extracted text were inspected. The DRAFT watermark and placeholder journal DOI, volume, and page fields belong to the official preview template. +Current author confirmations, actual-use records and outstanding submission requirements are maintained in [docs/joss](../docs/joss/README.md). Earlier readiness records do not override that record. The Acta Materialia DOI is identified; historical software revision and operation-to-output mapping still require processing records. Final human review remains pending. -The manuscript describes a bounded workflow contribution: linking candidate records, source CIFs, calculation settings, diagnostic states, theoretical references, and their exported result bundle. It does not claim a new diffraction or elasticity algorithm. The developer reports prior use in several published materials-science papers; those records and their exact software-version links have not yet been provided for citation-level traceability. - -## Submission inputs still required - -Complete these items before submitting to JOSS: - -1. **Public development history.** The repository became public at 10:02:29 UTC on 12 August 2026. The current JOSS guidance asks for more than six months of public development history when a repository is recent, with sustained development and supporting release, issue, or pull-request evidence. On the manuscript date of 26 September 2026, this period is not yet met; submission should be after 10:02:29 UTC on 12 February 2027, and 15 February 2027 is a practical earliest date if development and the other criteria remain adequate. -2. **Published-use evidence.** Provide representative paper titles and DOI or stable links, identify where DiffractScout was used in each study, and give the software version or commit where possible. The manuscript currently attributes this use to the developer's report and does not cite unidentified papers. -3. **Funding and acknowledgements.** Provide any funding body and grant identifiers and state whether funders influenced software design, data collection, analysis, or publication. If there was no external financial support, confirm that explicitly. Add any additional acknowledgements that belong in the manuscript. -4. **Author list.** Delun Gong's name, ORCID, and affiliation were confirmed for this draft. Confirm that the author list is complete and that all listed authors agree to authorship. -5. **Human review of AI-assisted work.** Earlier repository work and this revision used OpenAI GPT-5.6 Pro, OpenAI Codex GPT-6, and cooperating assistant agents across software, tests, figures, documentation, and manuscript work. Automated checks are described in the manuscript, but no assertion that the human author has completed review is made. The responsible author must review, edit, and validate every AI-assisted contribution and make the core design decisions before the JOSS AI disclosure can assert compliance. -6. **Submission date.** Update the JOSS metadata date to the actual submission date if the submission happens after this draft. -7. **Archived software citation.** JOSS requests a tagged, archived software release with a DOI after successful review. At that stage, replace or supplement the source-repository citation in paper.bib with the DOI for the reviewed release; do not invent a DOI beforehand. - -The independent analytic benchmark and synthetic fixtures are software checks. They are not experimental validation and should not be presented as research-use evidence. - -## Figures - -Regenerate figures from the current package: - - python -m pip install -e ".[paper]" - SOURCE_DATE_EPOCH=1786492800 python paper/make_figures.py - -## Paper build - -Build the draft with .github/workflows/draft-pdf.yml or Docker through scripts/build_paper.sh. Render and inspect every page after the paper source is frozen. The checked-in paper.pdf is not updated as part of a manuscript-only revision. +See [visual provenance](../docs/joss/visual-provenance.md) for conceptual README covers versus reproducible scientific plots. diff --git a/paper/fig_validation.pdf b/paper/fig_validation.pdf new file mode 100644 index 0000000..5daea33 Binary files /dev/null and b/paper/fig_validation.pdf differ diff --git a/paper/fig_validation.png b/paper/fig_validation.png index 9830ce9..c6b5587 100644 Binary files a/paper/fig_validation.png and b/paper/fig_validation.png differ diff --git a/paper/fig_validation.svg b/paper/fig_validation.svg index 12dc8eb..af40ce2 100644 --- a/paper/fig_validation.svg +++ b/paper/fig_validation.svg @@ -1,12 +1,11 @@ - + - 2026-08-12T00:00:00+00:00 image/svg+xml @@ -21,14807 +20,12098 @@ - - - - - - - - - - - + - + - - - - - - - - - + 40 - + - - - - - - - - + 50 - + - - - - - - - - + 60 - + - - - - - - - - + 70 - + - - - - - - - - + 80 - + - - - - - - - - + 90 - + - - - - - - - - - + 100 - + - - - - - - + 110 - + - - - - - - - - - + 120 - - - - - - - - - - - - - - - - - - - - - - - - + + + 2 + θ +   + ( + d + e + g + r + e + e + s + ) + - + - + - + - - - - + 0 - + - + - - - - - + 20 - + - + - - - - - + 40 - + - + - - - - - + 60 - + - + - - - - - + 80 - + - + - - - - - - + 100 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Relative intensity (%) - - + + - - + + - - - - - - - - + + + + + + + + - + - + - + - - - - - - - - - - + (1 1 1) - + - - - - - - - - - - + (2 0 0) - + - - - - - - - - - - + (2 2 0) - + - - - - - - - - - - - - - + (3 1 1) - + - - - - - - - - - - + (2 2 2) - + - - - - - - - - - - + (4 0 0) - + - - - - - - - - - - + (3 3 1) - + - - - - - - - - - - + (4 2 0) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + F + m + m + a + λ + 3 + = + 4 + . + 0 + 0 + 0 + = + 1 + . + 5 + 4 + 0 + 6 +   + · +   +   + Å +   + · +   + C + u +   + K + α + , +   +   + Å + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + a Synthetic FCC diffraction reference - - - - - - - - - - - - - - - - - - - - + + - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + b - - - - - - - - - - - - - - - + Reproducible checks - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + 8 - - - - - - - - - - - - - - - - - + allowed hkl families - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + 110.0 GPa - - - - - - - - - - - - - - - - - + isotropic E(hkl) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + 12 - - - - - - - - - - - - - - - - - - - - - - - + hashed artifacts - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Constructed verification fixture, not experimental material properties. - - + + - - + + diff --git a/paper/fig_workflow.pdf b/paper/fig_workflow.pdf new file mode 100644 index 0000000..4ac4010 Binary files /dev/null and b/paper/fig_workflow.pdf differ diff --git a/paper/fig_workflow.png b/paper/fig_workflow.png index 3b36477..4e2c120 100644 Binary files a/paper/fig_workflow.png and b/paper/fig_workflow.png differ diff --git a/paper/fig_workflow.svg b/paper/fig_workflow.svg index d299219..e21a8a6 100644 --- a/paper/fig_workflow.svg +++ b/paper/fig_workflow.svg @@ -1,12 +1,11 @@ - + - 2026-08-12T00:00:00+00:00 image/svg+xml @@ -21,2580 +20,184 @@ - - + - + - + - + - + +" clip-path="url(#p1b8bab138d)" style="fill: #516570; stroke: #516570; stroke-linecap: round"/> - + +" clip-path="url(#p1b8bab138d)" style="fill: #516570; stroke: #516570; stroke-linecap: round"/> - + +" clip-path="url(#p1b8bab138d)" style="fill: #516570; stroke: #516570; stroke-linecap: round"/> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + a - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + From source structures to a reproducible reference - - - - - - - - - - - - - - - - - + 01 Define inputs - - - - - - - - - - - - - - + Chemical system / IDs + or local CIF files - - - - - - - - - - - - - - - - - - + 02 Resolve sources - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Normalize candidates + Record exact CIF identity - - - - - - - - - - - - - - - - - - - - + 03 Calculate - - - - - - - - - - - - - - - - - - - - + Validate structure + Compute indexed reflections - - - - - - - - - - - - - - - - - - - - - - - + 04 Verify + export - - - - - - - - - - - - - - - - - - - - - - - - - - + CSV / XLSX / JSON + Checksummed result bundle - - - - - - - - - - - - - - - - - - - - - - + Recorded throughout - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Source IDs · settings · diagnostics · missing-data states - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Kinematic theoretical references; no measured-pattern phase identification or refinement. - - + + diff --git a/paper/make_figures.py b/paper/make_figures.py index f6ea1ac..0eeae68 100755 --- a/paper/make_figures.py +++ b/paper/make_figures.py @@ -1,332 +1,259 @@ -#!/usr/bin/env python3 -"""Regenerate the JOSS paper figures from the committed workflow and demo bundle.""" - -from __future__ import annotations +"""Reproducible publication graphics; no experimental measurements are synthesized.""" import argparse import json -import shutil -import sys import tempfile +import pandas as pd from pathlib import Path +import sys +import matplotlib +matplotlib.use("Agg") import matplotlib.pyplot as plt -from matplotlib.patches import FancyArrowPatch, FancyBboxPatch -import pandas as pd - -ROOT = Path(__file__).resolve().parents[1] -PAPER = Path(__file__).resolve().parent -SRC = ROOT / "src" -if str(SRC) not in sys.path: - sys.path.insert(0, str(SRC)) +from matplotlib.patches import FancyBboxPatch, FancyArrowPatch -# Colorblind-friendly, print-safe palette. -NAVY = "#183B56" -TEAL = "#167D8D" -BLUE = "#4C78A8" -GREEN = "#4F8F6B" -AMBER = "#D88A1E" -INK = "#1F2933" -MUTED = "#52616B" -LINE = "#C8D2D9" -PAPER_BG = "#FFFFFF" -CARD_BG = "#F7FAFC" -TEAL_BG = "#EAF5F6" -AMBER_BG = "#FFF5E5" +INK = "#142C3D" +MUTED = "#516570" +RULE = "#CCD7DC" +ACCENT = "#087F8C" +LIGHT = "#EFF7F8" +WARM = "#B86B20" -def _save(fig: plt.Figure, stem: str) -> None: - for suffix in ("png", "svg"): - fig.savefig( - PAPER / f"{stem}.{suffix}", - dpi=300 if suffix == "png" else None, - bbox_inches="tight", - facecolor=PAPER_BG, - ) - plt.close(fig) +def style(): + matplotlib.rcParams.update( + { + "font.family": "sans-serif", + "font.sans-serif": ["Arial", "DejaVu Sans"], + "font.size": 10, + "axes.labelsize": 10, + "axes.titlesize": 11, + "xtick.labelsize": 9, + "ytick.labelsize": 9, + "text.color": INK, + "axes.labelcolor": INK, + "axes.edgecolor": RULE, + "axes.linewidth": 0.7, + "lines.linewidth": 1.5, + "svg.fonttype": "none", + "svg.hashsalt": "publication-20260927", + "pdf.fonttype": 42, + "savefig.facecolor": "white", + } + ) -def make_workflow() -> None: - fig, ax = plt.subplots(figsize=(14.2, 5.0)) - fig.subplots_adjust(left=0.012, right=0.988, top=0.97, bottom=0.04) - fig.patch.set_facecolor(PAPER_BG) - ax.set_xlim(0, 1) - ax.set_ylim(0, 1) +def canvas(height=4.6): + fig = plt.figure(figsize=(7.2, height), facecolor="white") + ax = fig.add_axes([0, 0, 1, 1], xlim=(0, 1), ylim=(0, 1)) ax.axis("off") + return fig, ax - cards = [ - { - "title": "Input", - "lines": ["alloy grade", "chemical system", "Materials Project IDs", "or local CIFs"], - "color": BLUE, - }, - { - "title": "Discover", - "lines": ["expand subsystems", "normalize candidates", "deduplicate records", "rank deterministically"], - "color": TEAL, - }, - { - "title": "Acquire", - "lines": ["conventional CIF", "optional DFT Cij", "query context", "source metadata"], - "color": GREEN, - }, - { - "title": "Verify + calculate", - "lines": ["identity and symmetry", "occupancy and absences", "hkl, d, q, |F|², LP", "frame-checked E(hkl)"], - "color": NAVY, - }, - { - "title": "Evidence bundle", - "lines": ["CSV + XLSX tables", "structured diagnostics", "provenance + versions", "strict SHA-256 manifest"], - "color": AMBER, - }, - ] - x_positions = [0.025, 0.220, 0.415, 0.610, 0.805] - width, height, y = 0.170, 0.56, 0.31 +def label(ax, x, y, text, size=10, weight="normal", color=INK, ha="left", va="center"): + return ax.text( + x, y, text, fontsize=size, fontweight=weight, color=color, ha=ha, va=va, linespacing=1.45 + ) - for index, (x, card) in enumerate(zip(x_positions, cards, strict=True), start=1): - shadow = FancyBboxPatch( - (x + 0.006, y - 0.012), - width, - height, - boxstyle="round,pad=0.012,rounding_size=0.018", - linewidth=0, - facecolor="#D9E2E8", - alpha=0.50, - zorder=1, - ) - ax.add_patch(shadow) - box = FancyBboxPatch( - (x, y), - width, - height, - boxstyle="round,pad=0.012,rounding_size=0.018", - linewidth=1.6, - edgecolor=card["color"], - facecolor=CARD_BG, - zorder=2, - ) - ax.add_patch(box) - header = FancyBboxPatch( - (x, y + height - 0.135), - width, - 0.135, - boxstyle="round,pad=0.012,rounding_size=0.018", - linewidth=0, - facecolor=card["color"], - zorder=3, - ) - ax.add_patch(header) - # Cover the lower rounded corners of the header to create a flat divider. - ax.add_patch( - plt.Rectangle( - (x, y + height - 0.135), - width, - 0.050, - facecolor=card["color"], - edgecolor="none", - zorder=3, - ) - ) - title_size = 13.2 if index in {4, 5} else 14.6 - ax.text( - x + width / 2, - y + height - 0.067, - f"{index}. {card['title']}", - ha="center", - va="center", - fontsize=title_size, - fontweight="bold", - color=PAPER_BG, - zorder=5, - ) - for line_index, line in enumerate(card["lines"]): - ax.text( - x + width / 2, - y + height - 0.205 - line_index * 0.073, - line, - ha="center", - va="center", - fontsize=12.3, - color=INK, - zorder=5, - ) - if index < len(cards): - x0 = x + width + 0.008 - x1 = x_positions[index] - 0.009 - arrow = FancyArrowPatch( - (x0, y + height / 2), - (x1, y + height / 2), - arrowstyle="-|>", - mutation_scale=17, - linewidth=1.9, - color=MUTED, - shrinkA=0, - shrinkB=0, - zorder=6, - ) - ax.add_patch(arrow) +def panel(ax, x, y, letter, title): + label(ax, x, y, letter, 12, "bold", ACCENT) + label(ax, x + 0.038, y, title, 11, "bold") + - banner = FancyBboxPatch( - (0.065, 0.075), - 0.87, - 0.125, - boxstyle="round,pad=0.012,rounding_size=0.015", - linewidth=1.2, - edgecolor=AMBER, - facecolor=AMBER_BG, - zorder=2, +def box(ax, x, y, w, h, title, body="", accent=None, face=None, size=9.5): + accent = ACCENT if accent is None else accent + face = LIGHT if face is None else face + ax.add_patch( + FancyBboxPatch( + (x, y), + w, + h, + boxstyle="round,pad=0,rounding_size=0.012", + linewidth=0.7, + edgecolor=RULE, + facecolor=face, + ) ) - ax.add_patch(banner) - ax.text( - 0.090, - 0.137, - "Scientific boundary", - ha="left", - va="center", - fontsize=13.0, - fontweight="bold", - color=AMBER, + ax.plot( + [x + 0.015, x + 0.015], + [y + 0.02, y + h - 0.02], + color=accent, + lw=2.1, + solid_capstyle="round", ) - ax.text( - 0.252, - 0.137, - "Kinematic theoretical references only; missing and uncertain values remain explicit.", - ha="left", - va="center", - fontsize=12.6, - color=INK, + label(ax, x + 0.034, y + h - 0.037, title, 10, "bold", accent, va="top") + if body: + label(ax, x + 0.034, y + h - 0.099, body, size, va="top") + + +def arrow(ax, a, b, color=MUTED, style="-"): + ax.add_patch( + FancyArrowPatch( + a, + b, + arrowstyle="-|>", + mutation_scale=10, + linewidth=1, + color=color, + linestyle=style, + shrinkA=2, + shrinkB=2, + ) ) - _save(fig, "fig_workflow") - -def _ensure_demo_bundle(destination: Path) -> Path: - if (destination / "pattern_profiles.csv").is_file() and (destination / "manifest.json").is_file(): - return destination - from diffractscout.demo import write_demo_inputs - from diffractscout.pipeline import analyze_cifs - - if destination.exists(): - shutil.rmtree(destination) - with tempfile.TemporaryDirectory(prefix="diffractscout_paper_") as temporary: - inputs = write_demo_inputs(Path(temporary) / "inputs") - analyze_cifs([inputs], destination, include_excel=True, overwrite=True) - return destination - - -def make_validation(demo_dir: Path) -> None: - demo_dir = _ensure_demo_bundle(demo_dir) - profile = pd.read_csv(demo_dir / "pattern_profiles.csv", encoding="utf-8-sig") - peaks = pd.read_csv(demo_dir / "peak_reference.csv", encoding="utf-8-sig") - phase = pd.read_csv(demo_dir / "phase_summary.csv", encoding="utf-8-sig").iloc[0] - manifest = json.loads((demo_dir / "manifest.json").read_text(encoding="utf-8")) +def save(fig, folder, stem, formats=("png", "svg", "pdf")): + folder.mkdir(parents=True, exist_ok=True) + for ext in formats: + meta = ( + {"Date": None} + if ext == "svg" + else ({"CreationDate": None, "ModDate": None} if ext == "pdf" else {}) + ) + fig.savefig(folder / f"{stem}.{ext}", dpi=450, metadata=meta) + plt.close(fig) - fig = plt.figure(figsize=(13.6, 5.5)) - fig.patch.set_facecolor(PAPER_BG) - fig.subplots_adjust(left=0.065, right=0.985, top=0.84, bottom=0.16, wspace=0.08) - grid = fig.add_gridspec(1, 2, width_ratios=[1.72, 1.0], wspace=0.08) - ax = fig.add_subplot(grid[0, 0]) - info = fig.add_subplot(grid[0, 1]) - x = profile["two_theta_deg"].to_numpy() - y = profile["relative_intensity"].to_numpy() - ax.fill_between(x, 0, y, color=TEAL, alpha=0.11, linewidth=0) - ax.plot(x, y, color=TEAL, linewidth=2.2, solid_capstyle="round") - ax.axhline(0, color=INK, linewidth=0.9) +def clean_axes(ax): + ax.spines[["top", "right"]].set_visible(False) + ax.tick_params(length=3, width=0.6, color=RULE) + ax.grid(axis="y", color=RULE, linewidth=0.5, alpha=0.6) + ax.set_axisbelow(True) - peak_x = peaks["two_theta_deg"].to_numpy() - peak_y = peaks["normalized_intensity"].to_numpy() - ax.vlines(peak_x, 0, peak_y, color=NAVY, linewidth=1.15, alpha=0.78) - ax.scatter(peak_x, peak_y, s=20, facecolor=PAPER_BG, edgecolor=NAVY, linewidth=1.1, zorder=4) - for idx, row in peaks.reset_index(drop=True).iterrows(): - level = 106 if idx % 2 == 0 else 96 +ROOT = Path(__file__).resolve().parents[1] +PAPER = ROOT / "paper" +sys.path.insert(0, str(ROOT / "src")) +ACCENT = "#245AA5" +LIGHT = "#F0F5FC" + + +def make_workflow(): + fig, ax = canvas(4.1) + panel(ax, 0.04, 0.94, "a", "From source structures to a reproducible reference") + stages = [ + ("01 Define inputs", "Chemical system / IDs\nor local CIF files"), + ("02 Resolve sources", "Normalize candidates\nRecord exact CIF identity"), + ("03 Calculate", "Validate structure\nCompute indexed reflections"), + ("04 Verify + export", "CSV / XLSX / JSON\nChecksummed result bundle"), + ] + for i, (title, body) in enumerate(stages): + row, col = divmod(i, 2) + x = 0.04 + col * 0.49 + y = 0.59 - row * 0.31 + box(ax, x, y, 0.43, 0.235, title, body, size=10) + arrow(ax, (0.47, 0.705), (0.53, 0.705)) + arrow(ax, (0.745, 0.59), (0.745, 0.565)) + ax.plot([0.745, 0.255], [0.565, 0.565], color=MUTED, lw=1) + arrow(ax, (0.255, 0.565), (0.255, 0.515)) + arrow(ax, (0.47, 0.395), (0.53, 0.395)) + ax.plot([0.04, 0.96], [0.205, 0.205], color=RULE, lw=0.8) + label(ax, 0.04, 0.145, "Recorded throughout", 10, "bold", ACCENT) + label(ax, 0.36, 0.145, "Source IDs · settings · diagnostics · missing-data states", 9.2) + label( + ax, + 0.04, + 0.055, + "Kinematic theoretical references; no measured-pattern phase identification or refinement.", + 9, + color=MUTED, + ) + save(fig, PAPER, "fig_workflow") + + +def make_validation(demo): + from diffractscout.validation import verify_bundle + + result = verify_bundle(demo) + assert result["ok"], result + profile = pd.read_csv(demo / "pattern_profiles.csv") + peaks = pd.read_csv(demo / "peak_reference.csv") + phase = pd.read_csv(demo / "phase_summary.csv").iloc[0] + manifest = json.loads((demo / "manifest.json").read_text()) + modulus = peaks["young_modulus_hkl_normal_GPa"].astype(float) + assert ((modulus - 110.0).abs() < 1e-6).all(), "Expected isotropic synthetic fixture" + fig = plt.figure(figsize=(7.2, 4.6)) + ax = fig.add_axes([0.10, 0.39, 0.85, 0.45]) + x = profile["two_theta_deg"] + y = profile["relative_intensity"] + ax.fill_between(x, 0, y, color=ACCENT, alpha=0.08) + ax.plot(x, y, color=ACCENT, lw=1.4, label="Display profile") + ax.vlines( + peaks["two_theta_deg"], + 0, + peaks["normalized_intensity"], + color=WARM, + lw=0.85, + label="Reflections", + ) + for i, row in peaks.reset_index(drop=True).iterrows(): ax.annotate( str(row["hkl"]), - xy=(row["two_theta_deg"], min(float(row["normalized_intensity"]), 100.0)), - xytext=(row["two_theta_deg"], level), + xy=(row["two_theta_deg"], row["normalized_intensity"]), + xytext=(row["two_theta_deg"], 107 if i % 2 == 0 else 95), ha="center", - va="bottom", - fontsize=9.8, - color=INK, - arrowprops={"arrowstyle": "-", "color": LINE, "linewidth": 0.8}, + fontsize=8.7, + arrowprops={"arrowstyle": "-", "color": RULE, "lw": 0.7}, ) - - ax.set_xlim(32, 122) - ax.set_ylim(0, 114) - ax.set_xlabel(r"$2\theta$ (degrees)", fontsize=12.5) - ax.set_ylabel("Normalized intensity (%)", fontsize=12.5) - ax.text(0.0, 1.115, "(a) Offline diffraction fixture", transform=ax.transAxes, fontsize=14.2, fontweight="bold", color=NAVY, va="bottom") + ax.set( + xlim=(32, 122), + ylim=(0, 119), + xlabel=r"$2\theta$ (degrees)", + ylabel="Relative intensity (%)", + ) + ax.set_title("a Synthetic FCC diffraction reference", loc="left", fontweight="bold", pad=20) ax.text( - 0.0, - 1.045, - r"Synthetic $Fm\bar{3}m$ cell, $a=4.000$ Å; Cu K$\alpha$, $\lambda=1.5406$ Å", + 0, + 1.035, + r"$Fm\overline{3}m$ · $a=4.000$ Å · Cu Kα, $\lambda=1.5406$ Å", transform=ax.transAxes, - fontsize=10.7, + fontsize=9, color=MUTED, - va="bottom", ) - ax.grid(axis="y", color=LINE, linewidth=0.7, alpha=0.65) - ax.tick_params(axis="both", labelsize=10.6, colors=INK) - for spine in ("top", "right"): - ax.spines[spine].set_visible(False) - ax.spines["left"].set_color(LINE) - ax.spines["bottom"].set_color(LINE) - - info.set_xlim(0, 1) - info.set_ylim(0, 1) - info.axis("off") - info.text(0.0, 1.115, "(b) Reviewer-verifiable invariants", transform=info.transAxes, fontsize=14.2, fontweight="bold", color=NAVY, va="bottom") - - hash_short = str(phase["cif_sha256"])[:12] + "..." - cards = [ - ("Structure", f"{phase['space_group_symbol']} | a = {phase['a_A']:.3f} Å\nCIF SHA-256 {hash_short}", BLUE, TEAL_BG), - ("Diffraction", f"{int(phase['reflection_count'])} allowed hkl families\n{len(profile):,} profile points", TEAL, TEAL_BG), - ("Elasticity", "Synthetic isotropic tensor\nE(hkl) = 110.0 GPa for all families", GREEN, "#EEF7F1"), - ("Bundle integrity", f"{len(manifest['files'])} hashed artifacts\nStrict manifest verification: PASS", AMBER, AMBER_BG), + clean_axes(ax) + board = fig.add_axes([0, 0, 1, 1], xlim=(0, 1), ylim=(0, 1)) + board.axis("off") + panel(board, 0.10, 0.265, "b", "Reproducible checks") + values = [ + (f"{int(phase['reflection_count'])}", "allowed hkl families"), + ("110.0 GPa", "isotropic E(hkl)"), + (str(len(manifest["files"])), "hashed artifacts"), ] - y_positions = [0.75, 0.535, 0.320, 0.105] - for (heading, text, accent, face), y0 in zip(cards, y_positions, strict=True): - box = FancyBboxPatch( - (0.02, y0), - 0.96, - 0.165, - boxstyle="round,pad=0.012,rounding_size=0.018", - linewidth=1.25, - edgecolor=accent, - facecolor=face, - ) - info.add_patch(box) - info.add_patch(plt.Rectangle((0.02, y0), 0.015, 0.165, facecolor=accent, edgecolor="none")) - info.text(0.070, y0 + 0.112, heading, fontsize=12.5, fontweight="bold", color=accent, va="center") - info.text(0.070, y0 + 0.050, text, fontsize=10.6, color=INK, va="center", linespacing=1.35) - - fig.text( - 0.5, + for i, (value, caption) in enumerate(values): + xx = 0.10 + i * 0.30 + label(board, xx, 0.175, value, 17, "bold", ACCENT) + label(board, xx, 0.115, caption, 9, color=MUTED) + if i < 2: + board.plot([xx + 0.265, xx + 0.265], [0.10, 0.21], color=RULE, lw=0.6) + label( + board, + 0.10, 0.035, - "Synthetic verification fixture; values are constructed to test software contracts and are not experimental aluminium properties.", - ha="center", - va="bottom", - fontsize=10.2, + "Constructed verification fixture, not experimental material properties.", + 9, color=MUTED, ) - _save(fig, "fig_validation") + save(fig, PAPER, "fig_validation") -def main() -> None: +def main(): parser = argparse.ArgumentParser() - parser.add_argument( - "--demo-dir", - type=Path, - default=ROOT / "_paper_demo", - help="Existing or newly generated DiffractScout demo bundle.", - ) + parser.add_argument("--demo-dir", type=Path) args = parser.parse_args() + style() make_workflow() - make_validation(args.demo_dir.expanduser().resolve()) + if args.demo_dir: + make_validation(args.demo_dir.resolve()) + else: + from diffractscout.demo import write_demo_inputs + from diffractscout.pipeline import analyze_cifs + + with tempfile.TemporaryDirectory(prefix="diffractscout_figure_") as temp: + inputs = write_demo_inputs(Path(temp) / "inputs") + dest = Path(temp) / "results" + analyze_cifs([inputs], dest, include_excel=True) + make_validation(dest) if __name__ == "__main__": diff --git a/paper/paper.md b/paper/paper.md index 2dede29..db03592 100644 --- a/paper/paper.md +++ b/paper/paper.md @@ -63,6 +63,8 @@ The author reports using DiffractScout in the research underlying the Ti-24Nb-4Z OpenAI GPT-5.6 Pro assisted earlier repository development, testing, documentation, figure production, and manuscript drafting from 30 July to 12 August 2026. In this revision, OpenAI Codex GPT-6 and cooperating assistant agents supported code changes, test scaffolding and review, documentation, citation discovery, manuscript restructuring, and automated verification. Software claims were checked against project source and scientific contracts, references against publisher or upstream records, and automated tests and the analytic benchmark were run. Human author review of the AI-assisted contributions remains pending. +The README cover is AI-generated conceptual artwork. Manuscript diagrams and numerical plots are produced by repository scripts; scientific data are not retouched by an image-generation model. + # Acknowledgements No external funding was received for this software. There was no sponsor involvement. The author declares no competing interests. diff --git a/paper/paper.pdf b/paper/paper.pdf index 3c04c4e..1e1118a 100644 Binary files a/paper/paper.pdf and b/paper/paper.pdf differ