RustPDF Documentation

This project is maintained by ohara-lab-su

rust_pdf Python API / I/F Manual

0.0 Python package のファイル構成

高レベル I/F を __init__.py 一ファイルへ実装する構成は廃止した。 __init__.py は公開名の re-export に限定し、責務ごとに次のように分離する。

python/rust_pdf/
├── __init__.py   # public API の re-export
├── api.py        # 既存 low-level wrapper
├── config.py     # Config / parameter tree
├── result.py     # PDFResult / OptimizeResult / 物理量変換
├── pdf.py        # RustPDF
└── _rust_pdf.*   # PyO3 native extension

公開 I/F 名はこの分割によって変更しない。 したがって既存の rust_pdf.run() / optimize() / quick_optimize() と、 追加した Config / RustPDF / result I/F は従来どおり package root から import できる。

Current authoritative high-level I/F — 2026-09-16

この節が現在の高レベル Python I/F の基準である。 後続の Part I は既存 low-level I/F の詳細資料として維持する。 後続の Part II–IV にある過去の「構造化 I/F」設計記述は開発履歴として残しているが、 RustPDF / Config の現在仕様についてはこの節を優先する。

数値計算、FFT/ZDST、optimizer の意味は Rust 実装を基準とし、 Python 側で新しい計算意味を作らない。

0.1 設計の基準

Python I/F は二層を併存させる。

既存 low-level I/F
    version()
    run() -> dict
    optimize() -> dict
    quick_optimize() -> dict
    to_gr()
    to_tr()
    to_rdf()
    to_nr()

追加 high-level I/F
    Config
    RustPDF
    PDFResult
    OptimizeResult

既存 low-level I/F は削除・改名・自動化しない。 利用者は従来どおり直接使用できる。

高レベル RustPDF は localPDF と物理的に同じ操作だけ名称を揃える。 Rust にしか存在しない parameter optimization は RustPDF 固有機能として追加する。

0.2 Config

Config は Rust の parameter tree を Python 側で保持する。

重要なのは、Rust optimizer が使用する fit-enabled node を壊さないことである。

{
  "Polarization Factor": {
    "value": 0.55,
    "constraint": {
      "type": "bounded",
      "lo": 0.4,
      "hi": 0.8
    }
  }
}

この構造は Config 内でそのまま保持される。

Config.pol = 0.60 のように scalar 値を変更した場合も、 既存 node が fit-enabled なら value だけを変更し、constraint は保持する。

Rust core が読む主要 key は次のとおり。

Python property Rust parameter key Rust Config field
energy_keV / energy energy_of_X-ray energy
atomList / atom_list atomNo[List] atom_list
atomConc / atom_conc atomConc atom_conc
pol Polarization Factor pol
recoil Recoil Factor recoil
q_cut Cut off LowQ q_cut
norm_min Q range from norm_min
norm_max Q range to norm_max
dr delta-r dr
r_max r Max r_max
density Density density
r_cut Cut off Distance r_cut
r_cut_width Cut off TaperWidth r_cut_width
bg_factor Back Ground Factor bg_factor

webPDF/localPDF JSON の bg_factor を読み込んだ場合は、 Rust core が読む Back Ground Factor へ対応付ける。

0.3 localPDF と RustPDF の共通部分

実験データを読む基本形は次のとおり。

from rust_pdf import Config
from rust_pdf import RustPDF

config = Config("config.json")
pdf = RustPDF(config=config)

pdf.load_xy("data.dat")
pdf.load_bg_xy("background.dat", factor=1.0)

pdf.run_all()

q, iq = pdf.get_iq()
q, sq = pdf.get_sq()
r, gr = pdf.get_gr()
r, gr_lorch = pdf.get_gr_lorch()

localPDF と共通化した公開名:

操作 LocalPDF RustPDF 備考
Config 読込 Config(path) Config(path) Rust Config は fit parameter tree も保持
実験データ load_xy() load_xy() 2theta / intensity
background load_bg_xy() load_bg_xy() factor 指定可
標準計算 run_all() run_all() 内部 pipeline の段数は異なる
i(Q) get_iq() get_iq() raw i(Q)
S(Q) get_sq() get_sq() S(Q)=i(Q)+1
G(r) get_gr() get_gr() reduced PDF G(r)
Lorch G(r) get_gr_lorch() get_gr_lorch() Lorch
g(r) get_ggr() get_ggr() pair distribution g(r)
Lorch g(r) get_ggr_lorch() get_ggr_lorch() Lorch
保存 save_iq() 等 save_iq() 等 2列 text

run_all() の内部差

localPDF の現在実装では完全計算は、

pdf.run_all()
pdf.run_bft()

であり、run_all() は raw G(r) まで、run_bft() が bFT + Lorch を行う。

Rust pipeline は一回の pipeline::run() で、

まで生成する。

したがって RustPDF.run_all() は Rust pipeline を一回実行する。 Rust 側に localPDF の内部二段階を偽装して追加しない。

0.4 RustPDF の parameter optimization

Rust に実在する optimizer をそのまま高レベル I/F へ上げる。

通常 optimize

opt = pdf.optimize()
pdf.run_all()

RustPDF.optimize() は low-level rust_pdf.optimize() を呼ぶ。

Rust optimize::run() の実装:

quick optimize

opt = pdf.quick_optimize()
pdf.run_all()

RustPDF.quick_optimize() は low-level rust_pdf.quick_optimize()、 すなわち Rust optimize::quick_run() を呼ぶ。

quick_run() は Halton multi-start を行わず、 現在の parameter 値から Levenberg-Marquardt を1回実行する。

これは Python I/F 側で作った意味ではなく、Rust optimize.rs の実装そのものである。

最適化後 Config

low-level optimizer の戻り値 parameters は、 元 JSON 構造を保持した最適化済み parameter tree である。

高レベル RustPDF.optimize() / quick_optimize() は、 この parameters を RustPDF.config へ反映する。

したがって、

pdf.optimize()
pdf.run_all()

