This project is maintained by ohara-lab-su
高レベル 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 側で新しい計算意味を作らない。
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 固有機能として追加する。
ConfigConfig は 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 へ対応付ける。
実験データを読む基本形は次のとおり。
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 の内部二段階を偽装して追加しない。
Rust に実在する optimizer をそのまま高レベル I/F へ上げる。
opt = pdf.optimize()
pdf.run_all()
RustPDF.optimize() は low-level rust_pdf.optimize() を呼ぶ。
Rust optimize::run() の実装:
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 の実装そのものである。
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 は行わない。
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) とする。
以下は高レベル 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 はこれらを置き換えるものではない。
python/rust_pdf/__init__.py は Python 3.5 grammar で解釈可能な構文だけを使用する。
使用しないもの:
from __future__ import annotationsX | Nonedict[str, ...]dataclassesnumpy.typingConfig, PDFResult, OptimizeResult, RustPDF は通常クラスで実装する。
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() は設けない。
計算済み結果を返すだけのメソッドは機能互換ではなくダミーになるためである。
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 を不必要に増やすため削除した。
localPDF と RustPDF の I/F は名前ではなく機能で対応させる。
この文書は、従来の 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 を併存させる。
以下は従来の python_api_manual.md の内容である。
既存 API、build/package 構成、config、workflow、error、maintainer 情報を失わないため、
要約による置換を行わず保持している。
この文書は rust_pdf を Python から使う人のための API 仕様書です。README は概要と最小例を示す文書ですが、この文書では Python I/F の実装場所、データの流れ、config の意味、戻り値の読み方、最適化対象パラメータの書き方まで踏み込んで説明します。
対象コード:
python/rust_pdf/__init__.pysrc/python.rssrc/bindings.rssrc/model.rs, src/fitting/params.rssrc/pipeline.rs, src/compute/common.rs, src/compute/zdst.rspyproject.toml, Cargo.tomlPython から見えるパッケージ名は 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
pyproject.tomlpyproject.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"
重要な点:
python-source = "python" により、python/rust_pdf/__init__.py が Python package として同梱されます。module-name = "rust_pdf._rust_pdf" により、Rust 拡張は rust_pdf package の中の _rust_pdf として配置されます。_rust_pdf を直接触らず、通常は import rust_pdf で wrapper を使います。Cargo.tomlPython 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"]
意味:
pyo3: Rust 関数を Python 拡張として公開します。numpy: Python の NumPy 配列を Rust 側で受け取ります。serde-pyobject: Rust/serde の値を Python object に変換します。pyo3-stub-gen: .pyi 型スタブ生成に使います。abi3-py310: Python 3.10 以上で使える ABI3 wheel を作るための設定です。src/python.rssrc/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 側です。
python/rust_pdf/__init__.pyユーザー向け API はここで定義されています。
主な役割:
config が dict なら json.dumps(config) で JSON 文字列に変換します。tth, intensity, bg_tth, bg_intensity を numpy.float64 の contiguous array に変換します。_rust_pdf.run, _rust_pdf.optimize, _rust_pdf.quick_optimize を呼びます。to_gr, to_tr, to_rdf, to_nr の換算ヘルパーを Python 側で提供します。リリース 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
rust_pdf.run() が実行する処理は概ね次の順です。
Parameters に読み込みます。Config::from_parameters() で Rust の型付き設定に変換します。data.mpack からフォームファクタと Compton 係数を読みます。Cut off LowQ と Q range to に従って Q grid を作ります。Back Ground Factor を掛けて差し引きます。ZdstEngine で I(Q) から還元 PDF G(r) を計算します。Cut off Distance 付近より低 r の成分を切り出し、Q 空間へ back transform します。iq_bft と reduced_gr_bft を作ります。reduced_gr_lorch も返します。Python API は 1 次元配列を受け取ります。
tth: array-like[float]
intensity: array-like[float]
意味:
tth: 2theta [degree]intensity: measured intensity典型的な読み込み:
import numpy as np
data = np.loadtxt("Ni-08mm-600s.dat")
tth = data[:, 0]
intensity = data[:, 1]
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 は使われません。
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,
}
| 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:
atomNo[List] is effectively required because Config::from_parameters() calls array_at() for it.atomConc should have the same length as atomNo[List]. If lengths differ, form-factor/Compton calculations return an error.Q range from must be lower than Q range to and both must fit inside the Q range produced from the measured 2theta data.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.
| 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.
rust_pdf.versionrust_pdf.version() -> str
Returns the package version from Cargo.toml.
Example:
import rust_pdf
print(rust_pdf.version())
print(rust_pdf.__version__)
rust_pdf.runrust_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:
out["q"].out["r"].run are NumPy arrays with float64 values.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"]]))
rust_pdf.optimizerust_pdf.optimize(
config,
tth,
intensity,
bg_tth=None,
bg_intensity=None,
) -> dict[str, Any]
Runs robust parameter fitting.
Algorithm:
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:
optimize when initial parameter values are uncertain.rust_pdf.quick_optimizerust_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.
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:
to_nr uses a cumulative trapezoidal rule.r contains zero, to_gr divides by r; inspect the first element before plotting or mask it if needed.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"]]))
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
opt = rust_pdf.optimize(config, tth, intensity)
best_config = opt["parameters"]
out = rust_pdf.run(best_config, tth, intensity)
Recommended pattern:
optimize.termination and history.run again using opt["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.
Python wrapper errors:
InputError("tth or intensity is None") if either required input array is None.Rust errors surfaced as Python ValueError:
atomNo[List].atomNo[List] contains non-integer or negative values.atomNo[List] and atomConc lengths differ.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")
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:
reduced_gr_raw: closest to direct transform result.reduced_gr_bft: usually the main corrected PDF output.reduced_gr_lorch: useful when termination ripples need damping.to_gr(..., reduced_gr_bft, rho): use when you need pair distribution function g(r) rather than reduced G(r).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
_rust_pdf signaturesThe 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.
To expose a new Rust calculation to Python:
src/python.rs with #[pyfunction]._rust_pdf() using m.add_function(wrap_pyfunction!(..., m)?).python/rust_pdf/__init__.py.__all__..pyi if needed.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 I の既存 API 仕様に追加する現行 I/F の対応仕様である。 Rust 内部名と公開名の対応は文字列ではなく、各コードが実際に表す物理量を根拠に決める。
この文書では三つの実装を区別する。
対応付けは変数名の文字列ではなく、コード中の定義式と実際に保持している物理量を根拠とする。
今回の修正では 既存 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
である。
既存 python/rust_pdf/__init__.py の公開 I/F はそのまま残す。
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)
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 互換性の条件とする。
既存マニュアルに記載されていた定義は 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) を意味する。
この既存名は互換性のため変更しない。
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
を描画しており、この定義と一致する。
webPDF CalcGrFFT() は DST の結果を
self._Gr
に格納する。
その直後に
self._ggr = self._Gr / (4.0 * np.pi * self.rr * self.density) + 1.0
としているため、_Gr は reduced PDF 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 ]
である。
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 ]
である。
webPDF は
self._RDF = self._Tr * self.rr
なので、
[ RDF(r)=rT(r) ]
である。
webPDF CalcGrLorch() は _Gr_Lorch を積分から生成し、
_ggr_Lorch
_Tr_Lorch
_RDF_Lorch
を上記と同じ関係式で生成する。
名称だけを見ると 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)
である。
localPDF は webPDF の Local_Calc_PDF_from_xy を wrapper している。
localPDF は基準ではないため、各 getter は webPDF の物理量へ戻って確認する。
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) と対応する。
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 であり同じ物理量である。
今回確認した主要 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 がないことは「誤り」ではなく、単に公開範囲が狭いだけである。今回それを追加する必然性もないため変更しない。
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
とする。
既存 I/F を変更せず、別名で追加する。
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
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) |
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)
である。
前版のように 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 で行える。
既存:
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 の計算は変更しない。
| 種別 | 既存 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 |
python_api_manual.md から維持される仕様既存マニュアルで定義されていた次の内容は変更しない。
rust_pdfrust_pdf._rust_pdfconfig は dict または JSON stringnumpy.float64 に変換run() の dict keyoptimize() / quick_optimize() の dict shapeto_gr, to_tr, to_rdf, to_nr新 I/F はこれらを置換するのではなく上に追加される。
今回の 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()
という物理量を介した対応である。
この統合マニュアルでは、情報源の役割を次のように分ける。
webPDF: 物理量の意味と既存計算動作の基準。rust_pdf Python I/F: 後方互換性を維持する API。rust_pdf Python I/F: webPDF の物理量に対応した構造化公開 API。localPDF: webPDF のローカル wrapper。webPDF と照合して正しい部分のみ互換 I/F の参考にする。python_api_manual.md: package/build/config/API/workflow/maintenance 仕様の既存資料。したがって、localPDF と webPDF が食い違う場合は localPDF を自動的に正しいとは扱わない。 まず webPDF の計算対象と物理量を確認し、必要な場合だけ 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 の実データ・物理量へ戻って確認する。
既存 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 を同一バージョン上で直接比較できる。
公開 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 の物理量体系へ合わせる。
今後このマニュアルを更新する場合、Part I に存在する情報を新仕様の説明で置換して削除しない。
新しい I/F、対応関係、build 方法、config、workflow、maintenance 情報が追加された場合は、 既存情報を保持した上で追加・訂正する。
古い仕様が本当に無効になった場合のみ、削除ではなく「旧仕様」「廃止」「互換用」などの位置付けを明記して履歴を残す。
この 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
は変更しない。
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 は使用しない。