From 533a0afbce2cfef0541275f68aec725ac80a8f8a Mon Sep 17 00:00:00 2001 From: Roy Kid Date: Fri, 24 Jul 2026 14:48:01 +0200 Subject: [PATCH 1/7] docs: drop paper gallery; use theme manual-home components MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Homepage is an index only — remove paper figure dump and molpack-specific system-flow/result CSS. Align EN/ZH home, quickstart, and Python index with molcrafts-zensical-theme manual components; leave extra.css minimal. --- docs/assets/stylesheets/extra.css | 540 +----------------------------- docs/getting_started.md | 29 +- docs/index.md | 258 ++++++++------ docs/python/index.md | 44 ++- docs/zh/index.md | 209 ++++++------ 5 files changed, 316 insertions(+), 764 deletions(-) diff --git a/docs/assets/stylesheets/extra.css b/docs/assets/stylesheets/extra.css index 351b40f..c6501f8 100644 --- a/docs/assets/stylesheets/extra.css +++ b/docs/assets/stylesheets/extra.css @@ -1,535 +1,17 @@ -:root { - --molpack-accent: #c2410c; - --molpack-accent-strong: #9a3412; - --molpack-accent-soft: rgba(194, 65, 12, 0.14); - --molpack-secondary: #d97706; - --molpack-secondary-soft: rgba(217, 119, 6, 0.12); - --molpack-warm: #ea580c; - --molpack-panel-bg: color-mix(in srgb, var(--md-default-bg-color) 88%, white); - --molpack-panel-border: color-mix(in srgb, var(--molpack-accent) 24%, transparent); - --molpack-panel-shadow: 0 0.65rem 1.7rem rgba(92, 43, 17, 0.1); -} - -[data-md-color-scheme="molcrafts"], -[data-md-color-scheme="molcrafts-slate"] { - --md-typeset-color: var(--md-default-fg-color); - color: var(--md-typeset-color); -} - -[data-md-color-scheme="molcrafts-slate"] { - --molpack-accent: #fb923c; - --molpack-accent-strong: #fdba74; - --molpack-accent-soft: rgba(251, 146, 60, 0.16); - --molpack-secondary: #f97316; - --molpack-secondary-soft: rgba(249, 115, 22, 0.13); - --molpack-warm: #fdba74; - --molpack-panel-bg: color-mix(in srgb, var(--md-default-bg-color) 88%, #23170f); - --molpack-panel-border: color-mix(in srgb, var(--molpack-accent) 26%, transparent); - --molpack-panel-shadow: 0 0.65rem 1.7rem rgba(0, 0, 0, 0.28); -} - -[data-md-color-scheme="molcrafts-slate"] .md-typeset, -[data-md-color-scheme="molcrafts-slate"] .md-content, -[data-md-color-scheme="molcrafts-slate"] .molcrafts-home-hero, -[data-md-color-scheme="molcrafts-slate"] .molcrafts-home-hero__button { - color: var(--md-default-fg-color); -} - -[data-md-color-scheme="molcrafts-slate"] .md-typeset p, -[data-md-color-scheme="molcrafts-slate"] .md-typeset dd, -[data-md-color-scheme="molcrafts-slate"] .molcrafts-home-hero__description { - color: var(--md-default-fg-color--light); -} - -html[data-molcrafts-product="molpack"] .molcrafts-home-hero__aside::before { - display: none; - content: none; -} - -html[data-molcrafts-product="molpack"] .molcrafts-home-hero__aside { - width: min(100%, 23rem); -} - -html[data-molcrafts-product="molpack"] .molcrafts-home-hero__install { - width: 100%; - min-width: 0; - box-sizing: border-box; -} - -html[data-molcrafts-product="molpack"] .molcrafts-home-hero__install-tabs { - width: 100%; - display: flex; - min-width: 0; -} - -html[data-molcrafts-product="molpack"] .molcrafts-home-hero__install-row { - min-width: 0; -} - -html[data-molcrafts-product="molpack"] .molcrafts-home-hero__install-row code { - min-width: 0; -} - -html { - scroll-behavior: smooth; -} - -html[data-molcrafts-product="molpack"] .molcrafts-home-hero { - min-height: 27rem; - background: - linear-gradient(112deg, rgba(255, 247, 237, 0.98) 0%, rgba(255, 247, 237, 0.9) 44%, rgba(194, 65, 12, 0.13) 100%), - linear-gradient(90deg, rgba(194, 65, 12, 0.12) 0 1px, transparent 1px 100%), - linear-gradient(0deg, rgba(217, 119, 6, 0.09) 0 1px, transparent 1px 100%), - linear-gradient(135deg, var(--molpack-secondary-soft), transparent 46%), - var(--md-default-bg-color); - background-size: auto, 4.2rem 4.2rem, 4.2rem 4.2rem, auto, auto; -} - -html[data-molcrafts-product="molpack"] [data-md-color-scheme="molcrafts-slate"] .molcrafts-home-hero, -html[data-molcrafts-product="molpack"][data-md-color-scheme="molcrafts-slate"] .molcrafts-home-hero { - background: - linear-gradient(112deg, rgba(24, 18, 14, 0.98) 0%, rgba(24, 18, 14, 0.9) 44%, rgba(251, 146, 60, 0.16) 100%), - linear-gradient(90deg, rgba(251, 146, 60, 0.11) 0 1px, transparent 1px 100%), - linear-gradient(0deg, rgba(255, 247, 237, 0.07) 0 1px, transparent 1px 100%), - linear-gradient(135deg, var(--molpack-secondary-soft), transparent 46%), - var(--md-default-bg-color); - background-size: auto, 4.2rem 4.2rem, 4.2rem 4.2rem, auto, auto; -} - -html[data-molcrafts-product="molpack"] .molcrafts-home-hero::before { - background: - linear-gradient(112deg, transparent 0 48%, rgba(194, 65, 12, 0.12) 48% 48.3%, transparent 48.3% 100%), - linear-gradient(112deg, transparent 0 64%, rgba(217, 119, 6, 0.1) 64% 64.25%, transparent 64.25% 100%), - repeating-linear-gradient(112deg, transparent 0 1.4rem, rgba(92, 43, 17, 0.04) 1.4rem 1.45rem, transparent 1.45rem 3.7rem); -} - -html[data-molcrafts-product="molpack"] .molcrafts-home-hero__kicker, -html[data-molcrafts-product="molpack"] .molcrafts-manual-eyebrow { - color: var(--molpack-accent); -} - -html[data-molcrafts-product="molpack"] .molcrafts-home-hero h1 { - max-width: 17rem; - background: linear-gradient(96deg, var(--molpack-accent) 0%, var(--molpack-secondary) 58%, var(--molpack-warm) 100%); - -webkit-background-clip: text; - background-clip: text; - color: transparent; -} - -html[data-molcrafts-product="molpack"] .molcrafts-home-hero__description { - max-width: 39rem; -} - -.md-typeset .molcrafts-manual-home.molpack-home { - max-width: 1160px; - padding-top: 1.45rem; -} - -.md-typeset .molpack-system-section { - grid-template-columns: minmax(0, 1fr); - gap: 1rem; -} - -.md-typeset .molpack-system-section .molcrafts-manual-section__header { - position: static; - max-width: 43rem; -} - -.md-typeset .molpack-system-panel { - overflow: hidden; - margin: 0; - border: 1px solid var(--molpack-panel-border); - border-radius: var(--molcrafts-radius); - background: - linear-gradient(135deg, var(--molpack-panel-bg), color-mix(in srgb, var(--molpack-accent-soft) 56%, var(--md-default-bg-color))), - var(--md-default-bg-color); - box-shadow: var(--molpack-panel-shadow); -} - -.md-typeset .molpack-system-panel__header { - display: grid; - gap: 0.25rem; - padding: 0.85rem 1rem; - border-bottom: 1px solid color-mix(in srgb, var(--md-primary-fg-color) 12%, transparent); -} - -.md-typeset .molpack-system-panel__header span { - color: var(--md-default-fg-color--lighter); - font-size: 0.62rem; - font-weight: 850; - line-height: 1.35; - letter-spacing: 0; - text-transform: uppercase; -} - -.md-typeset .molpack-system-panel__header strong { - color: var(--md-default-fg-color); - font-size: 0.92rem; - line-height: 1.25; -} - -.md-typeset .molpack-system-flow { - position: relative; - display: grid; - grid-template-columns: repeat(4, minmax(0, 1fr)); - gap: 0.65rem; - padding: 1.35rem 1rem 1.05rem; -} - -.md-typeset .molpack-system-flow::before { - content: ""; - position: absolute; - top: 2.15rem; - right: 1.25rem; - left: 1.25rem; - height: 0.12rem; - background: linear-gradient(90deg, var(--molpack-accent), var(--molpack-secondary), var(--molpack-warm)); - opacity: 0.62; -} - -.md-typeset .molpack-system-flow > div { - position: relative; - min-width: 0; - min-height: 7.1rem; - padding: 1.25rem 0.75rem 0.75rem; - border: 1px solid color-mix(in srgb, var(--md-primary-fg-color) 13%, transparent); - border-radius: var(--molcrafts-radius); - background: color-mix(in srgb, var(--md-default-bg-color) 84%, transparent); - transition: border-color 180ms ease, box-shadow 180ms ease, transform 180ms ease, background-color 180ms ease; -} - -.md-typeset .molpack-system-flow > div:is(:focus-within, :hover) { - border-color: color-mix(in srgb, var(--molpack-accent) 46%, transparent); - box-shadow: 0 0.5rem 1.4rem color-mix(in srgb, var(--molpack-accent) 22%, transparent); - background: color-mix(in srgb, var(--md-default-bg-color) 94%, var(--molpack-accent-soft)); - transform: translateY(-2px); -} - -.md-typeset .molpack-system-flow > div::before { - content: ""; - position: absolute; - top: -0.32rem; - left: 0.72rem; - width: 0.62rem; - height: 0.62rem; - border: 0.14rem solid var(--molpack-accent); - border-radius: 999px; - background: var(--md-default-bg-color); -} - -.md-typeset .molpack-system-flow > div:nth-child(3)::before { - border-color: var(--molpack-secondary); -} - -.md-typeset .molpack-system-flow span, -.md-typeset .molcrafts-workflow-list.molpack-workflow-list .molcrafts-workflow-list__meta { - font-family: var(--md-code-font); - letter-spacing: 0; -} - -.md-typeset .molpack-system-flow span { - display: block; - color: var(--md-default-fg-color--lighter); - font-size: 0.58rem; - font-weight: 850; - line-height: 1.35; - text-transform: uppercase; -} - -.md-typeset .molpack-system-flow strong { - display: block; - margin-top: 0.38rem; - color: var(--md-default-fg-color); - font-size: 0.72rem; - line-height: 1.35; - overflow-wrap: anywhere; -} - -.md-typeset .molpack-system-flow a { - display: block; - color: inherit; - text-decoration: none; -} - -.md-typeset .molpack-results-section { - grid-template-columns: minmax(0, 1fr); - gap: 1rem; -} - -.md-typeset .molpack-results-section .molcrafts-manual-section__header { - position: static; - max-width: 50rem; -} - -.md-typeset .molpack-result-gallery { - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: 0.85rem; -} - -.md-typeset .molpack-result-figure { - min-width: 0; - margin: 0; - padding: 0.65rem; - border: 1px solid color-mix(in srgb, var(--md-primary-fg-color) 12%, transparent); - border-radius: var(--molcrafts-radius); - background: color-mix(in srgb, var(--md-default-bg-color) 92%, white); -} - -[data-md-color-scheme="molcrafts-slate"] .md-typeset .molpack-result-figure { - background: color-mix(in srgb, var(--md-default-bg-color) 92%, #23170f); -} - -.md-typeset .molpack-result-figure--wide { - grid-column: 1 / -1; -} - -.md-typeset .molpack-result-figure img { - display: block; - width: 100%; - max-height: 24rem; - object-fit: contain; - border-radius: calc(var(--molcrafts-radius) - 2px); - background: #fff; -} - -.md-typeset .molpack-result-figure--wide img { - max-height: 34rem; -} - -.md-typeset .molpack-result-figure figcaption { - margin: 0.55rem 0 0; - color: var(--md-default-fg-color--light); - font-size: 0.66rem; - line-height: 1.45; -} - +/* + * Product-level docs tweaks only. + * + * Visual system (hero, manual-home, workflow-list, doc-map, feature-matrix, + * grids, light/dark palettes) is owned by molcrafts-zensical-theme. + * Product accent is set in zensical.toml under [project.extra.molcrafts]. + * + * Do not re-skin manual components here — that fights the theme. + */ + +/* Keep figure embeds readable when parity / extending pages show charts. */ .md-typeset img.molpack-figure-small { display: block; width: min(100%, 34rem); margin-right: auto; margin-left: auto; } - -.md-typeset .molcrafts-workflow-list.molpack-workflow-list > article { - grid-template-columns: 7.5rem minmax(0, 1fr); - padding: 1.05rem 0.9rem; - border-radius: var(--molcrafts-radius); -} - -.md-typeset .molcrafts-workflow-list.molpack-workflow-list h3 { - font-size: 0.9rem; -} - -.md-typeset .molcrafts-workflow-list.molpack-workflow-list h3 a { - color: inherit; -} - -.md-typeset .molcrafts-workflow-list.molpack-workflow-list p { - max-width: 44rem; - font-size: 0.7rem; -} - -.md-typeset .molcrafts-workflow-list.molpack-workflow-list pre > code { - border-left-color: var(--molpack-accent); - font-size: 0.61rem; -} - -.md-typeset .molpack-doc-map h3 a { - color: inherit; - text-decoration: none; -} - -.md-typeset .molpack-doc-map { - grid-template-columns: repeat(5, minmax(0, 1fr)); - gap: 0.6rem; -} - -.md-typeset .molpack-doc-map section { - position: relative; - padding: 0.85rem 0.9rem; - border-bottom: 0; - border-radius: var(--molcrafts-radius); - background: color-mix(in srgb, var(--md-default-fg-color) 4%, transparent); - transition: background-color 160ms ease, transform 160ms ease; -} - -.md-typeset .molpack-doc-map section:is(:focus-within, :hover) { - background: color-mix(in srgb, var(--molpack-accent-soft) 58%, transparent); - transform: translateY(-1px); -} - -.md-typeset .molpack-system-flow a::after, -.md-typeset .molpack-doc-map h3 a::after { - content: ""; - position: absolute; - inset: 0; - z-index: 1; -} - -@media screen and (max-width: 76.2344em) { - .md-typeset .molpack-system-flow { - grid-template-columns: repeat(3, minmax(0, 1fr)); - } - - .md-typeset .molpack-system-flow::before { - display: none; - } - - .md-typeset .molpack-doc-map { - grid-template-columns: repeat(2, minmax(0, 1fr)); - } - - .md-typeset .molpack-result-gallery { - grid-template-columns: minmax(0, 1fr); - } -} - -@media screen and (max-width: 60em) { - html[data-molcrafts-product="molpack"] .molcrafts-home-hero { - min-height: 24rem; - } - - .md-typeset .molcrafts-manual-home.molpack-home { - padding-top: 1.2rem; - } - - .md-typeset .molpack-system-flow { - grid-template-columns: repeat(2, minmax(0, 1fr)); - } - - .md-typeset .molcrafts-workflow-list.molpack-workflow-list > article { - grid-template-columns: minmax(0, 1fr); - } -} - -@media screen and (max-width: 36em) { - html[data-molcrafts-product="molpack"] .molcrafts-home-hero h1 { - max-width: none; - } - - .md-typeset .molpack-system-panel__header, - .md-typeset .molpack-system-flow { - padding-right: 0.75rem; - padding-left: 0.75rem; - } - - .md-typeset .molpack-system-flow, - .md-typeset .molpack-doc-map, - .md-typeset .molpack-result-gallery { - grid-template-columns: minmax(0, 1fr); - } - - .md-typeset .molpack-system-flow > div { - min-height: 5.8rem; - } - - .md-typeset .molpack-result-figure { - padding: 0.45rem; - } - - .md-typeset .molpack-result-figure img, - .md-typeset .molpack-result-figure--wide img { - max-height: none; - } -} - -/* ── content pages (install, quickstart, guides) ─────────────────────── */ - -.md-typeset .molpack-next-cards { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(14rem, 1fr)); - gap: 0.65rem; - margin: 1rem 0 0.25rem; -} - -.md-typeset .molpack-next-cards > ul { - display: contents; - list-style: none; - margin: 0; - padding: 0; -} - -.md-typeset .molpack-next-cards li { - margin: 0; - padding: 0.85rem 0.95rem; - border: 1px solid color-mix(in srgb, var(--molpack-accent) 18%, transparent); - border-radius: var(--molcrafts-radius, 0.45rem); - background: - linear-gradient(145deg, color-mix(in srgb, var(--molpack-accent-soft) 40%, transparent), transparent 70%), - color-mix(in srgb, var(--md-default-bg-color) 94%, var(--molpack-accent-soft)); - box-shadow: 0 0.35rem 1rem color-mix(in srgb, var(--molpack-accent) 8%, transparent); - transition: border-color 160ms ease, transform 160ms ease, box-shadow 160ms ease; -} - -.md-typeset .molpack-next-cards li:is(:hover, :focus-within) { - border-color: color-mix(in srgb, var(--molpack-accent) 48%, transparent); - box-shadow: 0 0.5rem 1.35rem color-mix(in srgb, var(--molpack-accent) 16%, transparent); - transform: translateY(-1px); -} - -.md-typeset .molpack-next-cards li::marker { - content: ""; -} - -.md-typeset .molpack-next-cards a { - color: var(--md-default-fg-color); - font-weight: 650; - text-decoration: none; -} - -.md-typeset .molpack-next-cards strong a, -.md-typeset .molpack-next-cards a strong { - color: var(--molpack-accent-strong, var(--molpack-accent)); -} - -.md-typeset table:not([class]) { - border-radius: var(--molcrafts-radius, 0.45rem); - overflow: hidden; - box-shadow: 0 0 0 1px color-mix(in srgb, var(--molpack-accent) 12%, transparent); -} - -.md-typeset table:not([class]) th { - background: color-mix(in srgb, var(--molpack-accent-soft) 55%, var(--md-default-bg-color)); - color: var(--md-default-fg-color); - font-weight: 700; -} - -.md-typeset .admonition, -.md-typeset details { - border-radius: var(--molcrafts-radius, 0.45rem); - border-color: color-mix(in srgb, var(--molpack-accent) 28%, transparent); -} - -.md-typeset .admonition.tip, -.md-typeset details.tip { - border-color: color-mix(in srgb, var(--molpack-secondary) 40%, transparent); -} - -.md-typeset .tabbed-set { - border-radius: var(--molcrafts-radius, 0.45rem); - overflow: hidden; -} - -/* Inline figures in tutorial / parity pages */ -.md-typeset figure.molpack-inline-figure { - margin: 1.1rem 0; - padding: 0.75rem; - border: 1px solid color-mix(in srgb, var(--md-primary-fg-color) 12%, transparent); - border-radius: var(--molcrafts-radius, 0.45rem); - background: color-mix(in srgb, var(--md-default-bg-color) 92%, white); -} - -.md-typeset figure.molpack-inline-figure img { - display: block; - width: 100%; - border-radius: calc(var(--molcrafts-radius, 0.45rem) - 2px); -} - -.md-typeset figure.molpack-inline-figure figcaption { - margin-top: 0.5rem; - color: var(--md-default-fg-color--light); - font-size: 0.68rem; - line-height: 1.45; - text-align: center; -} diff --git a/docs/getting_started.md b/docs/getting_started.md index c22a0e3..0f06c91 100644 --- a/docs/getting_started.md +++ b/docs/getting_started.md @@ -96,12 +96,25 @@ molrs.write_pdb(packed, "water_box.pdb") ## Where next -
- -- **[Concepts](concepts/)** — targets, restraints, phases of the packer -- **[CLI](cli/)** — same job as a Packmol-style `.inp` -- **[Python guide](python/)** — fixed solutes, PBC, collective restraints -- **[Rust API](rust/)** — embed the engine in a native crate -- **[Packmol parity](packmol_parity/)** — what matches Packmol, and what does not - + diff --git a/docs/index.md b/docs/index.md index 6415309..0b1ac62 100644 --- a/docs/index.md +++ b/docs/index.md @@ -44,109 +44,79 @@ hero:

molpack

-
+
-
+
-Core model +Start here -## Templates, counts, restraints — then one packing run +## Find the right page -molpack is organized around a fixed packing job at every entry point. Provide -molecule templates, copy counts, geometric restraints, and optional fixed -placements or periodic boundaries. The engine returns one packed configuration -with the requested distances and regions satisfied. +This page is an index into the manual. Every surface lowers to the same model: +molecule templates, copy counts, geometric restraints, then one packing run.
-
-
-One engine · four surfaces -Scripts for reproducible jobs · APIs for pipelines · handlers for observation -
- -
- -The [Quickstart](getting_started/) packs a first water box. Use -[Packmol scripts](cli/) for checked-in input files, [Python](python/) for -notebooks and pipelines, and [Rust](rust/) when packing is part of a native -application. +
-
+
-Results - -## What the engine produces +Workflows -Canonical Packmol-style workloads — confined solutes, interface distributions, -and multi-target boxes — pack under the same restraint model through every -surface. - -
- - - -
- -
- -
- -In practice - -## The same packing model through four entry points +## Same packing model, four entry points All entry points lower to the same target / count / restraint model. Pick the surface that matches how the rest of your workflow is written.
-
+
-
01 · Packmol script
+
Path 01 · Script
-### [Run a `.inp` job](cli/) +### Run a `.inp` job Use the CLI for reproducible packing jobs that already live as Packmol-style -input files. +input files. See the [CLI guide](cli/). ```text structure water.pdb @@ -159,12 +129,12 @@ end structure
-
02 · Python API
+
Path 02 · Python
-### [Pack from a notebook or pipeline](python/) +### Pack from a notebook or pipeline Load or build a frame, create immutable targets, and pass the packed frame to -the writer or analysis code you already use. +the writer or analysis code you already use. See the [Python API](python/). ```python water = Target(frame, 100).with_restraint( @@ -177,12 +147,12 @@ packed = Molpack().with_seed(42).pack([water])
-
03 · Rust API
+
Path 03 · Rust
-### [Embed the engine in a crate](rust/) +### Embed the engine in a crate -Use the native builder API for Rust applications, services, tests, and new -engine features. +Use the native builder API for applications, services, and tests. See the +[Rust API](rust/). ```rust let frame = Molpack::new() @@ -194,12 +164,12 @@ let frame = Molpack::new()
-
04 · Custom handler
+
Path 04 · Handler
-### [Observe or stop a run](rust/handlers-relaxers/) +### Observe or stop a run -Handlers receive structured events from the packing loop, so you can log -diagnostics, dump frames, or request early stop without changing the packer. +Handlers receive structured events from the packing loop. See +[Handlers and relaxers](rust/handlers-relaxers/). ```rust impl Handler for WatchFdist { @@ -215,41 +185,107 @@ impl Handler for WatchFdist {
-
+
-Find your page +Features -## The manual +## Core capabilities -Organized by entry point. **Tutorial** teaches the common packing model. -**Packmol script**, **Python**, and **Rust** cover the public surfaces. -**Handlers** cover observation and early-stop hooks. +Dense summary of what the engine and each surface cover.
-
-
-

Install

-

CLI binary, crates.io crate, and PyPI wheel — pick a surface and verify it loads.

-
-
-

Quickstart

-

Pack 100 waters in a 40 Å cube end-to-end, then read convergence diagnostics.

-
-
-

Packmol Script

-

`.inp` compatibility, formats, path resolution, and CLI examples.

-
-
-

Python API

-

Targets, restraints, packer options, PBC, examples, and API reference.

+
+
+
Targets
+
Molecule templates, copy counts, fixed placements, centering, and rotation bounds.
+
+
+
Restraints
+
Geometric boxes and spheres, planes, and collective distribution-matching terms.
+
+
+
Packer
+
GENCAN outer loop, pair-distance objective, optional rayon evaluation, seed control.
+
+
+
Scripts
+
Packmol-compatible `.inp` keywords, path resolution, and extra filetypes (SDF, LAMMPS).
+
+
+
Python
+
Frame-in / frame-out packing for notebooks and pipelines via molrs.
+
+
+
Rust
+
Native builders, handlers, relaxers, and feature-gated IO / FF / rayon.
+
+
+
-
-

Rust API

-

Builders, restraint scopes, periodic boxes, handlers, relaxers, examples.

+ +
+ +
+ +Map + +## Documentation map + +A compact mirror of the navigation tree for returning users. + +
+ +
+
+

Tutorial

+

Install, quickstart, concepts, examples, and Packmol parity notes.

+
+
+

Packmol Script

+

`.inp` overview, script notes, formats, and CLI examples.

+
+
+

Python

+

Installation, targets, restraints, packer options, PBC, and API reference.

+
+
+

Rust

+

Builders, restraints, PBC, handlers, relaxers, and example programs.

+
+
+

Development

+

Contributing, architecture, and extending the engine.

+
+
+
+ +
+ +
+ +Surfaces + +## Jump to a surface + +
+ +
diff --git a/docs/python/index.md b/docs/python/index.md index f910899..8c671ce 100644 --- a/docs/python/index.md +++ b/docs/python/index.md @@ -28,17 +28,39 @@ print(f"packed {frame['atoms'].nrows} atoms") ## Next steps -
- -- **[Installation](installation.md)** — pip install and verification -- **[Quickstart](getting-started.md)** — first pack end-to-end -- **[Targets](guide/targets.md)** — templates, counts, fixed placement -- **[Restraints](guide/restraints.md)** — geometric and collective restraints -- **[Packer](guide/packer.md)** — builder options, diagnostics, handlers -- **[Periodic boundaries](guide/periodic-boundaries.md)** — PBC rules -- **[Examples](examples.md)** — Packmol-equivalent workloads -- **[API reference](api-reference.md)** — class-by-class summary - + ## See also diff --git a/docs/zh/index.md b/docs/zh/index.md index 300c0bc..7238d13 100644 --- a/docs/zh/index.md +++ b/docs/zh/index.md @@ -44,102 +44,80 @@ hero:

molpack

-
+
-
+
-核心模型 +从这里开始 -## 分子模板、数量、约束 — 然后运行一次装填 +## 找到对应页面 -molpack 在每个入口下都围绕同一个固定装填任务:给出分子模板、副本数量、几何约束, -以及可选的固定放置或周期边界;引擎返回一个满足距离和区域要求的装填构型。 +本页是手册索引,不是营销页。所有入口共用同一模型:分子模板、副本数量、几何约束, +然后运行一次装填。 -
- -
-
-一个引擎 · 四个入口 -脚本用于可复现任务 · API 用于流水线 · handler 用于观测 -
- -
- -[快速开始](../getting_started/) 会装填第一个水盒子。有 `.inp` 时用 -[Packmol 脚本](../cli/),在 notebook 与流水线里用 [Python](../python/),嵌入 -原生应用时用 [Rust](../rust/)。 - -
- -
- -
- -结果示意 - -## 引擎能装出什么 - -受限溶质、界面分布、多组分盒子 — 同一套约束模型贯穿所有入口。 +!!! note "中文内容" + 中文首页为导航入口;详细章节目前以英文正文为主。语言切换在页眉下拉菜单。
- +
-
+
-实际使用 +工作流 ## 同一个装填模型,四个入口 -所有入口都会落到同一个 target / count / restraint 模型。按工作流语言选择入口。 +所有入口都会落到 target / count / restraint。按现有工作流语言选择入口。
-
+
-
01 · Packmol 脚本
+
路径 01 · 脚本
-### [运行 `.inp` 任务](../cli/) +### 运行 `.inp` 任务 -CLI 适合已经写成 Packmol 风格输入、需要纳入版本控制的可复现任务。 +适合已写成 Packmol 风格、需要纳入版本控制的可复现任务。见 [CLI](../cli/)。 ```text structure water.pdb @@ -152,11 +130,11 @@ end structure
-
02 · Python API
+
路径 02 · Python
-### [在 notebook 或流水线中装填](../python/) +### 在 notebook 或流水线中装填 -加载或构造 frame,创建不可变 target,再把结果交给已有的写出或分析代码。 +加载或构造 frame,创建不可变 target,再把结果交给写出或分析代码。见 [Python](../python/)。 ```python water = Target(frame, 100).with_restraint( @@ -169,11 +147,11 @@ packed = Molpack().with_seed(42).pack([water])
-
03 · Rust API
+
路径 03 · Rust
-### [把引擎嵌入 crate](../rust/) +### 把引擎嵌入 crate -适合原生应用、服务、测试,以及引擎本身的新功能。 +适合原生应用、服务与测试。见 [Rust](../rust/)。 ```rust let frame = Molpack::new() @@ -185,11 +163,11 @@ let frame = Molpack::new()
-
04 · 自定义 handler
+
路径 04 · Handler
-### [观测或停止一次运行](../rust/handlers-relaxers/) +### 观测或停止一次运行 -Handler 接收装填循环中的结构化事件,便于记录诊断、导出中间 frame 或请求提前停止。 +Handler 接收装填循环中的结构化事件。见 [Handlers](../rust/handlers-relaxers/)。 ```rust impl Handler for WatchFdist { @@ -209,39 +187,60 @@ impl Handler for WatchFdist {
-页面索引 +地图 -## 手册 +## 手册结构 -**Tutorial** 讲通用装填模型;**Packmol 脚本**、**Python**、**Rust** 覆盖公开界面; -**Handler** 讲观测与提前停止。 - -!!! note "中文内容" - 中文首页为导航入口;详细章节目前以英文正文为主。语言切换在页眉下拉菜单。 +
+
+
+

Tutorial

+

安装、快速开始、概念、示例与 Packmol 对等说明。

+
+
+

Packmol 脚本

+

`.inp` 概览、脚本说明、格式与 CLI 示例。

+
+
+

Python

+

安装、targets、restraints、packer、PBC 与 API 参考。

+
+
+

Rust

+

Builder、约束、周期边界、handlers 与 relaxers。

+
+
+

Development

+

贡献流程、架构与扩展点。

+
-
-
-

安装

-

CLI、crates.io 与 PyPI — 选一个入口并验证可导入 / 可运行。

-
-
-

快速开始

-

在 40 Å 立方体中装填 100 个水分子,并读取收敛诊断。

-
-
-

Packmol 脚本

-

兼容 `.inp`、文件格式、路径解析与 CLI 示例。

-
-
-

Python API

-

Targets、restraints、packer、PBC、示例与 API 参考。

-
-
-

Rust API

-

Builder、约束作用域、周期边界、handlers、relaxers。

+ +
+ +
+ +入口 + +## 跳转到表面 + +
+ +
From d5e33012098df800b344f256cd20b1af75002185 Mon Sep 17 00:00:00 2001 From: Roy Kid Date: Fri, 24 Jul 2026 14:54:51 +0200 Subject: [PATCH 2/7] docs: dual-language trees under docs/en and docs/zh MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Restructure per Zensical alternate + molexp pattern so Chinese never falls out of /zh/. English sources live in docs/en (zensical.toml → site/en); Chinese parallel tree in docs/zh (zensical.zh.toml → site/zh) with shared assets via symlink. Update rustdoc include paths and dual-build notes. --- CONTRIBUTING.md | 2 +- README.md | 13 +- docs/README.md | 29 ++ docs/{ => en}/architecture.md | 0 .../paper-compatibility-distributions.png | Bin .../images/paper-confinement-sphere.png | Bin .../assets/images/paper-mt-scaling.png | Bin docs/{ => en}/assets/stylesheets/extra.css | 0 docs/{ => en}/cli/examples.md | 0 docs/{ => en}/cli/formats.md | 0 docs/{ => en}/cli/index.md | 0 docs/{ => en}/cli/script-format.md | 0 docs/{ => en}/concepts.md | 0 docs/{ => en}/development/index.md | 0 docs/{ => en}/examples.md | 0 docs/{ => en}/extending.md | 0 docs/{ => en}/getting_started.md | 0 docs/{ => en}/img/fig_profile_density.png | Bin docs/{ => en}/index.md | 0 docs/{ => en}/install.md | 0 docs/{ => en}/packmol_parity.md | 0 docs/{ => en}/python/api-reference.md | 0 docs/{ => en}/python/examples.md | 0 docs/{ => en}/python/getting-started.md | 0 docs/{ => en}/python/guide/packer.md | 0 .../python/guide/periodic-boundaries.md | 0 docs/{ => en}/python/guide/restraints.md | 0 docs/{ => en}/python/guide/targets.md | 0 docs/{ => en}/python/index.md | 0 docs/{ => en}/python/installation.md | 0 docs/{ => en}/rust/examples.md | 0 docs/{ => en}/rust/getting-started.md | 0 docs/{ => en}/rust/handlers-relaxers.md | 0 docs/{ => en}/rust/index.md | 0 docs/{ => en}/rust/restraints-and-pbc.md | 0 docs/root-redirect.html | 13 + docs/zh/architecture.md | 336 +++++++++++++ docs/zh/assets | 1 + docs/zh/cli/examples.md | 34 ++ docs/zh/cli/formats.md | 30 ++ docs/zh/cli/index.md | 56 +++ docs/zh/cli/script-format.md | 66 +++ docs/zh/concepts.md | 307 ++++++++++++ docs/zh/development/index.md | 31 ++ docs/zh/examples.md | 62 +++ docs/zh/extending.md | 474 ++++++++++++++++++ docs/zh/getting_started.md | 120 +++++ docs/zh/img | 1 + docs/zh/index.md | 21 +- docs/zh/install.md | 86 ++++ docs/zh/packmol_parity.md | 115 +++++ docs/zh/python/api-reference.md | 416 +++++++++++++++ docs/zh/python/examples.md | 81 +++ docs/zh/python/getting-started.md | 131 +++++ docs/zh/python/guide/packer.md | 140 ++++++ docs/zh/python/guide/periodic-boundaries.md | 108 ++++ docs/zh/python/guide/restraints.md | 165 ++++++ docs/zh/python/guide/targets.md | 187 +++++++ docs/zh/python/index.md | 71 +++ docs/zh/python/installation.md | 68 +++ docs/zh/rust/examples.md | 30 ++ docs/zh/rust/getting-started.md | 68 +++ docs/zh/rust/handlers-relaxers.md | 72 +++ docs/zh/rust/index.md | 46 ++ docs/zh/rust/restraints-and-pbc.md | 84 ++++ python/pyproject.toml | 10 +- src/lib.rs | 12 +- src/restraint/mod.rs | 2 +- zensical.toml | 36 +- zensical.zh.toml | 72 +++ 70 files changed, 3543 insertions(+), 53 deletions(-) create mode 100644 docs/README.md rename docs/{ => en}/architecture.md (100%) rename docs/{ => en}/assets/images/paper-compatibility-distributions.png (100%) rename docs/{ => en}/assets/images/paper-confinement-sphere.png (100%) rename docs/{ => en}/assets/images/paper-mt-scaling.png (100%) rename docs/{ => en}/assets/stylesheets/extra.css (100%) rename docs/{ => en}/cli/examples.md (100%) rename docs/{ => en}/cli/formats.md (100%) rename docs/{ => en}/cli/index.md (100%) rename docs/{ => en}/cli/script-format.md (100%) rename docs/{ => en}/concepts.md (100%) rename docs/{ => en}/development/index.md (100%) rename docs/{ => en}/examples.md (100%) rename docs/{ => en}/extending.md (100%) rename docs/{ => en}/getting_started.md (100%) rename docs/{ => en}/img/fig_profile_density.png (100%) rename docs/{ => en}/index.md (100%) rename docs/{ => en}/install.md (100%) rename docs/{ => en}/packmol_parity.md (100%) rename docs/{ => en}/python/api-reference.md (100%) rename docs/{ => en}/python/examples.md (100%) rename docs/{ => en}/python/getting-started.md (100%) rename docs/{ => en}/python/guide/packer.md (100%) rename docs/{ => en}/python/guide/periodic-boundaries.md (100%) rename docs/{ => en}/python/guide/restraints.md (100%) rename docs/{ => en}/python/guide/targets.md (100%) rename docs/{ => en}/python/index.md (100%) rename docs/{ => en}/python/installation.md (100%) rename docs/{ => en}/rust/examples.md (100%) rename docs/{ => en}/rust/getting-started.md (100%) rename docs/{ => en}/rust/handlers-relaxers.md (100%) rename docs/{ => en}/rust/index.md (100%) rename docs/{ => en}/rust/restraints-and-pbc.md (100%) create mode 100644 docs/root-redirect.html create mode 100644 docs/zh/architecture.md create mode 120000 docs/zh/assets create mode 100644 docs/zh/cli/examples.md create mode 100644 docs/zh/cli/formats.md create mode 100644 docs/zh/cli/index.md create mode 100644 docs/zh/cli/script-format.md create mode 100644 docs/zh/concepts.md create mode 100644 docs/zh/development/index.md create mode 100644 docs/zh/examples.md create mode 100644 docs/zh/extending.md create mode 100644 docs/zh/getting_started.md create mode 120000 docs/zh/img create mode 100644 docs/zh/install.md create mode 100644 docs/zh/packmol_parity.md create mode 100644 docs/zh/python/api-reference.md create mode 100644 docs/zh/python/examples.md create mode 100644 docs/zh/python/getting-started.md create mode 100644 docs/zh/python/guide/packer.md create mode 100644 docs/zh/python/guide/periodic-boundaries.md create mode 100644 docs/zh/python/guide/restraints.md create mode 100644 docs/zh/python/guide/targets.md create mode 100644 docs/zh/python/index.md create mode 100644 docs/zh/python/installation.md create mode 100644 docs/zh/rust/examples.md create mode 100644 docs/zh/rust/getting-started.md create mode 100644 docs/zh/rust/handlers-relaxers.md create mode 100644 docs/zh/rust/index.md create mode 100644 docs/zh/rust/restraints-and-pbc.md create mode 100644 zensical.zh.toml diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f4c785e..4cd67a1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -76,7 +76,7 @@ venv; **do not** rely on that for the gate — pre-push always uses 2. Implement `Restraint` (both `f` and `fg`; `fg` must match the gradient of `f`) 3. Re-export it from `src/restraint/mod.rs`, then from the crate root in `src/lib.rs` 4. Add a unit test in `tests/restraint.rs` -5. Document it in `docs/concepts.md` under the restraint table +5. Document it in `docs/en/concepts.md` under the restraint table See the `extending` rustdoc chapter (`cargo doc --open`) for detailed tutorials. diff --git a/README.md b/README.md index 8ed2f44..fe1a088 100644 --- a/README.md +++ b/README.md @@ -130,17 +130,18 @@ cd python && maturin develop --release && pytest # Python wheel ## Documentation -- **Guide** — the Markdown chapters under [`docs/`](./docs/): install, - getting started, concepts, examples, Packmol parity, architecture, and - extending. +- **Guide** — dual-language trees under [`docs/en/`](./docs/en/) and + [`docs/zh/`](./docs/zh/) (Zensical: `zensical.toml` → `site/en`, + `zensical.zh.toml` → `site/zh`). Chapters: install, getting started, + concepts, examples, Packmol parity, architecture, and extending. - **Rust API** — `cargo doc --open`, or [docs.rs](https://docs.rs/molcrafts-molpack). The four long-form chapters (getting started, concepts, architecture, extending) are also embedded in the rustdoc as `molpack::getting_started`, `molpack::concepts`, `molpack::architecture`, and `molpack::extending`. -- **Python** — the binding docs under [`docs/python/`](./docs/python/) - (installation, guide, examples, and the Python API reference), published as - the **Python** section of the same Zensical site. +- **Python** — binding docs live under `docs/en/python/` (and the parallel + `docs/zh/python/` tree), published as the **Python** section of the + English / Chinese Zensical sites. ## Contributing diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..1bf2735 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,29 @@ +# molpack documentation sources + +Layout follows [Zensical language alternate](https://zensical.org/docs/setup/language/) +and the MolCrafts dual-tree pattern (same as molexp): + +``` +docs/ +├── en/ # English sources → zensical.toml → site/en +├── zh/ # Chinese sources → zensical.zh.toml → site/zh +│ ├── assets → ../en/assets # shared (symlink) +│ └── img → ../en/img +└── root-redirect.html # optional deploy root → /en/ +``` + +## Build + +From the repo root (with the `doc` dependency group installed): + +```bash +zensical build -f zensical.toml # site/en +zensical build -f zensical.zh.toml # site/zh +# Optional combined deploy root: +# cp docs/root-redirect.html site/index.html +# and publish site/en + site/zh beside it. +``` + +Language switcher links are absolute under `site_url`: `/en/` and `/zh/`. +Clicking 简体中文 stays inside the Chinese tree; nav targets never jump to +English paths. diff --git a/docs/architecture.md b/docs/en/architecture.md similarity index 100% rename from docs/architecture.md rename to docs/en/architecture.md diff --git a/docs/assets/images/paper-compatibility-distributions.png b/docs/en/assets/images/paper-compatibility-distributions.png similarity index 100% rename from docs/assets/images/paper-compatibility-distributions.png rename to docs/en/assets/images/paper-compatibility-distributions.png diff --git a/docs/assets/images/paper-confinement-sphere.png b/docs/en/assets/images/paper-confinement-sphere.png similarity index 100% rename from docs/assets/images/paper-confinement-sphere.png rename to docs/en/assets/images/paper-confinement-sphere.png diff --git a/docs/assets/images/paper-mt-scaling.png b/docs/en/assets/images/paper-mt-scaling.png similarity index 100% rename from docs/assets/images/paper-mt-scaling.png rename to docs/en/assets/images/paper-mt-scaling.png diff --git a/docs/assets/stylesheets/extra.css b/docs/en/assets/stylesheets/extra.css similarity index 100% rename from docs/assets/stylesheets/extra.css rename to docs/en/assets/stylesheets/extra.css diff --git a/docs/cli/examples.md b/docs/en/cli/examples.md similarity index 100% rename from docs/cli/examples.md rename to docs/en/cli/examples.md diff --git a/docs/cli/formats.md b/docs/en/cli/formats.md similarity index 100% rename from docs/cli/formats.md rename to docs/en/cli/formats.md diff --git a/docs/cli/index.md b/docs/en/cli/index.md similarity index 100% rename from docs/cli/index.md rename to docs/en/cli/index.md diff --git a/docs/cli/script-format.md b/docs/en/cli/script-format.md similarity index 100% rename from docs/cli/script-format.md rename to docs/en/cli/script-format.md diff --git a/docs/concepts.md b/docs/en/concepts.md similarity index 100% rename from docs/concepts.md rename to docs/en/concepts.md diff --git a/docs/development/index.md b/docs/en/development/index.md similarity index 100% rename from docs/development/index.md rename to docs/en/development/index.md diff --git a/docs/examples.md b/docs/en/examples.md similarity index 100% rename from docs/examples.md rename to docs/en/examples.md diff --git a/docs/extending.md b/docs/en/extending.md similarity index 100% rename from docs/extending.md rename to docs/en/extending.md diff --git a/docs/getting_started.md b/docs/en/getting_started.md similarity index 100% rename from docs/getting_started.md rename to docs/en/getting_started.md diff --git a/docs/img/fig_profile_density.png b/docs/en/img/fig_profile_density.png similarity index 100% rename from docs/img/fig_profile_density.png rename to docs/en/img/fig_profile_density.png diff --git a/docs/index.md b/docs/en/index.md similarity index 100% rename from docs/index.md rename to docs/en/index.md diff --git a/docs/install.md b/docs/en/install.md similarity index 100% rename from docs/install.md rename to docs/en/install.md diff --git a/docs/packmol_parity.md b/docs/en/packmol_parity.md similarity index 100% rename from docs/packmol_parity.md rename to docs/en/packmol_parity.md diff --git a/docs/python/api-reference.md b/docs/en/python/api-reference.md similarity index 100% rename from docs/python/api-reference.md rename to docs/en/python/api-reference.md diff --git a/docs/python/examples.md b/docs/en/python/examples.md similarity index 100% rename from docs/python/examples.md rename to docs/en/python/examples.md diff --git a/docs/python/getting-started.md b/docs/en/python/getting-started.md similarity index 100% rename from docs/python/getting-started.md rename to docs/en/python/getting-started.md diff --git a/docs/python/guide/packer.md b/docs/en/python/guide/packer.md similarity index 100% rename from docs/python/guide/packer.md rename to docs/en/python/guide/packer.md diff --git a/docs/python/guide/periodic-boundaries.md b/docs/en/python/guide/periodic-boundaries.md similarity index 100% rename from docs/python/guide/periodic-boundaries.md rename to docs/en/python/guide/periodic-boundaries.md diff --git a/docs/python/guide/restraints.md b/docs/en/python/guide/restraints.md similarity index 100% rename from docs/python/guide/restraints.md rename to docs/en/python/guide/restraints.md diff --git a/docs/python/guide/targets.md b/docs/en/python/guide/targets.md similarity index 100% rename from docs/python/guide/targets.md rename to docs/en/python/guide/targets.md diff --git a/docs/python/index.md b/docs/en/python/index.md similarity index 100% rename from docs/python/index.md rename to docs/en/python/index.md diff --git a/docs/python/installation.md b/docs/en/python/installation.md similarity index 100% rename from docs/python/installation.md rename to docs/en/python/installation.md diff --git a/docs/rust/examples.md b/docs/en/rust/examples.md similarity index 100% rename from docs/rust/examples.md rename to docs/en/rust/examples.md diff --git a/docs/rust/getting-started.md b/docs/en/rust/getting-started.md similarity index 100% rename from docs/rust/getting-started.md rename to docs/en/rust/getting-started.md diff --git a/docs/rust/handlers-relaxers.md b/docs/en/rust/handlers-relaxers.md similarity index 100% rename from docs/rust/handlers-relaxers.md rename to docs/en/rust/handlers-relaxers.md diff --git a/docs/rust/index.md b/docs/en/rust/index.md similarity index 100% rename from docs/rust/index.md rename to docs/en/rust/index.md diff --git a/docs/rust/restraints-and-pbc.md b/docs/en/rust/restraints-and-pbc.md similarity index 100% rename from docs/rust/restraints-and-pbc.md rename to docs/en/rust/restraints-and-pbc.md diff --git a/docs/root-redirect.html b/docs/root-redirect.html new file mode 100644 index 0000000..98a953d --- /dev/null +++ b/docs/root-redirect.html @@ -0,0 +1,13 @@ + + + + + + + molpack docs + + + +

English · 简体中文

+ + diff --git a/docs/zh/architecture.md b/docs/zh/architecture.md new file mode 100644 index 0000000..a3cd711 --- /dev/null +++ b/docs/zh/architecture.md @@ -0,0 +1,336 @@ +# Architecture + +Developer-oriented view of the crate. Read [`concepts`](crate::concepts) +first for the abstractions this chapter assumes. + +This page covers four things, in order: + +1. [Module map](#module-map) — where everything lives +2. [Data flow](#data-flow) — how values travel from user input to packed frame +3. [Algorithms](#algorithms) — pseudo-code for the three nested loops +4. [Hot path](#hot-path-objective-evaluation) — what one objective evaluation does +5. [Invariants and conventions](#invariants-and-conventions) — load-bearing rules + +## Module map + +```text +src/ +├── lib.rs public re-exports + rustdoc chapters +├── packer.rs Molpack builder + pack() driver + phase / iteration loops +├── target.rs Target — molecule type + per-molecule restraints +├── restraint.rs Restraint trait + 14 concrete *Restraint structs +├── region.rs Region trait + And/Or/Not + RegionRestraint +├── relaxer.rs Relaxer / RelaxerRunner + TorsionMcRelaxer +├── handler.rs Handler trait + 4 built-in observers +├── objective.rs compute_f / compute_g / compute_fg + Objective impl +├── context/ PackContext = single owner of mutable packing state +│ ├── pack_context.rs +│ ├── model.rs immutable topology + inputs +│ ├── state.rs mutable per-iteration state +│ └── work_buffers.rs scratch arrays (xcart, gxcar, …) +├── constraints/ EvalMode / EvalOutput facade +├── gencan/ bound-constrained quasi-Newton optimizer +│ ├── mod.rs pgencan / gencan / tn_linesearch +│ ├── cg.rs conjugate-gradient inner solve +│ └── spg.rs spectral projected gradient fallback +├── initial.rs initial random placement + restmol pre-fit +├── movebad.rs worst-molecule perturbation heuristic +├── euler.rs Euler angles ↔ rotation matrices +├── cell.rs cell-list neighbor lookup +├── frame.rs PackContext ↔ molrs::Frame conversions +├── validation.rs post-pack correctness check +├── script/ .inp parser + lowering to Targets +├── api/ builder facade re-exports +└── bin/molpack/ CLI front-end (cli feature) +``` + +### Dependency direction + +```text + lib.rs + │ + packer.rs (driver — depends on everything below) + │ + ┌──────────┬──────┴──────┬──────────┬──────────┐ + ▼ ▼ ▼ ▼ ▼ + target initial gencan movebad handler + │ │ │ │ + │ └────────┐ │ │ + ▼ ▼ ▼ ▼ + restraint + region context/PackContext + │ + ▼ + objective.rs ← hot path + │ + └── constraints/ (EvalMode facade) +``` + +`target` / `restraint` / `region` are pure data — no driver imports. +`packer` is the only module that imports everything else. `objective` +is the narrow waist through which all per-atom work flows. + +## Data flow + +```text +USER INPUTS ─→ Target / Molpack builders + Frame, count, restraints, + handlers, tolerance, seed + ─→ pack() entry + a. broadcast global → per-target restraints + b. snapshot every Target + c. build PackContext + ModelData (immutable topology) + RuntimeState (x, coor, radius) + WorkBuffers (xcart, gxcar, scratch) + d. flatten restraints → CSR pool + e. initial placement → x[0..6·ntotmol] + +PER-ITERATION ─→ evaluate(x, mode, &mut g) + GENCAN reads x, → expand_molecules: x → xcart + reads f / g via → restraint penalties per atom + &mut dyn Objective → cell list + pair penalties + → project gradient back: gxcar → g + returns f_total, fdist, frest + +OUTPUT ─→ Frame + pack_with_report() also exposes + converged, fdist, frest +``` + +Three rules govern this flow: + +- **`PackContext` owns mutable state.** GENCAN, movebad, handlers, and the + phase driver all take `&mut PackContext` (writers) or `&PackContext` + (observers). No other module owns mutable state across iterations. +- **`Arc` for polymorphic storage.** Cheap clone (refcount + bump) into the per-atom CSR pool. The hot path does one virtual call + per restraint per atom. +- **GENCAN is decoupled.** `gencan/pgencan` takes `&mut dyn Objective`, + not `&mut PackContext`. Synthetic objectives (Rosenbrock, Booth, Beale) + exercise the optimizer in isolation. + +### Coordinate layout + +The optimizer variable vector `x` packs centers of mass and Euler angles: + +```text +x = [com₀(3), com₁(3), …, comₙ(3), eul₀(3), eul₁(3), …, eulₙ(3)] +length = 6 · ntotmol +``` + +Cartesian atom positions `xcart: Vec<[F; 3]>` of length `ntotat` are +expanded each evaluation: + +```text +xcart[icart_for(i, m, a)] = com_m + R(eul_m) · ref_coords[i, a] +``` + +where `i` is molecule type, `m` is copy index, `a` is atom index. + +## Algorithms + +Three nested loops drive the packer. + +### Outer: `pack()` (one call) + +```text +fn pack(targets, max_loops): + validate inputs (non-empty, valid PBC, atoms > 0) + broadcast Molpack.global_restraints → each target's molecule_restraints + split targets into free / fixed + build PackContext + run init_passes of restmol(): // geometric pre-fit, no pair kernel + for each free target type: + place molecules randomly inside their restraints + relax restraint penalties only + handlers.on_start, handlers.on_initialized + for phase in 0 ..= ntype: + if phase < ntype: + comptype[i] := (i == phase) // PER-TYPE pre-compaction + else: + comptype[i] := true // ALL-TYPES main phase + report := run_phase(phase, max_loops, …) + if report.error_phase: break + handlers.on_finish + build Frame; pack_with_report() also returns converged/fdist/frest +``` + +Why per-type pre-compaction first: if every type optimizes simultaneously +from a random start, cross-type interference traps the solver in shallow +minima. Compacting one type at a time inside its own restraint volume +gives the all-types phase a much better seed. + +### Middle: `run_phase` (one phase) + +```text +fn run_phase(phase_id, max_loops): + handlers.on_phase_start(phase_info) + radscale := discale // start with inflated radii (default 1.1) + relax_runners := build relaxer runners for this phase + // Quick-exit: if the unscaled objective is already below precision, + // skip the whole phase. + if evaluate_unscaled(sys, x).below(precision): return Converged + for loop_idx in 0 .. max_loops: + result := run_iteration(loop_idx, radscale, relax_runners) + radscale := decay(radscale) // → 1.0 over the phase + handlers.on_step(step_info, sys) + if result.converged: return Converged + if handlers.should_stop(): return EarlyStop + return MaxLoops +``` + +`radscale` starts at `discale` (1.1) and decays toward 1.0 over the +phase. This soft-starts the pair penalty: the optimizer first sees +slightly oversized atoms (easier to push apart) and tightens to true +tolerance as the phase progresses. + +### Inner: `run_iteration` (one outer step) + +```text +fn run_iteration(loop_idx, radscale, runners): + // 1. Movebad — relocate the K worst molecules. + if movebad enabled: + identify atoms with largest restraint + pair penalty + perturb their COM/Euler within init_box_half_size + // 2. Relaxers — update reference geometry per type (count == 1 only). + for (type, runner) in runners: + runner.on_iter(ref_coords, f_current, &mut evaluate, rng) + if accepted: write back new ref_coords + // 3. GENCAN — bound-constrained quasi-Newton solve. + pgencan(x, &mut sys, params, precision) + // Internally: tn_linesearch → CG inner solve → SPG fallback, + // each step calls sys.evaluate(x, mode, g). + // 4. Convergence check on the unscaled objective. + f_unscaled := evaluate_unscaled(sys, x) + fimp := percentage improvement vs previous loop + converged := fdist < precision AND frest < precision + return { converged, fimp, fdist, frest } +``` + +GENCAN itself runs three nested solvers: + +```text +pgencan: project x onto bounds, then call gencan +gencan: truncated-Newton outer; calls tn_linesearch +tn_ls: conjugate-gradient line search; SPG fallback if CG stalls +``` + +Each leaf step calls `sys.evaluate(x, mode, &mut g)` — the hot path. + +## Hot path: objective evaluation + +`PackContext::evaluate` is invoked O(10³–10⁴) times per `pack()` run. +Performance lives here. + +```text +evaluate(x, mode, g) dispatches by mode: + FOnly → compute_f + GradientOnly → compute_g + FAndGradient → compute_fg + RestMol → compute_fg (init phase, pair kernel skipped) +``` + +`compute_fg` is the canonical path — it does five steps: + +```text +1. expand_molecules(x): + for each molecule type t, copy m, atom a: + xcart[icart] := com_t,m + R(eul_t,m) · ref_coords[t, a] + +2. accumulate_constraint_value_and_gradient (per atom icart): + range := iratom_offsets[icart] .. iratom_offsets[icart + 1] + for &irest in iratom_data[range]: + f += sys.restraints[irest].fg(xcart[icart], scale, scale2, + &mut grad_xcart[icart]) + // Linear penalties consume `scale`; quadratic consume `scale2`. + +3. insert_atom_in_cell (per atom): + linked-list bucket atoms into cells + cell side ≈ 2 × max_radius × radscale + +4. accumulate_pair_fg (or _parallel under rayon): + for each non-empty cell c: + for each neighbor cell c′ in 13-cell stencil: + for each (i ∈ c, j ∈ c′): + d := pbc_distance(xi, xj) + σ := (rᵢ + rⱼ) · radscale + if d < σ: + penalty := (σ − d)² + grad_xcart[i] += d penalty / d xi + grad_xcart[j] += d penalty / d xj + +5. project_cartesian_gradient: + for each molecule m, atom a: + g_com[m] += grad_xcart[icart] + g_euler[m] += Jᵀ(eul_m, ref_a) · grad_xcart[icart] + // J = ∂xcart/∂eul, derived once per molecule from R(eul). +``` + +Cost breakdown: steps 1–3 are O(N_atoms); step 4 is +O(N_atoms × neighbor_avg) ≈ O(N_atoms × 32) and dominates wall time on +realistic workloads. Step 4 is the rayon parallelization point +(`accumulate_pair_fg_parallel`), reducing into per-atom gradient slots +via `AtomicU64` (since `Cell` is not `Sync`). + +The `Arc` virtual call in step 2 measured at +0.22% e2e +versus the prior monomorphic dispatch — a negligible cost for the +flexibility of user-defined restraints. + +## Invariants and conventions + +**Gradient accumulation.** `Restraint::fg` accumulates the true +gradient (∂penalty/∂x) into `g` with `+=`. Optimizer negates for descent. +Multiple restraints may touch one atom, so never overwrite. + +**Two-scale contract.** Linear penalties (Packmol kinds 2/3/6/7/10/11) +consume `scale`; quadratic penalties (kinds 4/5/8/9/12/13/14/15) consume +`scale2`. Each `impl Restraint` picks one internally. + +**Rotation convention.** `R_new = δR · R_old` (LEFT multiplication). +Single-atom tests cannot detect LEFT/RIGHT bugs — always test with +≥ 2 atoms. + +**Coordinate layout.** GENCAN's `x` is `[com₀..n, eul₀..n]` of length +`6·ntotmol`. Cartesian atom positions `xcart` are `Vec<[F; 3]>` of length +`ntotat`. + +**Thread safety.** All trait objects are `Send + Sync`. Interior +mutability inside parallel reductions uses `AtomicU64` with +`f64::to_bits` / `f64::from_bits` — `Cell` is not `Sync`. + +**Scope equivalence.** + +```text +molpack.with_global_restraint(r) + ≡ for t in targets: t.with_restraint(r.clone()) +``` + +There is no separate global-restraint storage path. The broadcast at +`pack()` entry is the implementation. + +**Restraint vs Constraint.** Packmol implements all 15 "constraints" as +soft penalties. Naming reflects mechanism, not user intent → `Restraint`. + +**Direction-3 extension pattern.** Every extension trait follows the +same shape: public trait, N concrete `pub struct` impls, user types +`impl Trait` identically. No `Builtin*` / `Native*` wrappers in the +public API. + +**`init1` short-circuit.** Set during the initial geometric pre-fit. +Skips the pair kernel — the restraint-only objective is enough to get +atoms into their regions before pair conflicts matter. + +## Cheatsheet + +| Question | Where to look | +|---|---| +| How is one restraint's penalty computed for one atom? | `restraint.rs::*::f` / `*::fg` | +| Where does `with_global_restraint` broadcast? | `packer.rs::pack` (top of fn) | +| Where is the per-atom CSR pool built? | `packer.rs::pack` (CSR build loop) | +| How are `x` ↔ Cartesian coords expanded? | `objective.rs::expand_molecules`, `euler.rs::eulerrmat` | +| Where is the pair-overlap kernel? | `objective.rs::accumulate_pair_fg_parallel` | +| What does the initial pre-fit do? | `initial.rs::initial`, `initial.rs::restmol` | +| How is precision-based termination tested? | `gencan/mod.rs::packmolprecision` | +| What does `movebad` do? | `movebad.rs::movebad` | +| How is torsion MC wired in? | `relaxer.rs::TorsionMcRelaxer::on_iter` | +| Where does periodic boundary wrap apply? | `context/pack_context.rs::pbc_distance` | diff --git a/docs/zh/assets b/docs/zh/assets new file mode 120000 index 0000000..947146d --- /dev/null +++ b/docs/zh/assets @@ -0,0 +1 @@ +../en/assets \ No newline at end of file diff --git a/docs/zh/cli/examples.md b/docs/zh/cli/examples.md new file mode 100644 index 0000000..1335167 --- /dev/null +++ b/docs/zh/cli/examples.md @@ -0,0 +1,34 @@ +# Examples + +The repository includes five canonical Packmol-equivalent workloads. Each can +be run directly through the CLI: + +```bash +molpack examples/pack_mixture/mixture.inp +molpack examples/pack_bilayer/bilayer-comment.inp +molpack examples/pack_interface/interface.inp +molpack examples/pack_spherical/spherical-comment.inp +molpack examples/pack_solvprotein/solvprotein.inp +``` + +## Workloads + +| Workload | Script | Demonstrates | +|---|---|---| +| Mixture | `examples/pack_mixture/mixture.inp` | Two species co-packed in a cube. | +| Bilayer | `examples/pack_bilayer/bilayer-comment.inp` | Atom-subset plane restraints for layer orientation. | +| Interface | `examples/pack_interface/interface.inp` | Liquid-liquid interface around a fixed solute. | +| Spherical | `examples/pack_spherical/spherical-comment.inp` | Nested radial shells. | +| Solvated protein | `examples/pack_solvprotein/solvprotein.inp` | Fixed protein plus water and ions. | + +## Compare with programmatic APIs + +The same workloads also exist as Rust examples under `examples/pack_*` and as +Python scripts under `python/examples/`. Use them when you want to compare a +scripted `.inp` job with the builder APIs. + +```bash +cargo run --release --features io --example pack_mixture +cd python +python examples/pack_mixture.py +``` diff --git a/docs/zh/cli/formats.md b/docs/zh/cli/formats.md new file mode 100644 index 0000000..892bd92 --- /dev/null +++ b/docs/zh/cli/formats.md @@ -0,0 +1,30 @@ +# Formats + +The CLI reads molecule templates through the `io` feature and writes the final +packed structure to the path named by the script's `output` keyword. + +The output format is inferred from the output file extension. Input formats can +be inferred from structure-file extensions or set globally with `filetype`. + +| Format | Read | Write | Extension or `filetype` | +|---|---:|---:|---| +| PDB | Yes | Yes | `.pdb`, `pdb` | +| XYZ | Yes | Yes | `.xyz`, `xyz` | +| SDF / MOL | Yes | No | `.sdf`, `.mol`, `sdf` | +| LAMMPS dump | Yes | Yes | `.lammpstrj`, `lammps_dump` | +| LAMMPS data | Yes | No | `.data`, `lammps_data` | + +## Example + +```text +filetype pdb +output packed.xyz + +structure water.pdb + number 100 + inside box 0. 0. 0. 30. 30. 30. +end structure +``` + +The input template is read as PDB because of `filetype pdb`; the output is +written as XYZ because the output path ends with `.xyz`. diff --git a/docs/zh/cli/index.md b/docs/zh/cli/index.md new file mode 100644 index 0000000..12fe593 --- /dev/null +++ b/docs/zh/cli/index.md @@ -0,0 +1,56 @@ +# CLI + +Use the `molpack` binary when you already have a Packmol-style `.inp` +script, or when you want a reproducible packing job that can be checked into a +simulation workflow. + +```bash +molpack mixture.inp +``` + +The file form resolves paths inside the script relative to the script's +directory. Stdin mode is also supported and resolves relative paths against the +current working directory: + +```bash +molpack < mixture.inp +``` + +## Install + +```bash +cargo install molcrafts-molpack --features cli +molpack --help +``` + +For source builds: + +```bash +cargo build --features cli --bin molpack +``` + +## Minimal input + +```text +tolerance 2.0 +seed 42 +filetype pdb +output packed.pdb +nloop 400 + +structure water.pdb + number 1000 + inside box 0. 0. 0. 40. 40. 40. +end structure +``` + +Every mobile structure should have a spatial restraint such as `inside box`, +`inside sphere`, or a global `pbc` declaration. Without one, initial placement +has to infer a huge free-space box and the run can become impractical. + +## Next steps + +- [Packmol Script Notes](script-format.md) links to the Packmol manual and + lists molpack-specific keywords, formats, and parser behavior. +- [Formats](formats.md) documents readable and writable molecular file types. +- [Examples](examples.md) shows the five checked-in canonical workloads. diff --git a/docs/zh/cli/script-format.md b/docs/zh/cli/script-format.md new file mode 100644 index 0000000..e640546 --- /dev/null +++ b/docs/zh/cli/script-format.md @@ -0,0 +1,66 @@ +# Packmol Script Notes + +The CLI reads Packmol-style `.inp` files. For the script language itself, use +the official [Packmol user guide](https://m3g.github.io/packmol/userguide.shtml) +as the reference. molpack intentionally keeps that job shape: global settings, +then one `structure ... end structure` block per molecule type. + +```text +tolerance 2.0 +filetype pdb +output packed.pdb + +structure water.pdb + number 1000 + inside box 0. 0. 0. 40. 40. 40. +end structure +``` + +This page only documents molpack-specific behavior and extensions. If a Packmol +keyword is not listed here, treat Packmol's manual as the source of truth and +use [Packmol parity](../packmol_parity.md) for implementation-level +compatibility notes. + +## Invocation and paths + +- `molpack mixture.inp`: paths inside `mixture.inp` resolve relative to the + directory that contains `mixture.inp`. +- `molpack < mixture.inp`: paths resolve relative to the current working + directory. + +This matches the two common Packmol invocation styles while making file-argument +mode safe to run from another directory. + +## molpack additions + +| Keyword | Meaning | +|---|---| +| `avoid_overlap ` | Controls whether initial random placements are rejected when they overlap a fixed molecule. Default: on. Use `avoid_overlap no` only when you explicitly want Packmol's fixed-solute exclusion guard disabled. | +| `filetype sdf` | Read SDF/MOL inputs through the CLI. Read-only. | +| `filetype lammps_dump` | Read LAMMPS dump inputs and write `.lammpstrj` outputs. | +| `filetype lammps_data` | Read LAMMPS data inputs. Read-only. | + +Output format is inferred from the `output` extension. The extra molpack output +extension is `.lammpstrj` for LAMMPS dump trajectories. + +## Strict parsing + +molpack rejects unknown keywords instead of silently ignoring them. A misspelled +top-level keyword returns `ScriptError::UnknownKeyword`, which prevents broken +inputs from falling through to an expensive or invalid packing run. + +## Supported Packmol subset + +The parser covers the Packmol syntax used by the bundled examples and +regression suite: + +- global `tolerance`, `seed`, `filetype`, `output`, `nloop`, and `pbc`; +- `structure ... end structure` blocks; +- `number`, `center`, `centerofmass`, and `fixed`; +- whole-molecule and `atoms ... end atoms` restraints; +- `inside`/`outside` box, cube, sphere, ellipsoid, and cylinder; +- `over plane`, `above plane`, and `below plane`. + +Atom indices in `.inp` files are 1-based, matching Packmol. When translating +the same model to Python or Rust, subtract 1 because those APIs use 0-based +indices. diff --git a/docs/zh/concepts.md b/docs/zh/concepts.md new file mode 100644 index 0000000..f956817 --- /dev/null +++ b/docs/zh/concepts.md @@ -0,0 +1,307 @@ +# Core Concepts + +This chapter defines each abstraction in the crate in one place. +Cross-link to the types for full API details. + +## AtomRestraint + +An [`AtomRestraint`](crate::AtomRestraint) is a **soft penalty** applied per +atom: `f(x, scale, scale2) -> F` and `fg(x, scale, scale2, g) -> F`. +It contributes to the packing objective and — in all current +implementations — derives from Packmol's `comprest.f90` / `gwalls.f90`. + +```text +pub trait AtomRestraint: Send + Sync + std::fmt::Debug { + fn f (&self, x: &[F; 3], scale: F, scale2: F) -> F; + fn fg(&self, x: &[F; 3], scale: F, scale2: F, g: &mut [F; 3]) -> F; + fn is_parallel_safe(&self) -> bool { true } + fn name(&self) -> &'static str { std::any::type_name::() } +} +``` + +The crate ships 14 concrete `*Restraint` structs (one per Packmol +`kind` 2..=15), each holding its own semantically-named geometric +fields. User types `impl Restraint` sit in the same type slot — +there is no `Builtin*` wrapper in the public API. See +[`extending`](crate::extending) for a tutorial. + +### Gradient convention + +`fg` accumulates the TRUE gradient (∂penalty/∂x) INTO `g` with `+=`. +Do not overwrite: many restraints may touch the same atom. The +optimizer negates for descent. + +### Two-scale contract + +Packmol convention (mirrored in the port): + +- Linear penalties — kinds 2, 3, 6, 7, 10, 11 (box / cube / plane) — + consume `scale`. +- Quadratic penalties — kinds 4, 5, 8, 9, 12, 13, 14, 15 (sphere / + ellipsoid / cylinder / gaussian) — consume `scale2`. + +Each `impl Restraint` picks one internally. User-defined restraints +may ignore both knobs and use their own stiffness coefficient as an +instance field. + +## CollectiveRestraint + +A [`CollectiveRestraint`](crate::restraint::Restraint) is a +**group-level** penalty — unlike [`AtomRestraint`](#atomrestraint), which sees one atom +at a time and contributes an independent external field Σᵢ U(xᵢ), a +collective restraint sees *every* copy of a species at once and returns a +single penalty whose gradient is **coupled across the whole group**: + +```text +pub trait Restraint: Send + Sync + std::fmt::Debug { + fn f (&self, coords: &[[F; 3]], scale: F, scale2: F) -> F; + fn fg(&self, coords: &[[F; 3]], scale: F, scale2: F, grads: &mut [[F; 3]]) -> F; + fn is_parallel_safe(&self) -> bool { true } + fn name(&self) -> &'static str { std::any::type_name::() } +} +``` + +`coords` and `grads` have equal length — one entry per atom in the group. +The gradient convention mirrors [`AtomRestraint`](#atomrestraint): `fg` accumulates INTO +`grads[i]` with `+=`. + +### Why collective? + +A per-atom field built from a target density ρ\* (e.g. Boltzmann inversion +U = −kT·ln ρ\*) is minimised by driving *every* site to the single minimum +of U — the mode of ρ\* — so the sites collapse onto the peak instead of +spreading over the distribution. A collective penalty over the whole +species that matches the empirical distribution to a target profile via +the squared 1-D Wasserstein (sorted-CDF) distance has the correct fixed +point: *empirical distribution = target*. + +### Geometry × distribution cross-product + +Every member of this family matches a target distribution of a scalar +reaction coordinate ξ defined by a **geometry**, via the Wasserstein +engine. The two axes are orthogonal: + +- **Geometry** — maps Cartesian coordinates to ξ and scatters ∂L/∂ξ back: + `plane` (ξ = signed distance to a plane → slab profile), `point` + (ξ = distance to a centre → radial profile). +- **Distribution** — the target quantile function q(p) = F⁻¹(p): + Gaussian, exponential, or tabulated (arbitrary user-supplied profile). + +Concrete types are the cross product, named ``: + +| | Gaussian | Exponential | Tabulated | +|---|---|---|---| +| Plane | `GaussianPlane` | `ExponentialPlane` | `TabulatedPlane` | +| Point | `GaussianPoint` | `ExponentialPoint` | `TabulatedPoint` | + +Attach with `Target::with_collective_restraint(r)`. A `TabulatedPlane` +with a histogram from a target simulation is the one-line way to drive a +species toward an experimentally-observed density profile. + +## Region + +A [`Region`](crate::Region) is a **geometric predicate** with a signed +distance function: + +```text +pub trait Region: Send + Sync + std::fmt::Debug { + fn contains(&self, x: &[F; 3]) -> bool; + fn signed_distance(&self, x: &[F; 3]) -> F; + fn signed_distance_grad(&self, x: &[F; 3]) -> [F; 3] { /* default FD */ } + fn bounding_box(&self) -> Option { None } +} +``` + +Regions compose via the zero-cost combinators +[`And`](crate::And) / [`Or`](crate::Or) / [`Not`](crate::Not), with +analytic chain-rule gradients (max / min / negate). The +[`RegionExt`](crate::RegionExt) trait gives every `Region` ergonomic +`.and(...)` / `.or(...)` / `.not()` methods. + +Any `Region` lifts to a `Restraint` via +[`RegionRestraint`](crate::RegionRestraint): + +```text +penalty(x) = scale2 * max(0, signed_distance(x))² +``` + +Use `Region` when you want compositional geometry (intersection / +union / complement). Use `Restraint` directly when you want a specific +penalty shape (linear vs quadratic, custom stiffness, multi-atom). + +## Relaxer + +A [`Relaxer`](crate::Relaxer) modifies a target's **reference geometry** +between outer optimizer calls. Use cases: torsion-MC sampling for +flexible chains, local MD relaxation, gradient descent on bond-angle +targets. + +Two-part design — builder + runner: + +- `Relaxer::spawn(&self, frame, ref_coords) -> Box` is + called once at `pack()` entry. +- `RelaxerRunner::on_iter(&mut self, coords, f_current, evaluate, rng)` + runs between movebad and GENCAN each outer iteration; returns + `Some(new_coords)` on accept, `None` on reject. + +Relaxers require `count == 1` because all copies share the same +reference coords. + +Built-in: [`TorsionMcRelaxer`](crate::TorsionMcRelaxer) (Metropolis +torsion sampling with self-avoidance). + +## Handler + +A [`Handler`](crate::Handler) is an observer invoked at well-defined +lifecycle points: + +```text +pub trait Handler: Send { + fn on_start (&mut self, ntotat, ntotmol) {} + fn on_initialized (&mut self, sys: &PackContext) {} + fn on_step (&mut self, info: &StepInfo, sys); // required + fn on_phase_start (&mut self, info: &PhaseInfo) {} + fn on_phase_end (&mut self, info, report: &PhaseReport) {} + fn on_inner_iter (&mut self, iter, f, sys) {} + fn on_finish (&mut self, sys: &PackContext) {} + fn should_stop (&self) -> bool { false } +} +``` + +Observer contract: `sys` is always `&PackContext`, never `&mut`. +Handlers cannot modify packer state — use a `Relaxer` if you need to. + +Built-ins: [`NullHandler`](crate::NullHandler), +[`ProgressHandler`](crate::ProgressHandler), +[`EarlyStopHandler`](crate::EarlyStopHandler), +[`XYZHandler`](crate::XYZHandler). + +## Objective + +The [`Objective`](crate::objective::Objective) trait abstracts over +what GENCAN sees. `PackContext` implements it; synthetic test +objectives (Rosenbrock / Booth / Beale) can implement it to exercise +the optimizer in isolation. + +```text +pub trait Objective { + fn evaluate(&mut self, x: &[F], mode: EvalMode, g: Option<&mut [F]>) -> EvalOutput; + fn fdist(&self) -> F; + fn frest(&self) -> F; + fn ncf(&self) -> u32; + fn ncg(&self) -> u32; + fn reset_eval_counters(&mut self); + fn bounds(&self, l: &mut [F], u: &mut [F]); +} +``` + +GENCAN (`pgencan`, `gencan`, `tn_ls`, `spg`, `cg`) takes `&mut dyn +Objective` rather than `&mut PackContext` — the optimizer is +decoupled from the packing state. + +## Target + +A [`Target`](crate::Target) describes one molecule type: + +- Input coordinates + centered reference coordinates. +- Van der Waals radii, element symbols, copy count, name. +- Its attached restraints (per-target + per-atom-subset). +- Its attached relaxers. +- Optional fixed placement (Euler + translation). +- Optional Euler-angle bounds (`with_rotation_bound(Axis, Angle, Angle)`). + +Targets are snapshotted at `pack()` entry — mutating a `Target` after +passing it to the packer has no effect. + +## Molpack + +[`Molpack`](crate::Molpack) is the builder facade: + +```text +Molpack::new() + .with_log_level(...) + .with_handler(...) + .with_global_restraint(...) // broadcast to every target + .with_periodic_box(min, max) // or via periodic InsideBoxRestraint + .pack(&[targets], max_loops) +``` + +Every tuning knob (`with_tolerance`, `with_precision`, +`with_inner_iterations`, `with_seed`, `with_avoid_overlap`, …) has a +Packmol-matching default, so `Molpack::new().pack(&targets, max_loops)` +is a complete call. You only set a knob to *change* its default — e.g. +`with_avoid_overlap(false)` to let solvent seed inside a fixed solute +(on by default), or `with_seed(n)` to pick a different RNG stream (the +default seed is Packmol's `1_234_567`). + +Every setter consumes and returns `self`. `pack` takes `&mut self` +(handlers are invoked through it). + +## PackContext + +[`PackContext`](crate::PackContext) is the single owner of mutable +packing state — coordinates, cell lists, restraint pool, rotation +buffers, counters. All optimizer / movebad / handler code paths take +`&mut PackContext` (for writers) or `&PackContext` (for observers). + +Structure (`molpack/src/context/`): + +- `ModelData` — topology and inputs (immutable after init). +- `RuntimeState` — mutable per-iteration state (x, coor, radius). +- `WorkBuffers` — scratch arrays (xcart, gxcar, radiuswork). + +Users rarely touch `PackContext` directly — it's passed through +handlers and relaxers. Power users implementing a custom `Objective` +against synthetic test problems will interact with it. + +## Scope equivalence law + +```text +molpack.with_global_restraint(r) + ≡ for t in targets { t.with_restraint(r.clone()) } +``` + +There is no separate "global-restraint" storage path in `PackContext`. +The broadcast happens inside `pack()`; each target receives an +`Arc::clone` of every global restraint (refcount bump, not a deep +copy). + +Per-atom-subset scope is a method-argument pair +`(indices: &[usize], restraint: impl Restraint)`, not a wrapper +struct. There is no `AtomRestraint` public type. + +## Restraint versus Constraint + +- **Restraint** = soft penalty (violable; pays energy cost). +- **Constraint** = hard constraint (must satisfy; SHAKE / RATTLE / + LINCS / Lagrange-multiplier mechanisms). + +Packmol implements all 15 "constraints" as soft penalties +(`scale * max(0, d)` or `scale2 * max(0, d)²`). Honest naming ⇒ +`Restraint`. This crate does not currently define a `Constraint` +trait; adding hard constraints is future work. + +## Direction-3 extension pattern + +Every extension trait in this crate follows the same shape: + +1. Public trait: `pub trait X`. +2. N concrete `pub struct` types that `impl X`, each holding its own + semantically-named fields. +3. User types `impl X` identically — zero type-level distinction from + built-ins. + +Forbidden in the public API: + +- `Builtin*` / `Native*` / `Packmol*` prefixed wrapper types. +- Tagged-union enums that package N built-ins as a single exposed + type. +- Builder pattern (`X::new().add(...).add(...)`). +- Injection of composition operators into the main trait + (composition lives on separate traits, e.g. `Region` vs `Restraint`). +- Wrapper types for per-atom-subset scope (that's a method-argument + pair, not a type). + +If you need internal AoS performance structures (e.g. tagged unions +for hot-path match dispatch), they go behind `pub(crate)` and opt-in +via a crate-private hook — invisible to users. diff --git a/docs/zh/development/index.md b/docs/zh/development/index.md new file mode 100644 index 0000000..d251cf7 --- /dev/null +++ b/docs/zh/development/index.md @@ -0,0 +1,31 @@ +# Development + +These pages are for modifying molpack rather than just using it. + +molpack is a Rust packing engine with three public surfaces: + +- `molcrafts-molpack` Rust library (`lib` name: `molpack`) +- `molpack` CLI binary (`cli` feature) +- `molcrafts-molpack` Python wheel under `python/` + +## Read first + +- [Architecture](../architecture.md) maps modules, data flow, optimizer loops, + and the objective-evaluation hot path. +- [Extending](../extending.md) walks through custom `AtomRestraint`, + `Region`, `Handler`, and `Relaxer` implementations. + +## Validation commands + +```bash +cargo test -p molcrafts-molpack --lib --tests +cargo test -p molcrafts-molpack --release --test examples_batch -- --ignored +cd python +maturin develop --release +pytest +cargo fmt +cargo clippy -- -D warnings +``` + +The ignored `examples_batch` test runs the canonical Packmol-equivalent +workloads and is intentionally slower than the fast unit/integration tier. diff --git a/docs/zh/examples.md b/docs/zh/examples.md new file mode 100644 index 0000000..3b4807b --- /dev/null +++ b/docs/zh/examples.md @@ -0,0 +1,62 @@ +# Examples + +Five canonical Packmol-equivalent workloads ship in `examples/`. Each +exercises a different combination of restraints, fixed placements, and +target counts, and all five are covered by the regression suite +(`tests/examples_batch.rs`). + +| Workload | Rust example | Python example | Molecules | Restraints | Demonstrates | +|---|---|---|---|---|---| +| Mixture | `examples/pack_mixture` | `python/examples/pack_mixture.py` | 1000 water + 400 urea | one `inside box` per type | the simplest two-component fill in a cube | +| Bilayer | `examples/pack_bilayer` | `python/examples/pack_bilayer.py` | water slabs + palmitoil lipids | per-atom `above`/`below plane` | orienting the two leaflets of a membrane | +| Interface | `examples/pack_interface` | `python/examples/pack_interface.py` | water + chloroform + 1 fixed t3 | `inside box` per solvent + one `fixed` molecule | a liquid–liquid interface around a fixed structure | +| Spherical | `examples/pack_spherical` | `python/examples/pack_spherical.py` | concentric lipid + water shells | per-atom radial `inside`/`outside sphere` | nested shell packing (largest / slowest case) | +| Solvated protein | `examples/pack_solvprotein` | `python/examples/pack_solvprotein.py` | 1 fixed protein + water + Na⁺ + Cl⁻ | `inside sphere` solvent around a `fixed` solute | dense solvation that relies on `avoid_overlap` | + +## Run the Rust examples + +The examples need the `io` feature so they can read the bundled structure +files: + +```bash +cargo run --release --example pack_mixture --features io +cargo run --release --example pack_bilayer --features io +cargo run --release --example pack_interface --features io +cargo run --release --example pack_spherical --features io +cargo run --release --example pack_solvprotein --features io +``` + +Each directory also contains the matching `.inp` script +(`mixture.inp`, `bilayer-comment.inp`, `interface.inp`, +`spherical-comment.inp`, `solvprotein.inp`), so the CLI form and the +programmatic form can be compared side by side: + +```bash +cargo run --release --features cli --bin molpack -- examples/pack_mixture/mixture.inp +``` + +Optional progress / trajectory dumps are gated behind environment +variables: + +```bash +MOLRS_PACK_EXAMPLE_PROGRESS=1 cargo run --release --example pack_mixture --features io +MOLRS_PACK_EXAMPLE_XYZ=1 cargo run --release --example pack_mixture --features io +``` + +## Run the Python examples + +The Python examples mirror the Rust ones and load their structure files +through the `molrs` dependency installed with molpack: + +```bash +cd python +maturin develop --release +python examples/pack_mixture.py +python examples/pack_bilayer.py +python examples/pack_interface.py +python examples/pack_spherical.py +python examples/pack_solvprotein.py +``` + +`python/examples/pack_water_cube.py` is a minimal standalone starter +(single-species cube) for a first look at the Python API. diff --git a/docs/zh/extending.md b/docs/zh/extending.md new file mode 100644 index 0000000..d451ce3 --- /dev/null +++ b/docs/zh/extending.md @@ -0,0 +1,474 @@ +# Extending the Crate + +Tutorials for writing your own `AtomRestraint` / `Region` / `Handler` / +`Relaxer` types. Every extension trait in this crate follows the same +shape (direction-3 rule — see [`concepts`](crate::concepts)): + +> `pub trait X` + N concrete `pub struct` types implementing it. +> User types `impl X` identically. No built-in/plugin type-level +> distinction. + +## Custom `AtomRestraint` + +Goal: pull atoms toward a target plane with a quadratic attractive +well. + +### Step 1 — define the struct + +```rust +use molrs::types::F; +# use molpack::AtomRestraint; + +#[derive(Debug, Clone, Copy)] +pub struct PlaneTether { + pub normal: [F; 3], + pub offset: F, + pub k: F, +} +# impl AtomRestraint for PlaneTether { +# fn f(&self, _x: &[F; 3], _s: F, _s2: F) -> F { 0.0 } +# fn fg(&self, _x: &[F; 3], _s: F, _s2: F, _g: &mut [F; 3]) -> F { 0.0 } +# } +``` + +- `pub` fields — users construct with `PlaneTether { normal, offset, + k }`, no builder. +- `Debug` required because [`AtomRestraint`](crate::AtomRestraint) has a + `Debug` supertrait bound (so `Target`'s derived `Debug` keeps + working). + +### Step 2 — implement `AtomRestraint` + +```rust +# use molrs::types::F; +# use molpack::AtomRestraint; +# #[derive(Debug)] +# pub struct PlaneTether { pub normal: [F; 3], pub offset: F, pub k: F } +impl AtomRestraint for PlaneTether { + fn f(&self, pos: &[F; 3], _scale: F, _scale2: F) -> F { + let d = self.normal[0] * pos[0] + + self.normal[1] * pos[1] + + self.normal[2] * pos[2] + - self.offset; + 0.5 * self.k * d * d + } + fn fg(&self, pos: &[F; 3], scale: F, scale2: F, g: &mut [F; 3]) -> F { + let d = self.normal[0] * pos[0] + + self.normal[1] * pos[1] + + self.normal[2] * pos[2] + - self.offset; + g[0] += self.k * d * self.normal[0]; + g[1] += self.k * d * self.normal[1]; + g[2] += self.k * d * self.normal[2]; + self.f(pos, scale, scale2) + } +} +``` + +Three contracts that all restraints must obey: + +1. **Gradient accumulates with `+=`.** Multiple restraints may touch + the same atom. +2. **`fg` returns the value.** The hot path uses the returned value + for the `fdist`/`frest` accumulation — don't return `0.0` just + because the caller might discard it. +3. **Scale/scale2 usage is your choice.** Linear-penalty restraints + typically use `scale`; quadratic-penalty ones use `scale2`. Your + tether uses its own `k` — ignore both knobs if you prefer. + +### Step 3 — write a gradient test + +```no_run +# use molrs::types::F; +# use molpack::AtomRestraint; +# #[derive(Debug)] pub struct PlaneTether { pub normal: [F; 3], pub offset: F, pub k: F } +# impl AtomRestraint for PlaneTether { +# fn f(&self, _x: &[F; 3], _s: F, _s2: F) -> F { 0.0 } +# fn fg(&self, _x: &[F; 3], _s: F, _s2: F, _g: &mut [F; 3]) -> F { 0.0 } +# } +#[test] +fn plane_tether_gradient_matches_fd() { + let r = PlaneTether { normal: [0.0, 0.0, 1.0], offset: 5.0, k: 2.0 }; + let x = [1.0, 2.0, 7.0]; + let mut g = [0.0; 3]; + let _ = r.fg(&x, 1.0, 1.0, &mut g); + let h: F = 1e-5; + for k in 0..3 { + let mut xp = x; xp[k] += h; + let mut xm = x; xm[k] -= h; + let fd = (r.f(&xp, 1.0, 1.0) - r.f(&xm, 1.0, 1.0)) / (2.0 * h); + assert!( + (g[k] - fd).abs() < 1e-4, + "axis {k}: analytic={}, fd={}", g[k], fd, + ); + } +} +``` + +Convention: `ε = 1e-5`, tolerance `1e-3` (looser if the restraint has +kinks). + +### Step 4 — use it + +```no_run +# use molrs::types::F; +# use molpack::AtomRestraint; +# #[derive(Debug, Clone, Copy)] pub struct PlaneTether { pub normal: [F; 3], pub offset: F, pub k: F } +# impl AtomRestraint for PlaneTether { +# fn f(&self, _x: &[F; 3], _s: F, _s2: F) -> F { 0.0 } +# fn fg(&self, _x: &[F; 3], _s: F, _s2: F, _g: &mut [F; 3]) -> F { 0.0 } +# } +use molpack::{InsideBoxRestraint, Target}; +# let (pos, rad) = (&[[0.0; 3]][..], &[1.0][..]); + +let target = Target::from_coords(pos, rad, 100) + .with_restraint(InsideBoxRestraint::new([0.0; 3], [40.0; 3], [false; 3])) + .with_restraint(PlaneTether { normal: [0.0, 0.0, 1.0], offset: 20.0, k: 1.0 }); +``` + +Built-in `InsideBoxRestraint` and user `PlaneTether` take the same +code path — direction-3 in action. + +### In Python — the same restraint, duck-typed + +The interfaces are also exposed to Python through a *duck-typed* protocol: a +restraint is any object exposing `f` and `fg`, and the packer consumes it on the +same code path as a built-in. That makes Python the natural place to *prototype* +a restraint the input grammar can't express — a few lines, no recompile — and +then, once it earns its place, swap in the native equivalent with the driver +script unchanged. + +A duck-typed restraint is any object exposing `f` and `fg`. The Python protocol +differs from the Rust trait in exactly one way: `fg` **returns** +`(energy, (gx, gy, gz))` instead of accumulating into a `&mut [F; 3]` — the +binding does the `+=` for you. The contracts are otherwise identical: `fg` +returns the energy, the gradient is the chain rule `(dU/dξ)·∇ξ`, a linear-energy +penalty rides `scale` (a quadratic one `scale2`). A missing `f` or `fg` is +rejected at attach time with a `TypeError`, and an exception raised inside `fg` +propagates back out of `pack`. The `PlaneTether` above, duck-typed in Python: + +```python +import numpy as np + +class PlaneTether: + """Pull a site toward the plane z = offset (a per-site geometric bias).""" + def __init__(self, normal, offset, k): + n = np.asarray(normal, float) + self.n, self.offset, self.k = n / np.linalg.norm(n), offset, k + + def f(self, x, scale, scale2): + d = float(self.n @ np.asarray(x, float)) - self.offset + return scale * 0.5 * self.k * d * d + + def fg(self, x, scale, scale2): + d = float(self.n @ np.asarray(x, float)) - self.offset + g = scale * self.k * d * self.n + return scale * 0.5 * self.k * d * d, (float(g[0]), float(g[1]), float(g[2])) +``` + +Attach it with `with_atom_restraint` (a site subset) or `with_restraint` (every +copy), and pack. + +> **A per-site field cannot reproduce a distribution.** It is tempting to build a +> per-site penalty from a target density `ρ*` (e.g. Boltzmann inversion +> `U = −kT·ln ρ*`) to make sites *follow* `ρ*`. This fails under packing's energy +> **minimisation**: `∑ᵢ U(xᵢ)` is minimised by driving *every* site to the single +> minimum of `U` — the mode of `ρ*` — so the sites collapse onto the peak instead +> of spreading over the distribution. To drive a whole species onto a target +> profile, use a **collective** restraint (`Target.with_collective_restraint`, +> e.g. the built-in `ProfileMatch`), whose penalty is a function of the entire +> group and whose gradient couples the copies, so the fixed point is *empirical +> distribution = target*. + +## Custom `Region` + +Goal: a conical region with apex at origin, axis along +z, +half-angle 30°. + +```rust +use molrs::types::F; +# use molpack::Region; + +#[derive(Debug, Clone, Copy)] +pub struct ConeRegion { + pub apex: [F; 3], + pub axis: [F; 3], + pub half_angle_cos: F, +} + +impl Region for ConeRegion { + fn contains(&self, x: &[F; 3]) -> bool { + self.signed_distance(x) <= 0.0 + } + fn signed_distance(&self, x: &[F; 3]) -> F { + let dx = x[0] - self.apex[0]; + let dy = x[1] - self.apex[1]; + let dz = x[2] - self.apex[2]; + let r = (dx * dx + dy * dy + dz * dz).sqrt(); + if r < 1e-12 { return 0.0; } + let axis_dot = + (dx * self.axis[0] + dy * self.axis[1] + dz * self.axis[2]) / r; + self.half_angle_cos - axis_dot + } + // Default FD gradient is OK for prototypes. Override analytically + // for hot-path use — see below. +} +``` + +### Compose with built-ins + +```no_run +# use molrs::types::F; +# use molpack::Region; +# #[derive(Debug, Clone, Copy)] +# pub struct ConeRegion { pub apex: [F; 3], pub axis: [F; 3], pub half_angle_cos: F } +# impl Region for ConeRegion { +# fn contains(&self, _x: &[F; 3]) -> bool { true } +# fn signed_distance(&self, _x: &[F; 3]) -> F { 0.0 } +# } +use molpack::{InsideSphereRegion, RegionExt, RegionRestraint, Target}; +# let (pos, rad) = (&[[0.0; 3]][..], &[1.0][..]); + +let cone = ConeRegion { + apex: [0.0; 3], + axis: [0.0, 0.0, 1.0], + half_angle_cos: (std::f64::consts::PI / 6.0).cos(), +}; +let sphere = InsideSphereRegion::new([0.0; 3], 10.0); +let region = cone.and(sphere); + +let target = Target::from_coords(pos, rad, 100) + .with_restraint(RegionRestraint(region)); +``` + +[`RegionExt::and`](crate::RegionExt::and) / `or` / `not` come from a +blanket impl on every `Region`. The resulting type +`And` is static-dispatch — no heap. + +### Analytic gradient override + +For hot-path use, override `signed_distance_grad` analytically. The +cone above: + +```rust +# use molrs::types::F; +# use molpack::Region; +# #[derive(Debug, Clone, Copy)] +# pub struct ConeRegion { pub apex: [F; 3], pub axis: [F; 3], pub half_angle_cos: F } +# impl Region for ConeRegion { +# fn contains(&self, _x: &[F; 3]) -> bool { true } +# fn signed_distance(&self, _x: &[F; 3]) -> F { 0.0 } +fn signed_distance_grad(&self, x: &[F; 3]) -> [F; 3] { + let dx = x[0] - self.apex[0]; + let dy = x[1] - self.apex[1]; + let dz = x[2] - self.apex[2]; + let r2 = dx * dx + dy * dy + dz * dz; + let r = r2.sqrt(); + if r < 1e-12 { return [0.0; 3]; } + let axis_dot = (dx * self.axis[0] + dy * self.axis[1] + dz * self.axis[2]) / r; + let inv_r = 1.0 / r; + // signed_distance = cos(α) - axis_dot ⇒ grad = -∂axis_dot/∂x + [ + -(self.axis[0] * inv_r - axis_dot * dx * inv_r * inv_r), + -(self.axis[1] * inv_r - axis_dot * dy * inv_r * inv_r), + -(self.axis[2] * inv_r - axis_dot * dz * inv_r * inv_r), + ] +} +# } +``` + +Then finite-difference check it — same pattern as the `AtomRestraint` +test. + +## Custom `Handler` + +Goal: a handler that writes a CSV row per step so you can plot the +objective evolution. + +```no_run +use std::fs::File; +use std::io::{BufWriter, Write}; +use molpack::{F, Handler, PackContext, StepInfo}; + +pub struct CsvHandler { writer: BufWriter } + +impl CsvHandler { + pub fn new(path: &str) -> std::io::Result { + let mut w = BufWriter::new(File::create(path)?); + writeln!(w, "phase,loop_idx,fdist,frest,improvement_pct")?; + Ok(Self { writer: w }) + } +} + +impl Handler for CsvHandler { + fn on_step(&mut self, info: &StepInfo, _sys: &PackContext) { + let _ = writeln!( + self.writer, + "{},{},{},{},{}", + info.phase.phase, + info.loop_idx, + info.fdist, + info.frest, + info.improvement_pct, + ); + } +} +``` + +Handler notes: + +- **`on_step` is the only required method.** Everything else has a + default no-op. +- **`sys` is `&PackContext`, never `&mut`.** Handlers cannot mutate + packer state — use a `Relaxer` if you need to. +- **Multiple handlers run in registration order.** Register your CSV + handler before `ProgressHandler` to get a row on every step, + vice-versa otherwise. +- **`should_stop` is polled every iteration.** Return `true` to break + the outer loop early. Useful for time budgets or custom convergence + criteria. + +## Custom `Relaxer` + +Goal: a relaxer that tries random rigid-body translations and accepts +if the objective decreases. + +![Confinement placement with a custom pivot-MC relaxer](assets/images/paper-confinement-sphere.png) + +The confinement example is a molpack extension workflow, not a Packmol parity +claim: a custom pivot-MC relaxer folds flexible PEO chains before packing, so +the same engine can place them inside a tight spherical cavity. + +```no_run +use molrs::Frame; +use molrs::types::F; +use molpack::{Relaxer, RelaxerRunner}; +use rand::{Rng, RngCore}; + +#[derive(Debug, Clone)] +pub struct JiggleRelaxer { + pub steps: usize, + pub max_delta: F, +} + +impl Relaxer for JiggleRelaxer { + fn spawn(&self, _frame: Option<&Frame>, _ref_coords: &[[F; 3]]) -> Box { + Box::new(JiggleRunner { + steps: self.steps, + max_delta: self.max_delta, + accepted: 0, + total: 0, + }) + } +} + +pub struct JiggleRunner { + steps: usize, + max_delta: F, + accepted: usize, + total: usize, +} + +impl RelaxerRunner for JiggleRunner { + fn on_iter( + &mut self, + coords: &[[F; 3]], + f_current: F, + evaluate: &mut dyn FnMut(&[[F; 3]]) -> F, + rng: &mut dyn RngCore, + ) -> Option> { + let mut best = coords.to_vec(); + let mut best_f = f_current; + let mut accepted_any = false; + for _ in 0..self.steps { + let dx = (rng.next_u32() as f64 / u32::MAX as f64 * 2.0 - 1.0) * self.max_delta; + let dy = (rng.next_u32() as f64 / u32::MAX as f64 * 2.0 - 1.0) * self.max_delta; + let dz = (rng.next_u32() as f64 / u32::MAX as f64 * 2.0 - 1.0) * self.max_delta; + let trial: Vec<[F; 3]> = best.iter() + .map(|p| [p[0] + dx, p[1] + dy, p[2] + dz]) + .collect(); + let f_trial = evaluate(&trial); + self.total += 1; + if f_trial < best_f { + self.accepted += 1; + best_f = f_trial; + best = trial; + accepted_any = true; + } + } + if accepted_any { Some(best) } else { None } + } + fn acceptance_rate(&self) -> F { + if self.total == 0 { 0.0 } else { self.accepted as F / self.total as F } + } +} +``` + +Relaxer notes: + +- **Two-part design.** [`Relaxer`](crate::Relaxer) is the immutable + builder; [`RelaxerRunner`](crate::RelaxerRunner) holds per-pack + state. `spawn()` is called once per target type at `pack()` entry. +- **`evaluate` closure tests trial coords against the full objective** + without mutating the reference — use it as often as you like. +- **Return `Some(new_coords)` only if something changed.** The packer + skips unnecessary cache invalidation when you return `None`. +- **`count == 1` required.** Multi-copy targets share reference + coords; a relaxer that mutates them would silently change all + copies. + +## Testing discipline + +| Kind | Location | Convention | +|---|---|---| +| Unit test | `#[cfg(test)] mod tests` in the same file | One `#[test]` fn per behavior | +| Integration test | `tests/.rs` | `use molpack::{…};` only public API | +| Gradient finite-difference | alongside unit test | ε=1e-5, tol=1e-3 | +| Regression vs Packmol | `tests/examples_batch.rs` (`#[ignore]`) | Run with `--ignored --release` | + +Run all: + +```bash +cargo test --all-features +cargo test --release --test examples_batch -- --ignored +``` + +Rules: + +- Every new `AtomRestraint` gets an FD gradient test. +- Every new `Region` gets a boolean-algebra + signed-distance sign + test and (for hot-path use) an analytic-gradient FD test. + +## Common pitfalls + +- **Gradient sign.** Every `AtomRestraint` accumulates `∂penalty/∂x`. + Optimizer negates for descent. If your molecules fly out of the + region, the gradient has the wrong sign — penalty should point + toward the violation boundary. +- **Rotation convention.** Single-atom tests pass with both LEFT and + RIGHT Euler multiplication; multi-atom tests don't. Always test + Euler changes with ≥ 2 atoms. +- **`Cell` is not `Sync`.** Use `AtomicU64` + + `f64::to_bits` / `from_bits` for interior mutability in + `Send + Sync` contexts. +- **0-based atom indexing.** `Target::with_atom_restraint` uses + Rust-native 0-based indices. `&[0, 1]` selects the first two atoms. + Packmol `.inp` files use 1-based — subtract 1 at the parse boundary. +- **`count == 1` required for relaxers.** Multi-copy targets share + reference coords. +- **`radscale` is phase-dependent.** Don't hard-code atomic radii — + always go through `sys.radius[i]`. `evaluate_unscaled` temporarily + swaps `radius` with `radius_ini` for user-facing numbers. +- **PBC boxes must be valid.** Zero-length axis returns + `PackError::InvalidPBCBox`. + +## Contributing flow + +1. Write a failing test. +2. Implement until it passes. +3. Run the full gate: + ```bash + cargo test --all-features + cargo clippy -- -D warnings + cargo fmt --all --check + ``` diff --git a/docs/zh/getting_started.md b/docs/zh/getting_started.md new file mode 100644 index 0000000..0f06c91 --- /dev/null +++ b/docs/zh/getting_started.md @@ -0,0 +1,120 @@ +# Quickstart + +Pack **100 water molecules** into a **40 Å** cube. This walkthrough uses the +Python package — the shortest path from a loaded frame to a packed result. The +same model is available as a [CLI script](cli/) or the [Rust builder](rust/). + +## 1. Install + +```bash +pip install molcrafts-molpack +``` + +This installs the packing engine and pulls in `molcrafts-molrs` for the frame +type plus PDB/XYZ I/O. + +## 2. Load or build a template + +=== "From a PDB file" + + ```python + import molrs + + frame = molrs.read_pdb("water.pdb") + ``` + +=== "In-memory (no file)" + + ```python + import molrs + import numpy as np + + frame = molrs.Frame.from_dict({ + "blocks": { + "atoms": { + "x": np.array([0.00, 0.96, -0.24]), + "y": np.array([0.00, 0.00, 0.93]), + "z": np.zeros(3), + "element": ["O", "H", "H"], + } + } + }) + ``` + +## 3. Define the target + +A `Target` is one molecule species plus the number of copies to place. Every +mobile target needs a spatial restraint: + +```python +from molpack import InsideBoxRestraint, Target + +water = ( + Target(frame, count=100) + .with_name("water") + .with_restraint(InsideBoxRestraint([0.0, 0.0, 0.0], [40.0, 40.0, 40.0])) +) +``` + +!!! warning "Missing restraints" + Without a spatial restraint (or a global PBC box), initial placement has to + invent a huge free-space region and the run can become impractical. + +## 4. Pack + +```python +from molpack import Molpack + +result = ( + Molpack() + .with_tolerance(2.0) + .with_seed(42) + .pack_with_report([water], max_loops=200) +) + +print(result.converged, result.natoms, result.fdist, result.frest) +packed = result.frame +``` + +| Field | Meaning | +|---|---| +| `converged` | Both objectives fell below the packer precision threshold | +| `fdist` | Pair-distance (overlap) violations | +| `frest` | Restraint violations | +| `frame` | Topology-complete packed `molrs.Frame` | + +For a frame-only return, use `Molpack().pack([water], max_loops=200)`. + +## 5. Save + +```python +import molrs + +molrs.write_pdb(packed, "water_box.pdb") +# or: molrs.write_xyz(packed, "water_box.xyz") +``` + +## Where next + + diff --git a/docs/zh/img b/docs/zh/img new file mode 120000 index 0000000..21d4e91 --- /dev/null +++ b/docs/zh/img @@ -0,0 +1 @@ +../en/img \ No newline at end of file diff --git a/docs/zh/index.md b/docs/zh/index.md index 7238d13..5900f82 100644 --- a/docs/zh/index.md +++ b/docs/zh/index.md @@ -32,14 +32,14 @@ hero: alt: BSD-3-Clause 许可证 actions: - label: 快速开始 - href: ../getting_started/ + href: getting_started/ style: primary - label: Packmol 脚本 - href: ../cli/ + href: cli/ - label: Python API - href: ../python/ + href: python/ - label: Rust API - href: ../rust/ + href: rust/ ---

molpack

@@ -57,8 +57,9 @@ hero: 本页是手册索引,不是营销页。所有入口共用同一模型:分子模板、副本数量、几何约束, 然后运行一次装填。 -!!! note "中文内容" - 中文首页为导航入口;详细章节目前以英文正文为主。语言切换在页眉下拉菜单。 +!!! note "翻译进度" + 中文站与英文站平行(`docs/zh/` ↔ `docs/en/`)。导航与首页为中文; + 部分章节正文仍暂用英文,点导航会留在 `/zh/` 下,不会跳回英文站。
@@ -117,7 +118,7 @@ hero: ### 运行 `.inp` 任务 -适合已写成 Packmol 风格、需要纳入版本控制的可复现任务。见 [CLI](../cli/)。 +适合已写成 Packmol 风格、需要纳入版本控制的可复现任务。见 [CLI](cli/)。 ```text structure water.pdb @@ -134,7 +135,7 @@ end structure ### 在 notebook 或流水线中装填 -加载或构造 frame,创建不可变 target,再把结果交给写出或分析代码。见 [Python](../python/)。 +加载或构造 frame,创建不可变 target,再把结果交给写出或分析代码。见 [Python](python/)。 ```python water = Target(frame, 100).with_restraint( @@ -151,7 +152,7 @@ packed = Molpack().with_seed(42).pack([water]) ### 把引擎嵌入 crate -适合原生应用、服务与测试。见 [Rust](../rust/)。 +适合原生应用、服务与测试。见 [Rust](rust/)。 ```rust let frame = Molpack::new() @@ -167,7 +168,7 @@ let frame = Molpack::new() ### 观测或停止一次运行 -Handler 接收装填循环中的结构化事件。见 [Handlers](../rust/handlers-relaxers/)。 +Handler 接收装填循环中的结构化事件。见 [Handlers](rust/handlers-relaxers/)。 ```rust impl Handler for WatchFdist { diff --git a/docs/zh/install.md b/docs/zh/install.md new file mode 100644 index 0000000..e499d90 --- /dev/null +++ b/docs/zh/install.md @@ -0,0 +1,86 @@ +# Install + +`molpack` ships three surfaces — the CLI binary, the Rust crate, and the +Python binding. Pick the one that matches your workflow; they share the same +engine and packing model. + +## CLI + +For Packmol-style `.inp` scripts: + +```bash +cargo install molcrafts-molpack --features cli +molpack --help +``` + +!!! tip "Path resolution" + In file-arg mode (`molpack job.inp`), relative paths inside the script are + resolved against the **script directory**. In stdin mode they resolve + against the current working directory. + +## Rust crate + +For programmatic use from Rust: + +```bash +cargo add molcrafts-molpack +``` + +Optional features (crate defaults to none enabled): + +| Feature | Purpose | +|---|---| +| `cli` | `molpack` binary + clap (implies `io`) | +| `io` | PDB / XYZ / SDF / LAMMPS readers via molrs | +| `rayon` | Parallel objective evaluation | +| `ff` | Force-field geometry relaxer (MMFF + L-BFGS) | + +```toml +# Cargo.toml — common combinations +molcrafts-molpack = { version = "0.1", features = ["io", "rayon"] } +``` + +## Python binding + +For notebooks and pipelines (Python 3.12+): + +```bash +pip install molcrafts-molpack +``` + +`molcrafts-molrs` is installed as a dependency and provides `molrs.Frame` plus +PDB / XYZ readers. The wheel itself is I/O-free — pass frames in, get frames +out. + +```python +import molpack +print(molpack.Molpack) +``` + +!!! note "Pre-built wheels" + Wheels are published for CPython **3.12** and **3.13** on Linux + (manylinux x86-64) and macOS (universal2). Other platforms fall back to the + sdist and need a Rust toolchain to build. + +## Build from source + +When you are modifying the crate or Python binding, check out **molrs** as a +sibling (path deps resolve `../molrs/molrs`): + +```bash +# sibling layout +# workspace/ +# ├── molrs/ +# └── molpack/ + +git clone https://github.com/MolCrafts/molpack +cd molpack + +# Rust library + CLI +cargo build --features cli + +# Python wheel (editable) +cd python && maturin develop --release +``` + +See [Development](development/) for tests, hooks, and contribution workflow. diff --git a/docs/zh/packmol_parity.md b/docs/zh/packmol_parity.md new file mode 100644 index 0000000..7f52d19 --- /dev/null +++ b/docs/zh/packmol_parity.md @@ -0,0 +1,115 @@ +# Packmol parity + +molpack tracks the original Packmol's behavior (`L. Martínez et al., +J. Comput. Chem. 2009`). Coordinates are not byte-identical with the +same seed, but functional equivalence is enforced on five canonical +workloads. + +For script syntax, use the official +[Packmol user guide](https://m3g.github.io/packmol/userguide.shtml). This page +is about parity checks, accepted differences, and molpack-specific extensions. + +## Paper figures + +### Canonical workloads + +![Packmol and molpack distribution checks across canonical examples](assets/images/paper-compatibility-distributions.png) + +The five Packmol examples are run unchanged through Packmol and molpack. +Coordinate identity is not the target; valid packing and matching spatial +distributions are. + +### Thread scaling + +![Multithreaded objective-kernel scaling](assets/images/paper-mt-scaling.png){ .molpack-figure-small } + +The fused objective-and-gradient kernel is too small to parallelize profitably +for tiny systems, then crosses over and scales on larger systems. + +## What is matched + +**Objective structure** + +- Geometric restraint penalties — equivalent of `comprest` / `gwalls`. +- Minimum-distance overlap penalty — equivalent of `computef` / + `fparc`. + +**Optimization workflow** + +- Initialization with constraint-only fitting (`initial` / `restmol` / + `swaptype`). +- `avoid_overlap` (on by default, faithful to `initial.f90`): initial + placements that land inside a fixed molecule are rejected. This matters + for dense solvation around a large fixed solute — without it a sizeable + fraction of solvent seeds inside the solute, roughly doubling the + initial overlap and slowing convergence by about an order of magnitude. +- Phased main optimization — per-type pre-compaction, then all-types. +- `movebad` heuristic for stalled molecules. +- Radius-scaling (`radscale`) decay across each phase. +- GENCAN / SPG / CG inner solver chain. +- Precision gate on `fdist` (overlap) and `frest` (restraint + violation). + +**Restraint vocabulary** + +- `inside`/`outside box`, `cube`, `sphere`, `ellipsoid`, `cylinder` +- `over plane` (above) / `below plane` +- fixed molecule placement + +All twelve box/cube/sphere/ellipsoid/cylinder/plane kinds lower through a single +`restraint_from_spec` table in `script::build`, so each is reachable from both +whole-molecule and `atoms … end atoms` blocks. The two Gaussian-surface kinds +(14/15) exist in the Rust/Python API but have no `.inp` keyword yet — Packmol's +Gaussian grammar is not pinned down here, and emitting a wrong parameter mapping +would silently mis-pack, so the parser rejects them rather than guessing. + +**Periodicity** + +- `pbc X Y Z` and `pbc X0 Y0 Z0 X1 Y1 Z1`, mirroring Packmol's + `getinp.f90`. The packer's cell grid is built directly from the + declared box. + +**Script parser strictness** + +- Unknown top-level keywords are rejected via + `ScriptError::UnknownKeyword`. A silently dropped `pbc` previously + triggered a 42 GB cell-grid allocation — strict parsing prevents + that class of failure. + +**Determinism** + +- Explicit seeds; identical seed values are used for paired Packmol + vs molpack runs in the regression suite. + +## Verification + +### Batch example validation + +`tests/examples_batch.rs` runs all five canonical workloads. Marked +`#[ignore]` because the run is expensive — invoke explicitly: + +```bash +cargo test -p molcrafts-molpack --release --test examples_batch -- --ignored +``` + +The test asserts: + +- atom and molecule counts match the expanded target specs; +- XYZ output is structurally sound; +- quantified violation metrics stay within tolerance / precision. + +### Violation metrics + +Each side reports `max_distance_violation`, `max_constraint_penalty`, +`violating_pairs`, and `violating_atoms`. Both packers must satisfy the +same thresholds. + +## Accepted differences + +- Packed coordinates are not bit-identical with Packmol, even at the same + seed — the inner solver and RNG are independent implementations. +- The two Gaussian-surface restraint kinds have no `.inp` keyword; they + are reachable only from the Rust/Python API. +- Acceptance is therefore functional rather than numerical: the same + restraints, the same conflict criteria, the same order of magnitude of + tolerance / precision, and comparable violation metrics. diff --git a/docs/zh/python/api-reference.md b/docs/zh/python/api-reference.md new file mode 100644 index 0000000..eeadf40 --- /dev/null +++ b/docs/zh/python/api-reference.md @@ -0,0 +1,416 @@ +# API Reference + +Import surface: + +```python +from molpack import ( + # Core + Target, Molpack, PackResult, StepInfo, + # Typed values + Angle, Axis, CenteringMode, + # Geometric (per-atom) restraints + InsideBoxRestraint, InsideSphereRestraint, OutsideSphereRestraint, + AbovePlaneRestraint, BelowPlaneRestraint, + # Collective (distribution-matching) restraints + GaussianPlane, GaussianPoint, + ExponentialPlane, ExponentialPoint, + TabulatedPlane, TabulatedPoint, + # In-loop relaxers + TorsionMcRelaxer, LBFGSRelaxer, + # Script loader (`.inp`) + ScriptJob, load_script, + # Parallel evaluation + rayon_enabled, num_threads, init_thread_pool, + # Protocols + Handler, Restraint, + # Errors + PackError, + ConstraintsFailedError, + MaxIterationsError, + NoTargetsError, + EmptyMoleculeError, + InvalidPBCBoxError, + ConflictingPeriodicBoxesError, +) + +# Post-pack whole-system relaxation (optional molpy/LAMMPS backend) +from molpack.relaxer import LAMMPSRelaxer +``` + +--- + +## `Angle` + +Angular quantity with explicit units at the call site. + +```python +Angle.from_degrees(30.0).radians # 0.5235... +Angle.from_radians(0.5).degrees # 28.6... +Angle.ZERO # identity rotation +``` + +--- + +## `Axis` + +Cartesian axis enum: `Axis.X`, `Axis.Y`, `Axis.Z`. + +--- + +## `CenteringMode` + +Centering policy for target reference coords: + +- `CenteringMode.AUTO` — free targets centered, fixed kept in place (default). +- `CenteringMode.CENTER` — always center. +- `CenteringMode.OFF` — keep input coords unchanged. + +--- + +## `Target` + +Molecule-type specification. Immutable — builder methods return new +instances. + +**Constructor** + +```python +Target(frame, count: int) +``` + +- `frame` — a `molrs.Frame` or `molpy.Frame` with atom columns `"x"`, + `"y"`, `"z"`, and `"element"` (or `"symbol"` for `molrs` PDB frames). + Resolved zero-copy via its FFI capsule; a plain dict is not accepted. +- `count` — number of copies to produce. + +**Builders** + +- `.with_name(name: str)` — display label. +- `.with_restraint(r)` — attach a restraint to every atom (stackable). + Accepts a geometric built-in, a collective (distribution-matching) + restraint, or any duck-typed `f`/`fg` object — see [Restraints](#restraints). +- `.with_atom_restraint(indices: Sequence[int], r)` — 0-based indices. +- `.with_relaxer(relaxer)` — attach an in-loop geometry relaxer + (`TorsionMcRelaxer` or `LBFGSRelaxer`); requires `count == 1`. See + [In-loop relaxers](#in-loop-relaxers). +- `.with_perturb_budget(n: int)` — per-target perturbation budget. +- `.with_centering(mode: CenteringMode)`. +- `.with_rotation_bound(axis: Axis, center: Angle, half_width: Angle)`. +- `.fixed_at(position: [x, y, z])` — pin the target. +- `.with_orientation((ax, ay, az))` — Euler tuple of `Angle`s; must + follow `fixed_at`. + +**Properties** + +- `.name : str | None` +- `.natoms : int` +- `.count : int` +- `.elements : list[str]` +- `.radii : list[float]` +- `.is_fixed : bool` + +--- + +## `Molpack` + +Orchestrator for the three-phase GENCAN optimizer. Zero-arg +constructor — all tuning is via `with_*` builders. + +**Constructor** + +```python +Molpack() +``` + +**Builders** + +- `.with_tolerance(t: float)` — minimum pairwise distance (Å; default 2.0). +- `.with_precision(p: float)` — convergence threshold (default 0.01). +- `.with_inner_iterations(n: int)` — GENCAN inner-loop cap (default 20). +- `.with_init_passes(n: int)` — init compaction passes (0 = auto). +- `.with_init_box_half_size(h: float)` — init placement bound (default 1000 Å). +- `.with_periodic_box(min: [x,y,z], max: [x,y,z])` — declare a + fully-periodic box directly on the packer (Packmol `pbc`). Alternative + to a periodic `InsideBoxRestraint`; see + [Periodic boundaries](guide/periodic-boundaries.md). +- `.with_perturb_fraction(f: float)` — stall perturbation fraction (default 0.05). +- `.with_random_perturb(enabled: bool)`. +- `.with_perturb(enabled: bool)` — master switch (default True). +- `.with_avoid_overlap(enabled: bool)` — reject initial random placements + overlapping a fixed molecule (Packmol `avoid_overlap`; default True). +- `.with_seed(seed: int)` — deterministic RNG (default 0). +- `.with_parallel_eval(enabled: bool)` — rayon-backed pair eval. Raises + `RuntimeError` if the wheel lacks the `rayon` feature (fail-fast). +- `.with_lammps_output(enabled: bool)` — enable LAMMPS-style screen output. +- `.with_log_level(level: str)` — `quiet`, `summary`, `progress`, or `verbose`. +- `.with_log_frequency(n: int)` — print every `n` outer steps. +- `.with_progress(enabled: bool)` — compatibility alias for progress output. +- `.with_handler(handler)` — attach a custom `Handler` (stackable). +- `.with_xyz_output(path: str, every: int = 1)` — record the packing + trajectory to a multi-frame extended-XYZ file (a frame every `every` + loops, loop 0 included). +- `.with_global_restraint(r)` — broadcast to every target (stackable). + +**Run** + +```python +.pack(targets: list[Target], max_loops: int = 200) -> molrs.Frame +.pack_with_report(targets: list[Target], max_loops: int = 200) -> PackResult +``` + +Raises a typed `PackError` subclass on failure. + +--- + +## `PackResult` + +Read-only output container returned by `pack_with_report()`. + +**Properties** + +- `.positions : ndarray (N, 3) float64` +- `.frame : molrs.Frame` — topology-complete frame (periodic box stamped if one was declared). +- `.elements : list[str]` +- `.natoms : int` +- `.converged : bool` +- `.fdist : float` +- `.frest : float` + +--- + +## `StepInfo` + +Read-only snapshot passed to `Handler.on_step`. + +```python +info.loop_idx # outer-loop iteration +info.max_loops +info.phase # phase index +info.total_phases +info.molecule_type # int | None +info.fdist +info.frest +info.improvement_pct +info.radscale +info.precision +info.relaxer_acceptance # list[tuple[int, float]] +``` + +--- + +## Restraints + +All restraint classes are immutable. Two families, both attached with +`target.with_restraint(r)` (or `Molpack.with_global_restraint(r)`): +**geometric** per-atom region restraints (below) and **collective** +distribution-matching restraints ([next section](#collective-distribution-matching-restraints)). + +### Geometric (per-atom) restraints + +Their `f`/`fg` see **one atom** at a time — a soft quadratic penalty that +is zero inside the region and rises outside. + +### `InsideBoxRestraint(min, max, periodic=(False, False, False))` + +Axis-aligned box. `periodic` is a 3-tuple of booleans declaring per-axis +periodicity — see [Periodic boundaries](guide/periodic-boundaries.md). + +### `InsideSphereRestraint(center, radius)` + +Closed ball. + +### `OutsideSphereRestraint(center, radius)` + +Complement of closed ball. + +### `AbovePlaneRestraint(normal, distance)` + +Half-space $\{\mathbf{x} : \mathbf{n}\cdot\mathbf{x} \ge d\}$. + +### `BelowPlaneRestraint(normal, distance)` + +Half-space $\{\mathbf{x} : \mathbf{n}\cdot\mathbf{x} \le d\}$. + +### Collective (distribution-matching) restraints + +Attached the same way (`target.with_restraint(r)`), but their `f`/`fg` +see **every copy** of the target at once and drive the species' spatial +distribution toward a target profile via a squared 1-D Wasserstein +(sorted-CDF) penalty. The reaction coordinate is either signed distance +to a plane ($\xi = \mathbf{x}\cdot\hat{\mathbf{n}} - \text{offset}$) or +radial distance to a point ($\xi = \lVert\mathbf{x} - \text{center}\rVert$). + +| Class | Constructor | Target profile | +|-------|-------------|----------------| +| `GaussianPlane` | `(normal, offset, strength, mu, sigma)` | Gaussian $N(\mu, \sigma)$ slab | +| `GaussianPoint` | `(center, strength, mu, sigma)` | Gaussian shell (radius `mu`, thickness `sigma`) | +| `ExponentialPlane` | `(normal, offset, strength, lambda_)` | diffuse layer $\propto e^{-\xi/\lambda}$, $\xi \ge 0$ | +| `ExponentialPoint` | `(center, strength, lambda_)` | radial atmosphere $\propto e^{-\xi/\lambda}$ | +| `TabulatedPlane` | `(normal, offset, strength, xs, rho)` | arbitrary planar prior on grid `(xs, rho)` | +| `TabulatedPoint` | `(center, strength, xs, rho)` | arbitrary radial prior on grid `(xs, rho)` | + +`strength` is the overall penalty multiplier $\lambda$. `sigma` / +`lambda_` must be `> 0`; tabulated `xs` must be strictly ascending +(≥ 2 points) with non-negative `rho` of positive total mass. Invalid +arguments raise `ValueError` at construction. + +--- + +## In-loop relaxers + +Relaxation-assisted packing: attach to a `Target` via +`target.with_relaxer(r)` to reshape a single molecule's reference +geometry *during* the pack loop. Both require the target's `count == 1` +(every copy shares the reference geometry the relaxer rewrites). Immutable. + +### `TorsionMcRelaxer(frame)` + +Monte-Carlo torsion-angle sampling — engine-free and force-field-free. +Rotatable bonds are detected from the frame's bond topology; proposed +rotations are accepted against the packer objective (Metropolis). + +- `.with_temperature(t: float)` — Metropolis temperature (default 1.0). +- `.with_steps(n: int)` — MC steps proposed per packing iteration (default 10). +- `.with_max_delta(rad: float)` — max per-step rotation, radians (default π/6). +- `.with_self_avoidance(radius: float)` — quadratic overlap penalty on + non-bonded intramolecular pairs closer than `2 * radius`; `0.0` + disables (default). + +```python +from molpack import Target, TorsionMcRelaxer + +chain = TorsionMcRelaxer(frame).with_steps(20).with_self_avoidance(1.5) +target = Target(frame, count=1).with_relaxer(chain) +``` + +### `LBFGSRelaxer(forcefield)` *(requires the `ff` feature)* + +Force-field L-BFGS geometry minimization. Built from a +`molrs.ForceField` / `molpy.ForceField` (zero-copy FFI capsule); the +potential is compiled lazily against the molecule's frame when packing +starts. + +- `.with_fmax(fmax: float)` — stop when the max per-atom force drops + below `fmax` (kcal/mol/Å; default 0.05). +- `.with_max_steps(max_steps: int)` — L-BFGS iteration cap per + relaxation call (default 500). + +--- + +## Post-pack relaxation + +### `LAMMPSRelaxer` *(from `molpack.relaxer`)* + +Whole-system relaxation of a **finished** packed box via LAMMPS — a +different axis from the in-loop relaxers above. A thin façade over +`molpy.engine.LAMMPSEngine`, imported **lazily** so `import molpack` +never requires molpy. + +```python +LAMMPSRelaxer( + ff, # a typified molpy ForceField + *, + executable: str | None = None, # None auto-detects lmp / lmp_serial / lmp_mpi + launcher: list[str] | None = None, # e.g. ["mpirun", "-np", "8"] + pair_style: str = "lj/cut/coul/cut 10.0", + atom_style: str = "full", + units: str = "real", + workdir: str | Path | None = None, +) +``` + +- `.minimize(target, **options) -> molrs.Frame` — energy minimisation. +- `.md(target, **options) -> molrs.Frame` — short MD settle. +- `.relax(target, **options)` — alias for `minimize` (also `__call__`). + +`target` is a `PackResult` (its `.frame` is used) or a bare +`molrs.Frame` carrying a periodic box; the input is never mutated. +Raises `ImportError` if `molcrafts-molpy` is not installed. + +--- + +## Script loader + +### `load_script(path, *, read_frame=None) -> ScriptJob` + +Parse and lower a Packmol-compatible `.inp` script. Template files are +read on the Python side (defaulting to `molrs.read_pdb` / `read_xyz` by +extension), so the wheel stays free of `molrs-io`. Pass `read_frame` +— a callable `(path, filetype) -> molrs.Frame` — to plug in another +loader (mdtraj, ASE, …). + +### `ScriptJob` + +Bundle returned by `load_script`. Access fields by attribute **or** +tuple-unpack it: + +```python +job = load_script("mix.inp") +packer, targets, output, nloop = load_script("mix.inp") # same object +``` + +- `.packer : Molpack` — pre-configured with the script's `tolerance`, + `seed`, and any `pbc` box. +- `.targets : list[Target]` +- `.output : pathlib.Path` — resolved output path. +- `.nloop : int` — outer-loop cap (`nloop` keyword; default 400). + +--- + +## Parallel evaluation + +The parallel evaluator runs on rayon's process-global thread pool, built +**once** per process and not resizable afterwards. + +- `rayon_enabled() -> bool` — was the wheel built with the `rayon` feature? +- `num_threads() -> int` — worker count the pool will use (1 on a serial build). +- `init_thread_pool(n: int)` — pin the pool size; must be called + **before** the first pack. Raises `RuntimeError` without the `rayon` + feature or if the pool was already initialized, and `ValueError` if + `n == 0`. For a scaling sweep, set the count once per process (or via + `RAYON_NUM_THREADS`) and launch one process per data point. + +--- + +## Duck-type protocols + +### `Restraint` + +```python +class Restraint(Protocol): + def f(self, x: tuple[float, float, float], scale: float, scale2: float) -> float: ... + def fg( + self, x: tuple[float, float, float], scale: float, scale2: float, + ) -> tuple[float, tuple[float, float, float]]: ... +``` + +### `Handler` + +```python +class Handler(Protocol): + def on_start(self, ntotat: int, ntotmol: int) -> None: ... + def on_step(self, info: StepInfo) -> bool | None: ... # True → stop + def on_finish(self) -> None: ... +``` + +All `Handler` methods are optional — missing ones are silently skipped. + +--- + +## Exceptions + +Typed hierarchy rooted at `PackError` (itself a `RuntimeError` +subclass). Catch the base to handle any packing failure uniformly. + +- `PackError` — base. +- `ConstraintsFailedError` — solver could not satisfy restraints even + without distance tolerances. +- `MaxIterationsError` — ran out of outer loops. +- `NoTargetsError` — empty target list. +- `EmptyMoleculeError` — a target has zero atoms. +- `InvalidPBCBoxError` — periodic box has a non-positive extent. +- `ConflictingPeriodicBoxesError` — two restraints declared + incompatible periodic boxes. + +`ValueError` / `TypeError` still surface on Python-side invariants +(bad atom indices, wrong restraint object, etc.). diff --git a/docs/zh/python/examples.md b/docs/zh/python/examples.md new file mode 100644 index 0000000..ff4c0d9 --- /dev/null +++ b/docs/zh/python/examples.md @@ -0,0 +1,81 @@ +# Examples + +Five canonical Packmol workloads ported to Python. Each lives under +`python/examples/` in the repo and is regression-tested against the +equivalent Rust example (same RNG seed → identical final coordinates). + +| Script | Packmol analogue | What it shows | +|-----------------------|-------------------|---------------| +| `pack_water_cube.py` | — | hello-world: 100 waters in a box, frame via `molrs.Frame.from_dict` | +| `pack_mixture.py` | `mixture.inp` | two species co-packed in one box | +| `pack_bilayer.py` | `bilayer.inp` | atom-subset restraints for layer-molecule orientation | +| `pack_interface.py` | `interface.inp` | fixed reference molecule + two solvents | +| `pack_spherical.py` | `spherical.inp` | nested spheres, double-layer shell | +| `pack_solvprotein.py` | `solvprotein.inp` | fixed solute solvated by water + ions | + +Install molpack once; the `molrs` dependency comes with it: + +```bash +pip install molcrafts-molpack +``` + +`pack_water_cube.py` builds its frame in memory with `molrs.Frame.from_dict` +(no PDB file); the others load PDB files via `molrs.read_pdb`. + +## Running + +```bash +cd molpack/python +pip install -e . +python examples/pack_water_cube.py # no PDB file +python examples/pack_mixture.py # requires molrs +``` + +Set `MOLPACK_EXAMPLE_PROGRESS=0` to suppress the per-iteration progress log. + +## Example: mixture + +The `pack_mixture.py` example reproduces Packmol's classic `mixture.inp`: + +```python +import molrs +from molpack import InsideBoxRestraint, Molpack, Target + +water_frame = molrs.read_pdb("water.pdb") +urea_frame = molrs.read_pdb("urea.pdb") + +box = InsideBoxRestraint([0, 0, 0], [40, 40, 40]) + +water = Target(water_frame, count=1000).with_name("water").with_restraint(box) +urea = Target(urea_frame, count=400).with_name("urea").with_restraint(box) + +packer = Molpack().with_tolerance(2.0).with_seed(1_234_567) +result = packer.pack_with_report([water, urea], max_loops=400) +print(f"converged={result.converged} natoms={result.natoms}") +``` + +## Example: water cube + +```python +import molrs +import numpy as np +from molpack import InsideBoxRestraint, Molpack, Target + +frame = molrs.Frame.from_dict({ + "blocks": { + "atoms": { + "x": np.array([0.00, 0.9572, -0.2400]), + "y": np.array([0.00, 0.0000, 0.9266]), + "z": np.zeros(3), + "element": ["O", "H", "H"], + } + } +}) + +water = Target(frame, count=100).with_name("water").with_restraint( + InsideBoxRestraint([0, 0, 0], [30, 30, 30]) +) +packer = Molpack().with_tolerance(2.0).with_log_level("quiet").with_seed(42) +result = packer.pack_with_report([water], max_loops=200) +print(f"converged={result.converged} natoms={result.natoms}") +``` diff --git a/docs/zh/python/getting-started.md b/docs/zh/python/getting-started.md new file mode 100644 index 0000000..d2e5e40 --- /dev/null +++ b/docs/zh/python/getting-started.md @@ -0,0 +1,131 @@ +# Quickstart + +A minimal end-to-end pack: 100 water molecules inside a 40 Å cube. + +## 1. Load a molecule + +Use `molrs.read_pdb` to load a template PDB file — the returned +`Frame` can be passed directly to `Target`: + +```python +import molrs + +frame = molrs.read_pdb("water.pdb") +``` + +No PDB file? Build a `molrs.Frame` from arrays with `Frame.from_dict`: + +```python +import molrs +import numpy as np + +frame = molrs.Frame.from_dict({ + "blocks": { + "atoms": { + "x": np.array([0.00, 0.96, -0.24]), + "y": np.array([0.00, 0.00, 0.93]), + "z": np.zeros(3), + "element": ["O", "H", "H"], + } + } +}) +``` + +## 2. Create a Target + +A `Target` bundles a molecule template with the number of copies to pack. +VdW radii are looked up automatically from element symbols (Bondi 1964). + +```python +from molpack import Target + +water = Target(frame, count=100).with_name("water") +``` + +Arguments: + +- `frame` — a `molrs.Frame` or `molpy.Frame` with columns `"x"`, `"y"`, + `"z"`, and `"element"` (or `"symbol"` for molrs PDB frames). +- `count` — number of copies to produce. + +A display label is optional — attach one via `.with_name("...")`. + +All builder methods are **immutable** — they return a new `Target`. + +## 3. Attach a restraint + +Every target needs at least one restraint — the geometric region it +should be packed into. + +```python +from molpack import InsideBoxRestraint + +water = water.with_restraint( + InsideBoxRestraint([0.0, 0.0, 0.0], [40.0, 40.0, 40.0]) +) +``` + +Five geometric built-in restraints: `InsideBoxRestraint`, +`InsideSphereRestraint`, `OutsideSphereRestraint`, `AbovePlaneRestraint`, +`BelowPlaneRestraint` — plus a family of collective +distribution-matching restraints. Stack multiple restraints with +repeated `.with_restraint()` calls — see +[Restraints](guide/restraints.md). + +## 4. Pack + +```python +from molpack import Molpack + +packer = Molpack().with_tolerance(2.0).with_seed(42) +frame = packer.pack([water], max_loops=200) + +print(frame["atoms"].nrows) +``` + +`pack()` returns a ready-to-use `molrs.Frame`. If you need structured +diagnostics, call `pack_with_report()` instead; it returns a +`PackResult` with `.converged`, `.fdist`, `.frest`, `.positions`, and +`.frame`. + +## 5. Save + +`molpack` does not write files directly — Frame is the canonical +output. Hand the returned frame to a writer: + +```python +import molrs + +molrs.write_xyz("packed.xyz", frame) +``` + +Or use `pack_with_report()` and write `result.frame` if you also need +the diagnostic fields. + +## Full script + +```python +import molrs +from molpack import InsideBoxRestraint, Molpack, Target + +frame = molrs.read_pdb("water.pdb") + +water = ( + Target(frame, count=100) + .with_name("water") + .with_restraint(InsideBoxRestraint([0.0, 0.0, 0.0], [40.0, 40.0, 40.0])) +) + +frame = ( + Molpack().with_tolerance(2.0).with_seed(42).pack([water], max_loops=200) +) + +print(f"packed {frame['atoms'].nrows} atoms") +``` + +## Next steps + +- [Targets](guide/targets.md) — orientation, centering, fixed placement. +- [Restraints](guide/restraints.md) — per-atom scoping, stacking. +- [Packer](guide/packer.md) — all builder options. +- [Examples](examples.md) — five complete Packmol workloads. diff --git a/docs/zh/python/guide/packer.md b/docs/zh/python/guide/packer.md new file mode 100644 index 0000000..323b70a --- /dev/null +++ b/docs/zh/python/guide/packer.md @@ -0,0 +1,140 @@ +# Packer + +`Molpack` drives the GENCAN-based three-phase optimizer. All tuning +is through `with_*` builder methods — the constructor takes no +arguments. + +## Constructor + +```python +from molpack import Molpack + +packer = Molpack() +``` + +All defaults match Packmol's reference behaviour. Override any of +them via the builders below. + +## Builder methods + +Every builder returns a **new** `Molpack`: + +```python +packer = ( + Molpack() + .with_tolerance(2.0) # minimum allowed pairwise distance (Å) + .with_precision(0.01) # convergence threshold on fdist and frest + .with_inner_iterations(20) # GENCAN inner-loop cap (Packmol `maxit`) + .with_init_passes(0) # initial compaction passes (Packmol `nloop0`; 0 = auto) + .with_init_box_half_size(1000) # hard bound on init placement (Packmol `sidemax`) + .with_perturb_fraction(0.05) # fraction of atoms re-sampled per stall + .with_random_perturb(False) # randomize perturbation target selection + .with_perturb(True) # enable the stall-perturbation heuristic + .with_seed(42) # deterministic RNG + .with_parallel_eval(False) # rayon-backed pair-kernel eval (opt-in) + .with_lammps_output(True) # attach LAMMPS-style screen output + .with_log_level("progress") # quiet | summary | progress | verbose + .with_log_frequency(1) # print every N outer steps +) +``` + +Use `.with_lammps_output(False)` or `.with_log_level("quiet")` to run +silently (the default). `.with_progress()` is kept as a compatibility +alias for enabling/disabling progress output. + +A few more builders cover specific needs: + +```python +packer = ( + packer + .with_periodic_box([0, 0, 0], [30, 30, 30]) # fully-periodic cell (Packmol `pbc`) + .with_avoid_overlap(True) # reject init placements onto a fixed molecule + .with_xyz_output("traj.xyz", every=5) # record the packing trajectory +) +``` + +## Global restraints + +Attach a single restraint to every target in a pack: + +```python +packer = packer.with_global_restraint( + InsideBoxRestraint([0, 0, 0], [40, 40, 40]) +) +``` + +Semantically equivalent to calling `.with_restraint(r)` on every +target. + +## Handlers + +Attach any object implementing some subset of `on_start(ntotat, ntotmol)`, +`on_step(info) -> bool | None`, `on_finish()`: + +```python +class MyHandler: + def on_step(self, info): + print(f"phase={info.phase} loop={info.loop_idx} fdist={info.fdist:.3f}") + return None # or True to request early stop + +packer = packer.with_handler(MyHandler()) +``` + +Returning `True` from `on_step` halts the run at the next check. See +the `Handler` Protocol in `molpack`. + +## Periodic boundaries + +PBC can be declared per-axis on an `InsideBoxRestraint`, or as a +fully-periodic cell directly on the packer via +`.with_periodic_box(min, max)`. See +[Periodic boundaries](periodic-boundaries.md). + +## Running + +```python +frame = packer.pack(targets, max_loops=200) +``` + +- `targets` — list of `Target` objects (must be non-empty). +- `max_loops` — per-phase outer-iteration budget. + +Raises one of the typed `PackError` subclasses on failure +(`NoTargetsError`, `InvalidPBCBoxError`, +`ConflictingPeriodicBoxesError`, …). + +`pack()` returns a `molrs.Frame`. To retrieve structured diagnostics, +use `pack_with_report()`: + +```python +result = packer.pack_with_report(targets, max_loops=200) +``` + +## PackResult + +```python +result.positions # (N, 3) float64 ndarray — packed coordinates +result.frame # molrs.Frame — topology-complete packed frame +result.elements # list[str] — one entry per atom +result.natoms # int +result.converged # bool — True iff both fdist and frest < precision +result.fdist # float — final distance-violation sum +result.frest # float — final restraint-violation sum +``` + +Inspect convergence: + +```python +if not result.converged: + print(f"not converged: fdist={result.fdist:.4f} frest={result.frest:.4f}") +``` + +`PackResult.frame` is the same Frame returned by `pack()`. Pass it to a +writer of your choice (e.g. `molrs.write_pdb`). molpack does **not** +provide writers. + +## Reproducibility + +Packing is deterministic for a given `(targets, tolerance, precision, +seed)` tuple. Capture the builder chain and `max_loops` to reproduce +a result later. diff --git a/docs/zh/python/guide/periodic-boundaries.md b/docs/zh/python/guide/periodic-boundaries.md new file mode 100644 index 0000000..3a521a6 --- /dev/null +++ b/docs/zh/python/guide/periodic-boundaries.md @@ -0,0 +1,108 @@ +# Periodic boundaries + +By default the packer works under **free boundary conditions** — atoms +are not wrapped and the only geometric limits come from the restraints +you attach. Use periodic boundaries (PBC) when packing for MD input. + +## Enabling PBC + +PBC is declared on the `InsideBoxRestraint` via the `periodic` keyword: + +```python +from molpack import InsideBoxRestraint + +# Fully periodic orthorhombic cell +box = InsideBoxRestraint( + [0.0, 0.0, 0.0], + [30.0, 30.0, 30.0], + periodic=(True, True, True), +) +``` + +`periodic` is a 3-tuple of booleans — one per axis. Only orthorhombic +cells are supported. + +Per-axis PBC is possible — e.g. slab geometry with in-plane PBC and +open Z: + +```python +slab = InsideBoxRestraint( + [0.0, 0.0, 0.0], + [30.0, 30.0, 100.0], + periodic=(True, True, False), +) +``` + +### Declaring PBC on the packer + +For a **fully-periodic** cell you can skip the restraint and declare the +box directly on the packer (Packmol's `pbc` keyword): + +```python +from molpack import Molpack + +packer = Molpack().with_periodic_box([0.0, 0.0, 0.0], [30.0, 30.0, 30.0]) +``` + +This is equivalent to a single `InsideBoxRestraint(..., periodic=(True, +True, True))` broadcast over the system. Use the restraint form when you +need per-axis control or a soft confining region; use `with_periodic_box` +when the cell is simply fully periodic. + +## Semantics + +Under PBC, the pairwise distance evaluator applies minimum-image +wrapping on the periodic axes, so atoms near opposite faces of the +cell "see" each other through the periodic images. The `tolerance` +setting still applies and is checked against the wrapped distance. + +Restraints themselves (`InsideBoxRestraint`, `InsideSphereRestraint`, +…) are evaluated in the **unwrapped** frame — they describe the +geometric region as defined, regardless of the periodic cell. + +## System-wide PBC derivation + +At `pack()` time the packer scans every restraint on every target for +a declared periodic box. The rules are: + +- **Zero declarations** — non-periodic run. +- **One declaration** — its bounds define the system PBC. +- **Multiple declarations** — they must all agree (same bounds, same + per-axis flags). Any mismatch raises `ConflictingPeriodicBoxesError`. + +## Errors + +A zero-length axis on a periodic box, or `max < min` on any axis, +raises `InvalidPBCBoxError` at `pack()` time: + +```python +from molpack import InvalidPBCBoxError + +try: + packer.pack(...) +except InvalidPBCBoxError as e: + ... +``` + +Both typed errors inherit from `molpack.PackError` (which itself is +a `RuntimeError` subclass) — a blanket `except PackError` catches any +packing failure. + +## Choosing a box + +A common pattern: pack into a single periodic `InsideBoxRestraint` +matching the desired cell. The restraint confines atoms softly and +simultaneously declares the PBC: + +```python +cell_min = [0.0, 0.0, 0.0] +cell_max = [30.0, 30.0, 30.0] + +box = InsideBoxRestraint(cell_min, cell_max, periodic=(True, True, True)) +target = target.with_restraint(box) + +result = Molpack().with_seed(42).pack([target], max_loops=200) +``` + +Or broadcast it globally via `Molpack.with_global_restraint(box)` when +several species share the same cell. diff --git a/docs/zh/python/guide/restraints.md b/docs/zh/python/guide/restraints.md new file mode 100644 index 0000000..2364c9e --- /dev/null +++ b/docs/zh/python/guide/restraints.md @@ -0,0 +1,165 @@ +# Restraints + +Restraints are geometric regions (or half-spaces) that every atom of a +target — or a chosen subset — must lie inside. molpack ships two +families of built-in restraints: five **geometric** region restraints +(below), and six **collective** distribution-matching restraints +([further down](#collective-distribution-matching-restraints)). Both +attach the same way, via `.with_restraint()`. + +## Geometric built-ins + +| Class | Constructor arguments | Meaning | +|--------------------------|------------------------------------------------|---------| +| `InsideBoxRestraint` | `min: [x,y,z]`, `max: [x,y,z]`, `periodic=(False, False, False)` | axis-aligned box | +| `InsideSphereRestraint` | `center: [x,y,z]`, `radius: float` | closed ball | +| `OutsideSphereRestraint` | `center: [x,y,z]`, `radius: float` | complement of closed ball | +| `AbovePlaneRestraint` | `normal: [nx,ny,nz]`, `distance: float` | half-space $\mathbf{n}\cdot\mathbf{x} \ge d$ | +| `BelowPlaneRestraint` | `normal: [nx,ny,nz]`, `distance: float` | half-space $\mathbf{n}\cdot\mathbf{x} \le d$ | + +All arguments are standard Python floats / lists. + +```python +from molpack import ( + AbovePlaneRestraint, + BelowPlaneRestraint, + InsideBoxRestraint, + InsideSphereRestraint, + OutsideSphereRestraint, +) + +box = InsideBoxRestraint([0, 0, 0], [40, 40, 40]) +ball = InsideSphereRestraint([0, 0, 0], 20.0) +shell = OutsideSphereRestraint([0, 0, 0], 10.0) +above = AbovePlaneRestraint(normal=[0, 0, 1], distance=5.0) +below = BelowPlaneRestraint(normal=[0, 0, 1], distance=20.0) +``` + +## Periodic boxes + +`InsideBoxRestraint` doubles as the PBC declaration. Passing a +`periodic` tuple turns any subset of axes periodic: + +```python +InsideBoxRestraint([0, 0, 0], [30, 30, 30], periodic=(True, True, True)) +``` + +See [Periodic boundaries](periodic-boundaries.md) for the full +semantics and validation rules. + +## Collective (distribution-matching) restraints + +Where a geometric restraint penalises **each atom** against a region, +a collective restraint sees **every copy of the target at once** and +drives the species' spatial *distribution* toward a target profile +(via a squared 1-D Wasserstein penalty). Six built-ins cover Gaussian, +exponential, and arbitrary-tabulated priors along either a plane or a +radius — e.g. a Gaussian slab centred at `z = 20`: + +```python +from molpack import GaussianPlane + +slab = GaussianPlane(normal=[0, 0, 1], offset=0.0, strength=1.0, mu=20.0, sigma=3.0) +target = Target(frame, count=200).with_restraint(slab) +``` + +The full list and constructor signatures are in the +[API reference](../api-reference.md#collective-distribution-matching-restraints). + +## Stacking multiple restraints + +Apply several restraints to the same target by chaining `.with_restraint()`: + +```python +target = ( + Target(frame, count=500) + .with_name("water") + .with_restraint(InsideBoxRestraint([0, 0, 0], [40, 40, 40])) + .with_restraint(OutsideSphereRestraint([20, 20, 20], 5.0)) +) +``` + +Each call attaches an independent restraint. All active restraints are +evaluated at every optimizer step. + +## Scopes + +A restraint can be applied at two scopes: + +- **Whole target** — `target.with_restraint(r)` — penalises every atom. +- **Atom subset** — `target.with_atom_restraint([0, 1, 2], r)` — + penalises only the listed atoms (0-based indices). + +Example — a bilayer: pin heads above z=12, tails below z=2: + +```python +lipid = ( + Target(frame, count=20) + .with_name("lipid") + .with_restraint(InsideBoxRestraint([0, 0, 0], [40, 40, 14])) + .with_atom_restraint([0, 1], AbovePlaneRestraint([0, 0, 1], 12.0)) + .with_atom_restraint([30, 31], BelowPlaneRestraint([0, 0, 1], 2.0)) +) +``` + +## Global restraints + +To apply one restraint to every target in a pack, attach it on the +packer: + +```python +packer = ( + Molpack() + .with_global_restraint(InsideBoxRestraint([0, 0, 0], [40, 40, 40])) +) +``` + +Semantically equivalent to calling `.with_restraint(r)` on every +target, but avoids the duplication. + +## Custom restraints + +Pass any object implementing `f(x, scale, scale2) -> float` and +`fg(x, scale, scale2) -> (float, (gx, gy, gz))`. See the +`Restraint` Protocol in `molpack` for the full contract. + +```python +class SphereRestraint: + def __init__(self, center, radius): + self.c = np.asarray(center) + self.r = radius + def f(self, x, scale, scale2): + d = np.linalg.norm(np.asarray(x) - self.c) - self.r + return scale2 * d * d if d > 0 else 0.0 + def fg(self, x, scale, scale2): + rel = np.asarray(x) - self.c + d = float(np.linalg.norm(rel)) + over = d - self.r + if over <= 0: + return 0.0, (0.0, 0.0, 0.0) + factor = 2 * scale2 * over / d + return scale2 * over * over, tuple(factor * rel) + +target = Target(frame, count=10).with_restraint(SphereRestraint([0,0,0], 6.0)) +``` + +## Semantics + +Every restraint contributes a continuously differentiable penalty +$f_{\text{rest}}(\mathbf{x})$ that is zero inside the allowed region +and rises quadratically outside. The aggregate objective minimised by +the packer is: + +$$ +U(\mathbf{x}) = f_{\text{dist}}(\mathbf{x}) + f_{\text{rest}}(\mathbf{x}) +$$ + +where $f_{\text{dist}}$ is the pairwise distance-violation sum for the +user-specified `tolerance`. Convergence is declared when both fall +below `precision`. + +!!! note "Restraints vs hard constraints" + All built-in restraints are *soft penalties* — the optimizer may + momentarily produce a violating configuration while searching. Hard + geometric constraints (frozen placement, rotation bounds) are set + on the `Target` directly via `fixed_at` and `with_rotation_bound`. diff --git a/docs/zh/python/guide/targets.md b/docs/zh/python/guide/targets.md new file mode 100644 index 0000000..a1eb5aa --- /dev/null +++ b/docs/zh/python/guide/targets.md @@ -0,0 +1,187 @@ +# Targets + +A `Target` describes one type of molecule to pack: its template +geometry, element symbols, and the number of copies to produce. +VdW radii are resolved automatically from element symbols via the +Bondi (1964) table. + +## Construction + +```python +from molpack import Target + +target = Target(frame, count) +``` + +- `frame` — a `molrs.Frame` or `molpy.Frame`, resolved zero-copy via its + FFI capsule. Supported sources: + + | Source | Element column | + |--------|---------------| + | `molrs.read_pdb(path)` | `"symbol"` | + | `molrs.read_xyz(path)` | `"element"` | + | `molrs.Frame.from_dict({"blocks": {"atoms": {...}}})` | `"element"` | + | `molpy.Frame` | `"element"` | + +- `count` — number of copies to produce. + +A display label is optional: + +```python +target = Target(frame, count).with_name("water") +``` + +Build a frame in memory (no PDB file) with `molrs.Frame.from_dict`: + +```python +import molrs +import numpy as np + +frame = molrs.Frame.from_dict({ + "blocks": { + "atoms": { + "x": np.array([0.00, 0.96, -0.24]), + "y": np.array([0.00, 0.00, 0.93]), + "z": np.zeros(3), + "element": ["O", "H", "H"], + } + } +}) +water = Target(frame, count=100).with_name("water") +``` + +## Read-only properties + +```python +target.name # Optional[str] +target.natoms # number of template atoms +target.count # requested copies +target.elements # list[str] +target.radii # list[float] +target.is_fixed # True if placement is frozen (see below) +``` + +All builder methods are **immutable** — they return a new `Target`. + +## Centering + +The default is [`CenteringMode.AUTO`](../api-reference.md#centeringmode) — free +targets are centered on their geometric center before packing; fixed +targets are kept in place. Override explicitly: + +```python +from molpack import CenteringMode + +target = target.with_centering(CenteringMode.CENTER) # always center +target = target.with_centering(CenteringMode.OFF) # keep input coords +``` + +## Fixed placement + +Pin a target at a specific location (e.g. a fixed reference molecule): + +```python +from molpack import Angle + +target = target.fixed_at([10.0, 20.0, 30.0]) + +# optional Euler orientation — three Angle values in Packmol's +# eulerfixed convention +target = ( + target.fixed_at([10.0, 20.0, 30.0]) + .with_orientation(( + Angle.from_degrees(0.0), + Angle.from_radians(1.57), + Angle.ZERO, + )) +) +``` + +Fixed targets are excluded from the optimizer but still contribute to +distance exclusion against other species. + +## Rotation bounds + +Restrict the rotational search window about each axis: + +```python +from molpack import Angle, Axis + +target = ( + target + .with_rotation_bound(Axis.X, Angle.from_degrees(0.0), Angle.from_degrees(15.0)) + .with_rotation_bound(Axis.Y, Angle.from_degrees(90.0), Angle.from_degrees(10.0)) + .with_rotation_bound(Axis.Z, Angle.from_degrees(0.0), Angle.from_degrees(5.0)) +) +``` + +`Angle` makes units explicit: use `Angle.from_degrees(...)` or +`Angle.from_radians(...)` — raw floats are rejected. + +## Attaching restraints + +### All atoms of the target + +```python +from molpack import InsideBoxRestraint + +target = target.with_restraint( + InsideBoxRestraint([0, 0, 0], [40, 40, 40]) +) +``` + +Stack multiple restraints by calling `.with_restraint()` again: + +```python +target = ( + target + .with_restraint(InsideBoxRestraint([0, 0, 0], [40, 40, 40])) + .with_restraint(OutsideSphereRestraint([20, 20, 20], 5.0)) +) +``` + +### A subset of atoms + +```python +from molpack import AbovePlaneRestraint, BelowPlaneRestraint + +target = target.with_atom_restraint( + [30, 31], # 0-based Rust-native indices + BelowPlaneRestraint([0.0, 0.0, 1.0], 2.0), +) +``` + +!!! note "0-based indexing" + `with_atom_restraint` uses **0-based** indices, matching Rust + convention. If you are porting from a Packmol `.inp` file (which + uses 1-based indices), subtract 1 at the call site. + +## Relaxation-assisted packing + +Attach an in-loop relaxer to reshape a flexible molecule's reference +geometry *during* packing — useful for long chains that must fold to +fit. `with_relaxer` requires `count == 1` (every copy shares the +reference geometry the relaxer rewrites): + +```python +from molpack import Target, TorsionMcRelaxer + +chain = TorsionMcRelaxer(frame).with_steps(20).with_self_avoidance(1.5) +target = Target(frame, count=1).with_relaxer(chain) +``` + +Two relaxers ship built in: `TorsionMcRelaxer` (engine-free Monte-Carlo +torsion sampling, always available) and `LBFGSRelaxer` (force-field +L-BFGS, `ff` feature). See +[In-loop relaxers](../api-reference.md#in-loop-relaxers) for their +options. + +## Per-target solver budget + +Override the maximum perturbation budget for this target: + +```python +target = target.with_perturb_budget(50) # default: derived from count +``` + +Useful when one species is significantly harder to place than the rest. diff --git a/docs/zh/python/index.md b/docs/zh/python/index.md new file mode 100644 index 0000000..8c671ce --- /dev/null +++ b/docs/zh/python/index.md @@ -0,0 +1,71 @@ +# Python + +Packmol-grade molecular packing in Rust, with a first-class Python binding. + +`molpack` arranges $N$ molecule types under geometric restraints without +overlaps. The engine is a faithful port of Packmol's GENCAN-driven algorithm +(Martínez *et al.* 2009); five canonical workloads stay pinned against Packmol +reference output. + +## At a glance + +```python +import molrs +from molpack import InsideBoxRestraint, Molpack, Target + +frame = molrs.read_pdb("water.pdb") + +water = ( + Target(frame, count=100) + .with_name("water") + .with_restraint(InsideBoxRestraint([0.0, 0.0, 0.0], [40.0, 40.0, 40.0])) +) + +packer = Molpack().with_tolerance(2.0).with_seed(42) +frame = packer.pack([water], max_loops=200) +print(f"packed {frame['atoms'].nrows} atoms") +``` + +## Next steps + + + +## See also + +- Rust crate: [`molcrafts-molpack`](https://crates.io/crates/molcrafts-molpack) — + the underlying engine +- [`molcrafts-molrs`](https://pypi.org/project/molcrafts-molrs/) — frame I/O and + the `Frame` data model (installed as a dependency) diff --git a/docs/zh/python/installation.md b/docs/zh/python/installation.md new file mode 100644 index 0000000..664300a --- /dev/null +++ b/docs/zh/python/installation.md @@ -0,0 +1,68 @@ +# Installation + +## From PyPI + +```bash +pip install molcrafts-molpack +``` + +The PyPI package `molcrafts-molpack` installs a Python module named `molpack`: + +```python +import molpack +``` + +Pre-built wheels are published for CPython **3.12** and **3.13** on Linux +(manylinux x86-64) and macOS (universal2). Other platforms fall back to the +sdist and need a Rust toolchain to build locally. + +## Frame I/O + +`molcrafts-molpack` installs `molcrafts-molrs` as a dependency. The +importable `molrs` module provides the frame type plus PDB and XYZ readers: + +```bash +pip install molcrafts-molpack +``` + +```python +import molrs +from molpack import InsideBoxRestraint, Molpack, Target + +frame = molrs.read_pdb("water.pdb") +water = ( + Target(frame, count=100) + .with_name("water") + .with_restraint(InsideBoxRestraint([0.0, 0.0, 0.0], [40.0, 40.0, 40.0])) +) +frame = Molpack().with_seed(42).pack([water], max_loops=200) +``` + +`Target` takes a `molrs.Frame` or `molpy.Frame`, resolved zero-copy through +its FFI capsule. + +## Building from source + +Requires a Rust toolchain (1.91+) and `maturin`: + +```bash +git clone https://github.com/MolCrafts/molpack +cd molpack/python +pip install maturin +maturin develop --release +``` + +This builds against the local `molpack` Rust crate under `../`. + +## Verification + +```python +import molrs +from molpack import Target + +frame = molrs.Frame.from_dict({ + "blocks": {"atoms": {"x": [0.0], "y": [0.0], "z": [0.0], "element": ["O"]}} +}) +target = Target(frame, count=1).with_name("mol") +print(target) # Target(natoms=1, count=1, name=Some("mol")) +``` diff --git a/docs/zh/rust/examples.md b/docs/zh/rust/examples.md new file mode 100644 index 0000000..80eae63 --- /dev/null +++ b/docs/zh/rust/examples.md @@ -0,0 +1,30 @@ +# Examples + +The Rust examples live under `examples/pack_*` and use the `io` feature to read +the bundled structure files. + +```bash +cargo run --release --features io --example pack_mixture +cargo run --release --features io --example pack_bilayer +cargo run --release --features io --example pack_interface +cargo run --release --features io --example pack_spherical +cargo run --release --features io --example pack_solvprotein +``` + +| Example | Demonstrates | +|---|---| +| `pack_mixture` | Two-component water/urea cube. | +| `pack_bilayer` | Plane restraints and atom-subset orientation. | +| `pack_interface` | Solvents around a fixed molecule. | +| `pack_spherical` | Nested spherical shells. | +| `pack_solvprotein` | Fixed protein solvation with water and ions. | + +Optional progress and trajectory output are controlled by environment +variables: + +```bash +MOLRS_PACK_EXAMPLE_PROGRESS=1 cargo run --release --features io --example pack_mixture +MOLRS_PACK_EXAMPLE_XYZ=1 cargo run --release --features io --example pack_mixture +``` + +For Packmol-style input files, use the matching [CLI examples](../cli/examples.md). diff --git a/docs/zh/rust/getting-started.md b/docs/zh/rust/getting-started.md new file mode 100644 index 0000000..ff73b7e --- /dev/null +++ b/docs/zh/rust/getting-started.md @@ -0,0 +1,68 @@ +# Quickstart + +A Rust packing job has three parts: + +1. Build one `Target` per molecule species. +2. Attach at least one spatial restraint to each mobile target, or use a + packer-level global restraint. +3. Run `Molpack::pack(&targets, max_loops)`. + +## One molecule type in a box + +```rust +use molpack::{InsideBoxRestraint, Molpack, Target}; + +let water_positions = [ + [0.0, 0.0, 0.0], + [0.96, 0.0, 0.0], + [-0.24, 0.93, 0.0], +]; +let water_radii = [1.52, 1.20, 1.20]; + +let water = Target::from_coords(&water_positions, &water_radii, 100) + .with_name("water") + .with_restraint(InsideBoxRestraint::new( + [0.0, 0.0, 0.0], + [40.0, 40.0, 40.0], + [false, false, false], + )); + +let mut packer = Molpack::new() + .with_tolerance(2.0) + .with_seed(42); + +let frame = packer.pack(&[water], 200)?; +let natoms = frame.get("atoms").and_then(|b| b.nrows()).unwrap_or(0); +println!("packed {natoms} atoms"); +``` + +`pack()` returns a packed `molrs::Frame`. Use `pack_with_report()` if you also +need convergence fields: + +```rust +let mut packer = Molpack::new().with_seed(42); +let result = packer.pack_with_report(&targets, 200)?; +println!("converged={} fdist={} frest={}", result.converged, result.fdist, result.frest); +``` + +## Builder defaults + +Every tuning knob except `max_loops` has a Packmol-compatible default. Set a +builder value only when you need to change the default: + +```rust +let packer = Molpack::new() + .with_tolerance(2.0) + .with_precision(0.01) + .with_inner_iterations(20) + .with_seed(42); +``` + +`max_loops` is positional because the right iteration budget depends on system +size and packing difficulty. + +## Targets are snapshots + +`Target` is a builder value. `pack()` snapshots the target slice at call time; +mutating or rebuilding a target after that does not affect an already running +pack. diff --git a/docs/zh/rust/handlers-relaxers.md b/docs/zh/rust/handlers-relaxers.md new file mode 100644 index 0000000..c9e41fc --- /dev/null +++ b/docs/zh/rust/handlers-relaxers.md @@ -0,0 +1,72 @@ +# Handlers and Relaxers + +Handlers observe a packing run. Relaxers modify a target's reference geometry +between optimizer iterations. + +## Screen output + +Enable LAMMPS-style progress output through the builder: + +```rust +use molpack::{Molpack, MolpackLogLevel}; + +let mut packer = Molpack::new() + .with_log_level(MolpackLogLevel::Progress) + .with_log_frequency(10); +``` + +The CLI enables screen output by default; library callers stay quiet unless you +opt in. + +## Built-in handlers + +```rust +use molpack::{EarlyStopHandler, Molpack, XYZHandler}; + +let mut packer = Molpack::new() + .with_handler(XYZHandler::new("traj.xyz", 10)) + .with_handler(EarlyStopHandler::new(1e-4)); +``` + +Use handlers for progress logs, trajectory snapshots, custom observation, and +early stop. Handler callbacks receive an immutable `PackContext` view; they do +not mutate packer state. + +## Custom handlers + +Implement the `Handler` trait when you need structured events from a run: + +```rust +use molpack::{Handler, PackContext, StepInfo}; + +#[derive(Debug)] +struct WatchFdist; + +impl Handler for WatchFdist { + fn on_step(&mut self, info: &StepInfo, _sys: &PackContext) { + eprintln!("phase={} loop={} fdist={}", info.phase, info.loop_idx, info.fdist); + } +} +``` + +See [Extending](../extending.md) for a full custom-handler walkthrough. + +## Relaxers + +Relaxers update a molecule's reference geometry during packing. They are useful +for flexible molecules that need to sample torsions while being placed. + +```rust +use molpack::{InsideSphereRestraint, Target, TorsionMcRelaxer}; + +let target = Target::from_coords(positions, radii, 1) + .with_restraint(InsideSphereRestraint::new([0.0; 3], 20.0)) + .with_relaxer( + TorsionMcRelaxer::new(&graph) + .with_temperature(0.5) + .with_steps(20), + ); +``` + +Relaxers require `count == 1` because every copy of a target shares one +reference geometry. diff --git a/docs/zh/rust/index.md b/docs/zh/rust/index.md new file mode 100644 index 0000000..4ee630e --- /dev/null +++ b/docs/zh/rust/index.md @@ -0,0 +1,46 @@ +# Rust + +Use the Rust API when packing is part of a Rust program, when you need +structured convergence diagnostics, or when you are extending molpack itself. + +```rust +use molpack::{InsideBoxRestraint, Molpack, Target}; + +let positions = [[0.0, 0.0, 0.0], [0.96, 0.0, 0.0], [-0.24, 0.93, 0.0]]; +let radii = [1.52, 1.20, 1.20]; + +let water = Target::from_coords(&positions, &radii, 100) + .with_name("water") + .with_restraint(InsideBoxRestraint::new( + [0.0, 0.0, 0.0], + [40.0, 40.0, 40.0], + [false, false, false], + )); + +let mut packer = Molpack::new().with_seed(42); +let frame = packer.pack(&[water], 200)?; +``` + +## Install + +```bash +cargo add molcrafts-molpack +``` + +Feature flags: + +| Feature | Enables | +|---|---| +| `io` | PDB, XYZ, SDF, and LAMMPS readers through `molrs_io`. | +| `cli` | The `molpack` binary plus `io`. | +| `rayon` | Parallel objective evaluation. | +| `ff` | Force-field-backed relaxers. | + +## Pages + +- [Quickstart](getting-started.md) walks through a first target and pack. +- [Restraints and PBC](restraints-and-pbc.md) explains target-level, + atom-subset, global, and periodic restraints. +- [Handlers and Relaxers](handlers-relaxers.md) covers progress output, + observers, early stop, trajectory dumping, and in-loop conformation sampling. +- [Examples](examples.md) lists the checked-in Rust workloads. diff --git a/docs/zh/rust/restraints-and-pbc.md b/docs/zh/rust/restraints-and-pbc.md new file mode 100644 index 0000000..31ef250 --- /dev/null +++ b/docs/zh/rust/restraints-and-pbc.md @@ -0,0 +1,84 @@ +# Restraints and PBC + +Restraints are soft penalties that guide atoms into allowed regions. They can +be attached to a target, to a subset of atoms on each target copy, or globally +on the packer. + +## Whole-target restraints + +```rust +use molpack::{InsideBoxRestraint, Target}; + +let target = Target::from_coords(positions, radii, 100) + .with_restraint(InsideBoxRestraint::new( + [0.0, 0.0, 0.0], + [40.0, 40.0, 40.0], + [false, false, false], + )); +``` + +Built-in geometric restraints include boxes, spheres, outside-sphere +exclusions, and plane half-spaces. Collective restraints match an entire +species to a distribution profile; see [Concepts](../concepts.md). + +## Atom-subset restraints + +Atom-subset restraints apply to selected atoms of every copy. Indices are +0-based: + +```rust +use molpack::{BelowPlaneRestraint, Target}; + +let target = Target::from_coords(positions, radii, 100) + .with_atom_restraint( + &[0, 1], + BelowPlaneRestraint::new([0.0, 0.0, 1.0], 2.0), + ); +``` + +If you are translating from a Packmol `.inp` `atoms ... end atoms` block, +subtract 1 from each atom index. + +## Global restraints + +Attach a restraint to every target through the packer: + +```rust +use molpack::{InsideSphereRestraint, Molpack, Target}; + +let mut packer = Molpack::new() + .with_global_restraint(InsideSphereRestraint::new([20.0, 20.0, 20.0], 30.0)); + +let frame = packer.pack(&[a, b], 200)?; +``` + +This is equivalent to cloning the same restraint onto every target before +packing. + +## Periodic boxes + +There are two ways to declare periodic boundary conditions. + +For fully periodic boxes, set PBC on the packer: + +```rust +use molpack::Molpack; + +let packer = Molpack::new().with_periodic_box([0.0; 3], [30.0; 3]); +``` + +For per-axis control, set periodic flags on `InsideBoxRestraint`: + +```rust +use molpack::{InsideBoxRestraint, Target}; + +let slab = Target::from_coords(positions, radii, 100) + .with_restraint(InsideBoxRestraint::new( + [0.0, 0.0, 0.0], + [30.0, 30.0, 100.0], + [true, true, false], + )); +``` + +Multiple PBC declarations must agree exactly. Invalid or conflicting boxes +return typed `PackError` variants. diff --git a/python/pyproject.toml b/python/pyproject.toml index 1ab70f2..90f232e 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -58,11 +58,11 @@ dev = [ # `uv run --group dev tox -e py` from python/. "tox>=4.23", ] -# Documentation build. Zensical is the static-site generator; it reads the -# single site config at ../zensical.toml (the Python binding docs live under -# ../docs/python/ as the "Python" nav section). That config sets -# `theme.name = "molcrafts"` — the shared MolCrafts brand extension, now on -# PyPI, so the docs build is reproducible from this group alone. +# Documentation build. Dual-language Zensical trees: +# zensical.toml → docs/en → site/en +# zensical.zh.toml → docs/zh → site/zh +# Cloudflare / CI should run both builds and place a root redirect to /en/. +# Theme: `theme.name = "molcrafts"` (molcrafts-zensical-theme on PyPI). doc = [ "zensical>=0.0.51", # The molcrafts docs theme (zensical.toml sets `theme.name = "molcrafts"`). diff --git a/src/lib.rs b/src/lib.rs index 82edbad..f2953ef 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -32,7 +32,7 @@ //! //! Reference material (not rustdoc): //! -//! - [Packmol parity](https://github.com/MolCrafts/molpack/blob/master/docs/packmol_parity.md) +//! - [Packmol parity](https://github.com/MolCrafts/molpack/blob/master/docs/en/packmol_parity.md) //! — kind-number ↔ Rust struct mapping with Fortran pointers. //! //! ## Quick example @@ -153,20 +153,20 @@ pub use objective::Objective; // ──────────────────────────────────────────────────────────────────────────── // Documentation modules (rustdoc-only; no runtime items). -// Content lives in `docs/*.md`, loaded via `include_str!` so each markdown +// Content lives in `docs/en/*.md`, loaded via `include_str!` so each markdown // file can be edited independently while rustdoc renders the whole chapter. // ──────────────────────────────────────────────────────────────────────────── -#[doc = include_str!("../docs/getting_started.md")] +#[doc = include_str!("../docs/en/getting_started.md")] pub mod getting_started {} -#[doc = include_str!("../docs/concepts.md")] +#[doc = include_str!("../docs/en/concepts.md")] pub mod concepts {} -#[doc = include_str!("../docs/architecture.md")] +#[doc = include_str!("../docs/en/architecture.md")] pub mod architecture {} -#[doc = include_str!("../docs/extending.md")] +#[doc = include_str!("../docs/en/extending.md")] pub mod extending {} // ──────────────────────────────────────────────────────────────────────────── diff --git a/src/restraint/mod.rs b/src/restraint/mod.rs index 30d6627..d2a3463 100644 --- a/src/restraint/mod.rs +++ b/src/restraint/mod.rs @@ -6,7 +6,7 @@ //! beside the 14 Packmol-originals in type space. //! //! Numerical equivalence to the Fortran `comprest.f90` (value) and `gwalls.f90` -//! (gradient) is preserved branch-for-branch; see `docs/packmol_parity.md`. +//! (gradient) is preserved branch-for-branch; see `docs/en/packmol_parity.md`. //! //! **Gradient convention**: `AtomRestraint::fg` accumulates INTO `g` with `+=`. //! Do not overwrite; many restraints may contribute to the same atom. diff --git a/zensical.toml b/zensical.toml index ec031b4..417840b 100644 --- a/zensical.toml +++ b/zensical.toml @@ -1,20 +1,19 @@ [project] site_name = "molpack" site_description = "Packmol-grade molecular packing in pure Rust, with Python bindings." -# Site is built by Cloudflare Pages (pip install "molcrafts-molpack[doc]" + zensical). -# Keep site_url in sync with the Cloudflare custom domain when one is set. +# Dual-language layout (Zensical alternate + molexp pattern): +# docs/en → site/en (this file) +# docs/zh → site/zh (zensical.zh.toml) +# Language switcher points at /en/ and /zh/ under site_url. site_url = "https://molcrafts.github.io/molpack/" repo_url = "https://github.com/MolCrafts/molpack" repo_name = "MolCrafts/molpack" copyright = "Copyright © 2026 MolCrafts" -docs_dir = "docs" -site_dir = "site" +docs_dir = "docs/en" +site_dir = "site/en" # Markdown extensions are intentionally not listed: Zensical's built-in default -# set (admonition, attr_list, def_list, footnotes, md_in_html, toc permalink, -# pymdownx.{arithmatex,details,emoji,highlight,inlinehilite,superfences,tabbed, -# tasklist}, and more) already covers everything these docs use — the CLI/Rust/ -# Python tabbed blocks, admonitions, and code annotations. +# set already covers admonitions, tabbed blocks, and code annotations. nav = [ { "Home" = "index.md" }, @@ -58,32 +57,21 @@ nav = [ extra_css = ["assets/stylesheets/extra.css"] -# Shared MolCrafts theme (github.com/MolCrafts/molcrafts-zensical-theme). Provides -# the brand palette, light/dark schemes, navigation features, and the home-page -# hero/manual-home component system used by docs/index.md. The package must be -# installed to build (see the `doc` dependency group in python/pyproject.toml). [project.theme] name = "molcrafts" language = "en" -# Language selector. English is the default and lives at the site root; a -# Chinese (zh) tree lives under docs/zh/. Zensical renders the header dropdown -# from `extra.alternate` (theme partial partials/alternate.html). The `url` -# filter passes absolute paths through unchanged, so each `link` carries the -# site's base path (`/molpack/`, matching site_url) to stay correct from every -# page and depth. The zh pages are placeholders for now — no docs are -# translated yet. [project.extra] +# Absolute path under site_url (domain prepended when link has no host). alternate = [ - { name = "English", link = "/molpack/", lang = "en" }, - { name = "简体中文", link = "/molpack/zh/", lang = "zh" }, + { name = "English", link = "/en/", lang = "en" }, + { name = "简体中文", link = "/zh/", lang = "zh" }, ] -# Selects the molpack accent (rust orange) for links, hovers, and hero eyebrows. [project.extra.molcrafts] product = "molpack" -accent = "#c2410c" # primary (主) — molpack signature colour -accent_soft = "rgba(194, 65, 12, 0.14)" # secondary (副) — soft fill behind it +accent = "#c2410c" +accent_soft = "rgba(194, 65, 12, 0.14)" [[project.extra.social]] icon = "fontawesome/brands/github" diff --git a/zensical.zh.toml b/zensical.zh.toml new file mode 100644 index 0000000..2e49be9 --- /dev/null +++ b/zensical.zh.toml @@ -0,0 +1,72 @@ +[project] +site_name = "molpack 手册" +site_description = "纯 Rust 实现的 Packmol 级分子装填,提供命令行、Python API 和 Rust API。" +# Chinese site tree. Build with: zensical build -f zensical.zh.toml +# Deploy beside English under site/zh so the language switcher /zh/ works. +site_url = "https://molcrafts.github.io/molpack/" +repo_url = "https://github.com/MolCrafts/molpack" +repo_name = "MolCrafts/molpack" +copyright = "Copyright © 2026 MolCrafts" +docs_dir = "docs/zh" +site_dir = "site/zh" + +nav = [ + { "首页" = "index.md" }, + { "教程" = [ + { "安装" = "install.md" }, + { "快速开始" = "getting_started.md" }, + { "概念" = "concepts.md" }, + { "示例" = "examples.md" }, + { "Packmol 对等" = "packmol_parity.md" }, + ] }, + { "Packmol 脚本" = [ + { "概览" = "cli/index.md" }, + { "脚本说明" = "cli/script-format.md" }, + { "格式" = "cli/formats.md" }, + { "示例" = "cli/examples.md" }, + ] }, + { "Python" = [ + { "概览" = "python/index.md" }, + { "安装" = "python/installation.md" }, + { "快速开始" = "python/getting-started.md" }, + { "Targets" = "python/guide/targets.md" }, + { "Restraints" = "python/guide/restraints.md" }, + { "Packer" = "python/guide/packer.md" }, + { "周期边界" = "python/guide/periodic-boundaries.md" }, + { "示例" = "python/examples.md" }, + { "API 参考" = "python/api-reference.md" }, + ] }, + { "Rust" = [ + { "概览" = "rust/index.md" }, + { "快速开始" = "rust/getting-started.md" }, + { "约束与 PBC" = "rust/restraints-and-pbc.md" }, + { "Handlers 与 Relaxers" = "rust/handlers-relaxers.md" }, + { "示例" = "rust/examples.md" }, + ] }, + { "开发" = [ + { "贡献" = "development/index.md" }, + { "架构" = "architecture.md" }, + { "扩展" = "extending.md" }, + ] }, +] + +extra_css = ["assets/stylesheets/extra.css"] + +[project.theme] +name = "molcrafts" +language = "zh" + +[project.extra] +alternate = [ + { name = "English", link = "/en/", lang = "en" }, + { name = "简体中文", link = "/zh/", lang = "zh" }, +] + +[project.extra.molcrafts] +product = "molpack" +accent = "#c2410c" +accent_soft = "rgba(194, 65, 12, 0.14)" + +[[project.extra.social]] +icon = "fontawesome/brands/github" +link = "https://github.com/MolCrafts/molpack" From c4228a0eeebb5b1a6e8285ce216445d0277083f1 Mon Sep 17 00:00:00 2001 From: Roy Kid Date: Fri, 24 Jul 2026 14:59:08 +0200 Subject: [PATCH 3/7] docs: match Zensical official language + alternate setup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per https://zensical.org/docs/setup/language/ — one theme.language per project, dual builds for en/zh, and extra.alternate with absolute links. Set per-language site_url (…/en/, …/zh/) and full alternate URLs so the GitHub Pages /molpack/ prefix is not dropped. --- docs/README.md | 79 +++++++++++++++++++++++++++++++++++++----------- zensical.toml | 32 +++++++++++++------- zensical.zh.toml | 20 ++++++++---- 3 files changed, 97 insertions(+), 34 deletions(-) diff --git a/docs/README.md b/docs/README.md index 1bf2735..2d1b6b9 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,29 +1,74 @@ # molpack documentation sources -Layout follows [Zensical language alternate](https://zensical.org/docs/setup/language/) -and the MolCrafts dual-tree pattern (same as molexp): +Multi-language setup matches the **official Zensical language guide**: + +https://zensical.org/docs/setup/language/ + +## What the official docs require + +1. **`[project.theme] language = "…"`** — one canonical UI language per project + (HTML only allows a single language per document). +2. **`[project.extra] alternate = […]`** — header language selector with + `name` / `link` / `lang` pointing at each language’s published root. +3. **Content trees** — separate sources per language, built as separate sites: ``` docs/ -├── en/ # English sources → zensical.toml → site/en -├── zh/ # Chinese sources → zensical.zh.toml → site/zh -│ ├── assets → ../en/assets # shared (symlink) -│ └── img → ../en/img -└── root-redirect.html # optional deploy root → /en/ +├── en/ # English → zensical.toml → site/en (language = en) +└── zh/ # 简体中文 → zensical.zh.toml → site/zh (language = zh) + ├── assets → ../en/assets + └── img → ../en/img +``` + +There is **no** single-build “folder language mode” in Zensical. Chinese is not +a subdirectory of the English site; it is its own project with `language = "zh"`. + +## Config (same shape as the official example) + +English (`zensical.toml`): + +```toml +[project] +site_url = "https://molcrafts.github.io/molpack/en/" +docs_dir = "docs/en" +site_dir = "site/en" + +[project.theme] +language = "en" + +[project.extra] +alternate = [ + { name = "English", link = "https://molcrafts.github.io/molpack/en/", lang = "en" }, + { name = "简体中文", link = "https://molcrafts.github.io/molpack/zh/", lang = "zh" }, +] ``` -## Build +Chinese (`zensical.zh.toml`): same `alternate`, but `language = "zh"`, +`docs_dir = "docs/zh"`, `site_dir = "site/zh"`, +`site_url = "https://molcrafts.github.io/molpack/zh/"`. + +> **Why full URLs in `alternate.link`?** +> The guide says absolute links may include a host. If you only set `link = "/en/"`, +> Zensical prepends **only the domain** of `site_url`, so a project site under +> `/molpack/` would jump to `https://molcrafts.github.io/en/` (wrong). Full URLs +> keep the `/molpack/en/` and `/molpack/zh/` prefixes. -From the repo root (with the `doc` dependency group installed): +## Build & deploy ```bash -zensical build -f zensical.toml # site/en -zensical build -f zensical.zh.toml # site/zh -# Optional combined deploy root: -# cp docs/root-redirect.html site/index.html -# and publish site/en + site/zh beside it. +zensical build -f zensical.toml # → site/en +zensical build -f zensical.zh.toml # → site/zh ``` -Language switcher links are absolute under `site_url`: `/en/` and `/zh/`. -Clicking 简体中文 stays inside the Chinese tree; nav targets never jump to -English paths. +Publish so those directories are served at: + +| Build output | Public URL | +|---|---| +| `site/en/` | `…/molpack/en/` | +| `site/zh/` | `…/molpack/zh/` | + +Optional root landing: copy `docs/root-redirect.html` to the deploy root as +`index.html` so `/molpack/` redirects to `/molpack/en/`. + +Cloudflare / CI must run **both** builds; a single `zensical build` only +produces one language. diff --git a/zensical.toml b/zensical.toml index 417840b..73899ae 100644 --- a/zensical.toml +++ b/zensical.toml @@ -1,20 +1,27 @@ +# English documentation site. +# +# Multi-language follows Zensical's official model +# (https://zensical.org/docs/setup/language/): +# - One project = one theme.language (HTML allows a single lang per document). +# - Other languages are separate builds + header language selector via +# [project.extra].alternate (absolute links). +# - Content trees live under docs/en and docs/zh; this file builds English. +# +# Build: zensical build -f zensical.toml +# Pair: zensical build -f zensical.zh.toml +# Deploy: publish site/en → …/en/ and site/zh → …/zh/ (and optional root → /en/). + [project] site_name = "molpack" site_description = "Packmol-grade molecular packing in pure Rust, with Python bindings." -# Dual-language layout (Zensical alternate + molexp pattern): -# docs/en → site/en (this file) -# docs/zh → site/zh (zensical.zh.toml) -# Language switcher points at /en/ and /zh/ under site_url. -site_url = "https://molcrafts.github.io/molpack/" +# Per-language site root (Material / Zensical multi-language practice). +site_url = "https://molcrafts.github.io/molpack/en/" repo_url = "https://github.com/MolCrafts/molpack" repo_name = "MolCrafts/molpack" copyright = "Copyright © 2026 MolCrafts" docs_dir = "docs/en" site_dir = "site/en" -# Markdown extensions are intentionally not listed: Zensical's built-in default -# set already covers admonitions, tabbed blocks, and code annotations. - nav = [ { "Home" = "index.md" }, { "Tutorial" = [ @@ -57,15 +64,18 @@ nav = [ extra_css = ["assets/stylesheets/extra.css"] +# https://zensical.org/docs/setup/language/#site-language [project.theme] name = "molcrafts" language = "en" +# https://zensical.org/docs/setup/language/#site-language-selector +# Absolute links (with host) so the /molpack/ project path is not dropped — +# Zensical only prepends the *domain* of site_url when the link has no host. [project.extra] -# Absolute path under site_url (domain prepended when link has no host). alternate = [ - { name = "English", link = "/en/", lang = "en" }, - { name = "简体中文", link = "/zh/", lang = "zh" }, + { name = "English", link = "https://molcrafts.github.io/molpack/en/", lang = "en" }, + { name = "简体中文", link = "https://molcrafts.github.io/molpack/zh/", lang = "zh" }, ] [project.extra.molcrafts] diff --git a/zensical.zh.toml b/zensical.zh.toml index 2e49be9..bc383ba 100644 --- a/zensical.zh.toml +++ b/zensical.zh.toml @@ -1,9 +1,15 @@ +# Chinese documentation site (pair of zensical.toml). +# +# Official multi-language model (https://zensical.org/docs/setup/language/): +# theme.language = "zh" for this project only +# extra.alternate language selector → absolute links to each build +# +# Build: zensical build -f zensical.zh.toml → site/zh + [project] -site_name = "molpack 手册" +site_name = "molpack" site_description = "纯 Rust 实现的 Packmol 级分子装填,提供命令行、Python API 和 Rust API。" -# Chinese site tree. Build with: zensical build -f zensical.zh.toml -# Deploy beside English under site/zh so the language switcher /zh/ works. -site_url = "https://molcrafts.github.io/molpack/" +site_url = "https://molcrafts.github.io/molpack/zh/" repo_url = "https://github.com/MolCrafts/molpack" repo_name = "MolCrafts/molpack" copyright = "Copyright © 2026 MolCrafts" @@ -52,14 +58,16 @@ nav = [ extra_css = ["assets/stylesheets/extra.css"] +# https://zensical.org/docs/setup/language/#site-language [project.theme] name = "molcrafts" language = "zh" +# https://zensical.org/docs/setup/language/#site-language-selector [project.extra] alternate = [ - { name = "English", link = "/en/", lang = "en" }, - { name = "简体中文", link = "/zh/", lang = "zh" }, + { name = "English", link = "https://molcrafts.github.io/molpack/en/", lang = "en" }, + { name = "简体中文", link = "https://molcrafts.github.io/molpack/zh/", lang = "zh" }, ] [project.extra.molcrafts] From dc1a5732a494feed2d7d4659ad1bd506f8274155 Mon Sep 17 00:00:00 2001 From: Roy Kid Date: Fri, 24 Jul 2026 15:06:56 +0200 Subject: [PATCH 4/7] docs: one zensical.toml with official alternate /en/ /zh/ Match https://zensical.org/docs/setup/language/ exactly: single project config, theme.language = en, extra.alternate links /en/ and /zh/. Content under docs/en and docs/zh; one `zensical build` publishes both trees. Remove zensical.zh.toml. --- README.md | 4 +- docs/README.md | 66 +++------ .../paper-compatibility-distributions.png | Bin .../images/paper-confinement-sphere.png | Bin .../assets/images/paper-mt-scaling.png | Bin docs/{en => }/assets/stylesheets/extra.css | 0 docs/en/assets | 1 + docs/en/img | 1 + docs/{en => }/img/fig_profile_density.png | Bin docs/zh/assets | 2 +- docs/zh/img | 2 +- zensical.toml | 125 +++++++++++------- zensical.zh.toml | 80 ----------- 13 files changed, 103 insertions(+), 178 deletions(-) rename docs/{en => }/assets/images/paper-compatibility-distributions.png (100%) rename docs/{en => }/assets/images/paper-confinement-sphere.png (100%) rename docs/{en => }/assets/images/paper-mt-scaling.png (100%) rename docs/{en => }/assets/stylesheets/extra.css (100%) create mode 120000 docs/en/assets create mode 120000 docs/en/img rename docs/{en => }/img/fig_profile_density.png (100%) delete mode 100644 zensical.zh.toml diff --git a/README.md b/README.md index fe1a088..b89cd9a 100644 --- a/README.md +++ b/README.md @@ -131,8 +131,8 @@ cd python && maturin develop --release && pytest # Python wheel ## Documentation - **Guide** — dual-language trees under [`docs/en/`](./docs/en/) and - [`docs/zh/`](./docs/zh/) (Zensical: `zensical.toml` → `site/en`, - `zensical.zh.toml` → `site/zh`). Chapters: install, getting started, + [`docs/zh/`](./docs/zh/), one `zensical.toml` (official `alternate` + language selector → `/en/`, `/zh/`). Chapters: install, getting started, concepts, examples, Packmol parity, architecture, and extending. - **Rust API** — `cargo doc --open`, or [docs.rs](https://docs.rs/molcrafts-molpack). The four long-form chapters (getting started, concepts, architecture, diff --git a/docs/README.md b/docs/README.md index 2d1b6b9..9378acb 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,74 +1,42 @@ -# molpack documentation sources +# molpack documentation -Multi-language setup matches the **official Zensical language guide**: +Multi-language follows the **official Zensical language setup** — one config, +one build, language selector via `alternate`: https://zensical.org/docs/setup/language/ -## What the official docs require - -1. **`[project.theme] language = "…"`** — one canonical UI language per project - (HTML only allows a single language per document). -2. **`[project.extra] alternate = […]`** — header language selector with - `name` / `link` / `lang` pointing at each language’s published root. -3. **Content trees** — separate sources per language, built as separate sites: +## Layout ``` docs/ -├── en/ # English → zensical.toml → site/en (language = en) -└── zh/ # 简体中文 → zensical.zh.toml → site/zh (language = zh) - ├── assets → ../en/assets - └── img → ../en/img +├── assets/ # shared static files +├── en/ # English pages → /en/… +└── zh/ # 简体中文 pages → /zh/… ``` -There is **no** single-build “folder language mode” in Zensical. Chinese is not -a subdirectory of the English site; it is its own project with `language = "zh"`. - -## Config (same shape as the official example) - -English (`zensical.toml`): +## `zensical.toml` (only config) ```toml [project] -site_url = "https://molcrafts.github.io/molpack/en/" -docs_dir = "docs/en" -site_dir = "site/en" +docs_dir = "docs" +site_dir = "site" [project.theme] language = "en" [project.extra] alternate = [ - { name = "English", link = "https://molcrafts.github.io/molpack/en/", lang = "en" }, - { name = "简体中文", link = "https://molcrafts.github.io/molpack/zh/", lang = "zh" }, + { name = "English", link = "/en/", lang = "en" }, + { name = "简体中文", link = "/zh/", lang = "zh" }, ] ``` -Chinese (`zensical.zh.toml`): same `alternate`, but `language = "zh"`, -`docs_dir = "docs/zh"`, `site_dir = "site/zh"`, -`site_url = "https://molcrafts.github.io/molpack/zh/"`. - -> **Why full URLs in `alternate.link`?** -> The guide says absolute links may include a host. If you only set `link = "/en/"`, -> Zensical prepends **only the domain** of `site_url`, so a project site under -> `/molpack/` would jump to `https://molcrafts.github.io/en/` (wrong). Full URLs -> keep the `/molpack/en/` and `/molpack/zh/` prefixes. +Nav points at `en/…` and `zh/…` so both URL trees are published in a single +`zensical build`. The header language selector jumps between `/en/` and `/zh/`. -## Build & deploy +## Build ```bash -zensical build -f zensical.toml # → site/en -zensical build -f zensical.zh.toml # → site/zh +zensical build +# optional: cp docs/root-redirect.html site/index.html ``` - -Publish so those directories are served at: - -| Build output | Public URL | -|---|---| -| `site/en/` | `…/molpack/en/` | -| `site/zh/` | `…/molpack/zh/` | - -Optional root landing: copy `docs/root-redirect.html` to the deploy root as -`index.html` so `/molpack/` redirects to `/molpack/en/`. - -Cloudflare / CI must run **both** builds; a single `zensical build` only -produces one language. diff --git a/docs/en/assets/images/paper-compatibility-distributions.png b/docs/assets/images/paper-compatibility-distributions.png similarity index 100% rename from docs/en/assets/images/paper-compatibility-distributions.png rename to docs/assets/images/paper-compatibility-distributions.png diff --git a/docs/en/assets/images/paper-confinement-sphere.png b/docs/assets/images/paper-confinement-sphere.png similarity index 100% rename from docs/en/assets/images/paper-confinement-sphere.png rename to docs/assets/images/paper-confinement-sphere.png diff --git a/docs/en/assets/images/paper-mt-scaling.png b/docs/assets/images/paper-mt-scaling.png similarity index 100% rename from docs/en/assets/images/paper-mt-scaling.png rename to docs/assets/images/paper-mt-scaling.png diff --git a/docs/en/assets/stylesheets/extra.css b/docs/assets/stylesheets/extra.css similarity index 100% rename from docs/en/assets/stylesheets/extra.css rename to docs/assets/stylesheets/extra.css diff --git a/docs/en/assets b/docs/en/assets new file mode 120000 index 0000000..ec2e4be --- /dev/null +++ b/docs/en/assets @@ -0,0 +1 @@ +../assets \ No newline at end of file diff --git a/docs/en/img b/docs/en/img new file mode 120000 index 0000000..6ffc6ca --- /dev/null +++ b/docs/en/img @@ -0,0 +1 @@ +../img \ No newline at end of file diff --git a/docs/en/img/fig_profile_density.png b/docs/img/fig_profile_density.png similarity index 100% rename from docs/en/img/fig_profile_density.png rename to docs/img/fig_profile_density.png diff --git a/docs/zh/assets b/docs/zh/assets index 947146d..ec2e4be 120000 --- a/docs/zh/assets +++ b/docs/zh/assets @@ -1 +1 @@ -../en/assets \ No newline at end of file +../assets \ No newline at end of file diff --git a/docs/zh/img b/docs/zh/img index 21d4e91..6ffc6ca 120000 --- a/docs/zh/img +++ b/docs/zh/img @@ -1 +1 @@ -../en/img \ No newline at end of file +../img \ No newline at end of file diff --git a/zensical.toml b/zensical.toml index 73899ae..3e2d121 100644 --- a/zensical.toml +++ b/zensical.toml @@ -1,64 +1,101 @@ -# English documentation site. +# Single Zensical project — multi-language per official guide: +# https://zensical.org/docs/setup/language/ # -# Multi-language follows Zensical's official model -# (https://zensical.org/docs/setup/language/): -# - One project = one theme.language (HTML allows a single lang per document). -# - Other languages are separate builds + header language selector via -# [project.extra].alternate (absolute links). -# - Content trees live under docs/en and docs/zh; this file builds English. +# [project.theme] language → one UI language for the build +# [project.extra] alternate → header language selector (absolute /en/, /zh/) # -# Build: zensical build -f zensical.toml -# Pair: zensical build -f zensical.zh.toml -# Deploy: publish site/en → …/en/ and site/zh → …/zh/ (and optional root → /en/). +# Content lives under docs/en and docs/zh; one build emits both URL trees. +# Build: zensical build [project] site_name = "molpack" site_description = "Packmol-grade molecular packing in pure Rust, with Python bindings." -# Per-language site root (Material / Zensical multi-language practice). -site_url = "https://molcrafts.github.io/molpack/en/" +site_url = "https://molcrafts.github.io/molpack/" repo_url = "https://github.com/MolCrafts/molpack" repo_name = "MolCrafts/molpack" copyright = "Copyright © 2026 MolCrafts" -docs_dir = "docs/en" -site_dir = "site/en" +docs_dir = "docs" +site_dir = "site" +# Default nav is the English tree (paths under en/). Chinese pages live at +# zh/… and are opened via the language selector (alternate → /zh/). nav = [ - { "Home" = "index.md" }, + { "Home" = "en/index.md" }, { "Tutorial" = [ - { "Install" = "install.md" }, - { "Quickstart" = "getting_started.md" }, - { "Concepts" = "concepts.md" }, - { "Examples" = "examples.md" }, - { "Packmol Parity" = "packmol_parity.md" }, + { "Install" = "en/install.md" }, + { "Quickstart" = "en/getting_started.md" }, + { "Concepts" = "en/concepts.md" }, + { "Examples" = "en/examples.md" }, + { "Packmol Parity" = "en/packmol_parity.md" }, ] }, { "Packmol Script" = [ - { "Overview" = "cli/index.md" }, - { "Script Notes" = "cli/script-format.md" }, - { "Formats" = "cli/formats.md" }, - { "Examples" = "cli/examples.md" }, + { "Overview" = "en/cli/index.md" }, + { "Script Notes" = "en/cli/script-format.md" }, + { "Formats" = "en/cli/formats.md" }, + { "Examples" = "en/cli/examples.md" }, ] }, { "Python" = [ - { "Overview" = "python/index.md" }, - { "Installation" = "python/installation.md" }, - { "Quickstart" = "python/getting-started.md" }, - { "Targets" = "python/guide/targets.md" }, - { "Restraints" = "python/guide/restraints.md" }, - { "Packer" = "python/guide/packer.md" }, - { "Periodic Boundaries" = "python/guide/periodic-boundaries.md" }, - { "Examples" = "python/examples.md" }, - { "API Reference" = "python/api-reference.md" }, + { "Overview" = "en/python/index.md" }, + { "Installation" = "en/python/installation.md" }, + { "Quickstart" = "en/python/getting-started.md" }, + { "Targets" = "en/python/guide/targets.md" }, + { "Restraints" = "en/python/guide/restraints.md" }, + { "Packer" = "en/python/guide/packer.md" }, + { "Periodic Boundaries" = "en/python/guide/periodic-boundaries.md" }, + { "Examples" = "en/python/examples.md" }, + { "API Reference" = "en/python/api-reference.md" }, ] }, { "Rust" = [ - { "Overview" = "rust/index.md" }, - { "Quickstart" = "rust/getting-started.md" }, - { "Restraints and PBC" = "rust/restraints-and-pbc.md" }, - { "Handlers and Relaxers" = "rust/handlers-relaxers.md" }, - { "Examples" = "rust/examples.md" }, + { "Overview" = "en/rust/index.md" }, + { "Quickstart" = "en/rust/getting-started.md" }, + { "Restraints and PBC" = "en/rust/restraints-and-pbc.md" }, + { "Handlers and Relaxers" = "en/rust/handlers-relaxers.md" }, + { "Examples" = "en/rust/examples.md" }, ] }, { "Development" = [ - { "Contributing" = "development/index.md" }, - { "Architecture" = "architecture.md" }, - { "Extending" = "extending.md" }, + { "Contributing" = "en/development/index.md" }, + { "Architecture" = "en/architecture.md" }, + { "Extending" = "en/extending.md" }, + ] }, + # Chinese tree — same structure, URLs under /zh/ (language selector). + { "中文" = [ + { "首页" = "zh/index.md" }, + { "教程" = [ + { "安装" = "zh/install.md" }, + { "快速开始" = "zh/getting_started.md" }, + { "概念" = "zh/concepts.md" }, + { "示例" = "zh/examples.md" }, + { "Packmol 对等" = "zh/packmol_parity.md" }, + ] }, + { "Packmol 脚本" = [ + { "概览" = "zh/cli/index.md" }, + { "脚本说明" = "zh/cli/script-format.md" }, + { "格式" = "zh/cli/formats.md" }, + { "示例" = "zh/cli/examples.md" }, + ] }, + { "Python" = [ + { "概览" = "zh/python/index.md" }, + { "安装" = "zh/python/installation.md" }, + { "快速开始" = "zh/python/getting-started.md" }, + { "Targets" = "zh/python/guide/targets.md" }, + { "Restraints" = "zh/python/guide/restraints.md" }, + { "Packer" = "zh/python/guide/packer.md" }, + { "周期边界" = "zh/python/guide/periodic-boundaries.md" }, + { "示例" = "zh/python/examples.md" }, + { "API 参考" = "zh/python/api-reference.md" }, + ] }, + { "Rust" = [ + { "概览" = "zh/rust/index.md" }, + { "快速开始" = "zh/rust/getting-started.md" }, + { "约束与 PBC" = "zh/rust/restraints-and-pbc.md" }, + { "Handlers 与 Relaxers" = "zh/rust/handlers-relaxers.md" }, + { "示例" = "zh/rust/examples.md" }, + ] }, + { "开发" = [ + { "贡献" = "zh/development/index.md" }, + { "架构" = "zh/architecture.md" }, + { "扩展" = "zh/extending.md" }, + ] }, ] }, ] @@ -70,12 +107,10 @@ name = "molcrafts" language = "en" # https://zensical.org/docs/setup/language/#site-language-selector -# Absolute links (with host) so the /molpack/ project path is not dropped — -# Zensical only prepends the *domain* of site_url when the link has no host. [project.extra] alternate = [ - { name = "English", link = "https://molcrafts.github.io/molpack/en/", lang = "en" }, - { name = "简体中文", link = "https://molcrafts.github.io/molpack/zh/", lang = "zh" }, + { name = "English", link = "/en/", lang = "en" }, + { name = "简体中文", link = "/zh/", lang = "zh" }, ] [project.extra.molcrafts] diff --git a/zensical.zh.toml b/zensical.zh.toml deleted file mode 100644 index bc383ba..0000000 --- a/zensical.zh.toml +++ /dev/null @@ -1,80 +0,0 @@ -# Chinese documentation site (pair of zensical.toml). -# -# Official multi-language model (https://zensical.org/docs/setup/language/): -# theme.language = "zh" for this project only -# extra.alternate language selector → absolute links to each build -# -# Build: zensical build -f zensical.zh.toml → site/zh - -[project] -site_name = "molpack" -site_description = "纯 Rust 实现的 Packmol 级分子装填,提供命令行、Python API 和 Rust API。" -site_url = "https://molcrafts.github.io/molpack/zh/" -repo_url = "https://github.com/MolCrafts/molpack" -repo_name = "MolCrafts/molpack" -copyright = "Copyright © 2026 MolCrafts" -docs_dir = "docs/zh" -site_dir = "site/zh" - -nav = [ - { "首页" = "index.md" }, - { "教程" = [ - { "安装" = "install.md" }, - { "快速开始" = "getting_started.md" }, - { "概念" = "concepts.md" }, - { "示例" = "examples.md" }, - { "Packmol 对等" = "packmol_parity.md" }, - ] }, - { "Packmol 脚本" = [ - { "概览" = "cli/index.md" }, - { "脚本说明" = "cli/script-format.md" }, - { "格式" = "cli/formats.md" }, - { "示例" = "cli/examples.md" }, - ] }, - { "Python" = [ - { "概览" = "python/index.md" }, - { "安装" = "python/installation.md" }, - { "快速开始" = "python/getting-started.md" }, - { "Targets" = "python/guide/targets.md" }, - { "Restraints" = "python/guide/restraints.md" }, - { "Packer" = "python/guide/packer.md" }, - { "周期边界" = "python/guide/periodic-boundaries.md" }, - { "示例" = "python/examples.md" }, - { "API 参考" = "python/api-reference.md" }, - ] }, - { "Rust" = [ - { "概览" = "rust/index.md" }, - { "快速开始" = "rust/getting-started.md" }, - { "约束与 PBC" = "rust/restraints-and-pbc.md" }, - { "Handlers 与 Relaxers" = "rust/handlers-relaxers.md" }, - { "示例" = "rust/examples.md" }, - ] }, - { "开发" = [ - { "贡献" = "development/index.md" }, - { "架构" = "architecture.md" }, - { "扩展" = "extending.md" }, - ] }, -] - -extra_css = ["assets/stylesheets/extra.css"] - -# https://zensical.org/docs/setup/language/#site-language -[project.theme] -name = "molcrafts" -language = "zh" - -# https://zensical.org/docs/setup/language/#site-language-selector -[project.extra] -alternate = [ - { name = "English", link = "https://molcrafts.github.io/molpack/en/", lang = "en" }, - { name = "简体中文", link = "https://molcrafts.github.io/molpack/zh/", lang = "zh" }, -] - -[project.extra.molcrafts] -product = "molpack" -accent = "#c2410c" -accent_soft = "rgba(194, 65, 12, 0.14)" - -[[project.extra.social]] -icon = "fontawesome/brands/github" -link = "https://github.com/MolCrafts/molpack" From 1177ec63478416f99ab391ec410d9c906df72631 Mon Sep 17 00:00:00 2001 From: Roy Kid Date: Fri, 24 Jul 2026 15:52:09 +0200 Subject: [PATCH 5/7] docs: single English site at the root MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drop the Chinese tree (docs/zh) and the `alternate` language selector, and move docs/en back to docs/ so the site is served from the root — which also removes the reason the root-redirect.html hack existed. Follow the doc-include paths in src/lib.rs, the restraint module comment, README, and CONTRIBUTING to the flattened layout. --- CONTRIBUTING.md | 2 +- README.md | 12 +- docs/README.md | 42 -- docs/{en => }/architecture.md | 0 docs/{en => }/cli/examples.md | 0 docs/{en => }/cli/formats.md | 0 docs/{en => }/cli/index.md | 0 docs/{en => }/cli/script-format.md | 0 docs/{en => }/concepts.md | 0 docs/{en => }/development/index.md | 0 docs/en/assets | 1 - docs/en/img | 1 - docs/{en => }/examples.md | 0 docs/{en => }/extending.md | 0 docs/{en => }/getting_started.md | 0 docs/{en => }/index.md | 0 docs/{en => }/install.md | 0 docs/{en => }/packmol_parity.md | 0 docs/{en => }/python/api-reference.md | 0 docs/{en => }/python/examples.md | 0 docs/{en => }/python/getting-started.md | 0 docs/{en => }/python/guide/packer.md | 0 .../python/guide/periodic-boundaries.md | 0 docs/{en => }/python/guide/restraints.md | 0 docs/{en => }/python/guide/targets.md | 0 docs/{en => }/python/index.md | 0 docs/{en => }/python/installation.md | 0 docs/root-redirect.html | 13 - docs/{en => }/rust/examples.md | 0 docs/{en => }/rust/getting-started.md | 0 docs/{en => }/rust/handlers-relaxers.md | 0 docs/{en => }/rust/index.md | 0 docs/{en => }/rust/restraints-and-pbc.md | 0 docs/zh/architecture.md | 336 ------------- docs/zh/assets | 1 - docs/zh/cli/examples.md | 34 -- docs/zh/cli/formats.md | 30 -- docs/zh/cli/index.md | 56 --- docs/zh/cli/script-format.md | 66 --- docs/zh/concepts.md | 307 ------------ docs/zh/development/index.md | 31 -- docs/zh/examples.md | 62 --- docs/zh/extending.md | 474 ------------------ docs/zh/getting_started.md | 120 ----- docs/zh/img | 1 - docs/zh/index.md | 249 --------- docs/zh/install.md | 86 ---- docs/zh/packmol_parity.md | 115 ----- docs/zh/python/api-reference.md | 416 --------------- docs/zh/python/examples.md | 81 --- docs/zh/python/getting-started.md | 131 ----- docs/zh/python/guide/packer.md | 140 ------ docs/zh/python/guide/periodic-boundaries.md | 108 ---- docs/zh/python/guide/restraints.md | 165 ------ docs/zh/python/guide/targets.md | 187 ------- docs/zh/python/index.md | 71 --- docs/zh/python/installation.md | 68 --- docs/zh/rust/examples.md | 30 -- docs/zh/rust/getting-started.md | 68 --- docs/zh/rust/handlers-relaxers.md | 72 --- docs/zh/rust/index.md | 46 -- docs/zh/rust/restraints-and-pbc.md | 84 ---- python/pyproject.toml | 5 +- src/lib.rs | 12 +- src/restraint/mod.rs | 2 +- zensical.toml | 114 ++--- 66 files changed, 44 insertions(+), 3795 deletions(-) delete mode 100644 docs/README.md rename docs/{en => }/architecture.md (100%) rename docs/{en => }/cli/examples.md (100%) rename docs/{en => }/cli/formats.md (100%) rename docs/{en => }/cli/index.md (100%) rename docs/{en => }/cli/script-format.md (100%) rename docs/{en => }/concepts.md (100%) rename docs/{en => }/development/index.md (100%) delete mode 120000 docs/en/assets delete mode 120000 docs/en/img rename docs/{en => }/examples.md (100%) rename docs/{en => }/extending.md (100%) rename docs/{en => }/getting_started.md (100%) rename docs/{en => }/index.md (100%) rename docs/{en => }/install.md (100%) rename docs/{en => }/packmol_parity.md (100%) rename docs/{en => }/python/api-reference.md (100%) rename docs/{en => }/python/examples.md (100%) rename docs/{en => }/python/getting-started.md (100%) rename docs/{en => }/python/guide/packer.md (100%) rename docs/{en => }/python/guide/periodic-boundaries.md (100%) rename docs/{en => }/python/guide/restraints.md (100%) rename docs/{en => }/python/guide/targets.md (100%) rename docs/{en => }/python/index.md (100%) rename docs/{en => }/python/installation.md (100%) delete mode 100644 docs/root-redirect.html rename docs/{en => }/rust/examples.md (100%) rename docs/{en => }/rust/getting-started.md (100%) rename docs/{en => }/rust/handlers-relaxers.md (100%) rename docs/{en => }/rust/index.md (100%) rename docs/{en => }/rust/restraints-and-pbc.md (100%) delete mode 100644 docs/zh/architecture.md delete mode 120000 docs/zh/assets delete mode 100644 docs/zh/cli/examples.md delete mode 100644 docs/zh/cli/formats.md delete mode 100644 docs/zh/cli/index.md delete mode 100644 docs/zh/cli/script-format.md delete mode 100644 docs/zh/concepts.md delete mode 100644 docs/zh/development/index.md delete mode 100644 docs/zh/examples.md delete mode 100644 docs/zh/extending.md delete mode 100644 docs/zh/getting_started.md delete mode 120000 docs/zh/img delete mode 100644 docs/zh/index.md delete mode 100644 docs/zh/install.md delete mode 100644 docs/zh/packmol_parity.md delete mode 100644 docs/zh/python/api-reference.md delete mode 100644 docs/zh/python/examples.md delete mode 100644 docs/zh/python/getting-started.md delete mode 100644 docs/zh/python/guide/packer.md delete mode 100644 docs/zh/python/guide/periodic-boundaries.md delete mode 100644 docs/zh/python/guide/restraints.md delete mode 100644 docs/zh/python/guide/targets.md delete mode 100644 docs/zh/python/index.md delete mode 100644 docs/zh/python/installation.md delete mode 100644 docs/zh/rust/examples.md delete mode 100644 docs/zh/rust/getting-started.md delete mode 100644 docs/zh/rust/handlers-relaxers.md delete mode 100644 docs/zh/rust/index.md delete mode 100644 docs/zh/rust/restraints-and-pbc.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4cd67a1..f4c785e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -76,7 +76,7 @@ venv; **do not** rely on that for the gate — pre-push always uses 2. Implement `Restraint` (both `f` and `fg`; `fg` must match the gradient of `f`) 3. Re-export it from `src/restraint/mod.rs`, then from the crate root in `src/lib.rs` 4. Add a unit test in `tests/restraint.rs` -5. Document it in `docs/en/concepts.md` under the restraint table +5. Document it in `docs/concepts.md` under the restraint table See the `extending` rustdoc chapter (`cargo doc --open`) for detailed tutorials. diff --git a/README.md b/README.md index b89cd9a..e60f92d 100644 --- a/README.md +++ b/README.md @@ -130,18 +130,16 @@ cd python && maturin develop --release && pytest # Python wheel ## Documentation -- **Guide** — dual-language trees under [`docs/en/`](./docs/en/) and - [`docs/zh/`](./docs/zh/), one `zensical.toml` (official `alternate` - language selector → `/en/`, `/zh/`). Chapters: install, getting started, - concepts, examples, Packmol parity, architecture, and extending. +- **Guide** — [`docs/`](./docs/), built with `zensical build`. Chapters: + install, getting started, concepts, examples, Packmol parity, architecture, + and extending. - **Rust API** — `cargo doc --open`, or [docs.rs](https://docs.rs/molcrafts-molpack). The four long-form chapters (getting started, concepts, architecture, extending) are also embedded in the rustdoc as `molpack::getting_started`, `molpack::concepts`, `molpack::architecture`, and `molpack::extending`. -- **Python** — binding docs live under `docs/en/python/` (and the parallel - `docs/zh/python/` tree), published as the **Python** section of the - English / Chinese Zensical sites. +- **Python** — binding docs live under `docs/python/`, published as the + **Python** section of the site. ## Contributing diff --git a/docs/README.md b/docs/README.md deleted file mode 100644 index 9378acb..0000000 --- a/docs/README.md +++ /dev/null @@ -1,42 +0,0 @@ -# molpack documentation - -Multi-language follows the **official Zensical language setup** — one config, -one build, language selector via `alternate`: - -https://zensical.org/docs/setup/language/ - -## Layout - -``` -docs/ -├── assets/ # shared static files -├── en/ # English pages → /en/… -└── zh/ # 简体中文 pages → /zh/… -``` - -## `zensical.toml` (only config) - -```toml -[project] -docs_dir = "docs" -site_dir = "site" - -[project.theme] -language = "en" - -[project.extra] -alternate = [ - { name = "English", link = "/en/", lang = "en" }, - { name = "简体中文", link = "/zh/", lang = "zh" }, -] -``` - -Nav points at `en/…` and `zh/…` so both URL trees are published in a single -`zensical build`. The header language selector jumps between `/en/` and `/zh/`. - -## Build - -```bash -zensical build -# optional: cp docs/root-redirect.html site/index.html -``` diff --git a/docs/en/architecture.md b/docs/architecture.md similarity index 100% rename from docs/en/architecture.md rename to docs/architecture.md diff --git a/docs/en/cli/examples.md b/docs/cli/examples.md similarity index 100% rename from docs/en/cli/examples.md rename to docs/cli/examples.md diff --git a/docs/en/cli/formats.md b/docs/cli/formats.md similarity index 100% rename from docs/en/cli/formats.md rename to docs/cli/formats.md diff --git a/docs/en/cli/index.md b/docs/cli/index.md similarity index 100% rename from docs/en/cli/index.md rename to docs/cli/index.md diff --git a/docs/en/cli/script-format.md b/docs/cli/script-format.md similarity index 100% rename from docs/en/cli/script-format.md rename to docs/cli/script-format.md diff --git a/docs/en/concepts.md b/docs/concepts.md similarity index 100% rename from docs/en/concepts.md rename to docs/concepts.md diff --git a/docs/en/development/index.md b/docs/development/index.md similarity index 100% rename from docs/en/development/index.md rename to docs/development/index.md diff --git a/docs/en/assets b/docs/en/assets deleted file mode 120000 index ec2e4be..0000000 --- a/docs/en/assets +++ /dev/null @@ -1 +0,0 @@ -../assets \ No newline at end of file diff --git a/docs/en/img b/docs/en/img deleted file mode 120000 index 6ffc6ca..0000000 --- a/docs/en/img +++ /dev/null @@ -1 +0,0 @@ -../img \ No newline at end of file diff --git a/docs/en/examples.md b/docs/examples.md similarity index 100% rename from docs/en/examples.md rename to docs/examples.md diff --git a/docs/en/extending.md b/docs/extending.md similarity index 100% rename from docs/en/extending.md rename to docs/extending.md diff --git a/docs/en/getting_started.md b/docs/getting_started.md similarity index 100% rename from docs/en/getting_started.md rename to docs/getting_started.md diff --git a/docs/en/index.md b/docs/index.md similarity index 100% rename from docs/en/index.md rename to docs/index.md diff --git a/docs/en/install.md b/docs/install.md similarity index 100% rename from docs/en/install.md rename to docs/install.md diff --git a/docs/en/packmol_parity.md b/docs/packmol_parity.md similarity index 100% rename from docs/en/packmol_parity.md rename to docs/packmol_parity.md diff --git a/docs/en/python/api-reference.md b/docs/python/api-reference.md similarity index 100% rename from docs/en/python/api-reference.md rename to docs/python/api-reference.md diff --git a/docs/en/python/examples.md b/docs/python/examples.md similarity index 100% rename from docs/en/python/examples.md rename to docs/python/examples.md diff --git a/docs/en/python/getting-started.md b/docs/python/getting-started.md similarity index 100% rename from docs/en/python/getting-started.md rename to docs/python/getting-started.md diff --git a/docs/en/python/guide/packer.md b/docs/python/guide/packer.md similarity index 100% rename from docs/en/python/guide/packer.md rename to docs/python/guide/packer.md diff --git a/docs/en/python/guide/periodic-boundaries.md b/docs/python/guide/periodic-boundaries.md similarity index 100% rename from docs/en/python/guide/periodic-boundaries.md rename to docs/python/guide/periodic-boundaries.md diff --git a/docs/en/python/guide/restraints.md b/docs/python/guide/restraints.md similarity index 100% rename from docs/en/python/guide/restraints.md rename to docs/python/guide/restraints.md diff --git a/docs/en/python/guide/targets.md b/docs/python/guide/targets.md similarity index 100% rename from docs/en/python/guide/targets.md rename to docs/python/guide/targets.md diff --git a/docs/en/python/index.md b/docs/python/index.md similarity index 100% rename from docs/en/python/index.md rename to docs/python/index.md diff --git a/docs/en/python/installation.md b/docs/python/installation.md similarity index 100% rename from docs/en/python/installation.md rename to docs/python/installation.md diff --git a/docs/root-redirect.html b/docs/root-redirect.html deleted file mode 100644 index 98a953d..0000000 --- a/docs/root-redirect.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - molpack docs - - - -

English · 简体中文

- - diff --git a/docs/en/rust/examples.md b/docs/rust/examples.md similarity index 100% rename from docs/en/rust/examples.md rename to docs/rust/examples.md diff --git a/docs/en/rust/getting-started.md b/docs/rust/getting-started.md similarity index 100% rename from docs/en/rust/getting-started.md rename to docs/rust/getting-started.md diff --git a/docs/en/rust/handlers-relaxers.md b/docs/rust/handlers-relaxers.md similarity index 100% rename from docs/en/rust/handlers-relaxers.md rename to docs/rust/handlers-relaxers.md diff --git a/docs/en/rust/index.md b/docs/rust/index.md similarity index 100% rename from docs/en/rust/index.md rename to docs/rust/index.md diff --git a/docs/en/rust/restraints-and-pbc.md b/docs/rust/restraints-and-pbc.md similarity index 100% rename from docs/en/rust/restraints-and-pbc.md rename to docs/rust/restraints-and-pbc.md diff --git a/docs/zh/architecture.md b/docs/zh/architecture.md deleted file mode 100644 index a3cd711..0000000 --- a/docs/zh/architecture.md +++ /dev/null @@ -1,336 +0,0 @@ -# Architecture - -Developer-oriented view of the crate. Read [`concepts`](crate::concepts) -first for the abstractions this chapter assumes. - -This page covers four things, in order: - -1. [Module map](#module-map) — where everything lives -2. [Data flow](#data-flow) — how values travel from user input to packed frame -3. [Algorithms](#algorithms) — pseudo-code for the three nested loops -4. [Hot path](#hot-path-objective-evaluation) — what one objective evaluation does -5. [Invariants and conventions](#invariants-and-conventions) — load-bearing rules - -## Module map - -```text -src/ -├── lib.rs public re-exports + rustdoc chapters -├── packer.rs Molpack builder + pack() driver + phase / iteration loops -├── target.rs Target — molecule type + per-molecule restraints -├── restraint.rs Restraint trait + 14 concrete *Restraint structs -├── region.rs Region trait + And/Or/Not + RegionRestraint -├── relaxer.rs Relaxer / RelaxerRunner + TorsionMcRelaxer -├── handler.rs Handler trait + 4 built-in observers -├── objective.rs compute_f / compute_g / compute_fg + Objective impl -├── context/ PackContext = single owner of mutable packing state -│ ├── pack_context.rs -│ ├── model.rs immutable topology + inputs -│ ├── state.rs mutable per-iteration state -│ └── work_buffers.rs scratch arrays (xcart, gxcar, …) -├── constraints/ EvalMode / EvalOutput facade -├── gencan/ bound-constrained quasi-Newton optimizer -│ ├── mod.rs pgencan / gencan / tn_linesearch -│ ├── cg.rs conjugate-gradient inner solve -│ └── spg.rs spectral projected gradient fallback -├── initial.rs initial random placement + restmol pre-fit -├── movebad.rs worst-molecule perturbation heuristic -├── euler.rs Euler angles ↔ rotation matrices -├── cell.rs cell-list neighbor lookup -├── frame.rs PackContext ↔ molrs::Frame conversions -├── validation.rs post-pack correctness check -├── script/ .inp parser + lowering to Targets -├── api/ builder facade re-exports -└── bin/molpack/ CLI front-end (cli feature) -``` - -### Dependency direction - -```text - lib.rs - │ - packer.rs (driver — depends on everything below) - │ - ┌──────────┬──────┴──────┬──────────┬──────────┐ - ▼ ▼ ▼ ▼ ▼ - target initial gencan movebad handler - │ │ │ │ - │ └────────┐ │ │ - ▼ ▼ ▼ ▼ - restraint + region context/PackContext - │ - ▼ - objective.rs ← hot path - │ - └── constraints/ (EvalMode facade) -``` - -`target` / `restraint` / `region` are pure data — no driver imports. -`packer` is the only module that imports everything else. `objective` -is the narrow waist through which all per-atom work flows. - -## Data flow - -```text -USER INPUTS ─→ Target / Molpack builders - Frame, count, restraints, - handlers, tolerance, seed - ─→ pack() entry - a. broadcast global → per-target restraints - b. snapshot every Target - c. build PackContext - ModelData (immutable topology) - RuntimeState (x, coor, radius) - WorkBuffers (xcart, gxcar, scratch) - d. flatten restraints → CSR pool - e. initial placement → x[0..6·ntotmol] - -PER-ITERATION ─→ evaluate(x, mode, &mut g) - GENCAN reads x, → expand_molecules: x → xcart - reads f / g via → restraint penalties per atom - &mut dyn Objective → cell list + pair penalties - → project gradient back: gxcar → g - returns f_total, fdist, frest - -OUTPUT ─→ Frame - pack_with_report() also exposes - converged, fdist, frest -``` - -Three rules govern this flow: - -- **`PackContext` owns mutable state.** GENCAN, movebad, handlers, and the - phase driver all take `&mut PackContext` (writers) or `&PackContext` - (observers). No other module owns mutable state across iterations. -- **`Arc` for polymorphic storage.** Cheap clone (refcount - bump) into the per-atom CSR pool. The hot path does one virtual call - per restraint per atom. -- **GENCAN is decoupled.** `gencan/pgencan` takes `&mut dyn Objective`, - not `&mut PackContext`. Synthetic objectives (Rosenbrock, Booth, Beale) - exercise the optimizer in isolation. - -### Coordinate layout - -The optimizer variable vector `x` packs centers of mass and Euler angles: - -```text -x = [com₀(3), com₁(3), …, comₙ(3), eul₀(3), eul₁(3), …, eulₙ(3)] -length = 6 · ntotmol -``` - -Cartesian atom positions `xcart: Vec<[F; 3]>` of length `ntotat` are -expanded each evaluation: - -```text -xcart[icart_for(i, m, a)] = com_m + R(eul_m) · ref_coords[i, a] -``` - -where `i` is molecule type, `m` is copy index, `a` is atom index. - -## Algorithms - -Three nested loops drive the packer. - -### Outer: `pack()` (one call) - -```text -fn pack(targets, max_loops): - validate inputs (non-empty, valid PBC, atoms > 0) - broadcast Molpack.global_restraints → each target's molecule_restraints - split targets into free / fixed - build PackContext - run init_passes of restmol(): // geometric pre-fit, no pair kernel - for each free target type: - place molecules randomly inside their restraints - relax restraint penalties only - handlers.on_start, handlers.on_initialized - for phase in 0 ..= ntype: - if phase < ntype: - comptype[i] := (i == phase) // PER-TYPE pre-compaction - else: - comptype[i] := true // ALL-TYPES main phase - report := run_phase(phase, max_loops, …) - if report.error_phase: break - handlers.on_finish - build Frame; pack_with_report() also returns converged/fdist/frest -``` - -Why per-type pre-compaction first: if every type optimizes simultaneously -from a random start, cross-type interference traps the solver in shallow -minima. Compacting one type at a time inside its own restraint volume -gives the all-types phase a much better seed. - -### Middle: `run_phase` (one phase) - -```text -fn run_phase(phase_id, max_loops): - handlers.on_phase_start(phase_info) - radscale := discale // start with inflated radii (default 1.1) - relax_runners := build relaxer runners for this phase - // Quick-exit: if the unscaled objective is already below precision, - // skip the whole phase. - if evaluate_unscaled(sys, x).below(precision): return Converged - for loop_idx in 0 .. max_loops: - result := run_iteration(loop_idx, radscale, relax_runners) - radscale := decay(radscale) // → 1.0 over the phase - handlers.on_step(step_info, sys) - if result.converged: return Converged - if handlers.should_stop(): return EarlyStop - return MaxLoops -``` - -`radscale` starts at `discale` (1.1) and decays toward 1.0 over the -phase. This soft-starts the pair penalty: the optimizer first sees -slightly oversized atoms (easier to push apart) and tightens to true -tolerance as the phase progresses. - -### Inner: `run_iteration` (one outer step) - -```text -fn run_iteration(loop_idx, radscale, runners): - // 1. Movebad — relocate the K worst molecules. - if movebad enabled: - identify atoms with largest restraint + pair penalty - perturb their COM/Euler within init_box_half_size - // 2. Relaxers — update reference geometry per type (count == 1 only). - for (type, runner) in runners: - runner.on_iter(ref_coords, f_current, &mut evaluate, rng) - if accepted: write back new ref_coords - // 3. GENCAN — bound-constrained quasi-Newton solve. - pgencan(x, &mut sys, params, precision) - // Internally: tn_linesearch → CG inner solve → SPG fallback, - // each step calls sys.evaluate(x, mode, g). - // 4. Convergence check on the unscaled objective. - f_unscaled := evaluate_unscaled(sys, x) - fimp := percentage improvement vs previous loop - converged := fdist < precision AND frest < precision - return { converged, fimp, fdist, frest } -``` - -GENCAN itself runs three nested solvers: - -```text -pgencan: project x onto bounds, then call gencan -gencan: truncated-Newton outer; calls tn_linesearch -tn_ls: conjugate-gradient line search; SPG fallback if CG stalls -``` - -Each leaf step calls `sys.evaluate(x, mode, &mut g)` — the hot path. - -## Hot path: objective evaluation - -`PackContext::evaluate` is invoked O(10³–10⁴) times per `pack()` run. -Performance lives here. - -```text -evaluate(x, mode, g) dispatches by mode: - FOnly → compute_f - GradientOnly → compute_g - FAndGradient → compute_fg - RestMol → compute_fg (init phase, pair kernel skipped) -``` - -`compute_fg` is the canonical path — it does five steps: - -```text -1. expand_molecules(x): - for each molecule type t, copy m, atom a: - xcart[icart] := com_t,m + R(eul_t,m) · ref_coords[t, a] - -2. accumulate_constraint_value_and_gradient (per atom icart): - range := iratom_offsets[icart] .. iratom_offsets[icart + 1] - for &irest in iratom_data[range]: - f += sys.restraints[irest].fg(xcart[icart], scale, scale2, - &mut grad_xcart[icart]) - // Linear penalties consume `scale`; quadratic consume `scale2`. - -3. insert_atom_in_cell (per atom): - linked-list bucket atoms into cells - cell side ≈ 2 × max_radius × radscale - -4. accumulate_pair_fg (or _parallel under rayon): - for each non-empty cell c: - for each neighbor cell c′ in 13-cell stencil: - for each (i ∈ c, j ∈ c′): - d := pbc_distance(xi, xj) - σ := (rᵢ + rⱼ) · radscale - if d < σ: - penalty := (σ − d)² - grad_xcart[i] += d penalty / d xi - grad_xcart[j] += d penalty / d xj - -5. project_cartesian_gradient: - for each molecule m, atom a: - g_com[m] += grad_xcart[icart] - g_euler[m] += Jᵀ(eul_m, ref_a) · grad_xcart[icart] - // J = ∂xcart/∂eul, derived once per molecule from R(eul). -``` - -Cost breakdown: steps 1–3 are O(N_atoms); step 4 is -O(N_atoms × neighbor_avg) ≈ O(N_atoms × 32) and dominates wall time on -realistic workloads. Step 4 is the rayon parallelization point -(`accumulate_pair_fg_parallel`), reducing into per-atom gradient slots -via `AtomicU64` (since `Cell` is not `Sync`). - -The `Arc` virtual call in step 2 measured at +0.22% e2e -versus the prior monomorphic dispatch — a negligible cost for the -flexibility of user-defined restraints. - -## Invariants and conventions - -**Gradient accumulation.** `Restraint::fg` accumulates the true -gradient (∂penalty/∂x) into `g` with `+=`. Optimizer negates for descent. -Multiple restraints may touch one atom, so never overwrite. - -**Two-scale contract.** Linear penalties (Packmol kinds 2/3/6/7/10/11) -consume `scale`; quadratic penalties (kinds 4/5/8/9/12/13/14/15) consume -`scale2`. Each `impl Restraint` picks one internally. - -**Rotation convention.** `R_new = δR · R_old` (LEFT multiplication). -Single-atom tests cannot detect LEFT/RIGHT bugs — always test with -≥ 2 atoms. - -**Coordinate layout.** GENCAN's `x` is `[com₀..n, eul₀..n]` of length -`6·ntotmol`. Cartesian atom positions `xcart` are `Vec<[F; 3]>` of length -`ntotat`. - -**Thread safety.** All trait objects are `Send + Sync`. Interior -mutability inside parallel reductions uses `AtomicU64` with -`f64::to_bits` / `f64::from_bits` — `Cell` is not `Sync`. - -**Scope equivalence.** - -```text -molpack.with_global_restraint(r) - ≡ for t in targets: t.with_restraint(r.clone()) -``` - -There is no separate global-restraint storage path. The broadcast at -`pack()` entry is the implementation. - -**Restraint vs Constraint.** Packmol implements all 15 "constraints" as -soft penalties. Naming reflects mechanism, not user intent → `Restraint`. - -**Direction-3 extension pattern.** Every extension trait follows the -same shape: public trait, N concrete `pub struct` impls, user types -`impl Trait` identically. No `Builtin*` / `Native*` wrappers in the -public API. - -**`init1` short-circuit.** Set during the initial geometric pre-fit. -Skips the pair kernel — the restraint-only objective is enough to get -atoms into their regions before pair conflicts matter. - -## Cheatsheet - -| Question | Where to look | -|---|---| -| How is one restraint's penalty computed for one atom? | `restraint.rs::*::f` / `*::fg` | -| Where does `with_global_restraint` broadcast? | `packer.rs::pack` (top of fn) | -| Where is the per-atom CSR pool built? | `packer.rs::pack` (CSR build loop) | -| How are `x` ↔ Cartesian coords expanded? | `objective.rs::expand_molecules`, `euler.rs::eulerrmat` | -| Where is the pair-overlap kernel? | `objective.rs::accumulate_pair_fg_parallel` | -| What does the initial pre-fit do? | `initial.rs::initial`, `initial.rs::restmol` | -| How is precision-based termination tested? | `gencan/mod.rs::packmolprecision` | -| What does `movebad` do? | `movebad.rs::movebad` | -| How is torsion MC wired in? | `relaxer.rs::TorsionMcRelaxer::on_iter` | -| Where does periodic boundary wrap apply? | `context/pack_context.rs::pbc_distance` | diff --git a/docs/zh/assets b/docs/zh/assets deleted file mode 120000 index ec2e4be..0000000 --- a/docs/zh/assets +++ /dev/null @@ -1 +0,0 @@ -../assets \ No newline at end of file diff --git a/docs/zh/cli/examples.md b/docs/zh/cli/examples.md deleted file mode 100644 index 1335167..0000000 --- a/docs/zh/cli/examples.md +++ /dev/null @@ -1,34 +0,0 @@ -# Examples - -The repository includes five canonical Packmol-equivalent workloads. Each can -be run directly through the CLI: - -```bash -molpack examples/pack_mixture/mixture.inp -molpack examples/pack_bilayer/bilayer-comment.inp -molpack examples/pack_interface/interface.inp -molpack examples/pack_spherical/spherical-comment.inp -molpack examples/pack_solvprotein/solvprotein.inp -``` - -## Workloads - -| Workload | Script | Demonstrates | -|---|---|---| -| Mixture | `examples/pack_mixture/mixture.inp` | Two species co-packed in a cube. | -| Bilayer | `examples/pack_bilayer/bilayer-comment.inp` | Atom-subset plane restraints for layer orientation. | -| Interface | `examples/pack_interface/interface.inp` | Liquid-liquid interface around a fixed solute. | -| Spherical | `examples/pack_spherical/spherical-comment.inp` | Nested radial shells. | -| Solvated protein | `examples/pack_solvprotein/solvprotein.inp` | Fixed protein plus water and ions. | - -## Compare with programmatic APIs - -The same workloads also exist as Rust examples under `examples/pack_*` and as -Python scripts under `python/examples/`. Use them when you want to compare a -scripted `.inp` job with the builder APIs. - -```bash -cargo run --release --features io --example pack_mixture -cd python -python examples/pack_mixture.py -``` diff --git a/docs/zh/cli/formats.md b/docs/zh/cli/formats.md deleted file mode 100644 index 892bd92..0000000 --- a/docs/zh/cli/formats.md +++ /dev/null @@ -1,30 +0,0 @@ -# Formats - -The CLI reads molecule templates through the `io` feature and writes the final -packed structure to the path named by the script's `output` keyword. - -The output format is inferred from the output file extension. Input formats can -be inferred from structure-file extensions or set globally with `filetype`. - -| Format | Read | Write | Extension or `filetype` | -|---|---:|---:|---| -| PDB | Yes | Yes | `.pdb`, `pdb` | -| XYZ | Yes | Yes | `.xyz`, `xyz` | -| SDF / MOL | Yes | No | `.sdf`, `.mol`, `sdf` | -| LAMMPS dump | Yes | Yes | `.lammpstrj`, `lammps_dump` | -| LAMMPS data | Yes | No | `.data`, `lammps_data` | - -## Example - -```text -filetype pdb -output packed.xyz - -structure water.pdb - number 100 - inside box 0. 0. 0. 30. 30. 30. -end structure -``` - -The input template is read as PDB because of `filetype pdb`; the output is -written as XYZ because the output path ends with `.xyz`. diff --git a/docs/zh/cli/index.md b/docs/zh/cli/index.md deleted file mode 100644 index 12fe593..0000000 --- a/docs/zh/cli/index.md +++ /dev/null @@ -1,56 +0,0 @@ -# CLI - -Use the `molpack` binary when you already have a Packmol-style `.inp` -script, or when you want a reproducible packing job that can be checked into a -simulation workflow. - -```bash -molpack mixture.inp -``` - -The file form resolves paths inside the script relative to the script's -directory. Stdin mode is also supported and resolves relative paths against the -current working directory: - -```bash -molpack < mixture.inp -``` - -## Install - -```bash -cargo install molcrafts-molpack --features cli -molpack --help -``` - -For source builds: - -```bash -cargo build --features cli --bin molpack -``` - -## Minimal input - -```text -tolerance 2.0 -seed 42 -filetype pdb -output packed.pdb -nloop 400 - -structure water.pdb - number 1000 - inside box 0. 0. 0. 40. 40. 40. -end structure -``` - -Every mobile structure should have a spatial restraint such as `inside box`, -`inside sphere`, or a global `pbc` declaration. Without one, initial placement -has to infer a huge free-space box and the run can become impractical. - -## Next steps - -- [Packmol Script Notes](script-format.md) links to the Packmol manual and - lists molpack-specific keywords, formats, and parser behavior. -- [Formats](formats.md) documents readable and writable molecular file types. -- [Examples](examples.md) shows the five checked-in canonical workloads. diff --git a/docs/zh/cli/script-format.md b/docs/zh/cli/script-format.md deleted file mode 100644 index e640546..0000000 --- a/docs/zh/cli/script-format.md +++ /dev/null @@ -1,66 +0,0 @@ -# Packmol Script Notes - -The CLI reads Packmol-style `.inp` files. For the script language itself, use -the official [Packmol user guide](https://m3g.github.io/packmol/userguide.shtml) -as the reference. molpack intentionally keeps that job shape: global settings, -then one `structure ... end structure` block per molecule type. - -```text -tolerance 2.0 -filetype pdb -output packed.pdb - -structure water.pdb - number 1000 - inside box 0. 0. 0. 40. 40. 40. -end structure -``` - -This page only documents molpack-specific behavior and extensions. If a Packmol -keyword is not listed here, treat Packmol's manual as the source of truth and -use [Packmol parity](../packmol_parity.md) for implementation-level -compatibility notes. - -## Invocation and paths - -- `molpack mixture.inp`: paths inside `mixture.inp` resolve relative to the - directory that contains `mixture.inp`. -- `molpack < mixture.inp`: paths resolve relative to the current working - directory. - -This matches the two common Packmol invocation styles while making file-argument -mode safe to run from another directory. - -## molpack additions - -| Keyword | Meaning | -|---|---| -| `avoid_overlap ` | Controls whether initial random placements are rejected when they overlap a fixed molecule. Default: on. Use `avoid_overlap no` only when you explicitly want Packmol's fixed-solute exclusion guard disabled. | -| `filetype sdf` | Read SDF/MOL inputs through the CLI. Read-only. | -| `filetype lammps_dump` | Read LAMMPS dump inputs and write `.lammpstrj` outputs. | -| `filetype lammps_data` | Read LAMMPS data inputs. Read-only. | - -Output format is inferred from the `output` extension. The extra molpack output -extension is `.lammpstrj` for LAMMPS dump trajectories. - -## Strict parsing - -molpack rejects unknown keywords instead of silently ignoring them. A misspelled -top-level keyword returns `ScriptError::UnknownKeyword`, which prevents broken -inputs from falling through to an expensive or invalid packing run. - -## Supported Packmol subset - -The parser covers the Packmol syntax used by the bundled examples and -regression suite: - -- global `tolerance`, `seed`, `filetype`, `output`, `nloop`, and `pbc`; -- `structure ... end structure` blocks; -- `number`, `center`, `centerofmass`, and `fixed`; -- whole-molecule and `atoms ... end atoms` restraints; -- `inside`/`outside` box, cube, sphere, ellipsoid, and cylinder; -- `over plane`, `above plane`, and `below plane`. - -Atom indices in `.inp` files are 1-based, matching Packmol. When translating -the same model to Python or Rust, subtract 1 because those APIs use 0-based -indices. diff --git a/docs/zh/concepts.md b/docs/zh/concepts.md deleted file mode 100644 index f956817..0000000 --- a/docs/zh/concepts.md +++ /dev/null @@ -1,307 +0,0 @@ -# Core Concepts - -This chapter defines each abstraction in the crate in one place. -Cross-link to the types for full API details. - -## AtomRestraint - -An [`AtomRestraint`](crate::AtomRestraint) is a **soft penalty** applied per -atom: `f(x, scale, scale2) -> F` and `fg(x, scale, scale2, g) -> F`. -It contributes to the packing objective and — in all current -implementations — derives from Packmol's `comprest.f90` / `gwalls.f90`. - -```text -pub trait AtomRestraint: Send + Sync + std::fmt::Debug { - fn f (&self, x: &[F; 3], scale: F, scale2: F) -> F; - fn fg(&self, x: &[F; 3], scale: F, scale2: F, g: &mut [F; 3]) -> F; - fn is_parallel_safe(&self) -> bool { true } - fn name(&self) -> &'static str { std::any::type_name::() } -} -``` - -The crate ships 14 concrete `*Restraint` structs (one per Packmol -`kind` 2..=15), each holding its own semantically-named geometric -fields. User types `impl Restraint` sit in the same type slot — -there is no `Builtin*` wrapper in the public API. See -[`extending`](crate::extending) for a tutorial. - -### Gradient convention - -`fg` accumulates the TRUE gradient (∂penalty/∂x) INTO `g` with `+=`. -Do not overwrite: many restraints may touch the same atom. The -optimizer negates for descent. - -### Two-scale contract - -Packmol convention (mirrored in the port): - -- Linear penalties — kinds 2, 3, 6, 7, 10, 11 (box / cube / plane) — - consume `scale`. -- Quadratic penalties — kinds 4, 5, 8, 9, 12, 13, 14, 15 (sphere / - ellipsoid / cylinder / gaussian) — consume `scale2`. - -Each `impl Restraint` picks one internally. User-defined restraints -may ignore both knobs and use their own stiffness coefficient as an -instance field. - -## CollectiveRestraint - -A [`CollectiveRestraint`](crate::restraint::Restraint) is a -**group-level** penalty — unlike [`AtomRestraint`](#atomrestraint), which sees one atom -at a time and contributes an independent external field Σᵢ U(xᵢ), a -collective restraint sees *every* copy of a species at once and returns a -single penalty whose gradient is **coupled across the whole group**: - -```text -pub trait Restraint: Send + Sync + std::fmt::Debug { - fn f (&self, coords: &[[F; 3]], scale: F, scale2: F) -> F; - fn fg(&self, coords: &[[F; 3]], scale: F, scale2: F, grads: &mut [[F; 3]]) -> F; - fn is_parallel_safe(&self) -> bool { true } - fn name(&self) -> &'static str { std::any::type_name::() } -} -``` - -`coords` and `grads` have equal length — one entry per atom in the group. -The gradient convention mirrors [`AtomRestraint`](#atomrestraint): `fg` accumulates INTO -`grads[i]` with `+=`. - -### Why collective? - -A per-atom field built from a target density ρ\* (e.g. Boltzmann inversion -U = −kT·ln ρ\*) is minimised by driving *every* site to the single minimum -of U — the mode of ρ\* — so the sites collapse onto the peak instead of -spreading over the distribution. A collective penalty over the whole -species that matches the empirical distribution to a target profile via -the squared 1-D Wasserstein (sorted-CDF) distance has the correct fixed -point: *empirical distribution = target*. - -### Geometry × distribution cross-product - -Every member of this family matches a target distribution of a scalar -reaction coordinate ξ defined by a **geometry**, via the Wasserstein -engine. The two axes are orthogonal: - -- **Geometry** — maps Cartesian coordinates to ξ and scatters ∂L/∂ξ back: - `plane` (ξ = signed distance to a plane → slab profile), `point` - (ξ = distance to a centre → radial profile). -- **Distribution** — the target quantile function q(p) = F⁻¹(p): - Gaussian, exponential, or tabulated (arbitrary user-supplied profile). - -Concrete types are the cross product, named ``: - -| | Gaussian | Exponential | Tabulated | -|---|---|---|---| -| Plane | `GaussianPlane` | `ExponentialPlane` | `TabulatedPlane` | -| Point | `GaussianPoint` | `ExponentialPoint` | `TabulatedPoint` | - -Attach with `Target::with_collective_restraint(r)`. A `TabulatedPlane` -with a histogram from a target simulation is the one-line way to drive a -species toward an experimentally-observed density profile. - -## Region - -A [`Region`](crate::Region) is a **geometric predicate** with a signed -distance function: - -```text -pub trait Region: Send + Sync + std::fmt::Debug { - fn contains(&self, x: &[F; 3]) -> bool; - fn signed_distance(&self, x: &[F; 3]) -> F; - fn signed_distance_grad(&self, x: &[F; 3]) -> [F; 3] { /* default FD */ } - fn bounding_box(&self) -> Option { None } -} -``` - -Regions compose via the zero-cost combinators -[`And`](crate::And) / [`Or`](crate::Or) / [`Not`](crate::Not), with -analytic chain-rule gradients (max / min / negate). The -[`RegionExt`](crate::RegionExt) trait gives every `Region` ergonomic -`.and(...)` / `.or(...)` / `.not()` methods. - -Any `Region` lifts to a `Restraint` via -[`RegionRestraint`](crate::RegionRestraint): - -```text -penalty(x) = scale2 * max(0, signed_distance(x))² -``` - -Use `Region` when you want compositional geometry (intersection / -union / complement). Use `Restraint` directly when you want a specific -penalty shape (linear vs quadratic, custom stiffness, multi-atom). - -## Relaxer - -A [`Relaxer`](crate::Relaxer) modifies a target's **reference geometry** -between outer optimizer calls. Use cases: torsion-MC sampling for -flexible chains, local MD relaxation, gradient descent on bond-angle -targets. - -Two-part design — builder + runner: - -- `Relaxer::spawn(&self, frame, ref_coords) -> Box` is - called once at `pack()` entry. -- `RelaxerRunner::on_iter(&mut self, coords, f_current, evaluate, rng)` - runs between movebad and GENCAN each outer iteration; returns - `Some(new_coords)` on accept, `None` on reject. - -Relaxers require `count == 1` because all copies share the same -reference coords. - -Built-in: [`TorsionMcRelaxer`](crate::TorsionMcRelaxer) (Metropolis -torsion sampling with self-avoidance). - -## Handler - -A [`Handler`](crate::Handler) is an observer invoked at well-defined -lifecycle points: - -```text -pub trait Handler: Send { - fn on_start (&mut self, ntotat, ntotmol) {} - fn on_initialized (&mut self, sys: &PackContext) {} - fn on_step (&mut self, info: &StepInfo, sys); // required - fn on_phase_start (&mut self, info: &PhaseInfo) {} - fn on_phase_end (&mut self, info, report: &PhaseReport) {} - fn on_inner_iter (&mut self, iter, f, sys) {} - fn on_finish (&mut self, sys: &PackContext) {} - fn should_stop (&self) -> bool { false } -} -``` - -Observer contract: `sys` is always `&PackContext`, never `&mut`. -Handlers cannot modify packer state — use a `Relaxer` if you need to. - -Built-ins: [`NullHandler`](crate::NullHandler), -[`ProgressHandler`](crate::ProgressHandler), -[`EarlyStopHandler`](crate::EarlyStopHandler), -[`XYZHandler`](crate::XYZHandler). - -## Objective - -The [`Objective`](crate::objective::Objective) trait abstracts over -what GENCAN sees. `PackContext` implements it; synthetic test -objectives (Rosenbrock / Booth / Beale) can implement it to exercise -the optimizer in isolation. - -```text -pub trait Objective { - fn evaluate(&mut self, x: &[F], mode: EvalMode, g: Option<&mut [F]>) -> EvalOutput; - fn fdist(&self) -> F; - fn frest(&self) -> F; - fn ncf(&self) -> u32; - fn ncg(&self) -> u32; - fn reset_eval_counters(&mut self); - fn bounds(&self, l: &mut [F], u: &mut [F]); -} -``` - -GENCAN (`pgencan`, `gencan`, `tn_ls`, `spg`, `cg`) takes `&mut dyn -Objective` rather than `&mut PackContext` — the optimizer is -decoupled from the packing state. - -## Target - -A [`Target`](crate::Target) describes one molecule type: - -- Input coordinates + centered reference coordinates. -- Van der Waals radii, element symbols, copy count, name. -- Its attached restraints (per-target + per-atom-subset). -- Its attached relaxers. -- Optional fixed placement (Euler + translation). -- Optional Euler-angle bounds (`with_rotation_bound(Axis, Angle, Angle)`). - -Targets are snapshotted at `pack()` entry — mutating a `Target` after -passing it to the packer has no effect. - -## Molpack - -[`Molpack`](crate::Molpack) is the builder facade: - -```text -Molpack::new() - .with_log_level(...) - .with_handler(...) - .with_global_restraint(...) // broadcast to every target - .with_periodic_box(min, max) // or via periodic InsideBoxRestraint - .pack(&[targets], max_loops) -``` - -Every tuning knob (`with_tolerance`, `with_precision`, -`with_inner_iterations`, `with_seed`, `with_avoid_overlap`, …) has a -Packmol-matching default, so `Molpack::new().pack(&targets, max_loops)` -is a complete call. You only set a knob to *change* its default — e.g. -`with_avoid_overlap(false)` to let solvent seed inside a fixed solute -(on by default), or `with_seed(n)` to pick a different RNG stream (the -default seed is Packmol's `1_234_567`). - -Every setter consumes and returns `self`. `pack` takes `&mut self` -(handlers are invoked through it). - -## PackContext - -[`PackContext`](crate::PackContext) is the single owner of mutable -packing state — coordinates, cell lists, restraint pool, rotation -buffers, counters. All optimizer / movebad / handler code paths take -`&mut PackContext` (for writers) or `&PackContext` (for observers). - -Structure (`molpack/src/context/`): - -- `ModelData` — topology and inputs (immutable after init). -- `RuntimeState` — mutable per-iteration state (x, coor, radius). -- `WorkBuffers` — scratch arrays (xcart, gxcar, radiuswork). - -Users rarely touch `PackContext` directly — it's passed through -handlers and relaxers. Power users implementing a custom `Objective` -against synthetic test problems will interact with it. - -## Scope equivalence law - -```text -molpack.with_global_restraint(r) - ≡ for t in targets { t.with_restraint(r.clone()) } -``` - -There is no separate "global-restraint" storage path in `PackContext`. -The broadcast happens inside `pack()`; each target receives an -`Arc::clone` of every global restraint (refcount bump, not a deep -copy). - -Per-atom-subset scope is a method-argument pair -`(indices: &[usize], restraint: impl Restraint)`, not a wrapper -struct. There is no `AtomRestraint` public type. - -## Restraint versus Constraint - -- **Restraint** = soft penalty (violable; pays energy cost). -- **Constraint** = hard constraint (must satisfy; SHAKE / RATTLE / - LINCS / Lagrange-multiplier mechanisms). - -Packmol implements all 15 "constraints" as soft penalties -(`scale * max(0, d)` or `scale2 * max(0, d)²`). Honest naming ⇒ -`Restraint`. This crate does not currently define a `Constraint` -trait; adding hard constraints is future work. - -## Direction-3 extension pattern - -Every extension trait in this crate follows the same shape: - -1. Public trait: `pub trait X`. -2. N concrete `pub struct` types that `impl X`, each holding its own - semantically-named fields. -3. User types `impl X` identically — zero type-level distinction from - built-ins. - -Forbidden in the public API: - -- `Builtin*` / `Native*` / `Packmol*` prefixed wrapper types. -- Tagged-union enums that package N built-ins as a single exposed - type. -- Builder pattern (`X::new().add(...).add(...)`). -- Injection of composition operators into the main trait - (composition lives on separate traits, e.g. `Region` vs `Restraint`). -- Wrapper types for per-atom-subset scope (that's a method-argument - pair, not a type). - -If you need internal AoS performance structures (e.g. tagged unions -for hot-path match dispatch), they go behind `pub(crate)` and opt-in -via a crate-private hook — invisible to users. diff --git a/docs/zh/development/index.md b/docs/zh/development/index.md deleted file mode 100644 index d251cf7..0000000 --- a/docs/zh/development/index.md +++ /dev/null @@ -1,31 +0,0 @@ -# Development - -These pages are for modifying molpack rather than just using it. - -molpack is a Rust packing engine with three public surfaces: - -- `molcrafts-molpack` Rust library (`lib` name: `molpack`) -- `molpack` CLI binary (`cli` feature) -- `molcrafts-molpack` Python wheel under `python/` - -## Read first - -- [Architecture](../architecture.md) maps modules, data flow, optimizer loops, - and the objective-evaluation hot path. -- [Extending](../extending.md) walks through custom `AtomRestraint`, - `Region`, `Handler`, and `Relaxer` implementations. - -## Validation commands - -```bash -cargo test -p molcrafts-molpack --lib --tests -cargo test -p molcrafts-molpack --release --test examples_batch -- --ignored -cd python -maturin develop --release -pytest -cargo fmt -cargo clippy -- -D warnings -``` - -The ignored `examples_batch` test runs the canonical Packmol-equivalent -workloads and is intentionally slower than the fast unit/integration tier. diff --git a/docs/zh/examples.md b/docs/zh/examples.md deleted file mode 100644 index 3b4807b..0000000 --- a/docs/zh/examples.md +++ /dev/null @@ -1,62 +0,0 @@ -# Examples - -Five canonical Packmol-equivalent workloads ship in `examples/`. Each -exercises a different combination of restraints, fixed placements, and -target counts, and all five are covered by the regression suite -(`tests/examples_batch.rs`). - -| Workload | Rust example | Python example | Molecules | Restraints | Demonstrates | -|---|---|---|---|---|---| -| Mixture | `examples/pack_mixture` | `python/examples/pack_mixture.py` | 1000 water + 400 urea | one `inside box` per type | the simplest two-component fill in a cube | -| Bilayer | `examples/pack_bilayer` | `python/examples/pack_bilayer.py` | water slabs + palmitoil lipids | per-atom `above`/`below plane` | orienting the two leaflets of a membrane | -| Interface | `examples/pack_interface` | `python/examples/pack_interface.py` | water + chloroform + 1 fixed t3 | `inside box` per solvent + one `fixed` molecule | a liquid–liquid interface around a fixed structure | -| Spherical | `examples/pack_spherical` | `python/examples/pack_spherical.py` | concentric lipid + water shells | per-atom radial `inside`/`outside sphere` | nested shell packing (largest / slowest case) | -| Solvated protein | `examples/pack_solvprotein` | `python/examples/pack_solvprotein.py` | 1 fixed protein + water + Na⁺ + Cl⁻ | `inside sphere` solvent around a `fixed` solute | dense solvation that relies on `avoid_overlap` | - -## Run the Rust examples - -The examples need the `io` feature so they can read the bundled structure -files: - -```bash -cargo run --release --example pack_mixture --features io -cargo run --release --example pack_bilayer --features io -cargo run --release --example pack_interface --features io -cargo run --release --example pack_spherical --features io -cargo run --release --example pack_solvprotein --features io -``` - -Each directory also contains the matching `.inp` script -(`mixture.inp`, `bilayer-comment.inp`, `interface.inp`, -`spherical-comment.inp`, `solvprotein.inp`), so the CLI form and the -programmatic form can be compared side by side: - -```bash -cargo run --release --features cli --bin molpack -- examples/pack_mixture/mixture.inp -``` - -Optional progress / trajectory dumps are gated behind environment -variables: - -```bash -MOLRS_PACK_EXAMPLE_PROGRESS=1 cargo run --release --example pack_mixture --features io -MOLRS_PACK_EXAMPLE_XYZ=1 cargo run --release --example pack_mixture --features io -``` - -## Run the Python examples - -The Python examples mirror the Rust ones and load their structure files -through the `molrs` dependency installed with molpack: - -```bash -cd python -maturin develop --release -python examples/pack_mixture.py -python examples/pack_bilayer.py -python examples/pack_interface.py -python examples/pack_spherical.py -python examples/pack_solvprotein.py -``` - -`python/examples/pack_water_cube.py` is a minimal standalone starter -(single-species cube) for a first look at the Python API. diff --git a/docs/zh/extending.md b/docs/zh/extending.md deleted file mode 100644 index d451ce3..0000000 --- a/docs/zh/extending.md +++ /dev/null @@ -1,474 +0,0 @@ -# Extending the Crate - -Tutorials for writing your own `AtomRestraint` / `Region` / `Handler` / -`Relaxer` types. Every extension trait in this crate follows the same -shape (direction-3 rule — see [`concepts`](crate::concepts)): - -> `pub trait X` + N concrete `pub struct` types implementing it. -> User types `impl X` identically. No built-in/plugin type-level -> distinction. - -## Custom `AtomRestraint` - -Goal: pull atoms toward a target plane with a quadratic attractive -well. - -### Step 1 — define the struct - -```rust -use molrs::types::F; -# use molpack::AtomRestraint; - -#[derive(Debug, Clone, Copy)] -pub struct PlaneTether { - pub normal: [F; 3], - pub offset: F, - pub k: F, -} -# impl AtomRestraint for PlaneTether { -# fn f(&self, _x: &[F; 3], _s: F, _s2: F) -> F { 0.0 } -# fn fg(&self, _x: &[F; 3], _s: F, _s2: F, _g: &mut [F; 3]) -> F { 0.0 } -# } -``` - -- `pub` fields — users construct with `PlaneTether { normal, offset, - k }`, no builder. -- `Debug` required because [`AtomRestraint`](crate::AtomRestraint) has a - `Debug` supertrait bound (so `Target`'s derived `Debug` keeps - working). - -### Step 2 — implement `AtomRestraint` - -```rust -# use molrs::types::F; -# use molpack::AtomRestraint; -# #[derive(Debug)] -# pub struct PlaneTether { pub normal: [F; 3], pub offset: F, pub k: F } -impl AtomRestraint for PlaneTether { - fn f(&self, pos: &[F; 3], _scale: F, _scale2: F) -> F { - let d = self.normal[0] * pos[0] - + self.normal[1] * pos[1] - + self.normal[2] * pos[2] - - self.offset; - 0.5 * self.k * d * d - } - fn fg(&self, pos: &[F; 3], scale: F, scale2: F, g: &mut [F; 3]) -> F { - let d = self.normal[0] * pos[0] - + self.normal[1] * pos[1] - + self.normal[2] * pos[2] - - self.offset; - g[0] += self.k * d * self.normal[0]; - g[1] += self.k * d * self.normal[1]; - g[2] += self.k * d * self.normal[2]; - self.f(pos, scale, scale2) - } -} -``` - -Three contracts that all restraints must obey: - -1. **Gradient accumulates with `+=`.** Multiple restraints may touch - the same atom. -2. **`fg` returns the value.** The hot path uses the returned value - for the `fdist`/`frest` accumulation — don't return `0.0` just - because the caller might discard it. -3. **Scale/scale2 usage is your choice.** Linear-penalty restraints - typically use `scale`; quadratic-penalty ones use `scale2`. Your - tether uses its own `k` — ignore both knobs if you prefer. - -### Step 3 — write a gradient test - -```no_run -# use molrs::types::F; -# use molpack::AtomRestraint; -# #[derive(Debug)] pub struct PlaneTether { pub normal: [F; 3], pub offset: F, pub k: F } -# impl AtomRestraint for PlaneTether { -# fn f(&self, _x: &[F; 3], _s: F, _s2: F) -> F { 0.0 } -# fn fg(&self, _x: &[F; 3], _s: F, _s2: F, _g: &mut [F; 3]) -> F { 0.0 } -# } -#[test] -fn plane_tether_gradient_matches_fd() { - let r = PlaneTether { normal: [0.0, 0.0, 1.0], offset: 5.0, k: 2.0 }; - let x = [1.0, 2.0, 7.0]; - let mut g = [0.0; 3]; - let _ = r.fg(&x, 1.0, 1.0, &mut g); - let h: F = 1e-5; - for k in 0..3 { - let mut xp = x; xp[k] += h; - let mut xm = x; xm[k] -= h; - let fd = (r.f(&xp, 1.0, 1.0) - r.f(&xm, 1.0, 1.0)) / (2.0 * h); - assert!( - (g[k] - fd).abs() < 1e-4, - "axis {k}: analytic={}, fd={}", g[k], fd, - ); - } -} -``` - -Convention: `ε = 1e-5`, tolerance `1e-3` (looser if the restraint has -kinks). - -### Step 4 — use it - -```no_run -# use molrs::types::F; -# use molpack::AtomRestraint; -# #[derive(Debug, Clone, Copy)] pub struct PlaneTether { pub normal: [F; 3], pub offset: F, pub k: F } -# impl AtomRestraint for PlaneTether { -# fn f(&self, _x: &[F; 3], _s: F, _s2: F) -> F { 0.0 } -# fn fg(&self, _x: &[F; 3], _s: F, _s2: F, _g: &mut [F; 3]) -> F { 0.0 } -# } -use molpack::{InsideBoxRestraint, Target}; -# let (pos, rad) = (&[[0.0; 3]][..], &[1.0][..]); - -let target = Target::from_coords(pos, rad, 100) - .with_restraint(InsideBoxRestraint::new([0.0; 3], [40.0; 3], [false; 3])) - .with_restraint(PlaneTether { normal: [0.0, 0.0, 1.0], offset: 20.0, k: 1.0 }); -``` - -Built-in `InsideBoxRestraint` and user `PlaneTether` take the same -code path — direction-3 in action. - -### In Python — the same restraint, duck-typed - -The interfaces are also exposed to Python through a *duck-typed* protocol: a -restraint is any object exposing `f` and `fg`, and the packer consumes it on the -same code path as a built-in. That makes Python the natural place to *prototype* -a restraint the input grammar can't express — a few lines, no recompile — and -then, once it earns its place, swap in the native equivalent with the driver -script unchanged. - -A duck-typed restraint is any object exposing `f` and `fg`. The Python protocol -differs from the Rust trait in exactly one way: `fg` **returns** -`(energy, (gx, gy, gz))` instead of accumulating into a `&mut [F; 3]` — the -binding does the `+=` for you. The contracts are otherwise identical: `fg` -returns the energy, the gradient is the chain rule `(dU/dξ)·∇ξ`, a linear-energy -penalty rides `scale` (a quadratic one `scale2`). A missing `f` or `fg` is -rejected at attach time with a `TypeError`, and an exception raised inside `fg` -propagates back out of `pack`. The `PlaneTether` above, duck-typed in Python: - -```python -import numpy as np - -class PlaneTether: - """Pull a site toward the plane z = offset (a per-site geometric bias).""" - def __init__(self, normal, offset, k): - n = np.asarray(normal, float) - self.n, self.offset, self.k = n / np.linalg.norm(n), offset, k - - def f(self, x, scale, scale2): - d = float(self.n @ np.asarray(x, float)) - self.offset - return scale * 0.5 * self.k * d * d - - def fg(self, x, scale, scale2): - d = float(self.n @ np.asarray(x, float)) - self.offset - g = scale * self.k * d * self.n - return scale * 0.5 * self.k * d * d, (float(g[0]), float(g[1]), float(g[2])) -``` - -Attach it with `with_atom_restraint` (a site subset) or `with_restraint` (every -copy), and pack. - -> **A per-site field cannot reproduce a distribution.** It is tempting to build a -> per-site penalty from a target density `ρ*` (e.g. Boltzmann inversion -> `U = −kT·ln ρ*`) to make sites *follow* `ρ*`. This fails under packing's energy -> **minimisation**: `∑ᵢ U(xᵢ)` is minimised by driving *every* site to the single -> minimum of `U` — the mode of `ρ*` — so the sites collapse onto the peak instead -> of spreading over the distribution. To drive a whole species onto a target -> profile, use a **collective** restraint (`Target.with_collective_restraint`, -> e.g. the built-in `ProfileMatch`), whose penalty is a function of the entire -> group and whose gradient couples the copies, so the fixed point is *empirical -> distribution = target*. - -## Custom `Region` - -Goal: a conical region with apex at origin, axis along +z, -half-angle 30°. - -```rust -use molrs::types::F; -# use molpack::Region; - -#[derive(Debug, Clone, Copy)] -pub struct ConeRegion { - pub apex: [F; 3], - pub axis: [F; 3], - pub half_angle_cos: F, -} - -impl Region for ConeRegion { - fn contains(&self, x: &[F; 3]) -> bool { - self.signed_distance(x) <= 0.0 - } - fn signed_distance(&self, x: &[F; 3]) -> F { - let dx = x[0] - self.apex[0]; - let dy = x[1] - self.apex[1]; - let dz = x[2] - self.apex[2]; - let r = (dx * dx + dy * dy + dz * dz).sqrt(); - if r < 1e-12 { return 0.0; } - let axis_dot = - (dx * self.axis[0] + dy * self.axis[1] + dz * self.axis[2]) / r; - self.half_angle_cos - axis_dot - } - // Default FD gradient is OK for prototypes. Override analytically - // for hot-path use — see below. -} -``` - -### Compose with built-ins - -```no_run -# use molrs::types::F; -# use molpack::Region; -# #[derive(Debug, Clone, Copy)] -# pub struct ConeRegion { pub apex: [F; 3], pub axis: [F; 3], pub half_angle_cos: F } -# impl Region for ConeRegion { -# fn contains(&self, _x: &[F; 3]) -> bool { true } -# fn signed_distance(&self, _x: &[F; 3]) -> F { 0.0 } -# } -use molpack::{InsideSphereRegion, RegionExt, RegionRestraint, Target}; -# let (pos, rad) = (&[[0.0; 3]][..], &[1.0][..]); - -let cone = ConeRegion { - apex: [0.0; 3], - axis: [0.0, 0.0, 1.0], - half_angle_cos: (std::f64::consts::PI / 6.0).cos(), -}; -let sphere = InsideSphereRegion::new([0.0; 3], 10.0); -let region = cone.and(sphere); - -let target = Target::from_coords(pos, rad, 100) - .with_restraint(RegionRestraint(region)); -``` - -[`RegionExt::and`](crate::RegionExt::and) / `or` / `not` come from a -blanket impl on every `Region`. The resulting type -`And` is static-dispatch — no heap. - -### Analytic gradient override - -For hot-path use, override `signed_distance_grad` analytically. The -cone above: - -```rust -# use molrs::types::F; -# use molpack::Region; -# #[derive(Debug, Clone, Copy)] -# pub struct ConeRegion { pub apex: [F; 3], pub axis: [F; 3], pub half_angle_cos: F } -# impl Region for ConeRegion { -# fn contains(&self, _x: &[F; 3]) -> bool { true } -# fn signed_distance(&self, _x: &[F; 3]) -> F { 0.0 } -fn signed_distance_grad(&self, x: &[F; 3]) -> [F; 3] { - let dx = x[0] - self.apex[0]; - let dy = x[1] - self.apex[1]; - let dz = x[2] - self.apex[2]; - let r2 = dx * dx + dy * dy + dz * dz; - let r = r2.sqrt(); - if r < 1e-12 { return [0.0; 3]; } - let axis_dot = (dx * self.axis[0] + dy * self.axis[1] + dz * self.axis[2]) / r; - let inv_r = 1.0 / r; - // signed_distance = cos(α) - axis_dot ⇒ grad = -∂axis_dot/∂x - [ - -(self.axis[0] * inv_r - axis_dot * dx * inv_r * inv_r), - -(self.axis[1] * inv_r - axis_dot * dy * inv_r * inv_r), - -(self.axis[2] * inv_r - axis_dot * dz * inv_r * inv_r), - ] -} -# } -``` - -Then finite-difference check it — same pattern as the `AtomRestraint` -test. - -## Custom `Handler` - -Goal: a handler that writes a CSV row per step so you can plot the -objective evolution. - -```no_run -use std::fs::File; -use std::io::{BufWriter, Write}; -use molpack::{F, Handler, PackContext, StepInfo}; - -pub struct CsvHandler { writer: BufWriter } - -impl CsvHandler { - pub fn new(path: &str) -> std::io::Result { - let mut w = BufWriter::new(File::create(path)?); - writeln!(w, "phase,loop_idx,fdist,frest,improvement_pct")?; - Ok(Self { writer: w }) - } -} - -impl Handler for CsvHandler { - fn on_step(&mut self, info: &StepInfo, _sys: &PackContext) { - let _ = writeln!( - self.writer, - "{},{},{},{},{}", - info.phase.phase, - info.loop_idx, - info.fdist, - info.frest, - info.improvement_pct, - ); - } -} -``` - -Handler notes: - -- **`on_step` is the only required method.** Everything else has a - default no-op. -- **`sys` is `&PackContext`, never `&mut`.** Handlers cannot mutate - packer state — use a `Relaxer` if you need to. -- **Multiple handlers run in registration order.** Register your CSV - handler before `ProgressHandler` to get a row on every step, - vice-versa otherwise. -- **`should_stop` is polled every iteration.** Return `true` to break - the outer loop early. Useful for time budgets or custom convergence - criteria. - -## Custom `Relaxer` - -Goal: a relaxer that tries random rigid-body translations and accepts -if the objective decreases. - -![Confinement placement with a custom pivot-MC relaxer](assets/images/paper-confinement-sphere.png) - -The confinement example is a molpack extension workflow, not a Packmol parity -claim: a custom pivot-MC relaxer folds flexible PEO chains before packing, so -the same engine can place them inside a tight spherical cavity. - -```no_run -use molrs::Frame; -use molrs::types::F; -use molpack::{Relaxer, RelaxerRunner}; -use rand::{Rng, RngCore}; - -#[derive(Debug, Clone)] -pub struct JiggleRelaxer { - pub steps: usize, - pub max_delta: F, -} - -impl Relaxer for JiggleRelaxer { - fn spawn(&self, _frame: Option<&Frame>, _ref_coords: &[[F; 3]]) -> Box { - Box::new(JiggleRunner { - steps: self.steps, - max_delta: self.max_delta, - accepted: 0, - total: 0, - }) - } -} - -pub struct JiggleRunner { - steps: usize, - max_delta: F, - accepted: usize, - total: usize, -} - -impl RelaxerRunner for JiggleRunner { - fn on_iter( - &mut self, - coords: &[[F; 3]], - f_current: F, - evaluate: &mut dyn FnMut(&[[F; 3]]) -> F, - rng: &mut dyn RngCore, - ) -> Option> { - let mut best = coords.to_vec(); - let mut best_f = f_current; - let mut accepted_any = false; - for _ in 0..self.steps { - let dx = (rng.next_u32() as f64 / u32::MAX as f64 * 2.0 - 1.0) * self.max_delta; - let dy = (rng.next_u32() as f64 / u32::MAX as f64 * 2.0 - 1.0) * self.max_delta; - let dz = (rng.next_u32() as f64 / u32::MAX as f64 * 2.0 - 1.0) * self.max_delta; - let trial: Vec<[F; 3]> = best.iter() - .map(|p| [p[0] + dx, p[1] + dy, p[2] + dz]) - .collect(); - let f_trial = evaluate(&trial); - self.total += 1; - if f_trial < best_f { - self.accepted += 1; - best_f = f_trial; - best = trial; - accepted_any = true; - } - } - if accepted_any { Some(best) } else { None } - } - fn acceptance_rate(&self) -> F { - if self.total == 0 { 0.0 } else { self.accepted as F / self.total as F } - } -} -``` - -Relaxer notes: - -- **Two-part design.** [`Relaxer`](crate::Relaxer) is the immutable - builder; [`RelaxerRunner`](crate::RelaxerRunner) holds per-pack - state. `spawn()` is called once per target type at `pack()` entry. -- **`evaluate` closure tests trial coords against the full objective** - without mutating the reference — use it as often as you like. -- **Return `Some(new_coords)` only if something changed.** The packer - skips unnecessary cache invalidation when you return `None`. -- **`count == 1` required.** Multi-copy targets share reference - coords; a relaxer that mutates them would silently change all - copies. - -## Testing discipline - -| Kind | Location | Convention | -|---|---|---| -| Unit test | `#[cfg(test)] mod tests` in the same file | One `#[test]` fn per behavior | -| Integration test | `tests/.rs` | `use molpack::{…};` only public API | -| Gradient finite-difference | alongside unit test | ε=1e-5, tol=1e-3 | -| Regression vs Packmol | `tests/examples_batch.rs` (`#[ignore]`) | Run with `--ignored --release` | - -Run all: - -```bash -cargo test --all-features -cargo test --release --test examples_batch -- --ignored -``` - -Rules: - -- Every new `AtomRestraint` gets an FD gradient test. -- Every new `Region` gets a boolean-algebra + signed-distance sign - test and (for hot-path use) an analytic-gradient FD test. - -## Common pitfalls - -- **Gradient sign.** Every `AtomRestraint` accumulates `∂penalty/∂x`. - Optimizer negates for descent. If your molecules fly out of the - region, the gradient has the wrong sign — penalty should point - toward the violation boundary. -- **Rotation convention.** Single-atom tests pass with both LEFT and - RIGHT Euler multiplication; multi-atom tests don't. Always test - Euler changes with ≥ 2 atoms. -- **`Cell` is not `Sync`.** Use `AtomicU64` + - `f64::to_bits` / `from_bits` for interior mutability in - `Send + Sync` contexts. -- **0-based atom indexing.** `Target::with_atom_restraint` uses - Rust-native 0-based indices. `&[0, 1]` selects the first two atoms. - Packmol `.inp` files use 1-based — subtract 1 at the parse boundary. -- **`count == 1` required for relaxers.** Multi-copy targets share - reference coords. -- **`radscale` is phase-dependent.** Don't hard-code atomic radii — - always go through `sys.radius[i]`. `evaluate_unscaled` temporarily - swaps `radius` with `radius_ini` for user-facing numbers. -- **PBC boxes must be valid.** Zero-length axis returns - `PackError::InvalidPBCBox`. - -## Contributing flow - -1. Write a failing test. -2. Implement until it passes. -3. Run the full gate: - ```bash - cargo test --all-features - cargo clippy -- -D warnings - cargo fmt --all --check - ``` diff --git a/docs/zh/getting_started.md b/docs/zh/getting_started.md deleted file mode 100644 index 0f06c91..0000000 --- a/docs/zh/getting_started.md +++ /dev/null @@ -1,120 +0,0 @@ -# Quickstart - -Pack **100 water molecules** into a **40 Å** cube. This walkthrough uses the -Python package — the shortest path from a loaded frame to a packed result. The -same model is available as a [CLI script](cli/) or the [Rust builder](rust/). - -## 1. Install - -```bash -pip install molcrafts-molpack -``` - -This installs the packing engine and pulls in `molcrafts-molrs` for the frame -type plus PDB/XYZ I/O. - -## 2. Load or build a template - -=== "From a PDB file" - - ```python - import molrs - - frame = molrs.read_pdb("water.pdb") - ``` - -=== "In-memory (no file)" - - ```python - import molrs - import numpy as np - - frame = molrs.Frame.from_dict({ - "blocks": { - "atoms": { - "x": np.array([0.00, 0.96, -0.24]), - "y": np.array([0.00, 0.00, 0.93]), - "z": np.zeros(3), - "element": ["O", "H", "H"], - } - } - }) - ``` - -## 3. Define the target - -A `Target` is one molecule species plus the number of copies to place. Every -mobile target needs a spatial restraint: - -```python -from molpack import InsideBoxRestraint, Target - -water = ( - Target(frame, count=100) - .with_name("water") - .with_restraint(InsideBoxRestraint([0.0, 0.0, 0.0], [40.0, 40.0, 40.0])) -) -``` - -!!! warning "Missing restraints" - Without a spatial restraint (or a global PBC box), initial placement has to - invent a huge free-space region and the run can become impractical. - -## 4. Pack - -```python -from molpack import Molpack - -result = ( - Molpack() - .with_tolerance(2.0) - .with_seed(42) - .pack_with_report([water], max_loops=200) -) - -print(result.converged, result.natoms, result.fdist, result.frest) -packed = result.frame -``` - -| Field | Meaning | -|---|---| -| `converged` | Both objectives fell below the packer precision threshold | -| `fdist` | Pair-distance (overlap) violations | -| `frest` | Restraint violations | -| `frame` | Topology-complete packed `molrs.Frame` | - -For a frame-only return, use `Molpack().pack([water], max_loops=200)`. - -## 5. Save - -```python -import molrs - -molrs.write_pdb(packed, "water_box.pdb") -# or: molrs.write_xyz(packed, "water_box.xyz") -``` - -## Where next - - diff --git a/docs/zh/img b/docs/zh/img deleted file mode 120000 index 6ffc6ca..0000000 --- a/docs/zh/img +++ /dev/null @@ -1 +0,0 @@ -../img \ No newline at end of file diff --git a/docs/zh/index.md b/docs/zh/index.md deleted file mode 100644 index 5900f82..0000000 --- a/docs/zh/index.md +++ /dev/null @@ -1,249 +0,0 @@ ---- -title: molpack(中文) -description: 纯 Rust 实现的 Packmol 级分子装填,提供命令行、Python API 和 Rust API。 -hide: - - navigation - - toc -hero: - kicker: molpack 手册 - title: molpack - description: 纯 Rust 实现的 Packmol 兼容分子装填引擎 — `.inp` 脚本、Python 包与原生 Rust crate 共用同一套装填模型。 - install: - label: 安装 - methods: - - label: CLI - command: cargo install molcrafts-molpack --features cli - - label: Rust 库 - command: cargo add molcrafts-molpack - - label: Python - command: pip install molcrafts-molpack - badges: - - img: https://img.shields.io/crates/v/molcrafts-molpack?color=c2410c&label=crates.io - href: https://crates.io/crates/molcrafts-molpack - alt: crates.io 版本 - - img: https://img.shields.io/pypi/v/molcrafts-molpack?color=d97706&label=PyPI - href: https://pypi.org/project/molcrafts-molpack/ - alt: PyPI 版本 - - img: https://github.com/MolCrafts/molpack/actions/workflows/ci.yml/badge.svg - href: https://github.com/MolCrafts/molpack/actions/workflows/ci.yml - alt: CI 状态 - - img: https://img.shields.io/badge/license-BSD--3--Clause-blue.svg - href: https://github.com/MolCrafts/molpack/blob/master/LICENSE - alt: BSD-3-Clause 许可证 - actions: - - label: 快速开始 - href: getting_started/ - style: primary - - label: Packmol 脚本 - href: cli/ - - label: Python API - href: python/ - - label: Rust API - href: rust/ ---- - -

molpack

- -
- -
- -
- -从这里开始 - -## 找到对应页面 - -本页是手册索引,不是营销页。所有入口共用同一模型:分子模板、副本数量、几何约束, -然后运行一次装填。 - -!!! note "翻译进度" - 中文站与英文站平行(`docs/zh/` ↔ `docs/en/`)。导航与首页为中文; - 部分章节正文仍暂用英文,点导航会留在 `/zh/` 下,不会跳回英文站。 - -
- - - -
- -
- -
- -工作流 - -## 同一个装填模型,四个入口 - -所有入口都会落到 target / count / restraint。按现有工作流语言选择入口。 - -
- -
- -
- -
路径 01 · 脚本
- -### 运行 `.inp` 任务 - -适合已写成 Packmol 风格、需要纳入版本控制的可复现任务。见 [CLI](cli/)。 - -```text -structure water.pdb - number 1000 - inside box 0. 0. 0. 40. 40. 40. -end structure -``` - -
- -
- -
路径 02 · Python
- -### 在 notebook 或流水线中装填 - -加载或构造 frame,创建不可变 target,再把结果交给写出或分析代码。见 [Python](python/)。 - -```python -water = Target(frame, 100).with_restraint( - InsideBoxRestraint([0, 0, 0], [40, 40, 40]) -) -packed = Molpack().with_seed(42).pack([water]) -``` - -
- -
- -
路径 03 · Rust
- -### 把引擎嵌入 crate - -适合原生应用、服务与测试。见 [Rust](rust/)。 - -```rust -let frame = Molpack::new() - .with_seed(42) - .pack(&[water], 200)?; -``` - -
- -
- -
路径 04 · Handler
- -### 观测或停止一次运行 - -Handler 接收装填循环中的结构化事件。见 [Handlers](rust/handlers-relaxers/)。 - -```rust -impl Handler for WatchFdist { - fn on_step(&mut self, info: &StepInfo, _sys: &PackContext) { - eprintln!("fdist={}", info.fdist); - } -} -``` - -
- -
- -
- -
- -
- -地图 - -## 手册结构 - -
- -
-
-

Tutorial

-

安装、快速开始、概念、示例与 Packmol 对等说明。

-
-
-

Packmol 脚本

-

`.inp` 概览、脚本说明、格式与 CLI 示例。

-
-
-

Python

-

安装、targets、restraints、packer、PBC 与 API 参考。

-
-
-

Rust

-

Builder、约束、周期边界、handlers 与 relaxers。

-
-
-

Development

-

贡献流程、架构与扩展点。

-
-
- -
- -
- -
- -入口 - -## 跳转到表面 - -
- - - -
- -
diff --git a/docs/zh/install.md b/docs/zh/install.md deleted file mode 100644 index e499d90..0000000 --- a/docs/zh/install.md +++ /dev/null @@ -1,86 +0,0 @@ -# Install - -`molpack` ships three surfaces — the CLI binary, the Rust crate, and the -Python binding. Pick the one that matches your workflow; they share the same -engine and packing model. - -## CLI - -For Packmol-style `.inp` scripts: - -```bash -cargo install molcrafts-molpack --features cli -molpack --help -``` - -!!! tip "Path resolution" - In file-arg mode (`molpack job.inp`), relative paths inside the script are - resolved against the **script directory**. In stdin mode they resolve - against the current working directory. - -## Rust crate - -For programmatic use from Rust: - -```bash -cargo add molcrafts-molpack -``` - -Optional features (crate defaults to none enabled): - -| Feature | Purpose | -|---|---| -| `cli` | `molpack` binary + clap (implies `io`) | -| `io` | PDB / XYZ / SDF / LAMMPS readers via molrs | -| `rayon` | Parallel objective evaluation | -| `ff` | Force-field geometry relaxer (MMFF + L-BFGS) | - -```toml -# Cargo.toml — common combinations -molcrafts-molpack = { version = "0.1", features = ["io", "rayon"] } -``` - -## Python binding - -For notebooks and pipelines (Python 3.12+): - -```bash -pip install molcrafts-molpack -``` - -`molcrafts-molrs` is installed as a dependency and provides `molrs.Frame` plus -PDB / XYZ readers. The wheel itself is I/O-free — pass frames in, get frames -out. - -```python -import molpack -print(molpack.Molpack) -``` - -!!! note "Pre-built wheels" - Wheels are published for CPython **3.12** and **3.13** on Linux - (manylinux x86-64) and macOS (universal2). Other platforms fall back to the - sdist and need a Rust toolchain to build. - -## Build from source - -When you are modifying the crate or Python binding, check out **molrs** as a -sibling (path deps resolve `../molrs/molrs`): - -```bash -# sibling layout -# workspace/ -# ├── molrs/ -# └── molpack/ - -git clone https://github.com/MolCrafts/molpack -cd molpack - -# Rust library + CLI -cargo build --features cli - -# Python wheel (editable) -cd python && maturin develop --release -``` - -See [Development](development/) for tests, hooks, and contribution workflow. diff --git a/docs/zh/packmol_parity.md b/docs/zh/packmol_parity.md deleted file mode 100644 index 7f52d19..0000000 --- a/docs/zh/packmol_parity.md +++ /dev/null @@ -1,115 +0,0 @@ -# Packmol parity - -molpack tracks the original Packmol's behavior (`L. Martínez et al., -J. Comput. Chem. 2009`). Coordinates are not byte-identical with the -same seed, but functional equivalence is enforced on five canonical -workloads. - -For script syntax, use the official -[Packmol user guide](https://m3g.github.io/packmol/userguide.shtml). This page -is about parity checks, accepted differences, and molpack-specific extensions. - -## Paper figures - -### Canonical workloads - -![Packmol and molpack distribution checks across canonical examples](assets/images/paper-compatibility-distributions.png) - -The five Packmol examples are run unchanged through Packmol and molpack. -Coordinate identity is not the target; valid packing and matching spatial -distributions are. - -### Thread scaling - -![Multithreaded objective-kernel scaling](assets/images/paper-mt-scaling.png){ .molpack-figure-small } - -The fused objective-and-gradient kernel is too small to parallelize profitably -for tiny systems, then crosses over and scales on larger systems. - -## What is matched - -**Objective structure** - -- Geometric restraint penalties — equivalent of `comprest` / `gwalls`. -- Minimum-distance overlap penalty — equivalent of `computef` / - `fparc`. - -**Optimization workflow** - -- Initialization with constraint-only fitting (`initial` / `restmol` / - `swaptype`). -- `avoid_overlap` (on by default, faithful to `initial.f90`): initial - placements that land inside a fixed molecule are rejected. This matters - for dense solvation around a large fixed solute — without it a sizeable - fraction of solvent seeds inside the solute, roughly doubling the - initial overlap and slowing convergence by about an order of magnitude. -- Phased main optimization — per-type pre-compaction, then all-types. -- `movebad` heuristic for stalled molecules. -- Radius-scaling (`radscale`) decay across each phase. -- GENCAN / SPG / CG inner solver chain. -- Precision gate on `fdist` (overlap) and `frest` (restraint - violation). - -**Restraint vocabulary** - -- `inside`/`outside box`, `cube`, `sphere`, `ellipsoid`, `cylinder` -- `over plane` (above) / `below plane` -- fixed molecule placement - -All twelve box/cube/sphere/ellipsoid/cylinder/plane kinds lower through a single -`restraint_from_spec` table in `script::build`, so each is reachable from both -whole-molecule and `atoms … end atoms` blocks. The two Gaussian-surface kinds -(14/15) exist in the Rust/Python API but have no `.inp` keyword yet — Packmol's -Gaussian grammar is not pinned down here, and emitting a wrong parameter mapping -would silently mis-pack, so the parser rejects them rather than guessing. - -**Periodicity** - -- `pbc X Y Z` and `pbc X0 Y0 Z0 X1 Y1 Z1`, mirroring Packmol's - `getinp.f90`. The packer's cell grid is built directly from the - declared box. - -**Script parser strictness** - -- Unknown top-level keywords are rejected via - `ScriptError::UnknownKeyword`. A silently dropped `pbc` previously - triggered a 42 GB cell-grid allocation — strict parsing prevents - that class of failure. - -**Determinism** - -- Explicit seeds; identical seed values are used for paired Packmol - vs molpack runs in the regression suite. - -## Verification - -### Batch example validation - -`tests/examples_batch.rs` runs all five canonical workloads. Marked -`#[ignore]` because the run is expensive — invoke explicitly: - -```bash -cargo test -p molcrafts-molpack --release --test examples_batch -- --ignored -``` - -The test asserts: - -- atom and molecule counts match the expanded target specs; -- XYZ output is structurally sound; -- quantified violation metrics stay within tolerance / precision. - -### Violation metrics - -Each side reports `max_distance_violation`, `max_constraint_penalty`, -`violating_pairs`, and `violating_atoms`. Both packers must satisfy the -same thresholds. - -## Accepted differences - -- Packed coordinates are not bit-identical with Packmol, even at the same - seed — the inner solver and RNG are independent implementations. -- The two Gaussian-surface restraint kinds have no `.inp` keyword; they - are reachable only from the Rust/Python API. -- Acceptance is therefore functional rather than numerical: the same - restraints, the same conflict criteria, the same order of magnitude of - tolerance / precision, and comparable violation metrics. diff --git a/docs/zh/python/api-reference.md b/docs/zh/python/api-reference.md deleted file mode 100644 index eeadf40..0000000 --- a/docs/zh/python/api-reference.md +++ /dev/null @@ -1,416 +0,0 @@ -# API Reference - -Import surface: - -```python -from molpack import ( - # Core - Target, Molpack, PackResult, StepInfo, - # Typed values - Angle, Axis, CenteringMode, - # Geometric (per-atom) restraints - InsideBoxRestraint, InsideSphereRestraint, OutsideSphereRestraint, - AbovePlaneRestraint, BelowPlaneRestraint, - # Collective (distribution-matching) restraints - GaussianPlane, GaussianPoint, - ExponentialPlane, ExponentialPoint, - TabulatedPlane, TabulatedPoint, - # In-loop relaxers - TorsionMcRelaxer, LBFGSRelaxer, - # Script loader (`.inp`) - ScriptJob, load_script, - # Parallel evaluation - rayon_enabled, num_threads, init_thread_pool, - # Protocols - Handler, Restraint, - # Errors - PackError, - ConstraintsFailedError, - MaxIterationsError, - NoTargetsError, - EmptyMoleculeError, - InvalidPBCBoxError, - ConflictingPeriodicBoxesError, -) - -# Post-pack whole-system relaxation (optional molpy/LAMMPS backend) -from molpack.relaxer import LAMMPSRelaxer -``` - ---- - -## `Angle` - -Angular quantity with explicit units at the call site. - -```python -Angle.from_degrees(30.0).radians # 0.5235... -Angle.from_radians(0.5).degrees # 28.6... -Angle.ZERO # identity rotation -``` - ---- - -## `Axis` - -Cartesian axis enum: `Axis.X`, `Axis.Y`, `Axis.Z`. - ---- - -## `CenteringMode` - -Centering policy for target reference coords: - -- `CenteringMode.AUTO` — free targets centered, fixed kept in place (default). -- `CenteringMode.CENTER` — always center. -- `CenteringMode.OFF` — keep input coords unchanged. - ---- - -## `Target` - -Molecule-type specification. Immutable — builder methods return new -instances. - -**Constructor** - -```python -Target(frame, count: int) -``` - -- `frame` — a `molrs.Frame` or `molpy.Frame` with atom columns `"x"`, - `"y"`, `"z"`, and `"element"` (or `"symbol"` for `molrs` PDB frames). - Resolved zero-copy via its FFI capsule; a plain dict is not accepted. -- `count` — number of copies to produce. - -**Builders** - -- `.with_name(name: str)` — display label. -- `.with_restraint(r)` — attach a restraint to every atom (stackable). - Accepts a geometric built-in, a collective (distribution-matching) - restraint, or any duck-typed `f`/`fg` object — see [Restraints](#restraints). -- `.with_atom_restraint(indices: Sequence[int], r)` — 0-based indices. -- `.with_relaxer(relaxer)` — attach an in-loop geometry relaxer - (`TorsionMcRelaxer` or `LBFGSRelaxer`); requires `count == 1`. See - [In-loop relaxers](#in-loop-relaxers). -- `.with_perturb_budget(n: int)` — per-target perturbation budget. -- `.with_centering(mode: CenteringMode)`. -- `.with_rotation_bound(axis: Axis, center: Angle, half_width: Angle)`. -- `.fixed_at(position: [x, y, z])` — pin the target. -- `.with_orientation((ax, ay, az))` — Euler tuple of `Angle`s; must - follow `fixed_at`. - -**Properties** - -- `.name : str | None` -- `.natoms : int` -- `.count : int` -- `.elements : list[str]` -- `.radii : list[float]` -- `.is_fixed : bool` - ---- - -## `Molpack` - -Orchestrator for the three-phase GENCAN optimizer. Zero-arg -constructor — all tuning is via `with_*` builders. - -**Constructor** - -```python -Molpack() -``` - -**Builders** - -- `.with_tolerance(t: float)` — minimum pairwise distance (Å; default 2.0). -- `.with_precision(p: float)` — convergence threshold (default 0.01). -- `.with_inner_iterations(n: int)` — GENCAN inner-loop cap (default 20). -- `.with_init_passes(n: int)` — init compaction passes (0 = auto). -- `.with_init_box_half_size(h: float)` — init placement bound (default 1000 Å). -- `.with_periodic_box(min: [x,y,z], max: [x,y,z])` — declare a - fully-periodic box directly on the packer (Packmol `pbc`). Alternative - to a periodic `InsideBoxRestraint`; see - [Periodic boundaries](guide/periodic-boundaries.md). -- `.with_perturb_fraction(f: float)` — stall perturbation fraction (default 0.05). -- `.with_random_perturb(enabled: bool)`. -- `.with_perturb(enabled: bool)` — master switch (default True). -- `.with_avoid_overlap(enabled: bool)` — reject initial random placements - overlapping a fixed molecule (Packmol `avoid_overlap`; default True). -- `.with_seed(seed: int)` — deterministic RNG (default 0). -- `.with_parallel_eval(enabled: bool)` — rayon-backed pair eval. Raises - `RuntimeError` if the wheel lacks the `rayon` feature (fail-fast). -- `.with_lammps_output(enabled: bool)` — enable LAMMPS-style screen output. -- `.with_log_level(level: str)` — `quiet`, `summary`, `progress`, or `verbose`. -- `.with_log_frequency(n: int)` — print every `n` outer steps. -- `.with_progress(enabled: bool)` — compatibility alias for progress output. -- `.with_handler(handler)` — attach a custom `Handler` (stackable). -- `.with_xyz_output(path: str, every: int = 1)` — record the packing - trajectory to a multi-frame extended-XYZ file (a frame every `every` - loops, loop 0 included). -- `.with_global_restraint(r)` — broadcast to every target (stackable). - -**Run** - -```python -.pack(targets: list[Target], max_loops: int = 200) -> molrs.Frame -.pack_with_report(targets: list[Target], max_loops: int = 200) -> PackResult -``` - -Raises a typed `PackError` subclass on failure. - ---- - -## `PackResult` - -Read-only output container returned by `pack_with_report()`. - -**Properties** - -- `.positions : ndarray (N, 3) float64` -- `.frame : molrs.Frame` — topology-complete frame (periodic box stamped if one was declared). -- `.elements : list[str]` -- `.natoms : int` -- `.converged : bool` -- `.fdist : float` -- `.frest : float` - ---- - -## `StepInfo` - -Read-only snapshot passed to `Handler.on_step`. - -```python -info.loop_idx # outer-loop iteration -info.max_loops -info.phase # phase index -info.total_phases -info.molecule_type # int | None -info.fdist -info.frest -info.improvement_pct -info.radscale -info.precision -info.relaxer_acceptance # list[tuple[int, float]] -``` - ---- - -## Restraints - -All restraint classes are immutable. Two families, both attached with -`target.with_restraint(r)` (or `Molpack.with_global_restraint(r)`): -**geometric** per-atom region restraints (below) and **collective** -distribution-matching restraints ([next section](#collective-distribution-matching-restraints)). - -### Geometric (per-atom) restraints - -Their `f`/`fg` see **one atom** at a time — a soft quadratic penalty that -is zero inside the region and rises outside. - -### `InsideBoxRestraint(min, max, periodic=(False, False, False))` - -Axis-aligned box. `periodic` is a 3-tuple of booleans declaring per-axis -periodicity — see [Periodic boundaries](guide/periodic-boundaries.md). - -### `InsideSphereRestraint(center, radius)` - -Closed ball. - -### `OutsideSphereRestraint(center, radius)` - -Complement of closed ball. - -### `AbovePlaneRestraint(normal, distance)` - -Half-space $\{\mathbf{x} : \mathbf{n}\cdot\mathbf{x} \ge d\}$. - -### `BelowPlaneRestraint(normal, distance)` - -Half-space $\{\mathbf{x} : \mathbf{n}\cdot\mathbf{x} \le d\}$. - -### Collective (distribution-matching) restraints - -Attached the same way (`target.with_restraint(r)`), but their `f`/`fg` -see **every copy** of the target at once and drive the species' spatial -distribution toward a target profile via a squared 1-D Wasserstein -(sorted-CDF) penalty. The reaction coordinate is either signed distance -to a plane ($\xi = \mathbf{x}\cdot\hat{\mathbf{n}} - \text{offset}$) or -radial distance to a point ($\xi = \lVert\mathbf{x} - \text{center}\rVert$). - -| Class | Constructor | Target profile | -|-------|-------------|----------------| -| `GaussianPlane` | `(normal, offset, strength, mu, sigma)` | Gaussian $N(\mu, \sigma)$ slab | -| `GaussianPoint` | `(center, strength, mu, sigma)` | Gaussian shell (radius `mu`, thickness `sigma`) | -| `ExponentialPlane` | `(normal, offset, strength, lambda_)` | diffuse layer $\propto e^{-\xi/\lambda}$, $\xi \ge 0$ | -| `ExponentialPoint` | `(center, strength, lambda_)` | radial atmosphere $\propto e^{-\xi/\lambda}$ | -| `TabulatedPlane` | `(normal, offset, strength, xs, rho)` | arbitrary planar prior on grid `(xs, rho)` | -| `TabulatedPoint` | `(center, strength, xs, rho)` | arbitrary radial prior on grid `(xs, rho)` | - -`strength` is the overall penalty multiplier $\lambda$. `sigma` / -`lambda_` must be `> 0`; tabulated `xs` must be strictly ascending -(≥ 2 points) with non-negative `rho` of positive total mass. Invalid -arguments raise `ValueError` at construction. - ---- - -## In-loop relaxers - -Relaxation-assisted packing: attach to a `Target` via -`target.with_relaxer(r)` to reshape a single molecule's reference -geometry *during* the pack loop. Both require the target's `count == 1` -(every copy shares the reference geometry the relaxer rewrites). Immutable. - -### `TorsionMcRelaxer(frame)` - -Monte-Carlo torsion-angle sampling — engine-free and force-field-free. -Rotatable bonds are detected from the frame's bond topology; proposed -rotations are accepted against the packer objective (Metropolis). - -- `.with_temperature(t: float)` — Metropolis temperature (default 1.0). -- `.with_steps(n: int)` — MC steps proposed per packing iteration (default 10). -- `.with_max_delta(rad: float)` — max per-step rotation, radians (default π/6). -- `.with_self_avoidance(radius: float)` — quadratic overlap penalty on - non-bonded intramolecular pairs closer than `2 * radius`; `0.0` - disables (default). - -```python -from molpack import Target, TorsionMcRelaxer - -chain = TorsionMcRelaxer(frame).with_steps(20).with_self_avoidance(1.5) -target = Target(frame, count=1).with_relaxer(chain) -``` - -### `LBFGSRelaxer(forcefield)` *(requires the `ff` feature)* - -Force-field L-BFGS geometry minimization. Built from a -`molrs.ForceField` / `molpy.ForceField` (zero-copy FFI capsule); the -potential is compiled lazily against the molecule's frame when packing -starts. - -- `.with_fmax(fmax: float)` — stop when the max per-atom force drops - below `fmax` (kcal/mol/Å; default 0.05). -- `.with_max_steps(max_steps: int)` — L-BFGS iteration cap per - relaxation call (default 500). - ---- - -## Post-pack relaxation - -### `LAMMPSRelaxer` *(from `molpack.relaxer`)* - -Whole-system relaxation of a **finished** packed box via LAMMPS — a -different axis from the in-loop relaxers above. A thin façade over -`molpy.engine.LAMMPSEngine`, imported **lazily** so `import molpack` -never requires molpy. - -```python -LAMMPSRelaxer( - ff, # a typified molpy ForceField - *, - executable: str | None = None, # None auto-detects lmp / lmp_serial / lmp_mpi - launcher: list[str] | None = None, # e.g. ["mpirun", "-np", "8"] - pair_style: str = "lj/cut/coul/cut 10.0", - atom_style: str = "full", - units: str = "real", - workdir: str | Path | None = None, -) -``` - -- `.minimize(target, **options) -> molrs.Frame` — energy minimisation. -- `.md(target, **options) -> molrs.Frame` — short MD settle. -- `.relax(target, **options)` — alias for `minimize` (also `__call__`). - -`target` is a `PackResult` (its `.frame` is used) or a bare -`molrs.Frame` carrying a periodic box; the input is never mutated. -Raises `ImportError` if `molcrafts-molpy` is not installed. - ---- - -## Script loader - -### `load_script(path, *, read_frame=None) -> ScriptJob` - -Parse and lower a Packmol-compatible `.inp` script. Template files are -read on the Python side (defaulting to `molrs.read_pdb` / `read_xyz` by -extension), so the wheel stays free of `molrs-io`. Pass `read_frame` -— a callable `(path, filetype) -> molrs.Frame` — to plug in another -loader (mdtraj, ASE, …). - -### `ScriptJob` - -Bundle returned by `load_script`. Access fields by attribute **or** -tuple-unpack it: - -```python -job = load_script("mix.inp") -packer, targets, output, nloop = load_script("mix.inp") # same object -``` - -- `.packer : Molpack` — pre-configured with the script's `tolerance`, - `seed`, and any `pbc` box. -- `.targets : list[Target]` -- `.output : pathlib.Path` — resolved output path. -- `.nloop : int` — outer-loop cap (`nloop` keyword; default 400). - ---- - -## Parallel evaluation - -The parallel evaluator runs on rayon's process-global thread pool, built -**once** per process and not resizable afterwards. - -- `rayon_enabled() -> bool` — was the wheel built with the `rayon` feature? -- `num_threads() -> int` — worker count the pool will use (1 on a serial build). -- `init_thread_pool(n: int)` — pin the pool size; must be called - **before** the first pack. Raises `RuntimeError` without the `rayon` - feature or if the pool was already initialized, and `ValueError` if - `n == 0`. For a scaling sweep, set the count once per process (or via - `RAYON_NUM_THREADS`) and launch one process per data point. - ---- - -## Duck-type protocols - -### `Restraint` - -```python -class Restraint(Protocol): - def f(self, x: tuple[float, float, float], scale: float, scale2: float) -> float: ... - def fg( - self, x: tuple[float, float, float], scale: float, scale2: float, - ) -> tuple[float, tuple[float, float, float]]: ... -``` - -### `Handler` - -```python -class Handler(Protocol): - def on_start(self, ntotat: int, ntotmol: int) -> None: ... - def on_step(self, info: StepInfo) -> bool | None: ... # True → stop - def on_finish(self) -> None: ... -``` - -All `Handler` methods are optional — missing ones are silently skipped. - ---- - -## Exceptions - -Typed hierarchy rooted at `PackError` (itself a `RuntimeError` -subclass). Catch the base to handle any packing failure uniformly. - -- `PackError` — base. -- `ConstraintsFailedError` — solver could not satisfy restraints even - without distance tolerances. -- `MaxIterationsError` — ran out of outer loops. -- `NoTargetsError` — empty target list. -- `EmptyMoleculeError` — a target has zero atoms. -- `InvalidPBCBoxError` — periodic box has a non-positive extent. -- `ConflictingPeriodicBoxesError` — two restraints declared - incompatible periodic boxes. - -`ValueError` / `TypeError` still surface on Python-side invariants -(bad atom indices, wrong restraint object, etc.). diff --git a/docs/zh/python/examples.md b/docs/zh/python/examples.md deleted file mode 100644 index ff4c0d9..0000000 --- a/docs/zh/python/examples.md +++ /dev/null @@ -1,81 +0,0 @@ -# Examples - -Five canonical Packmol workloads ported to Python. Each lives under -`python/examples/` in the repo and is regression-tested against the -equivalent Rust example (same RNG seed → identical final coordinates). - -| Script | Packmol analogue | What it shows | -|-----------------------|-------------------|---------------| -| `pack_water_cube.py` | — | hello-world: 100 waters in a box, frame via `molrs.Frame.from_dict` | -| `pack_mixture.py` | `mixture.inp` | two species co-packed in one box | -| `pack_bilayer.py` | `bilayer.inp` | atom-subset restraints for layer-molecule orientation | -| `pack_interface.py` | `interface.inp` | fixed reference molecule + two solvents | -| `pack_spherical.py` | `spherical.inp` | nested spheres, double-layer shell | -| `pack_solvprotein.py` | `solvprotein.inp` | fixed solute solvated by water + ions | - -Install molpack once; the `molrs` dependency comes with it: - -```bash -pip install molcrafts-molpack -``` - -`pack_water_cube.py` builds its frame in memory with `molrs.Frame.from_dict` -(no PDB file); the others load PDB files via `molrs.read_pdb`. - -## Running - -```bash -cd molpack/python -pip install -e . -python examples/pack_water_cube.py # no PDB file -python examples/pack_mixture.py # requires molrs -``` - -Set `MOLPACK_EXAMPLE_PROGRESS=0` to suppress the per-iteration progress log. - -## Example: mixture - -The `pack_mixture.py` example reproduces Packmol's classic `mixture.inp`: - -```python -import molrs -from molpack import InsideBoxRestraint, Molpack, Target - -water_frame = molrs.read_pdb("water.pdb") -urea_frame = molrs.read_pdb("urea.pdb") - -box = InsideBoxRestraint([0, 0, 0], [40, 40, 40]) - -water = Target(water_frame, count=1000).with_name("water").with_restraint(box) -urea = Target(urea_frame, count=400).with_name("urea").with_restraint(box) - -packer = Molpack().with_tolerance(2.0).with_seed(1_234_567) -result = packer.pack_with_report([water, urea], max_loops=400) -print(f"converged={result.converged} natoms={result.natoms}") -``` - -## Example: water cube - -```python -import molrs -import numpy as np -from molpack import InsideBoxRestraint, Molpack, Target - -frame = molrs.Frame.from_dict({ - "blocks": { - "atoms": { - "x": np.array([0.00, 0.9572, -0.2400]), - "y": np.array([0.00, 0.0000, 0.9266]), - "z": np.zeros(3), - "element": ["O", "H", "H"], - } - } -}) - -water = Target(frame, count=100).with_name("water").with_restraint( - InsideBoxRestraint([0, 0, 0], [30, 30, 30]) -) -packer = Molpack().with_tolerance(2.0).with_log_level("quiet").with_seed(42) -result = packer.pack_with_report([water], max_loops=200) -print(f"converged={result.converged} natoms={result.natoms}") -``` diff --git a/docs/zh/python/getting-started.md b/docs/zh/python/getting-started.md deleted file mode 100644 index d2e5e40..0000000 --- a/docs/zh/python/getting-started.md +++ /dev/null @@ -1,131 +0,0 @@ -# Quickstart - -A minimal end-to-end pack: 100 water molecules inside a 40 Å cube. - -## 1. Load a molecule - -Use `molrs.read_pdb` to load a template PDB file — the returned -`Frame` can be passed directly to `Target`: - -```python -import molrs - -frame = molrs.read_pdb("water.pdb") -``` - -No PDB file? Build a `molrs.Frame` from arrays with `Frame.from_dict`: - -```python -import molrs -import numpy as np - -frame = molrs.Frame.from_dict({ - "blocks": { - "atoms": { - "x": np.array([0.00, 0.96, -0.24]), - "y": np.array([0.00, 0.00, 0.93]), - "z": np.zeros(3), - "element": ["O", "H", "H"], - } - } -}) -``` - -## 2. Create a Target - -A `Target` bundles a molecule template with the number of copies to pack. -VdW radii are looked up automatically from element symbols (Bondi 1964). - -```python -from molpack import Target - -water = Target(frame, count=100).with_name("water") -``` - -Arguments: - -- `frame` — a `molrs.Frame` or `molpy.Frame` with columns `"x"`, `"y"`, - `"z"`, and `"element"` (or `"symbol"` for molrs PDB frames). -- `count` — number of copies to produce. - -A display label is optional — attach one via `.with_name("...")`. - -All builder methods are **immutable** — they return a new `Target`. - -## 3. Attach a restraint - -Every target needs at least one restraint — the geometric region it -should be packed into. - -```python -from molpack import InsideBoxRestraint - -water = water.with_restraint( - InsideBoxRestraint([0.0, 0.0, 0.0], [40.0, 40.0, 40.0]) -) -``` - -Five geometric built-in restraints: `InsideBoxRestraint`, -`InsideSphereRestraint`, `OutsideSphereRestraint`, `AbovePlaneRestraint`, -`BelowPlaneRestraint` — plus a family of collective -distribution-matching restraints. Stack multiple restraints with -repeated `.with_restraint()` calls — see -[Restraints](guide/restraints.md). - -## 4. Pack - -```python -from molpack import Molpack - -packer = Molpack().with_tolerance(2.0).with_seed(42) -frame = packer.pack([water], max_loops=200) - -print(frame["atoms"].nrows) -``` - -`pack()` returns a ready-to-use `molrs.Frame`. If you need structured -diagnostics, call `pack_with_report()` instead; it returns a -`PackResult` with `.converged`, `.fdist`, `.frest`, `.positions`, and -`.frame`. - -## 5. Save - -`molpack` does not write files directly — Frame is the canonical -output. Hand the returned frame to a writer: - -```python -import molrs - -molrs.write_xyz("packed.xyz", frame) -``` - -Or use `pack_with_report()` and write `result.frame` if you also need -the diagnostic fields. - -## Full script - -```python -import molrs -from molpack import InsideBoxRestraint, Molpack, Target - -frame = molrs.read_pdb("water.pdb") - -water = ( - Target(frame, count=100) - .with_name("water") - .with_restraint(InsideBoxRestraint([0.0, 0.0, 0.0], [40.0, 40.0, 40.0])) -) - -frame = ( - Molpack().with_tolerance(2.0).with_seed(42).pack([water], max_loops=200) -) - -print(f"packed {frame['atoms'].nrows} atoms") -``` - -## Next steps - -- [Targets](guide/targets.md) — orientation, centering, fixed placement. -- [Restraints](guide/restraints.md) — per-atom scoping, stacking. -- [Packer](guide/packer.md) — all builder options. -- [Examples](examples.md) — five complete Packmol workloads. diff --git a/docs/zh/python/guide/packer.md b/docs/zh/python/guide/packer.md deleted file mode 100644 index 323b70a..0000000 --- a/docs/zh/python/guide/packer.md +++ /dev/null @@ -1,140 +0,0 @@ -# Packer - -`Molpack` drives the GENCAN-based three-phase optimizer. All tuning -is through `with_*` builder methods — the constructor takes no -arguments. - -## Constructor - -```python -from molpack import Molpack - -packer = Molpack() -``` - -All defaults match Packmol's reference behaviour. Override any of -them via the builders below. - -## Builder methods - -Every builder returns a **new** `Molpack`: - -```python -packer = ( - Molpack() - .with_tolerance(2.0) # minimum allowed pairwise distance (Å) - .with_precision(0.01) # convergence threshold on fdist and frest - .with_inner_iterations(20) # GENCAN inner-loop cap (Packmol `maxit`) - .with_init_passes(0) # initial compaction passes (Packmol `nloop0`; 0 = auto) - .with_init_box_half_size(1000) # hard bound on init placement (Packmol `sidemax`) - .with_perturb_fraction(0.05) # fraction of atoms re-sampled per stall - .with_random_perturb(False) # randomize perturbation target selection - .with_perturb(True) # enable the stall-perturbation heuristic - .with_seed(42) # deterministic RNG - .with_parallel_eval(False) # rayon-backed pair-kernel eval (opt-in) - .with_lammps_output(True) # attach LAMMPS-style screen output - .with_log_level("progress") # quiet | summary | progress | verbose - .with_log_frequency(1) # print every N outer steps -) -``` - -Use `.with_lammps_output(False)` or `.with_log_level("quiet")` to run -silently (the default). `.with_progress()` is kept as a compatibility -alias for enabling/disabling progress output. - -A few more builders cover specific needs: - -```python -packer = ( - packer - .with_periodic_box([0, 0, 0], [30, 30, 30]) # fully-periodic cell (Packmol `pbc`) - .with_avoid_overlap(True) # reject init placements onto a fixed molecule - .with_xyz_output("traj.xyz", every=5) # record the packing trajectory -) -``` - -## Global restraints - -Attach a single restraint to every target in a pack: - -```python -packer = packer.with_global_restraint( - InsideBoxRestraint([0, 0, 0], [40, 40, 40]) -) -``` - -Semantically equivalent to calling `.with_restraint(r)` on every -target. - -## Handlers - -Attach any object implementing some subset of `on_start(ntotat, ntotmol)`, -`on_step(info) -> bool | None`, `on_finish()`: - -```python -class MyHandler: - def on_step(self, info): - print(f"phase={info.phase} loop={info.loop_idx} fdist={info.fdist:.3f}") - return None # or True to request early stop - -packer = packer.with_handler(MyHandler()) -``` - -Returning `True` from `on_step` halts the run at the next check. See -the `Handler` Protocol in `molpack`. - -## Periodic boundaries - -PBC can be declared per-axis on an `InsideBoxRestraint`, or as a -fully-periodic cell directly on the packer via -`.with_periodic_box(min, max)`. See -[Periodic boundaries](periodic-boundaries.md). - -## Running - -```python -frame = packer.pack(targets, max_loops=200) -``` - -- `targets` — list of `Target` objects (must be non-empty). -- `max_loops` — per-phase outer-iteration budget. - -Raises one of the typed `PackError` subclasses on failure -(`NoTargetsError`, `InvalidPBCBoxError`, -`ConflictingPeriodicBoxesError`, …). - -`pack()` returns a `molrs.Frame`. To retrieve structured diagnostics, -use `pack_with_report()`: - -```python -result = packer.pack_with_report(targets, max_loops=200) -``` - -## PackResult - -```python -result.positions # (N, 3) float64 ndarray — packed coordinates -result.frame # molrs.Frame — topology-complete packed frame -result.elements # list[str] — one entry per atom -result.natoms # int -result.converged # bool — True iff both fdist and frest < precision -result.fdist # float — final distance-violation sum -result.frest # float — final restraint-violation sum -``` - -Inspect convergence: - -```python -if not result.converged: - print(f"not converged: fdist={result.fdist:.4f} frest={result.frest:.4f}") -``` - -`PackResult.frame` is the same Frame returned by `pack()`. Pass it to a -writer of your choice (e.g. `molrs.write_pdb`). molpack does **not** -provide writers. - -## Reproducibility - -Packing is deterministic for a given `(targets, tolerance, precision, -seed)` tuple. Capture the builder chain and `max_loops` to reproduce -a result later. diff --git a/docs/zh/python/guide/periodic-boundaries.md b/docs/zh/python/guide/periodic-boundaries.md deleted file mode 100644 index 3a521a6..0000000 --- a/docs/zh/python/guide/periodic-boundaries.md +++ /dev/null @@ -1,108 +0,0 @@ -# Periodic boundaries - -By default the packer works under **free boundary conditions** — atoms -are not wrapped and the only geometric limits come from the restraints -you attach. Use periodic boundaries (PBC) when packing for MD input. - -## Enabling PBC - -PBC is declared on the `InsideBoxRestraint` via the `periodic` keyword: - -```python -from molpack import InsideBoxRestraint - -# Fully periodic orthorhombic cell -box = InsideBoxRestraint( - [0.0, 0.0, 0.0], - [30.0, 30.0, 30.0], - periodic=(True, True, True), -) -``` - -`periodic` is a 3-tuple of booleans — one per axis. Only orthorhombic -cells are supported. - -Per-axis PBC is possible — e.g. slab geometry with in-plane PBC and -open Z: - -```python -slab = InsideBoxRestraint( - [0.0, 0.0, 0.0], - [30.0, 30.0, 100.0], - periodic=(True, True, False), -) -``` - -### Declaring PBC on the packer - -For a **fully-periodic** cell you can skip the restraint and declare the -box directly on the packer (Packmol's `pbc` keyword): - -```python -from molpack import Molpack - -packer = Molpack().with_periodic_box([0.0, 0.0, 0.0], [30.0, 30.0, 30.0]) -``` - -This is equivalent to a single `InsideBoxRestraint(..., periodic=(True, -True, True))` broadcast over the system. Use the restraint form when you -need per-axis control or a soft confining region; use `with_periodic_box` -when the cell is simply fully periodic. - -## Semantics - -Under PBC, the pairwise distance evaluator applies minimum-image -wrapping on the periodic axes, so atoms near opposite faces of the -cell "see" each other through the periodic images. The `tolerance` -setting still applies and is checked against the wrapped distance. - -Restraints themselves (`InsideBoxRestraint`, `InsideSphereRestraint`, -…) are evaluated in the **unwrapped** frame — they describe the -geometric region as defined, regardless of the periodic cell. - -## System-wide PBC derivation - -At `pack()` time the packer scans every restraint on every target for -a declared periodic box. The rules are: - -- **Zero declarations** — non-periodic run. -- **One declaration** — its bounds define the system PBC. -- **Multiple declarations** — they must all agree (same bounds, same - per-axis flags). Any mismatch raises `ConflictingPeriodicBoxesError`. - -## Errors - -A zero-length axis on a periodic box, or `max < min` on any axis, -raises `InvalidPBCBoxError` at `pack()` time: - -```python -from molpack import InvalidPBCBoxError - -try: - packer.pack(...) -except InvalidPBCBoxError as e: - ... -``` - -Both typed errors inherit from `molpack.PackError` (which itself is -a `RuntimeError` subclass) — a blanket `except PackError` catches any -packing failure. - -## Choosing a box - -A common pattern: pack into a single periodic `InsideBoxRestraint` -matching the desired cell. The restraint confines atoms softly and -simultaneously declares the PBC: - -```python -cell_min = [0.0, 0.0, 0.0] -cell_max = [30.0, 30.0, 30.0] - -box = InsideBoxRestraint(cell_min, cell_max, periodic=(True, True, True)) -target = target.with_restraint(box) - -result = Molpack().with_seed(42).pack([target], max_loops=200) -``` - -Or broadcast it globally via `Molpack.with_global_restraint(box)` when -several species share the same cell. diff --git a/docs/zh/python/guide/restraints.md b/docs/zh/python/guide/restraints.md deleted file mode 100644 index 2364c9e..0000000 --- a/docs/zh/python/guide/restraints.md +++ /dev/null @@ -1,165 +0,0 @@ -# Restraints - -Restraints are geometric regions (or half-spaces) that every atom of a -target — or a chosen subset — must lie inside. molpack ships two -families of built-in restraints: five **geometric** region restraints -(below), and six **collective** distribution-matching restraints -([further down](#collective-distribution-matching-restraints)). Both -attach the same way, via `.with_restraint()`. - -## Geometric built-ins - -| Class | Constructor arguments | Meaning | -|--------------------------|------------------------------------------------|---------| -| `InsideBoxRestraint` | `min: [x,y,z]`, `max: [x,y,z]`, `periodic=(False, False, False)` | axis-aligned box | -| `InsideSphereRestraint` | `center: [x,y,z]`, `radius: float` | closed ball | -| `OutsideSphereRestraint` | `center: [x,y,z]`, `radius: float` | complement of closed ball | -| `AbovePlaneRestraint` | `normal: [nx,ny,nz]`, `distance: float` | half-space $\mathbf{n}\cdot\mathbf{x} \ge d$ | -| `BelowPlaneRestraint` | `normal: [nx,ny,nz]`, `distance: float` | half-space $\mathbf{n}\cdot\mathbf{x} \le d$ | - -All arguments are standard Python floats / lists. - -```python -from molpack import ( - AbovePlaneRestraint, - BelowPlaneRestraint, - InsideBoxRestraint, - InsideSphereRestraint, - OutsideSphereRestraint, -) - -box = InsideBoxRestraint([0, 0, 0], [40, 40, 40]) -ball = InsideSphereRestraint([0, 0, 0], 20.0) -shell = OutsideSphereRestraint([0, 0, 0], 10.0) -above = AbovePlaneRestraint(normal=[0, 0, 1], distance=5.0) -below = BelowPlaneRestraint(normal=[0, 0, 1], distance=20.0) -``` - -## Periodic boxes - -`InsideBoxRestraint` doubles as the PBC declaration. Passing a -`periodic` tuple turns any subset of axes periodic: - -```python -InsideBoxRestraint([0, 0, 0], [30, 30, 30], periodic=(True, True, True)) -``` - -See [Periodic boundaries](periodic-boundaries.md) for the full -semantics and validation rules. - -## Collective (distribution-matching) restraints - -Where a geometric restraint penalises **each atom** against a region, -a collective restraint sees **every copy of the target at once** and -drives the species' spatial *distribution* toward a target profile -(via a squared 1-D Wasserstein penalty). Six built-ins cover Gaussian, -exponential, and arbitrary-tabulated priors along either a plane or a -radius — e.g. a Gaussian slab centred at `z = 20`: - -```python -from molpack import GaussianPlane - -slab = GaussianPlane(normal=[0, 0, 1], offset=0.0, strength=1.0, mu=20.0, sigma=3.0) -target = Target(frame, count=200).with_restraint(slab) -``` - -The full list and constructor signatures are in the -[API reference](../api-reference.md#collective-distribution-matching-restraints). - -## Stacking multiple restraints - -Apply several restraints to the same target by chaining `.with_restraint()`: - -```python -target = ( - Target(frame, count=500) - .with_name("water") - .with_restraint(InsideBoxRestraint([0, 0, 0], [40, 40, 40])) - .with_restraint(OutsideSphereRestraint([20, 20, 20], 5.0)) -) -``` - -Each call attaches an independent restraint. All active restraints are -evaluated at every optimizer step. - -## Scopes - -A restraint can be applied at two scopes: - -- **Whole target** — `target.with_restraint(r)` — penalises every atom. -- **Atom subset** — `target.with_atom_restraint([0, 1, 2], r)` — - penalises only the listed atoms (0-based indices). - -Example — a bilayer: pin heads above z=12, tails below z=2: - -```python -lipid = ( - Target(frame, count=20) - .with_name("lipid") - .with_restraint(InsideBoxRestraint([0, 0, 0], [40, 40, 14])) - .with_atom_restraint([0, 1], AbovePlaneRestraint([0, 0, 1], 12.0)) - .with_atom_restraint([30, 31], BelowPlaneRestraint([0, 0, 1], 2.0)) -) -``` - -## Global restraints - -To apply one restraint to every target in a pack, attach it on the -packer: - -```python -packer = ( - Molpack() - .with_global_restraint(InsideBoxRestraint([0, 0, 0], [40, 40, 40])) -) -``` - -Semantically equivalent to calling `.with_restraint(r)` on every -target, but avoids the duplication. - -## Custom restraints - -Pass any object implementing `f(x, scale, scale2) -> float` and -`fg(x, scale, scale2) -> (float, (gx, gy, gz))`. See the -`Restraint` Protocol in `molpack` for the full contract. - -```python -class SphereRestraint: - def __init__(self, center, radius): - self.c = np.asarray(center) - self.r = radius - def f(self, x, scale, scale2): - d = np.linalg.norm(np.asarray(x) - self.c) - self.r - return scale2 * d * d if d > 0 else 0.0 - def fg(self, x, scale, scale2): - rel = np.asarray(x) - self.c - d = float(np.linalg.norm(rel)) - over = d - self.r - if over <= 0: - return 0.0, (0.0, 0.0, 0.0) - factor = 2 * scale2 * over / d - return scale2 * over * over, tuple(factor * rel) - -target = Target(frame, count=10).with_restraint(SphereRestraint([0,0,0], 6.0)) -``` - -## Semantics - -Every restraint contributes a continuously differentiable penalty -$f_{\text{rest}}(\mathbf{x})$ that is zero inside the allowed region -and rises quadratically outside. The aggregate objective minimised by -the packer is: - -$$ -U(\mathbf{x}) = f_{\text{dist}}(\mathbf{x}) + f_{\text{rest}}(\mathbf{x}) -$$ - -where $f_{\text{dist}}$ is the pairwise distance-violation sum for the -user-specified `tolerance`. Convergence is declared when both fall -below `precision`. - -!!! note "Restraints vs hard constraints" - All built-in restraints are *soft penalties* — the optimizer may - momentarily produce a violating configuration while searching. Hard - geometric constraints (frozen placement, rotation bounds) are set - on the `Target` directly via `fixed_at` and `with_rotation_bound`. diff --git a/docs/zh/python/guide/targets.md b/docs/zh/python/guide/targets.md deleted file mode 100644 index a1eb5aa..0000000 --- a/docs/zh/python/guide/targets.md +++ /dev/null @@ -1,187 +0,0 @@ -# Targets - -A `Target` describes one type of molecule to pack: its template -geometry, element symbols, and the number of copies to produce. -VdW radii are resolved automatically from element symbols via the -Bondi (1964) table. - -## Construction - -```python -from molpack import Target - -target = Target(frame, count) -``` - -- `frame` — a `molrs.Frame` or `molpy.Frame`, resolved zero-copy via its - FFI capsule. Supported sources: - - | Source | Element column | - |--------|---------------| - | `molrs.read_pdb(path)` | `"symbol"` | - | `molrs.read_xyz(path)` | `"element"` | - | `molrs.Frame.from_dict({"blocks": {"atoms": {...}}})` | `"element"` | - | `molpy.Frame` | `"element"` | - -- `count` — number of copies to produce. - -A display label is optional: - -```python -target = Target(frame, count).with_name("water") -``` - -Build a frame in memory (no PDB file) with `molrs.Frame.from_dict`: - -```python -import molrs -import numpy as np - -frame = molrs.Frame.from_dict({ - "blocks": { - "atoms": { - "x": np.array([0.00, 0.96, -0.24]), - "y": np.array([0.00, 0.00, 0.93]), - "z": np.zeros(3), - "element": ["O", "H", "H"], - } - } -}) -water = Target(frame, count=100).with_name("water") -``` - -## Read-only properties - -```python -target.name # Optional[str] -target.natoms # number of template atoms -target.count # requested copies -target.elements # list[str] -target.radii # list[float] -target.is_fixed # True if placement is frozen (see below) -``` - -All builder methods are **immutable** — they return a new `Target`. - -## Centering - -The default is [`CenteringMode.AUTO`](../api-reference.md#centeringmode) — free -targets are centered on their geometric center before packing; fixed -targets are kept in place. Override explicitly: - -```python -from molpack import CenteringMode - -target = target.with_centering(CenteringMode.CENTER) # always center -target = target.with_centering(CenteringMode.OFF) # keep input coords -``` - -## Fixed placement - -Pin a target at a specific location (e.g. a fixed reference molecule): - -```python -from molpack import Angle - -target = target.fixed_at([10.0, 20.0, 30.0]) - -# optional Euler orientation — three Angle values in Packmol's -# eulerfixed convention -target = ( - target.fixed_at([10.0, 20.0, 30.0]) - .with_orientation(( - Angle.from_degrees(0.0), - Angle.from_radians(1.57), - Angle.ZERO, - )) -) -``` - -Fixed targets are excluded from the optimizer but still contribute to -distance exclusion against other species. - -## Rotation bounds - -Restrict the rotational search window about each axis: - -```python -from molpack import Angle, Axis - -target = ( - target - .with_rotation_bound(Axis.X, Angle.from_degrees(0.0), Angle.from_degrees(15.0)) - .with_rotation_bound(Axis.Y, Angle.from_degrees(90.0), Angle.from_degrees(10.0)) - .with_rotation_bound(Axis.Z, Angle.from_degrees(0.0), Angle.from_degrees(5.0)) -) -``` - -`Angle` makes units explicit: use `Angle.from_degrees(...)` or -`Angle.from_radians(...)` — raw floats are rejected. - -## Attaching restraints - -### All atoms of the target - -```python -from molpack import InsideBoxRestraint - -target = target.with_restraint( - InsideBoxRestraint([0, 0, 0], [40, 40, 40]) -) -``` - -Stack multiple restraints by calling `.with_restraint()` again: - -```python -target = ( - target - .with_restraint(InsideBoxRestraint([0, 0, 0], [40, 40, 40])) - .with_restraint(OutsideSphereRestraint([20, 20, 20], 5.0)) -) -``` - -### A subset of atoms - -```python -from molpack import AbovePlaneRestraint, BelowPlaneRestraint - -target = target.with_atom_restraint( - [30, 31], # 0-based Rust-native indices - BelowPlaneRestraint([0.0, 0.0, 1.0], 2.0), -) -``` - -!!! note "0-based indexing" - `with_atom_restraint` uses **0-based** indices, matching Rust - convention. If you are porting from a Packmol `.inp` file (which - uses 1-based indices), subtract 1 at the call site. - -## Relaxation-assisted packing - -Attach an in-loop relaxer to reshape a flexible molecule's reference -geometry *during* packing — useful for long chains that must fold to -fit. `with_relaxer` requires `count == 1` (every copy shares the -reference geometry the relaxer rewrites): - -```python -from molpack import Target, TorsionMcRelaxer - -chain = TorsionMcRelaxer(frame).with_steps(20).with_self_avoidance(1.5) -target = Target(frame, count=1).with_relaxer(chain) -``` - -Two relaxers ship built in: `TorsionMcRelaxer` (engine-free Monte-Carlo -torsion sampling, always available) and `LBFGSRelaxer` (force-field -L-BFGS, `ff` feature). See -[In-loop relaxers](../api-reference.md#in-loop-relaxers) for their -options. - -## Per-target solver budget - -Override the maximum perturbation budget for this target: - -```python -target = target.with_perturb_budget(50) # default: derived from count -``` - -Useful when one species is significantly harder to place than the rest. diff --git a/docs/zh/python/index.md b/docs/zh/python/index.md deleted file mode 100644 index 8c671ce..0000000 --- a/docs/zh/python/index.md +++ /dev/null @@ -1,71 +0,0 @@ -# Python - -Packmol-grade molecular packing in Rust, with a first-class Python binding. - -`molpack` arranges $N$ molecule types under geometric restraints without -overlaps. The engine is a faithful port of Packmol's GENCAN-driven algorithm -(Martínez *et al.* 2009); five canonical workloads stay pinned against Packmol -reference output. - -## At a glance - -```python -import molrs -from molpack import InsideBoxRestraint, Molpack, Target - -frame = molrs.read_pdb("water.pdb") - -water = ( - Target(frame, count=100) - .with_name("water") - .with_restraint(InsideBoxRestraint([0.0, 0.0, 0.0], [40.0, 40.0, 40.0])) -) - -packer = Molpack().with_tolerance(2.0).with_seed(42) -frame = packer.pack([water], max_loops=200) -print(f"packed {frame['atoms'].nrows} atoms") -``` - -## Next steps - - - -## See also - -- Rust crate: [`molcrafts-molpack`](https://crates.io/crates/molcrafts-molpack) — - the underlying engine -- [`molcrafts-molrs`](https://pypi.org/project/molcrafts-molrs/) — frame I/O and - the `Frame` data model (installed as a dependency) diff --git a/docs/zh/python/installation.md b/docs/zh/python/installation.md deleted file mode 100644 index 664300a..0000000 --- a/docs/zh/python/installation.md +++ /dev/null @@ -1,68 +0,0 @@ -# Installation - -## From PyPI - -```bash -pip install molcrafts-molpack -``` - -The PyPI package `molcrafts-molpack` installs a Python module named `molpack`: - -```python -import molpack -``` - -Pre-built wheels are published for CPython **3.12** and **3.13** on Linux -(manylinux x86-64) and macOS (universal2). Other platforms fall back to the -sdist and need a Rust toolchain to build locally. - -## Frame I/O - -`molcrafts-molpack` installs `molcrafts-molrs` as a dependency. The -importable `molrs` module provides the frame type plus PDB and XYZ readers: - -```bash -pip install molcrafts-molpack -``` - -```python -import molrs -from molpack import InsideBoxRestraint, Molpack, Target - -frame = molrs.read_pdb("water.pdb") -water = ( - Target(frame, count=100) - .with_name("water") - .with_restraint(InsideBoxRestraint([0.0, 0.0, 0.0], [40.0, 40.0, 40.0])) -) -frame = Molpack().with_seed(42).pack([water], max_loops=200) -``` - -`Target` takes a `molrs.Frame` or `molpy.Frame`, resolved zero-copy through -its FFI capsule. - -## Building from source - -Requires a Rust toolchain (1.91+) and `maturin`: - -```bash -git clone https://github.com/MolCrafts/molpack -cd molpack/python -pip install maturin -maturin develop --release -``` - -This builds against the local `molpack` Rust crate under `../`. - -## Verification - -```python -import molrs -from molpack import Target - -frame = molrs.Frame.from_dict({ - "blocks": {"atoms": {"x": [0.0], "y": [0.0], "z": [0.0], "element": ["O"]}} -}) -target = Target(frame, count=1).with_name("mol") -print(target) # Target(natoms=1, count=1, name=Some("mol")) -``` diff --git a/docs/zh/rust/examples.md b/docs/zh/rust/examples.md deleted file mode 100644 index 80eae63..0000000 --- a/docs/zh/rust/examples.md +++ /dev/null @@ -1,30 +0,0 @@ -# Examples - -The Rust examples live under `examples/pack_*` and use the `io` feature to read -the bundled structure files. - -```bash -cargo run --release --features io --example pack_mixture -cargo run --release --features io --example pack_bilayer -cargo run --release --features io --example pack_interface -cargo run --release --features io --example pack_spherical -cargo run --release --features io --example pack_solvprotein -``` - -| Example | Demonstrates | -|---|---| -| `pack_mixture` | Two-component water/urea cube. | -| `pack_bilayer` | Plane restraints and atom-subset orientation. | -| `pack_interface` | Solvents around a fixed molecule. | -| `pack_spherical` | Nested spherical shells. | -| `pack_solvprotein` | Fixed protein solvation with water and ions. | - -Optional progress and trajectory output are controlled by environment -variables: - -```bash -MOLRS_PACK_EXAMPLE_PROGRESS=1 cargo run --release --features io --example pack_mixture -MOLRS_PACK_EXAMPLE_XYZ=1 cargo run --release --features io --example pack_mixture -``` - -For Packmol-style input files, use the matching [CLI examples](../cli/examples.md). diff --git a/docs/zh/rust/getting-started.md b/docs/zh/rust/getting-started.md deleted file mode 100644 index ff73b7e..0000000 --- a/docs/zh/rust/getting-started.md +++ /dev/null @@ -1,68 +0,0 @@ -# Quickstart - -A Rust packing job has three parts: - -1. Build one `Target` per molecule species. -2. Attach at least one spatial restraint to each mobile target, or use a - packer-level global restraint. -3. Run `Molpack::pack(&targets, max_loops)`. - -## One molecule type in a box - -```rust -use molpack::{InsideBoxRestraint, Molpack, Target}; - -let water_positions = [ - [0.0, 0.0, 0.0], - [0.96, 0.0, 0.0], - [-0.24, 0.93, 0.0], -]; -let water_radii = [1.52, 1.20, 1.20]; - -let water = Target::from_coords(&water_positions, &water_radii, 100) - .with_name("water") - .with_restraint(InsideBoxRestraint::new( - [0.0, 0.0, 0.0], - [40.0, 40.0, 40.0], - [false, false, false], - )); - -let mut packer = Molpack::new() - .with_tolerance(2.0) - .with_seed(42); - -let frame = packer.pack(&[water], 200)?; -let natoms = frame.get("atoms").and_then(|b| b.nrows()).unwrap_or(0); -println!("packed {natoms} atoms"); -``` - -`pack()` returns a packed `molrs::Frame`. Use `pack_with_report()` if you also -need convergence fields: - -```rust -let mut packer = Molpack::new().with_seed(42); -let result = packer.pack_with_report(&targets, 200)?; -println!("converged={} fdist={} frest={}", result.converged, result.fdist, result.frest); -``` - -## Builder defaults - -Every tuning knob except `max_loops` has a Packmol-compatible default. Set a -builder value only when you need to change the default: - -```rust -let packer = Molpack::new() - .with_tolerance(2.0) - .with_precision(0.01) - .with_inner_iterations(20) - .with_seed(42); -``` - -`max_loops` is positional because the right iteration budget depends on system -size and packing difficulty. - -## Targets are snapshots - -`Target` is a builder value. `pack()` snapshots the target slice at call time; -mutating or rebuilding a target after that does not affect an already running -pack. diff --git a/docs/zh/rust/handlers-relaxers.md b/docs/zh/rust/handlers-relaxers.md deleted file mode 100644 index c9e41fc..0000000 --- a/docs/zh/rust/handlers-relaxers.md +++ /dev/null @@ -1,72 +0,0 @@ -# Handlers and Relaxers - -Handlers observe a packing run. Relaxers modify a target's reference geometry -between optimizer iterations. - -## Screen output - -Enable LAMMPS-style progress output through the builder: - -```rust -use molpack::{Molpack, MolpackLogLevel}; - -let mut packer = Molpack::new() - .with_log_level(MolpackLogLevel::Progress) - .with_log_frequency(10); -``` - -The CLI enables screen output by default; library callers stay quiet unless you -opt in. - -## Built-in handlers - -```rust -use molpack::{EarlyStopHandler, Molpack, XYZHandler}; - -let mut packer = Molpack::new() - .with_handler(XYZHandler::new("traj.xyz", 10)) - .with_handler(EarlyStopHandler::new(1e-4)); -``` - -Use handlers for progress logs, trajectory snapshots, custom observation, and -early stop. Handler callbacks receive an immutable `PackContext` view; they do -not mutate packer state. - -## Custom handlers - -Implement the `Handler` trait when you need structured events from a run: - -```rust -use molpack::{Handler, PackContext, StepInfo}; - -#[derive(Debug)] -struct WatchFdist; - -impl Handler for WatchFdist { - fn on_step(&mut self, info: &StepInfo, _sys: &PackContext) { - eprintln!("phase={} loop={} fdist={}", info.phase, info.loop_idx, info.fdist); - } -} -``` - -See [Extending](../extending.md) for a full custom-handler walkthrough. - -## Relaxers - -Relaxers update a molecule's reference geometry during packing. They are useful -for flexible molecules that need to sample torsions while being placed. - -```rust -use molpack::{InsideSphereRestraint, Target, TorsionMcRelaxer}; - -let target = Target::from_coords(positions, radii, 1) - .with_restraint(InsideSphereRestraint::new([0.0; 3], 20.0)) - .with_relaxer( - TorsionMcRelaxer::new(&graph) - .with_temperature(0.5) - .with_steps(20), - ); -``` - -Relaxers require `count == 1` because every copy of a target shares one -reference geometry. diff --git a/docs/zh/rust/index.md b/docs/zh/rust/index.md deleted file mode 100644 index 4ee630e..0000000 --- a/docs/zh/rust/index.md +++ /dev/null @@ -1,46 +0,0 @@ -# Rust - -Use the Rust API when packing is part of a Rust program, when you need -structured convergence diagnostics, or when you are extending molpack itself. - -```rust -use molpack::{InsideBoxRestraint, Molpack, Target}; - -let positions = [[0.0, 0.0, 0.0], [0.96, 0.0, 0.0], [-0.24, 0.93, 0.0]]; -let radii = [1.52, 1.20, 1.20]; - -let water = Target::from_coords(&positions, &radii, 100) - .with_name("water") - .with_restraint(InsideBoxRestraint::new( - [0.0, 0.0, 0.0], - [40.0, 40.0, 40.0], - [false, false, false], - )); - -let mut packer = Molpack::new().with_seed(42); -let frame = packer.pack(&[water], 200)?; -``` - -## Install - -```bash -cargo add molcrafts-molpack -``` - -Feature flags: - -| Feature | Enables | -|---|---| -| `io` | PDB, XYZ, SDF, and LAMMPS readers through `molrs_io`. | -| `cli` | The `molpack` binary plus `io`. | -| `rayon` | Parallel objective evaluation. | -| `ff` | Force-field-backed relaxers. | - -## Pages - -- [Quickstart](getting-started.md) walks through a first target and pack. -- [Restraints and PBC](restraints-and-pbc.md) explains target-level, - atom-subset, global, and periodic restraints. -- [Handlers and Relaxers](handlers-relaxers.md) covers progress output, - observers, early stop, trajectory dumping, and in-loop conformation sampling. -- [Examples](examples.md) lists the checked-in Rust workloads. diff --git a/docs/zh/rust/restraints-and-pbc.md b/docs/zh/rust/restraints-and-pbc.md deleted file mode 100644 index 31ef250..0000000 --- a/docs/zh/rust/restraints-and-pbc.md +++ /dev/null @@ -1,84 +0,0 @@ -# Restraints and PBC - -Restraints are soft penalties that guide atoms into allowed regions. They can -be attached to a target, to a subset of atoms on each target copy, or globally -on the packer. - -## Whole-target restraints - -```rust -use molpack::{InsideBoxRestraint, Target}; - -let target = Target::from_coords(positions, radii, 100) - .with_restraint(InsideBoxRestraint::new( - [0.0, 0.0, 0.0], - [40.0, 40.0, 40.0], - [false, false, false], - )); -``` - -Built-in geometric restraints include boxes, spheres, outside-sphere -exclusions, and plane half-spaces. Collective restraints match an entire -species to a distribution profile; see [Concepts](../concepts.md). - -## Atom-subset restraints - -Atom-subset restraints apply to selected atoms of every copy. Indices are -0-based: - -```rust -use molpack::{BelowPlaneRestraint, Target}; - -let target = Target::from_coords(positions, radii, 100) - .with_atom_restraint( - &[0, 1], - BelowPlaneRestraint::new([0.0, 0.0, 1.0], 2.0), - ); -``` - -If you are translating from a Packmol `.inp` `atoms ... end atoms` block, -subtract 1 from each atom index. - -## Global restraints - -Attach a restraint to every target through the packer: - -```rust -use molpack::{InsideSphereRestraint, Molpack, Target}; - -let mut packer = Molpack::new() - .with_global_restraint(InsideSphereRestraint::new([20.0, 20.0, 20.0], 30.0)); - -let frame = packer.pack(&[a, b], 200)?; -``` - -This is equivalent to cloning the same restraint onto every target before -packing. - -## Periodic boxes - -There are two ways to declare periodic boundary conditions. - -For fully periodic boxes, set PBC on the packer: - -```rust -use molpack::Molpack; - -let packer = Molpack::new().with_periodic_box([0.0; 3], [30.0; 3]); -``` - -For per-axis control, set periodic flags on `InsideBoxRestraint`: - -```rust -use molpack::{InsideBoxRestraint, Target}; - -let slab = Target::from_coords(positions, radii, 100) - .with_restraint(InsideBoxRestraint::new( - [0.0, 0.0, 0.0], - [30.0, 30.0, 100.0], - [true, true, false], - )); -``` - -Multiple PBC declarations must agree exactly. Invalid or conflicting boxes -return typed `PackError` variants. diff --git a/python/pyproject.toml b/python/pyproject.toml index 90f232e..25964d9 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -58,10 +58,7 @@ dev = [ # `uv run --group dev tox -e py` from python/. "tox>=4.23", ] -# Documentation build. Dual-language Zensical trees: -# zensical.toml → docs/en → site/en -# zensical.zh.toml → docs/zh → site/zh -# Cloudflare / CI should run both builds and place a root redirect to /en/. +# Documentation build: `zensical build` reads zensical.toml (docs → site). # Theme: `theme.name = "molcrafts"` (molcrafts-zensical-theme on PyPI). doc = [ "zensical>=0.0.51", diff --git a/src/lib.rs b/src/lib.rs index f2953ef..82edbad 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -32,7 +32,7 @@ //! //! Reference material (not rustdoc): //! -//! - [Packmol parity](https://github.com/MolCrafts/molpack/blob/master/docs/en/packmol_parity.md) +//! - [Packmol parity](https://github.com/MolCrafts/molpack/blob/master/docs/packmol_parity.md) //! — kind-number ↔ Rust struct mapping with Fortran pointers. //! //! ## Quick example @@ -153,20 +153,20 @@ pub use objective::Objective; // ──────────────────────────────────────────────────────────────────────────── // Documentation modules (rustdoc-only; no runtime items). -// Content lives in `docs/en/*.md`, loaded via `include_str!` so each markdown +// Content lives in `docs/*.md`, loaded via `include_str!` so each markdown // file can be edited independently while rustdoc renders the whole chapter. // ──────────────────────────────────────────────────────────────────────────── -#[doc = include_str!("../docs/en/getting_started.md")] +#[doc = include_str!("../docs/getting_started.md")] pub mod getting_started {} -#[doc = include_str!("../docs/en/concepts.md")] +#[doc = include_str!("../docs/concepts.md")] pub mod concepts {} -#[doc = include_str!("../docs/en/architecture.md")] +#[doc = include_str!("../docs/architecture.md")] pub mod architecture {} -#[doc = include_str!("../docs/en/extending.md")] +#[doc = include_str!("../docs/extending.md")] pub mod extending {} // ──────────────────────────────────────────────────────────────────────────── diff --git a/src/restraint/mod.rs b/src/restraint/mod.rs index d2a3463..30d6627 100644 --- a/src/restraint/mod.rs +++ b/src/restraint/mod.rs @@ -6,7 +6,7 @@ //! beside the 14 Packmol-originals in type space. //! //! Numerical equivalence to the Fortran `comprest.f90` (value) and `gwalls.f90` -//! (gradient) is preserved branch-for-branch; see `docs/en/packmol_parity.md`. +//! (gradient) is preserved branch-for-branch; see `docs/packmol_parity.md`. //! //! **Gradient convention**: `AtomRestraint::fg` accumulates INTO `g` with `+=`. //! Do not overwrite; many restraints may contribute to the same atom. diff --git a/zensical.toml b/zensical.toml index 3e2d121..c7d6afe 100644 --- a/zensical.toml +++ b/zensical.toml @@ -1,101 +1,54 @@ -# Single Zensical project — multi-language per official guide: -# https://zensical.org/docs/setup/language/ +# molpack docs — single English site served at the root. # -# [project.theme] language → one UI language for the build -# [project.extra] alternate → header language selector (absolute /en/, /zh/) -# -# Content lives under docs/en and docs/zh; one build emits both URL trees. -# Build: zensical build +# zensical build # docs → site [project] site_name = "molpack" site_description = "Packmol-grade molecular packing in pure Rust, with Python bindings." -site_url = "https://molcrafts.github.io/molpack/" +site_url = "https://molpack.molcrafts.org/" repo_url = "https://github.com/MolCrafts/molpack" repo_name = "MolCrafts/molpack" copyright = "Copyright © 2026 MolCrafts" docs_dir = "docs" site_dir = "site" -# Default nav is the English tree (paths under en/). Chinese pages live at -# zh/… and are opened via the language selector (alternate → /zh/). nav = [ - { "Home" = "en/index.md" }, + { "Home" = "index.md" }, { "Tutorial" = [ - { "Install" = "en/install.md" }, - { "Quickstart" = "en/getting_started.md" }, - { "Concepts" = "en/concepts.md" }, - { "Examples" = "en/examples.md" }, - { "Packmol Parity" = "en/packmol_parity.md" }, + { "Install" = "install.md" }, + { "Quickstart" = "getting_started.md" }, + { "Concepts" = "concepts.md" }, + { "Examples" = "examples.md" }, + { "Packmol Parity" = "packmol_parity.md" }, ] }, { "Packmol Script" = [ - { "Overview" = "en/cli/index.md" }, - { "Script Notes" = "en/cli/script-format.md" }, - { "Formats" = "en/cli/formats.md" }, - { "Examples" = "en/cli/examples.md" }, + { "Overview" = "cli/index.md" }, + { "Script Notes" = "cli/script-format.md" }, + { "Formats" = "cli/formats.md" }, + { "Examples" = "cli/examples.md" }, ] }, { "Python" = [ - { "Overview" = "en/python/index.md" }, - { "Installation" = "en/python/installation.md" }, - { "Quickstart" = "en/python/getting-started.md" }, - { "Targets" = "en/python/guide/targets.md" }, - { "Restraints" = "en/python/guide/restraints.md" }, - { "Packer" = "en/python/guide/packer.md" }, - { "Periodic Boundaries" = "en/python/guide/periodic-boundaries.md" }, - { "Examples" = "en/python/examples.md" }, - { "API Reference" = "en/python/api-reference.md" }, + { "Overview" = "python/index.md" }, + { "Installation" = "python/installation.md" }, + { "Quickstart" = "python/getting-started.md" }, + { "Targets" = "python/guide/targets.md" }, + { "Restraints" = "python/guide/restraints.md" }, + { "Packer" = "python/guide/packer.md" }, + { "Periodic Boundaries" = "python/guide/periodic-boundaries.md" }, + { "Examples" = "python/examples.md" }, + { "API Reference" = "python/api-reference.md" }, ] }, { "Rust" = [ - { "Overview" = "en/rust/index.md" }, - { "Quickstart" = "en/rust/getting-started.md" }, - { "Restraints and PBC" = "en/rust/restraints-and-pbc.md" }, - { "Handlers and Relaxers" = "en/rust/handlers-relaxers.md" }, - { "Examples" = "en/rust/examples.md" }, + { "Overview" = "rust/index.md" }, + { "Quickstart" = "rust/getting-started.md" }, + { "Restraints and PBC" = "rust/restraints-and-pbc.md" }, + { "Handlers and Relaxers" = "rust/handlers-relaxers.md" }, + { "Examples" = "rust/examples.md" }, ] }, { "Development" = [ - { "Contributing" = "en/development/index.md" }, - { "Architecture" = "en/architecture.md" }, - { "Extending" = "en/extending.md" }, - ] }, - # Chinese tree — same structure, URLs under /zh/ (language selector). - { "中文" = [ - { "首页" = "zh/index.md" }, - { "教程" = [ - { "安装" = "zh/install.md" }, - { "快速开始" = "zh/getting_started.md" }, - { "概念" = "zh/concepts.md" }, - { "示例" = "zh/examples.md" }, - { "Packmol 对等" = "zh/packmol_parity.md" }, - ] }, - { "Packmol 脚本" = [ - { "概览" = "zh/cli/index.md" }, - { "脚本说明" = "zh/cli/script-format.md" }, - { "格式" = "zh/cli/formats.md" }, - { "示例" = "zh/cli/examples.md" }, - ] }, - { "Python" = [ - { "概览" = "zh/python/index.md" }, - { "安装" = "zh/python/installation.md" }, - { "快速开始" = "zh/python/getting-started.md" }, - { "Targets" = "zh/python/guide/targets.md" }, - { "Restraints" = "zh/python/guide/restraints.md" }, - { "Packer" = "zh/python/guide/packer.md" }, - { "周期边界" = "zh/python/guide/periodic-boundaries.md" }, - { "示例" = "zh/python/examples.md" }, - { "API 参考" = "zh/python/api-reference.md" }, - ] }, - { "Rust" = [ - { "概览" = "zh/rust/index.md" }, - { "快速开始" = "zh/rust/getting-started.md" }, - { "约束与 PBC" = "zh/rust/restraints-and-pbc.md" }, - { "Handlers 与 Relaxers" = "zh/rust/handlers-relaxers.md" }, - { "示例" = "zh/rust/examples.md" }, - ] }, - { "开发" = [ - { "贡献" = "zh/development/index.md" }, - { "架构" = "zh/architecture.md" }, - { "扩展" = "zh/extending.md" }, - ] }, + { "Contributing" = "development/index.md" }, + { "Architecture" = "architecture.md" }, + { "Extending" = "extending.md" }, ] }, ] @@ -106,13 +59,6 @@ extra_css = ["assets/stylesheets/extra.css"] name = "molcrafts" language = "en" -# https://zensical.org/docs/setup/language/#site-language-selector -[project.extra] -alternate = [ - { name = "English", link = "/en/", lang = "en" }, - { name = "简体中文", link = "/zh/", lang = "zh" }, -] - [project.extra.molcrafts] product = "molpack" accent = "#c2410c" From 10de63afa77d0ac6c3e2e8b4271d454322aa54c3 Mon Sep 17 00:00:00 2001 From: Roy Kid Date: Fri, 24 Jul 2026 16:01:34 +0200 Subject: [PATCH 6/7] test: drop the wall-clock ceiling from the pbc smoke test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `smoke_pack_pbc_only_finishes_quickly` asserted `elapsed < 30 s` on a subprocess run — a nondeterministic gate on a loaded runner. The hang it guards against (2000 A cell grid, ~10^8 cells) still fails the test by never completing, so the regression stays covered without the timing assert. Renamed to `smoke_pack_pbc_only_completes`. --- tests/cli.rs | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 deletions(-) diff --git a/tests/cli.rs b/tests/cli.rs index 52bae9d..ada053a 100644 --- a/tests/cli.rs +++ b/tests/cli.rs @@ -165,12 +165,12 @@ fn smoke_pack_mixture() { } /// Regression: a script with only a `pbc` directive and no `inside` -/// restraint must pack in O(seconds). Before the parser fix, `pbc` -/// was silently dropped and the packer fell back to a 2000 Å cell -/// grid (~10⁸ cells, 42 GB). With `pbc` wired through, the cell grid -/// is sized from the PBC box and the job finishes immediately. +/// restraint must pack. Before the parser fix, `pbc` was silently +/// dropped and the packer fell back to a 2000 Å cell grid (~10⁸ cells, +/// 42 GB), which never completed. With `pbc` wired through, the cell +/// grid is sized from the PBC box. #[test] -fn smoke_pack_pbc_only_finishes_quickly() { +fn smoke_pack_pbc_only_completes() { let dir = example_dir("pack_mixture"); let out_path = dir.join("_ci_pbc_only.pdb"); let _ = std::fs::remove_file(&out_path); @@ -182,7 +182,6 @@ fn smoke_pack_pbc_only_finishes_quickly() { out_path.display() ); - let start = std::time::Instant::now(); let out = Command::new(bin_path()) .current_dir(&dir) .stdin(std::process::Stdio::piped()) @@ -195,7 +194,6 @@ fn smoke_pack_pbc_only_finishes_quickly() { child.wait_with_output() }) .expect("run molpack"); - let elapsed = start.elapsed(); let _ = std::fs::remove_file(&out_path); @@ -205,13 +203,6 @@ fn smoke_pack_pbc_only_finishes_quickly() { out.status, String::from_utf8_lossy(&out.stderr) ); - // Previously this configuration hung indefinitely allocating cells. - // Giving ourselves a generous 30 s ceiling still catches a regression - // without being flaky on a loaded CI runner. - assert!( - elapsed.as_secs() < 30, - "pbc-only pack should finish fast; took {elapsed:?}" - ); } /// Verify file-argument mode resolves paths relative to the .inp directory. From 93110ec6626753ea84a17e4a2963b45b5bc16fc4 Mon Sep 17 00:00:00 2001 From: Roy Kid Date: Fri, 24 Jul 2026 16:13:44 +0200 Subject: [PATCH 7/7] test: remove the smoke tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `tests/cli.rs` dropped `smoke_pack_mixture` and `smoke_pack_pbc_only_completes` along with the "smoke tests" section; the CLI file keeps its argument- and error-path assertions. `python/tests/test_examples_smoke.py` is gone entirely — it only ast-parsed the example scripts and checked their PDB fixtures loaded. Packing behaviour stays covered by the Packmol regression suite (`tests/examples_batch.rs`, `--ignored`). --- python/tests/test_examples_smoke.py | 84 ---------------------------- tests/cli.rs | 87 ----------------------------- 2 files changed, 171 deletions(-) delete mode 100644 python/tests/test_examples_smoke.py diff --git a/python/tests/test_examples_smoke.py b/python/tests/test_examples_smoke.py deleted file mode 100644 index 55c9a0a..0000000 --- a/python/tests/test_examples_smoke.py +++ /dev/null @@ -1,84 +0,0 @@ -"""Smoke tests: every example script compiles and its PDB assets parse. - -Full workloads (the actual pack runs) are too large for the unit-test -suite — run ``python examples/pack_.py`` to exercise them. -""" - -from __future__ import annotations - -import ast -import sys -from pathlib import Path - -import pytest - -HERE = Path(__file__).resolve().parent -EXAMPLES = HERE.parent / "examples" -DATA_ROOT = HERE.parent.parent / "examples" - -sys.path.insert(0, str(EXAMPLES)) -from _common import read_pdb_as_arrays # noqa: E402 - -EXAMPLE_SCRIPTS = [ - "pack_water_cube.py", - "pack_mixture.py", - "pack_bilayer.py", - "pack_interface.py", - "pack_spherical.py", - "pack_solvprotein.py", -] - -# Maps every example script to the PDB fixtures it depends on. -EXAMPLE_FIXTURES = { - "pack_mixture.py": ["pack_mixture/water.pdb", "pack_mixture/urea.pdb"], - "pack_bilayer.py": ["pack_bilayer/water.pdb", "pack_bilayer/palmitoil.pdb"], - "pack_interface.py": [ - "pack_interface/water.pdb", - "pack_interface/chloroform.pdb", - "pack_interface/t3.pdb", - ], - "pack_spherical.py": ["pack_spherical/water.pdb", "pack_spherical/palmitoil.pdb"], - "pack_solvprotein.py": [ - "pack_solvprotein/protein.pdb", - "pack_solvprotein/water.pdb", - "pack_solvprotein/sodium.pdb", - "pack_solvprotein/chloride.pdb", - ], -} - - -@pytest.mark.parametrize("script", EXAMPLE_SCRIPTS) -def test_example_script_parses(script): - source = (EXAMPLES / script).read_text(encoding="utf-8") - ast.parse(source) - - -@pytest.mark.parametrize("script", EXAMPLE_SCRIPTS) -def test_example_script_has_main(script): - source = (EXAMPLES / script).read_text(encoding="utf-8") - tree = ast.parse(source) - fns = {n.name for n in ast.walk(tree) if isinstance(n, ast.FunctionDef)} - assert "main" in fns - - -@pytest.mark.parametrize( - "script,fixtures", - sorted(EXAMPLE_FIXTURES.items()), -) -def test_example_fixtures_exist(script, fixtures): - for rel in fixtures: - p = DATA_ROOT / rel - assert p.exists(), f"{script}: missing fixture {p}" - - -@pytest.mark.parametrize( - "script,fixtures", - sorted(EXAMPLE_FIXTURES.items()), -) -def test_example_fixtures_parse(script, fixtures): - for rel in fixtures: - pos, rad, els = read_pdb_as_arrays(DATA_ROOT / rel) - assert pos.shape[0] > 0 - assert pos.shape[1] == 3 - assert rad.shape[0] == pos.shape[0] - assert len(els) == pos.shape[0] diff --git a/tests/cli.rs b/tests/cli.rs index ada053a..6e91b50 100644 --- a/tests/cli.rs +++ b/tests/cli.rs @@ -118,93 +118,6 @@ end structure ); } -// ── smoke tests (run actual packing on canonical examples) ────────────────── - -/// Pack the mixture example and verify the output file is created. -#[test] -fn smoke_pack_mixture() { - let dir = example_dir("pack_mixture"); - let out_path = dir.join("_ci_mixture.pdb"); - let _ = std::fs::remove_file(&out_path); - - // Write a temp .inp that redirects output so we don't touch the committed file. - let inp = format!( - "tolerance 2.0\nseed 1234567\nfiletype pdb\noutput {}\n\n\ - structure water.pdb\n number 50\n inside box 0. 0. 0. 20. 20. 20.\nend structure\n", - out_path.display() - ); - - let out = Command::new(bin_path()) - .current_dir(&dir) - .stdin(std::process::Stdio::piped()) - .stdout(std::process::Stdio::null()) - .stderr(std::process::Stdio::null()) - .spawn() - .and_then(|mut child| { - use std::io::Write; - child.stdin.as_mut().unwrap().write_all(inp.as_bytes())?; - child.wait_with_output() - }) - .expect("run molpack"); - - assert!( - out.status.success(), - "molpack exited {:?}\nstdout: {}\nstderr: {}", - out.status, - String::from_utf8_lossy(&out.stdout), - String::from_utf8_lossy(&out.stderr) - ); - assert!( - out_path.exists(), - "output file not created: {}", - out_path.display() - ); - - // Clean up. - let _ = std::fs::remove_file(&out_path); -} - -/// Regression: a script with only a `pbc` directive and no `inside` -/// restraint must pack. Before the parser fix, `pbc` was silently -/// dropped and the packer fell back to a 2000 Å cell grid (~10⁸ cells, -/// 42 GB), which never completed. With `pbc` wired through, the cell -/// grid is sized from the PBC box. -#[test] -fn smoke_pack_pbc_only_completes() { - let dir = example_dir("pack_mixture"); - let out_path = dir.join("_ci_pbc_only.pdb"); - let _ = std::fs::remove_file(&out_path); - - let inp = format!( - "tolerance 2.0\nseed 1234567\nfiletype pdb\noutput {}\n\ - pbc 30.0 30.0 30.0\n\n\ - structure water.pdb\n number 10\nend structure\n", - out_path.display() - ); - - let out = Command::new(bin_path()) - .current_dir(&dir) - .stdin(std::process::Stdio::piped()) - .stdout(std::process::Stdio::null()) - .stderr(std::process::Stdio::piped()) - .spawn() - .and_then(|mut child| { - use std::io::Write; - child.stdin.as_mut().unwrap().write_all(inp.as_bytes())?; - child.wait_with_output() - }) - .expect("run molpack"); - - let _ = std::fs::remove_file(&out_path); - - assert!( - out.status.success(), - "molpack exited {:?}\nstderr: {}", - out.status, - String::from_utf8_lossy(&out.stderr) - ); -} - /// Verify file-argument mode resolves paths relative to the .inp directory. #[test] fn file_arg_resolves_paths_from_inp_dir() {