の run_all() は最適化後 parameter を使用する。

最適化を行わない場合は単に、

pdf.run_all()

とする。自動 optimize は行わない。

0.5 物理量名称

Rust core と webPDF/localPDF の名称差は次のとおり。

物理量 Rust core RustPDF / localPDF 公開名
reduced PDF G(r) reduced_gr gr, get_gr()
pair distribution g(r) RSpaceOutput::gr() ggr, get_ggr()
total correlation T(r) tr() tr
radial distribution function rdf() rdf
raw i(Q) iq iq, get_iq()
S(Q) iq + 1 sq, get_sq()

Rust 内部 reduced_gr は物理的に G(r) である。 localPDF/webPDF の _Gr / get_gr() も G(r) である。 したがって高レベル公開名 gr は G(r) とする。

0.6 既存 low-level I/F は残る

以下は高レベル I/F を使わなくても従来どおり使用できる。

out = rust_pdf.run(config, tth, intensity)
opt = rust_pdf.optimize(config, tth, intensity)
quick = rust_pdf.quick_optimize(config, tth, intensity)

高レベル RustPDF はこれらを置き換えるものではない。

0.7 Python 3.5

python/rust_pdf/__init__.py は Python 3.5 grammar で解釈可能な構文だけを使用する。

使用しないもの:

Config, PDFResult, OptimizeResult, RustPDF は通常クラスで実装する。

0.8 localPDF と RustPDF の pipeline API

localPDF の run_all() は複数の内部段階をまとめて raw PDF まで計算する実処理であり、 その名前に機能上の意味がある。

pdf.run_all()
pdf.run_bft()

run_bft() は BFT_smooth() を実行し、

Rcut_and_bFT
    ↓
UseSmoothBG
    ↓
CalcGrFFT
    ↓
CalcGrLorch

という独立した処理段階を実行する。

一方、Rust の native API では通常 PDF pipeline は run() であり、 その一回の pipeline の中で raw/BFT/Lorch が計算される。 したがって高レベル RustPDF の通常計算も、

pdf.run()

とする。

Rust に localPDF の run_bft() と同じ独立処理段階はないため、 名前だけを合わせた RustPDF.run_bft() は設けない。 計算済み結果を返すだけのメソッドは機能互換ではなくダミーになるためである。

0.9 Rust optimizer

Rust の optimizer は通常 pipeline と独立した API である。

pdf.optimize()
pdf.run()

または、

pdf.quick_optimize()
pdf.run()

と明示的に実行する。

low-level API でも同じ意味で、

opt = rust_pdf.optimize(config, tth, intensity, bg_tth, bg_intensity)
out = rust_pdf.run(
    opt["parameters"],
    tth,
    intensity,
    bg_tth,
    bg_intensity,
)

となる。

前版で追加した run_auto() はオリジナル Rust API にない独自概念であり、 I/F を不必要に増やすため削除した。

I/F 対応の判断基準

localPDF と RustPDF の I/F は名前ではなく機能で対応させる。


rust_pdf Python API / I/F Manual

この文書は、従来の python_api_manual.md に記載されていた Python API の情報を削らずに保持し、 その上へ現行 Rust Python I/F、webPDF、localPDF、新しい構造化 I/F の対応関係を追加した統合版である。

webPDF を物理量および既存動作の基準とする。 localPDF は webPDF の wrapper として実コードを照合して扱い、localPDF の実装を無条件には基準としない。 Rust Python I/F については既存 I/F を互換性確認のため残し、新 I/F を併存させる。


Part I — 従来 Python API Manual

以下は従来の python_api_manual.md の内容である。 既存 API、build/package 構成、config、workflow、error、maintainer 情報を失わないため、 要約による置換を行わず保持している。

rust_pdf Python API Manual

この文書は rust_pdf を Python から使う人のための API 仕様書です。README は概要と最小例を示す文書ですが、この文書では Python I/F の実装場所、データの流れ、config の意味、戻り値の読み方、最適化対象パラメータの書き方まで踏み込んで説明します。

対象コード:

1. Python I/F の構成

Python から見えるパッケージ名は rust_pdf です。内部では、Python wrapper と Rust 拡張モジュールの 2 層になっています。

Python user code
  |
  | import rust_pdf
  v
python/rust_pdf/__init__.py
  |
  | dict -> JSON string
  | array-like -> contiguous numpy.float64 ndarray
  v
rust_pdf._rust_pdf
  |
  | PyO3 module defined in src/python.rs
  | numpy ndarray -> Rust ndarray::Array1<f64>
  v
Rust pipeline
  |
  | src/pipeline.rs
  | src/compute/common.rs
  | src/compute/zdst.rs
  v
dict[str, numpy.ndarray] or optimization result dict

1.1 pyproject.toml

pyproject.toml で maturin を使った Python wheel ビルドを定義しています。

[build-system]
requires = ["maturin>=1.7,<2.0"]
build-backend = "maturin"

[tool.maturin]
features = ["python-extension"]
python-source = "python"
module-name = "rust_pdf._rust_pdf"

重要な点:

1.2 Cargo.toml

Python I/F に関係する feature は次です。

[features]
python = ["dep:pyo3", "dep:numpy", "dep:serde-pyobject", "dep:pyo3-stub-gen"]
python-extension = ["python", "pyo3/extension-module", "pyo3/abi3-py310"]

意味:

1.3 src/python.rs

src/python.rs が PyO3 module の本体です。

公開される低レベル関数:

#[pymodule]
fn _rust_pdf(m: &Bound<'_, PyModule>) -> PyResult<()> {
    m.add_function(wrap_pyfunction!(version, m)?)?;
    m.add_function(wrap_pyfunction!(run, m)?)?;
    m.add_function(wrap_pyfunction!(optimize, m)?)?;
    m.add_function(wrap_pyfunction!(quick_optimize, m)?)?;
    Ok(())
}

この _rust_pdf は Python wrapper から呼ばれます。低レベル関数の config 引数は &str、つまり JSON 文字列です。Python の dict を直接受けるのは python/rust_pdf/__init__.py 側です。

1.4 python/rust_pdf/__init__.py

ユーザー向け API はここで定義されています。

主な役割:

2. インストール

リリース wheel を使う場合:

pip install https://github.com/ohara-lab-su/rust_pdf/releases/download/v0.3.0/rust_pdf-0.3.0-cp310-abi3-win_amd64.whl

ローカル checkout から開発インストールする場合:

pip install maturin
maturin develop --features python-extension

wheel を作る場合:

maturin build --release

型スタブを生成する場合:

cargo run --bin py_stub_gen --features python

3. 解析パイプライン

rust_pdf.run() が実行する処理は概ね次の順です。

  1. config を Parameters に読み込みます。
  2. Config::from_parameters() で Rust の型付き設定に変換します。
  3. embedded reference data data.mpack からフォームファクタと Compton 係数を読みます。
  4. 入力の 2theta を Q に変換します。
  5. Cut off LowQ と Q range to に従って Q grid を作ります。
  6. 実験強度と optional background を同じ Q grid に補間します。
  7. background があれば Back Ground Factor を掛けて差し引きます。
  8. 偏光補正をかけます。
  9. フォームファクタ、Compton、recoil 補正を使って I(Q) を作ります。
  10. ZdstEngine で I(Q) から還元 PDF G(r) を計算します。
  11. Cut off Distance 付近より低 r の成分を切り出し、Q 空間へ back transform します。
  12. back transform した correction を I(Q) から差し引き、iq_bft と reduced_gr_bft を作ります。
  13. Lorch window を使った reduced_gr_lorch も返します。

4. 入力データ

4.1 実験プロファイル

Python API は 1 次元配列を受け取ります。

tth: array-like[float]
intensity: array-like[float]

意味:

典型的な読み込み:

import numpy as np

data = np.loadtxt("Ni-08mm-600s.dat")
tth = data[:, 0]
intensity = data[:, 1]

4.2 background

background を使う場合は、x と y の両方を渡します。

bg = np.loadtxt("background.dat")
bg_tth = bg[:, 0]
bg_intensity = bg[:, 1]

out = rust_pdf.run(config, tth, intensity, bg_tth, bg_intensity)

片方だけが None の場合、Rust 側では background は使われません。

5. config 仕様

config は Python dict または JSON 文字列です。通常は dict を使ってください。

config = {
    "energy_of_X-ray": 112.8,
    "Density": 0.09132,
    "Q range from": 16.0,
    "Q range to": 23.7,
    "r Max": 20.0,
    "delta-r": 0.01,
    "Cut off LowQ": 0.0,
    "Cut off Distance": 2.0,
    "Cut off TaperWidth": 0.0,
    "Back Ground Factor": 1.0,
    "atomNo[List]": [28],
    "atomConc": [1.0],
    "Polarization Factor": 0.65,
    "Recoil Factor": 1.0,
}

5.1 config key table

config key Rust field Unit Required Default if missing Purpose
energy_of_X-ray Config.energy keV No 113.0 Incident X-ray energy. Used for 2theta -> Q conversion and anomalous scattering interpolation.
Density Config.density atoms/A^3 No 0.065 Number density used when converting between G(r) and g(r), T(r), RDF(r).
atomNo[List] Config.atom_list atomic number Yes none Element atomic numbers used for reference-data lookup.
atomConc Config.atom_conc relative amount No [1.0, 2.0] Relative concentrations. Length must match atomNo[List] for physical correctness.
Polarization Factor Config.pol dimensionless No 0.55 Polarization correction parameter. Common fit target.
Recoil Factor Config.recoil dimensionless No 1.0 Exponent applied to Compton correction. Common fit target.
Cut off LowQ Config.q_cut 1/A No 0.0 Lower Q cutoff before interpolation.
Q range from Config.norm_min 1/A No 15.0 Lower Q bound for normalization.
Q range to Config.norm_max 1/A No 23.8 Upper Q bound for normalization and Q truncation.
delta-r Config.dr A No 0.01 Requested r-grid spacing.
r Max Config.r_max A No 10.0 Maximum r for output G(r).
Cut off Distance Config.r_cut A No 1.3 Low-r cutoff distance used for back-Fourier correction and fitting residual.
Cut off TaperWidth Config.r_cut_width A No 0.2 Width of cosine taper before r_cut.
Back Ground Factor Config.bg_factor dimensionless No 1.0 Scale factor for background subtraction.

Important:

5.2 Fixed value vs fit-enabled value

Any scalar config value can be written in two forms.

Fixed:

"Polarization Factor": 0.65

Fit-enabled:

"Polarization Factor": {
    "value": 0.65,
    "constraint": {"type": "bounded", "lo": 0.0, "hi": 1.0},
}

The fit-enabled object is parsed by src/fitting/params.rs as ParamSpec.

5.3 Constraint types

Type JSON form Meaning
free {"type": "free"} No lower or upper bound.
bounded {"type": "bounded", "lo": 0.0, "hi": 1.0} Value is constrained to [lo, hi].
l_bounded {"type": "l_bounded", "lo": 0.0} Value is constrained to [lo, +inf).
u_bounded {"type": "u_bounded", "hi": 1.0} Value is constrained to (-inf, hi].

Internally, constraints are mapped to unconstrained optimizer coordinates before Levenberg-Marquardt is called. This lets the optimizer run in an unconstrained space while final parameter values remain inside physical bounds.

6. Public Python API

6.1 rust_pdf.version

rust_pdf.version() -> str

Returns the package version from Cargo.toml.

Example:

import rust_pdf

print(rust_pdf.version())
print(rust_pdf.__version__)

6.2 rust_pdf.run

rust_pdf.run(
    config,
    tth,
    intensity,
    bg_tth=None,
    bg_intensity=None,
) -> dict[str, numpy.ndarray]

Runs one PDF calculation without changing the config.

Arguments:

Argument Type Required Meaning
config dict or JSON str Yes Measurement and calculation parameters.
tth 1D array-like Yes 2theta in degrees.
intensity 1D array-like Yes Experimental intensity.
bg_tth 1D array-like or None No Background 2theta in degrees.
bg_intensity 1D array-like or None No Background intensity.

Return keys:

Key Domain Meaning Typical x-axis
pre_q Q Q grid used after raw 2theta -> Q conversion and trimming. pre_q
pre_iq Q Reconstructed corrected intensity-like curve before final exposed I(Q) grouping. pre_q
compton Q Composition-weighted Compton correction after recoil correction. pre_q
form_factor Q Composition-weighted <\|f\|^2> term. pre_q
q Q Main Q grid. Usually same values as pre_q. q
iq_raw Q Reduced intensity I(Q) before back-Fourier correction. q
iq_bft Q I(Q) after subtracting low-r back-Fourier correction. q
iq_diff Q Correction curve subtracted from iq_raw. q
r R r grid in A. r
reduced_gr_raw R Reduced PDF G(r) from iq_raw. r
reduced_gr_bft R Reduced PDF G(r) after back-Fourier correction. r
reduced_gr_lorch R Reduced PDF G(r) calculated with Lorch window damping. r

Shape rule:

Minimal example:

import numpy as np
import rust_pdf

config = {
    "energy_of_X-ray": 112.8,
    "Density": 0.09132,
    "Q range from": 16.0,
    "Q range to": 23.7,
    "r Max": 20.0,
    "delta-r": 0.01,
    "Cut off Distance": 2.0,
    "Cut off TaperWidth": 0.0,
    "Back Ground Factor": 1.0,
    "atomNo[List]": [28],
    "atomConc": [1.0],
    "Polarization Factor": 0.65,
    "Recoil Factor": 1.0,
}

data = np.loadtxt("samples/Ni-08mm-600s.dat")
out = rust_pdf.run(config, data[:, 0], data[:, 1])

np.savetxt("iq_bft.txt", np.column_stack([out["q"], out["iq_bft"]]))
np.savetxt("gr_bft.txt", np.column_stack([out["r"], out["reduced_gr_bft"]]))

6.3 rust_pdf.optimize

rust_pdf.optimize(
    config,
    tth,
    intensity,
    bg_tth=None,
    bg_intensity=None,
) -> dict[str, Any]

Runs robust parameter fitting.

Algorithm:

  1. Fixed reference terms are precomputed.
  2. If config contains bounded fit parameters, Halton sampling generates candidate starting points inside their bounds.
  3. The best few Halton samples are refined with Levenberg-Marquardt.
  4. The result with the lowest RMS residual is returned.

Return keys:

Key Type Meaning
parameters dict Optimized config preserving the original JSON shape. Fit-enabled values remain objects with value and constraint.
residual numpy.ndarray Final residual vector. In ZdstEngine, this is a Q-space low-r back-transform residual scaled for fitting.
history numpy.ndarray RMS residual history recorded during Jacobian evaluations.
termination str Levenberg-Marquardt termination reason.

Example:

config["Polarization Factor"] = {
    "value": 0.65,
    "constraint": {"type": "bounded", "lo": 0.0, "hi": 1.0},
}
config["Recoil Factor"] = {
    "value": 1.0,
    "constraint": {"type": "bounded", "lo": 0.0, "hi": 2.0},
}
config["Back Ground Factor"] = {
    "value": 1.0,
    "constraint": {"type": "bounded", "lo": 0.0, "hi": 3.0},
}

opt = rust_pdf.optimize(config, tth, intensity, bg_tth, bg_intensity)
print(opt["termination"])
print(opt["parameters"])

out = rust_pdf.run(opt["parameters"], tth, intensity, bg_tth, bg_intensity)

When to use:

6.4 rust_pdf.quick_optimize

rust_pdf.quick_optimize(
    config,
    tth,
    intensity,
    bg_tth=None,
    bg_intensity=None,
) -> dict[str, Any]

Runs one local Levenberg-Marquardt fit from the current config values. It skips the Halton sampling phase.

Use this when:

The return shape is identical to optimize.

6.5 Conversion helpers

The Rust pipeline primarily returns reduced PDF G(r) under keys named reduced_gr_*. Python helper functions convert it to related real-space functions.

rust_pdf.to_gr(rs, reduced_gr, density) -> numpy.ndarray
rust_pdf.to_tr(rs, reduced_gr, density) -> numpy.ndarray
rust_pdf.to_rdf(rs, reduced_gr, density) -> numpy.ndarray
rust_pdf.to_nr(rs, reduced_gr, density) -> numpy.ndarray

Definitions used by the code:

G(r) = 4 pi r rho (g(r) - 1)
g(r) = G(r) / (4 pi r rho) + 1
T(r) = G(r) + 4 pi r rho
RDF(r) = r T(r)
N(r) = integral RDF(r) dr

Example:

rho = config["Density"]
r = out["r"]
g_raw = rust_pdf.to_gr(r, out["reduced_gr_raw"], rho)
g_bft = rust_pdf.to_gr(r, out["reduced_gr_bft"], rho)
rdf_bft = rust_pdf.to_rdf(r, out["reduced_gr_bft"], rho)
n_bft = rust_pdf.to_nr(r, out["reduced_gr_bft"], rho)

Note:

7. Practical workflows

7.1 One-shot calculation

import numpy as np
import rust_pdf

profile = np.loadtxt("profile.dat")
tth = profile[:, 0]
intensity = profile[:, 1]

out = rust_pdf.run(config, tth, intensity)

np.savetxt("S_or_IQ_raw.txt", np.column_stack([out["q"], out["iq_raw"]]))
np.savetxt("G_raw.txt", np.column_stack([out["r"], out["reduced_gr_raw"]]))
np.savetxt("G_bft.txt", np.column_stack([out["r"], out["reduced_gr_bft"]]))

7.2 Background-subtracted calculation

profile = np.loadtxt("profile.dat")
bg = np.loadtxt("background.dat")

out = rust_pdf.run(
    config,
    profile[:, 0],
    profile[:, 1],
    bg[:, 0],
    bg[:, 1],
)

Back Ground Factor controls how strongly background is subtracted.

corrected_intensity = intensity - background * Back Ground Factor

7.3 Optimize then calculate

opt = rust_pdf.optimize(config, tth, intensity)
best_config = opt["parameters"]
out = rust_pdf.run(best_config, tth, intensity)

Recommended pattern:

  1. Add constraints only to parameters that should actually move.
  2. Run optimize.
  3. Inspect termination and history.
  4. Run run again using opt["parameters"].
  5. Save both optimized config and output curves.

7.4 Reuse previous optimized parameters

best = rust_pdf.optimize(config, tth1, intensity1)
config2 = best["parameters"]

quick = rust_pdf.quick_optimize(config2, tth2, intensity2)
out2 = rust_pdf.run(quick["parameters"], tth2, intensity2)

This is useful for a series of similar measurements.

8. Error behavior and validation

Python wrapper errors:

Rust errors surfaced as Python ValueError:

The wrapper does not currently validate all shape constraints before entering Rust. For clearer user-side scripts, validate these yourself:

if len(tth) != len(intensity):
    raise ValueError("tth and intensity must have the same length")
if bg_tth is not None and bg_intensity is not None and len(bg_tth) != len(bg_intensity):
    raise ValueError("bg_tth and bg_intensity must have the same length")

9. Returned arrays and plotting guide

Common plots:

import matplotlib.pyplot as plt

plt.figure()
plt.plot(out["q"], out["iq_raw"], label="raw I(Q)")
plt.plot(out["q"], out["iq_bft"], label="BFT-corrected I(Q)")
plt.plot(out["q"], out["iq_diff"], label="removed correction")
plt.xlabel("Q [1/A]")
plt.legend()

plt.figure()
plt.plot(out["r"], out["reduced_gr_raw"], label="raw G(r)")
plt.plot(out["r"], out["reduced_gr_bft"], label="BFT-corrected G(r)")
plt.plot(out["r"], out["reduced_gr_lorch"], label="Lorch G(r)")
plt.xlabel("r [A]")
plt.legend()
plt.show()

Choosing which real-space output to inspect:

10. Notes for maintainers

10.1 Binding data shape

src/bindings.rs defines PDFOutput and PDFOptimizeResult. These structs are intentionally flat because both Python and WASM binding layers serialize them more easily than nested Rust pipeline structs.

src/python.rs converts:

PipelineOutput -> PDFOutput -> Python dict[str, numpy.ndarray]
OptimizeResult -> PDFOptimizeResult -> Python dict

10.2 Low-level _rust_pdf signatures

The Rust extension functions accept:

run(config_json: str, tth: float64 ndarray, intensity: float64 ndarray,
    bg_tth: float64 ndarray | None, bg_intensity: float64 ndarray | None)

optimize(config_json: str, tth: float64 ndarray, intensity: float64 ndarray,
         bg_tth: float64 ndarray | None, bg_intensity: float64 ndarray | None)

quick_optimize(config_json: str, tth: float64 ndarray, intensity: float64 ndarray,
               bg_tth: float64 ndarray | None, bg_intensity: float64 ndarray | None)

The public wrapper broadens this to dict | str config and arbitrary array-like inputs.

10.3 Where to add new Python functions

To expose a new Rust calculation to Python:

  1. Add a Rust function in src/python.rs with #[pyfunction].
  2. Add it to _rust_pdf() using m.add_function(wrap_pyfunction!(..., m)?).
  3. Add a Python wrapper function in python/rust_pdf/__init__.py.
  4. Add the function name to __all__.
  5. Update this manual and regenerate .pyi if needed.

11. Full example script

from __future__ import annotations

import json
from pathlib import Path

import numpy as np
import rust_pdf


def load_xy(path: str | Path) -> tuple[np.ndarray, np.ndarray]:
    arr = np.loadtxt(path)
    return arr[:, 0], arr[:, 1]


config = {
    "energy_of_X-ray": 112.8,
    "Density": 0.09132,
    "Q range from": 16.0,
    "Q range to": 23.7,
    "r Max": 20.0,
    "delta-r": 0.01,
    "Cut off LowQ": 0.0,
    "Cut off Distance": 2.0,
    "Cut off TaperWidth": 0.0,
    "Back Ground Factor": {
        "value": 1.0,
        "constraint": {"type": "bounded", "lo": 0.0, "hi": 3.0},
    },
    "atomNo[List]": [28],
    "atomConc": [1.0],
    "Polarization Factor": {
        "value": 0.65,
        "constraint": {"type": "bounded", "lo": 0.0, "hi": 1.0},
    },
    "Recoil Factor": {
        "value": 1.0,
        "constraint": {"type": "bounded", "lo": 0.0, "hi": 2.0},
    },
}

tth, intensity = load_xy("profile.dat")
bg_tth, bg_intensity = load_xy("background.dat")

opt = rust_pdf.optimize(config, tth, intensity, bg_tth, bg_intensity)
out = rust_pdf.run(opt["parameters"], tth, intensity, bg_tth, bg_intensity)

output_dir = Path("pdf_output")
output_dir.mkdir(exist_ok=True)

(output_dir / "optimized_config.json").write_text(
    json.dumps(opt["parameters"], indent=2),
    encoding="utf-8",
)

np.savetxt(output_dir / "iq_raw.txt", np.column_stack([out["q"], out["iq_raw"]]))
np.savetxt(output_dir / "iq_bft.txt", np.column_stack([out["q"], out["iq_bft"]]))
np.savetxt(output_dir / "g_reduced_raw.txt", np.column_stack([out["r"], out["reduced_gr_raw"]]))
np.savetxt(output_dir / "g_reduced_bft.txt", np.column_stack([out["r"], out["reduced_gr_bft"]]))

rho = opt["parameters"]["Density"]
g_r = rust_pdf.to_gr(out["r"], out["reduced_gr_bft"], rho)
rdf = rust_pdf.to_rdf(out["r"], out["reduced_gr_bft"], rho)
n_r = rust_pdf.to_nr(out["r"], out["reduced_gr_bft"], rho)

np.savetxt(output_dir / "g_r.txt", np.column_stack([out["r"], g_r]))
np.savetxt(output_dir / "rdf.txt", np.column_stack([out["r"], rdf]))
np.savetxt(output_dir / "n_r.txt", np.column_stack([out["r"], n_r]))

Part II — 現行 I/F / webPDF / localPDF 統合仕様

以下は、Part I の既存 API 仕様に追加する現行 I/F の対応仕様である。 Rust 内部名と公開名の対応は文字列ではなく、各コードが実際に表す物理量を根拠に決める。

1. 基準と方針

この文書では三つの実装を区別する。

  1. webPDF — 物理量と既存動作の基準とする。
  2. localPDF — webPDF をローカル Python から扱う wrapper。webPDF と一致する部分は利用するが、localPDF 自体を基準とはしない。
  3. rust_pdf Python I/F — Rust 計算 core の Python binding。既存 I/F を壊さず、その上に webPDF の物理量へ対応した構造化 I/F を追加する。

対応付けは変数名の文字列ではなく、コード中の定義式と実際に保持している物理量を根拠とする。

今回の修正では 既存 rust_pdf Python I/F は削除・改名・戻り値変更をしない。


2. rust_pdf Python I/F の層

Python user
    |
    +-- 既存 I/F ---------------------------------------+
    |   run()                 -> dict                   |
    |   optimize()            -> dict                   |
    |   quick_optimize()      -> dict                   |
    |   to_gr()/to_tr()/to_rdf()/to_nr()               |
    |                                                  |
    +-- 追加 I/F ---------------------------------------+
        run_result()          -> PDFResult
        optimize_result()     -> OptimizeResult
        quick_optimize_result()-> OptimizeResult
                              |
                              v
python/rust_pdf/__init__.py
                              |
                              v
rust_pdf._rust_pdf
                              |
                              v
src/python.rs -> Rust pipeline

低レベル PyO3 module _rust_pdf の公開関数も従来どおり

version
run
optimize
quick_optimize

である。


3. 既存 rust_pdf Python I/F

既存 python/rust_pdf/__init__.py の公開 I/F はそのまま残す。

3.1 関数

version()
run(config, tth, intensity, bg_tth=None, bg_intensity=None)
optimize(config, tth, intensity, bg_tth=None, bg_intensity=None)
quick_optimize(config, tth, intensity, bg_tth=None, bg_intensity=None)

to_gr(rs, reduced_gr, density)
to_tr(rs, reduced_gr, density)
to_rdf(rs, reduced_gr, density)
to_nr(rs, reduced_gr, density)

3.2 run() の戻り値

run() は従来どおり dict を返す。

pre_q
pre_iq
compton
form_factor

q
iq_raw
iq_bft
iq_diff

r
reduced_gr_raw
reduced_gr_bft
reduced_gr_lorch

既存コード

out = rust_pdf.run(...)
q = out["q"]
gr = out["reduced_gr_raw"]

はそのまま動くことを I/F 互換性の条件とする。

3.3 既存変換関数

既存マニュアルに記載されていた定義は Rust core と一致している。

[ G(r)=4\pi r\rho[g(r)-1] ]

[ g(r)=\frac{G(r)}{4\pi r\rho}+1 ]

[ T(r)=G(r)+4\pi r\rho ]

[ RDF(r)=rT(r) ]

to_gr() という名前は「G(r) を g(r) に変換する」という Rust 側の命名であり、ここでの gr は pair distribution function g(r) を意味する。

この既存名は互換性のため変更しない。


4. webPDF の物理量

4.1 i(Q) と S(Q)

webPDF Calc_iQ_SQ() は

self.iq = (
    self.intensityCorr / norm_value - self.f2sum - self.comp_total
) / self.fsum_2
self.sq = self.iq + 1

と定義している。

したがって webPDF の物理量は

[ S(Q)=i(Q)+1 ]

である。

webPDF の S(Q) plot も

tes.iq_interpolate_raw[...] + 1

を描画しており、この定義と一致する。

4.2 G(r)

webPDF CalcGrFFT() は DST の結果を

self._Gr

に格納する。

その直後に

self._ggr = self._Gr / (4.0 * np.pi * self.rr * self.density) + 1.0

としているため、_Gr は reduced PDF G(r) である。

4.3 g(r)

webPDF の定義は

self._ggr = self._Gr / (4.0 * np.pi * self.rr * self.density) + 1.0

であり、

[ g(r)=\frac{G(r)}{4\pi r\rho}+1 ]

である。

4.4 T(r)

webPDF は

self._Tr = self._ggr * (4.0 * np.pi * self.rr * self.density)

なので、

[ T(r)=4\pi r\rho g(r)=G(r)+4\pi r\rho ]

である。

4.5 RDF(r)

webPDF は

self._RDF = self._Tr * self.rr

なので、

[ RDF(r)=rT(r) ]

である。

4.6 Lorch

webPDF CalcGrLorch() は _Gr_Lorch を積分から生成し、

_ggr_Lorch
_Tr_Lorch
_RDF_Lorch

を上記と同じ関係式で生成する。


5. Rust core と webPDF の対応

名称だけを見ると Rust の gr と webPDF の _Gr を対応させたくなるが、それは誤りである。

Rust RSpaceOutput では

reduced_gr              = G(r)
gr(density)             = g(r)
tr(density)             = T(r)
rdf(density)            = RDF(r)

である。

したがって物理量による正しい対応は次のとおり。

物理量 定義 Rust core 既存 rust_pdf dict webPDF
i(Q) S(Q)-1 QSpaceOutput.iq iq_raw iq_interpolate_raw
i_bFT(Q) bFT 補正後 i(Q) QSpaceOutput.iq_bft iq_bft iq_interpolate_bFT / smooth 後の系列
G(r) reduced PDF RSpaceOutput.reduced_gr reduced_gr_raw _Gr
G_bFT(r) bFT 系 G(r) RSpaceOutput.reduced_gr_bft reduced_gr_bft _Gr_bFT
G_Lorch(r) Lorch G(r) RSpaceOutput.reduced_gr_lorch reduced_gr_lorch _Gr_Lorch
g(r) G/(4πrρ)+1 RSpaceOutput::gr() to_gr(...) _ggr
T(r) G+4πrρ RSpaceOutput::tr() to_tr(...) _Tr
RDF(r) rT(r) RSpaceOutput::rdf() to_rdf(...) _RDF

重要な対応は

Rust reduced_gr  -> webPDF _Gr  -> G(r)
Rust gr()        -> webPDF _ggr -> g(r)

である。


6. localPDF の検証

localPDF は webPDF の Local_Calc_PDF_from_xy を wrapper している。

localPDF は基準ではないため、各 getter は webPDF の物理量へ戻って確認する。

6.1 一致している getter

get_gr():

return self._pdf.rr[:rpos], self._pdf._Gr[:rpos]

webPDF _Gr = G(r) なので正しい。

get_ggr():

return self._pdf.rr[:rpos], self._pdf._ggr[:rpos]

webPDF _ggr = g(r) なので正しい。

Lorch 版も

get_gr_lorch()  -> _Gr_Lorch  -> G_Lorch(r)
get_ggr_lorch() -> _ggr_Lorch -> g_Lorch(r)

で一致している。

get_iq() は iq_interpolate_raw を返し、webPDF が S(Q) plot に使用する raw interpolated i(Q) と対応する。

6.2 get_sq()

localPDF は

return self._pdf.qq[: self._pdf.num_norm_max],
       self._pdf.sq[: self._pdf.num_norm_max]

を返す。

webPDF Calc_iQ_SQ() の

self.sq = self.iq + 1

に直接対応しており、物理量 S(Q) として整合する。

webPDF の通常 plot も iq_interpolate_raw + 1 であり同じ物理量である。

6.3 localPDF を今回変更しない理由

今回確認した主要 getter

get_iq
get_sq
get_gr
get_gr_lorch
get_ggr
get_ggr_lorch

は webPDF の物理量と対応している。

したがって、この I/F 対応作業のためだけに localPDF を変更する必要はない。

localPDF に T(r), RDF(r) の getter がないことは「誤り」ではなく、単に公開範囲が狭いだけである。今回それを追加する必然性もないため変更しない。


7. webPDF 内の個別コード経路について

webPDF には同じ物理量を複数の経路で表示・保存するコードがある。

物理量の基準は計算クラスの定義と通常表示に置く。

例えば Calc_iQ_SQ() は sq = iq + 1 と定義し、通常の S(Q) plot も iq_interpolate_raw + 1 を使用する。

一方、download 部には

tes.sq[...] + 1

という別経路が存在する。

この文書では webPDF の物理量定義を変更しない。また、この I/F 修正のために webPDF 自体も変更しない。

I/F 対応としての S(Q) は、webPDF の計算定義どおり

S(Q) = i(Q) + 1

とする。


8. 新しい rust_pdf 構造化 I/F

既存 I/F を変更せず、別名で追加する。

8.1 run_result()

result = rust_pdf.run_result(
    config,
    tth,
    intensity,
    bg_tth=None,
    bg_intensity=None,
)

戻り値は PDFResult。

内部では既存 run() をそのまま呼び、その dict を物理量名へ対応付ける。

したがって新 I/F は別計算経路ではない。

run_result()
    |
    v
run()                  ← 既存 I/F
    |
    v
_rust_pdf.run()
    |
    v
Rust pipeline

8.2 PDFResult の対応

新 I/F 既存 rust_pdf webPDF 物理量
result.q out["q"] qq_interpolate Q
result.iq out["iq_raw"] iq_interpolate_raw i(Q)
result.sq out["iq_raw"] + 1 i(Q)+1 S(Q)
result.iq_bft out["iq_bft"] bFT 系 i(Q) i_bFT(Q)
result.sq_bft out["iq_bft"] + 1 bFT i(Q)+1 S_bFT(Q)
result.gr out["reduced_gr_raw"] _Gr G(r)
result.gr_bft out["reduced_gr_bft"] _Gr_bFT G_bFT(r)
result.gr_lorch out["reduced_gr_lorch"] _Gr_Lorch G_Lorch(r)
result.ggr to_gr(r, gr, rho) _ggr g(r)
result.ggr_bft to_gr(r, gr_bft, rho) _ggr_bFT g_bFT(r)
result.ggr_lorch to_gr(r, gr_lorch, rho) _ggr_Lorch g_Lorch(r)
result.tr to_tr(r, gr, rho) _Tr T(r)
result.rdf to_rdf(r, gr, rho) _RDF RDF(r)
result.tr_lorch to_tr(r, gr_lorch, rho) _Tr_Lorch T_Lorch(r)
result.rdf_lorch to_rdf(r, gr_lorch, rho) _RDF_Lorch RDF_Lorch(r)

8.3 localPDF 互換 getter

PDFResult は、localPDF で既に使われている主要取得 I/F と同じ名前を追加する。

result.get_iq()
result.get_sq()
result.get_gr()
result.get_gr_lorch()
result.get_ggr()
result.get_ggr_lorch()

いずれも (x, y) を返す。

ここで名称の対応は

localPDF get_gr()  -> webPDF _Gr  -> G(r)
PDFResult.get_gr() -> G(r)

localPDF get_ggr()  -> webPDF _ggr -> g(r)
PDFResult.get_ggr() -> g(r)

である。


9. 旧 I/F と新 I/F を併存させる意味

前版のように run() の戻り値そのものを dataclass へ変更すると、

out["iq_raw"]

を使っている既存コードが壊れる。

今回の設計ではそれを行わない。

旧:
    out = rust_pdf.run(...)
    out["iq_raw"]
    out["reduced_gr_raw"]

新:
    result = rust_pdf.run_result(...)
    result.iq
    result.gr
    result.ggr

両方が同時に使用できる。

これにより、

を同じ package で行える。


10. optimize I/F

既存:

opt = rust_pdf.optimize(...)
opt["parameters"]
opt["residual"]
opt["history"]
opt["termination"]

はそのまま残す。

追加:

opt = rust_pdf.optimize_result(...)
opt.parameters
opt.residual
opt.history
opt.termination

quick_optimize_result() も同様である。

新 I/F は既存 optimize の結果を dataclass に対応付けるだけであり、optimizer の計算は変更しない。


11. 互換性表

種別 既存 I/F 維持 追加 I/F
version version() Yes なし
calculation run() -> dict Yes run_result() -> PDFResult
optimization optimize() -> dict Yes optimize_result() -> OptimizeResult
quick optimization quick_optimize() -> dict Yes quick_optimize_result() -> OptimizeResult
G→g to_gr() Yes PDFResult.ggr
G→T to_tr() Yes PDFResult.tr
G→RDF to_rdf() Yes PDFResult.rdf
G→N to_nr() Yes PDFResult.nr

12. 既存 python_api_manual.md から維持される仕様

既存マニュアルで定義されていた次の内容は変更しない。

新 I/F はこれらを置換するのではなく上に追加される。


13. 設計上の境界

今回の I/F 対応は数値計算法の比較ではない。

変更しないもの:

FFT / ZDST
Q/r grid
normalization
back-Fourier algorithm
Lorch algorithm
Rust PipelineOutput
Rust RSpaceOutput の内部名称
webPDF の計算コード
localPDF の計算コード

公開 I/F で行うのは

内部データ
    ↓
物理量を同定
    ↓
webPDF の物理量名へ対応
    ↓
Python 利用者向け名称

という対応付けである。

このため reduced_gr -> gr という対応は単なる rename ではない。

Rust reduced_gr
    ↓ [式から G(r) と同定]
webPDF _Gr
    ↓
公開名 gr / get_gr()

という物理量を介した対応である。


Part III — 統合後の参照方針

1. どの記述を何の基準にするか

この統合マニュアルでは、情報源の役割を次のように分ける。

したがって、localPDF と webPDF が食い違う場合は localPDF を自動的に正しいとは扱わない。 まず webPDF の計算対象と物理量を確認し、必要な場合だけ localPDF を修正する。

2. 今回の localPDF の扱い

今回確認対象となった主要 getter は、

get_iq()
get_sq()
get_gr()
get_gr_lorch()
get_ggr()
get_ggr_lorch()

である。

これらについては Part II に記載したとおり webPDF 側の対応データを確認した。 今回の I/F 統合作業だけを理由に localPDF へ変更を入れる必要はないため、localPDF は変更しない。

これは localPDF 全体を正しいと認定する意味ではない。 今後、別の localPDF I/F を Rust 側へ対応させる場合も、その都度 webPDF の実データ・物理量へ戻って確認する。

3. 互換性の原則

既存 Rust Python I/F は削除しない。

run()
optimize()
quick_optimize()
to_gr()
to_tr()
to_rdf()
to_nr()

は従来どおり利用できる。

新 I/F はこれらを置換せず、

run_result()
optimize_result()
quick_optimize_result()
PDFResult
OptimizeResult

を追加する。

このため旧 I/F と新 I/F を同一バージョン上で直接比較できる。

4. G(r) / g(r) の名称規則

公開 I/F で最も重要な名称規則は次である。

gr  = G(r)
ggr = g(r)

根拠は webPDF の実装であり、

_Gr  = G(r)
_ggr = G(r)/(4πrρ) + 1 = g(r)

である。

Rust core 内部では、

reduced_gr = G(r)
gr()       = g(r)

であるため、Rust 内部の識別子 gr と Python 公開 I/F の gr は同じ文字列規則ではない。

公開 I/F は Rust の内部識別子ではなく webPDF の物理量体系へ合わせる。

5. マニュアル更新時の原則

今後このマニュアルを更新する場合、Part I に存在する情報を新仕様の説明で置換して削除しない。

新しい I/F、対応関係、build 方法、config、workflow、maintenance 情報が追加された場合は、 既存情報を保持した上で追加・訂正する。

古い仕様が本当に無効になった場合のみ、削除ではなく「旧仕様」「廃止」「互換用」などの位置付けを明記して履歴を残す。


Part IV — Python 3.5 互換性

1. Python 言語バージョン

この Python I/F は Python 3.5 で解釈可能な構文を使用する。

したがって、I/F 実装では以下を使用しない。

from __future__ import annotations
X | None
dict[str, ...]
tuple[...]
list[...]
変数アノテーション
f-string
標準ライブラリ dataclasses

Python 3.5 では標準ライブラリ dataclasses が存在しないため、 PDFResult と OptimizeResult は追加依存を導入せず通常クラスとして定義する。

既存 I/F の

run() -> dict
optimize() -> dict
quick_optimize() -> dict

は変更しない。

2. 型情報

Python 3.5 で使用可能な typing の型を使用する場合は、

Dict
Mapping
Optional
Tuple
Union

の形式を使用する。

PEP 585 の

dict[str, Any]
tuple[Array1D, Array1D]

や PEP 604 の

Array1D | None

は使用しない。

また、numpy.typing を I/F の必須依存にしない。 Array1D は runtime 上 numpy.ndarray として扱う。

サンプルファイルの最終構成

サンプルは2本だけとする。

main_rust_manual.py
main_rust_auto.py

main_rust_manual.py は通常 pipeline を明示的に pdf.run() する手動版。

main_rust_auto.py は parameter optimization を行う自動版であり、 オリジナル Rust API の意味を保って、

pdf.optimize()
pdf.run()

と明示的に実行する。

run_auto() のような独自 API は使用しない。